diff --git a/.codecov.yml b/.codecov.yml index cd52e2604d..4268758e44 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,10 +1,32 @@ codecov: require_ci_to_pass: true + # The C++ and Rust uploads land minutes apart; without this gate Codecov + # publishes a near-zero total from whichever one arrives first. + notify: + after_n_builds: 2 + wait_for_ci: true comment: behavior: default layout: reach,diff,flags,tree,reach - show_carryforward_flags: false + show_carryforward_flags: true + after_n_builds: 2 + +# C++ and Rust coverage upload from independent workflows under the `cpp` and +# `rust` flags; carryforward keeps one language's total when only the other reran. +flag_management: + default_rules: + carryforward: true + individual_flags: + - name: cpp + carryforward: true + paths: + - include/ + - src/ + - name: rust + carryforward: true + paths: + - crates/ coverage: range: "70..85" diff --git a/.cspell.config.yaml b/.cspell.config.yaml index 7980f71321..c578851b77 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -7,8 +7,7 @@ ignorePaths: - cmake/** - LICENSE.md - .clang-tidy - - src/test/app/wasm_fixtures/**/*.wat - - src/test/app/wasm_fixtures/*.c + - nix/check-tools/*.txt # generated, and full of Nix store hashes language: en allowCompoundWords: true # TODO (#6334) ignoreRandomStrings: true @@ -71,6 +70,7 @@ words: - canonicality - cdylib - canonicalised + - cctools - changespq - checkme - choco @@ -106,12 +106,14 @@ words: - deleteme - demultiplexer - deserializaton + - desugars - desync - desynced - determ - disablerepo - distro - doxyfile + - dsymutil - dxrpl - elgamal - emittance @@ -131,6 +133,7 @@ words: - gcov - gcovr - ghead + - gmock - Gnutella - godexsoft - gpgcheck @@ -140,7 +143,9 @@ words: - hwaddress - hwrap - ifndef + - impls - inequation + - initialiser - insuf - insuff - invasively @@ -170,6 +175,7 @@ words: - LOCALGOOD - logwstream - Lombrozo + - lresolv - lseq - lsmf - ltype @@ -223,6 +229,7 @@ words: - Nyffenegger - onlatest - ostr + - otool - oxalica - pargs - partitioner @@ -248,15 +255,20 @@ words: - pyparsing - qalloc - qbsprofile + - qself - queuable - Raphson - rcflags - replayer + - repodata + - repomd - rerandomize - rerandomization - rerandomized - rerandomizes - rerere + - retargeted + - retargets - retriable - RIPD - ripdtop @@ -292,6 +304,7 @@ words: - sles - soci - socidb + - Sonatype - sponsee - sponsees - SRPMS @@ -313,6 +326,7 @@ words: - summands - superpeer - superpeers + - Swatinem - takergets - takerpays - ters @@ -341,6 +355,7 @@ words: - unflatten - unfund - unimpair + - unmetered - unroutable - unscalable - unserviced @@ -361,6 +376,8 @@ words: - vfalco - vinnie - wasmi + - wasmparser + - Werror - wextra - wptr - writeme @@ -368,13 +385,17 @@ words: - wthread - xbridge - xchain + - xcrun - xfloat - ximinez - XMACRO + - xored - xrpkuwait - xrpl - xrpld - xrplf - xxhash - xxhasher + - zstdio - CGNAT + - ungated diff --git a/.envrc b/.envrc index cecf4b4767..a3f6be96ea 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1,10 @@ watch_file nix/*.nix +# Pinned Rust toolchain, read by nix/packages.nix via fromRustupToolchainFile. +watch_file rust-toolchain.toml + +# The dev shell derivation includes all of conan/ (see nix/devshell.nix), so any +# change in there has to invalidate direnv's cached environment. +watch_dir conan + use flake diff --git a/.github/actions/cargo-cache/action.yml b/.github/actions/cargo-cache/action.yml new file mode 100644 index 0000000000..f716d3e4a4 --- /dev/null +++ b/.github/actions/cargo-cache/action.yml @@ -0,0 +1,39 @@ +name: Use cargo artifacts cache +description: > + Cache the cargo build artifacts with rust-cache. Never caches ~/.cargo/bin: + when saving the cache, rust-cache deletes all binaries that were already + present there, which on persistent self-hosted runners wipes the tools + installed by prepare-runner. Harmless on ephemeral runners, but kept + consistent everywhere. + +inputs: + workspaces: + description: "Workspaces to cache, as 'workspace -> target' lines." + required: false + default: crates + key: + description: "Additional part of the cache key." + required: false + default: "" + cache-directories: + description: "Additional non-workspace directories to cache." + required: false + default: "" + save-if: + description: > + Condition for saving the cache after the job. Defaults to save only from develop branch + required: false + default: ${{ github.ref == 'refs/heads/develop' }} + +runs: + using: composite + + steps: + - name: Use cargo artifacts cache + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + cache-bin: "false" + cache-directories: ${{ inputs.cache-directories }} + key: ${{ inputs.key }} + save-if: ${{ inputs.save-if }} + workspaces: ${{ inputs.workspaces }} diff --git a/.github/actions/generate-version/action.yml b/.github/actions/generate-version/action.yml deleted file mode 100644 index 50b3166596..0000000000 --- a/.github/actions/generate-version/action.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Generate build version number -description: "Generate build version number." - -outputs: - version: - description: "The generated build version number." - value: ${{ steps.version.outputs.version }} - -runs: - using: composite - steps: - # When a tag is pushed, the version is used as-is. - - name: Generate version for tag event - if: ${{ startsWith(github.ref, 'refs/tags/') }} - shell: bash - env: - VERSION: ${{ github.ref_name }} - run: echo "VERSION=${VERSION}" >>"${GITHUB_ENV}" - - # When a tag is not pushed, then the version (e.g. 1.2.3-b0) is extracted - # from the BuildInfo.cpp file and the shortened commit hash appended to it. - # We use a plus sign instead of a hyphen because Conan recipe versions do - # not support two hyphens. - - name: Generate version for non-tag event - if: ${{ !startsWith(github.ref, 'refs/tags/') }} - shell: bash - run: | - echo 'Extracting version from BuildInfo.cpp.' - VERSION="$(cat src/libxrpl/protocol/BuildInfo.cpp | grep "versionString =" | awk -F '"' '{print $2}')" - if [[ -z "${VERSION}" ]]; then - echo 'Unable to extract version from BuildInfo.cpp.' - exit 1 - fi - - echo 'Appending shortened commit hash to version.' - SHA='${{ github.sha }}' - VERSION="${VERSION}+${SHA:0:7}" - - echo "VERSION=${VERSION}" >>"${GITHUB_ENV}" - - - name: Output version - id: version - shell: bash - run: echo "version=${VERSION}" >>"${GITHUB_OUTPUT}" diff --git a/.github/actions/release-info/action.yml b/.github/actions/release-info/action.yml new file mode 100644 index 0000000000..e03170b2c8 --- /dev/null +++ b/.github/actions/release-info/action.yml @@ -0,0 +1,44 @@ +name: Release info +description: "Derive the version, release channel and package release number for this build." + +outputs: + version: + description: "The build version number." + value: ${{ steps.version.outputs.version }} + channel: + description: "The release channel this build belongs to." + value: ${{ steps.release_info.outputs.channel }} + pkg_release: + description: "The package release number: 1 for a tag, .git otherwise." + value: ${{ steps.release_info.outputs.pkg_release }} + +runs: + using: composite + steps: + # A tag names its own version. Anything else takes it from BuildInfo.cpp and + # appends the commit hash as build metadata, joined with a plus sign because a + # Conan version cannot contain two hyphens. + - name: Determine version + id: version + shell: bash + env: + IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }} + REF_NAME: ${{ github.ref_name }} + SHA: ${{ github.sha }} + run: | + if [[ "${IS_TAG}" == "true" ]]; then + version="${REF_NAME}" + else + version="$(awk -F'"' '/versionString =/ { print $2 }' src/libxrpl/protocol/BuildInfo.cpp)" + if [[ -z "${version}" ]]; then + echo "Unable to read versionString from BuildInfo.cpp." >&2 + exit 1 + fi + version="${version}+${SHA:0:7}" + fi + + echo "version=${version}" | tee -a "${GITHUB_OUTPUT}" + + - name: Determine release channel and package release + id: release_info + uses: XRPLF/actions/release-info@7cc0e4a8d9d0b838f92c48d312856b190341bbba diff --git a/.github/actions/setup-nix-env/action.yml b/.github/actions/setup-nix-env/action.yml new file mode 100644 index 0000000000..38b8365649 --- /dev/null +++ b/.github/actions/setup-nix-env/action.yml @@ -0,0 +1,70 @@ +name: Setup Nix environment +description: "Build the flake's CI environment and put its tools on PATH." + +# The environment from nix/ci-env.nix, the same one the Linux CI images bake in +# (see nix/docker). Exported onto PATH rather than entered with `nix develop`: +# the composite actions below run plain `bash` and would escape a dev shell. + +runs: + using: composite + + steps: + - name: Build the CI environment + id: build + shell: bash + env: + # --out-link doubles as a GC root for the length of the job. + OUT_LINK: ${{ runner.temp }}/xrpld-ci-env + run: | + # --extra-experimental-features: flakes may not be on in the runner's nix.conf. + nix --extra-experimental-features "nix-command flakes" \ + build .#default --out-link "${OUT_LINK}" --print-build-logs + echo "path=$(readlink -f "${OUT_LINK}")" >>"${GITHUB_OUTPUT}" + + - name: Export the environment + shell: bash + env: + ENV_PATH: ${{ steps.build.outputs.path }} + run: | + echo "${ENV_PATH}/bin" >>"${GITHUB_PATH}" + + # Already KEY=VALUE per line. See `darwinEnv` in nix/ci-env.nix. + ENV_FILE="${ENV_PATH}/share/xrpld-ci-env/env" + if [ -f "${ENV_FILE}" ]; then + cat "${ENV_FILE}" >>"${GITHUB_ENV}" + fi + + # XrplSanity.cmake otherwise rejects a Nix compiler as one that leaked. + echo "XRPL_DEVSHELL=ci-env" >>"${GITHUB_ENV}" + + # Unlike the Linux nix images, macOS needs no SSL_CERT_FILE: it has its + # own trust store, and pinning would break TLS to hosts relying on it. + + # In RUNNER_TEMP, which the runner empties per job, like the `.conan2` + # prepare-runner hands the system toolchain - but under its own name: + # that Conan is a different version, and the two would migrate each + # other's cache. + echo "CONAN_HOME=${RUNNER_TEMP}/.conan2-nix" >>"${GITHUB_ENV}" + + # Config, profiles and remote, exactly as the dev shell sets them up on + # entry; the `setup-conan` action is skipped for this toolchain. + - name: Setup Conan + shell: bash + run: ./conan/init.sh + + # `Check tools` runs later but swallows failures; a bad export would just + # build with the system toolchain. + - name: Verify the toolchain resolves into the Nix store + shell: bash + run: | + for tool in clang clang++ cmake ninja conan; do + path="$(command -v "${tool}" || true)" + echo "${tool} -> ${path:-}" + case "${path}" in + /nix/store/*) ;; + *) + echo "::error::${tool} does not resolve into the Nix store" + exit 1 + ;; + esac + done diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fcac44c44c..7361a3db63 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,8 @@ updates: directories: - / - .github/actions/build-deps/ - - .github/actions/generate-version/ + - .github/actions/cargo-cache/ + - .github/actions/release-info/ - .github/actions/set-compiler-env/ - .github/actions/setup-conan/ schedule: @@ -19,3 +20,19 @@ updates: github-actions: patterns: - "*" + + - package-ecosystem: cargo + directory: /crates + schedule: + interval: weekly + day: monday + time: "04:00" + timezone: Etc/GMT + commit-message: + prefix: "chore: [DEPENDABOT] " + target-branch: develop + open-pull-requests-limit: 10 + groups: + rust-dependencies: + patterns: + - "*" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 95d75c04b4..e0d511c467 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,7 +18,7 @@ If too broad, please consider splitting into multiple PRs. If there is a relevant task or issue, please link it here. --> -### Context of Change +## Context of Change -### API Impact +## API Impact State2 : Succeeded - State1 --> [*] : Aborted - State2 --> State3 : Succeeded - State2 --> [*] : Aborted - state State3 { - state "Accumulate Enough Data\nLong State Name" as long1 - long1 : Just a test - [*] --> long1 - long1 --> long1 : New Data - long1 --> ProcessData : Enough Data - } - State3 --> State3 : Failed - State3 --> [*] : Succeeded / Save Result - State3 --> [*] : Aborted - - \enduml -*/ diff --git a/include/xrpl/basics/Archive.h b/include/xrpl/basics/Archive.h index 66d6a019af..67261352e9 100644 --- a/include/xrpl/basics/Archive.h +++ b/include/xrpl/basics/Archive.h @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace xrpl { @@ -13,6 +13,6 @@ namespace xrpl { * @throws runtime_error */ void -extractTarLz4(boost::filesystem::path const& src, boost::filesystem::path const& dst); +extractTarLz4(std::filesystem::path const& src, std::filesystem::path const& dst); } // namespace xrpl diff --git a/include/xrpl/basics/Buffer.h b/include/xrpl/basics/Buffer.h index 05af6c409a..00a6b7ecf9 100644 --- a/include/xrpl/basics/Buffer.h +++ b/include/xrpl/basics/Buffer.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -156,6 +157,19 @@ public: } /** @} */ + /** + * Set every byte in the buffer to the given value. + * + * The size is unchanged, and this is a no-op on an empty buffer. + * + * @param value the byte to write to every position. + */ + void + fill(std::uint8_t value) noexcept + { + std::fill_n(p_.get(), size_, value); + } + /** * Reset the buffer. * All memory is deallocated. The resulting size is 0. @@ -226,10 +240,4 @@ operator==(Buffer const& lhs, Buffer const& rhs) noexcept return std::memcmp(lhs.data(), rhs.data(), lhs.size()) == 0; } -inline bool -operator!=(Buffer const& lhs, Buffer const& rhs) noexcept -{ - return !(lhs == rhs); -} - } // namespace xrpl diff --git a/include/xrpl/basics/FileUtilities.h b/include/xrpl/basics/FileUtilities.h index c7a427b8a9..ca3435be03 100644 --- a/include/xrpl/basics/FileUtilities.h +++ b/include/xrpl/basics/FileUtilities.h @@ -1,24 +1,79 @@ #pragma once -#include -#include - #include +#include #include #include +#include namespace xrpl { std::string getFileContents( - boost::system::error_code& ec, - boost::filesystem::path const& sourcePath, + std::error_code& ec, + std::filesystem::path const& sourcePath, std::optional maxSize = std::nullopt); void writeFileContents( - boost::system::error_code& ec, - boost::filesystem::path const& destPath, + std::error_code& ec, + std::filesystem::path const& destPath, std::string const& contents); +/** + * Generate a unique, non-existing path under @p base whose filename starts with + * @p prefix and ends with a random hex suffix. + * + * Attempts up to @p maxAttempts paths. Throws `std::runtime_error` if a unique + * path cannot be found or if the filesystem returns an error while checking for + * existence. + */ +std::filesystem::path +uniqueRandomPath( + std::filesystem::path const& base, + std::string const& prefix = "", + std::size_t maxAttempts = 100); + +/** + * RAII temporary directory. + * + * The directory and all its contents are deleted when + * the instance of `TempDir` is destroyed. + */ +class TempDir +{ + std::filesystem::path path_; + +public: +#if !GENERATING_DOCS + TempDir(TempDir const&) = delete; + TempDir& + operator=(TempDir const&) = delete; +#endif + + /** + * Construct a temporary directory. + */ + TempDir(); + + /** + * Destroy a temporary directory. + */ + ~TempDir(); + + /** + * Get the native path for the temporary directory. + */ + [[nodiscard]] std::string + path() const; + + /** + * Get the native path for a file. + * + * The file does not need to exist. + */ + [[nodiscard]] std::string + file(std::string const& name) const; +}; + } // namespace xrpl diff --git a/include/xrpl/basics/IntrusivePointer.h b/include/xrpl/basics/IntrusivePointer.h index 59853ad4d0..b978016860 100644 --- a/include/xrpl/basics/IntrusivePointer.h +++ b/include/xrpl/basics/IntrusivePointer.h @@ -96,9 +96,6 @@ public: SharedIntrusive& operator=(SharedIntrusive const& rhs); - bool - operator!=(std::nullptr_t) const; - bool operator==(std::nullptr_t) const; diff --git a/include/xrpl/basics/IntrusivePointer.ipp b/include/xrpl/basics/IntrusivePointer.ipp index 67d43b05d6..6c2a71f7eb 100644 --- a/include/xrpl/basics/IntrusivePointer.ipp +++ b/include/xrpl/basics/IntrusivePointer.ipp @@ -111,13 +111,6 @@ SharedIntrusive::operator=(SharedIntrusive&& rhs) return *this; } -template -bool -SharedIntrusive::operator!=(std::nullptr_t) const -{ - return this->get() != nullptr; -} - template bool SharedIntrusive::operator==(std::nullptr_t) const diff --git a/include/xrpl/basics/Log.h b/include/xrpl/basics/Log.h index 945dc1b4ec..3aceac5f4a 100644 --- a/include/xrpl/basics/Log.h +++ b/include/xrpl/basics/Log.h @@ -3,8 +3,8 @@ #include #include -#include +#include #include #include #include @@ -84,7 +84,7 @@ private: * @return `true` if the file was opened. */ bool - open(boost::filesystem::path const& path); + open(std::filesystem::path const& path); /** * Close and re-open the system file associated with the log @@ -133,7 +133,7 @@ private: private: std::unique_ptr stream_; - boost::filesystem::path path_; + std::filesystem::path path_; }; std::mutex mutable mutex_; @@ -152,7 +152,7 @@ public: virtual ~Logs() = default; bool - open(boost::filesystem::path const& pathToLogFile); + open(std::filesystem::path const& pathToLogFile); beast::Journal::Sink& get(std::string const& name); diff --git a/include/xrpl/basics/SHAMapHash.h b/include/xrpl/basics/SHAMapHash.h index 3c3d525022..1902a3b2ec 100644 --- a/include/xrpl/basics/SHAMapHash.h +++ b/include/xrpl/basics/SHAMapHash.h @@ -85,12 +85,6 @@ public: } }; -inline bool -operator!=(SHAMapHash const& x, SHAMapHash const& y) -{ - return !(x == y); -} - template <> inline std::size_t extract(SHAMapHash const& key) diff --git a/include/xrpl/basics/Slice.h b/include/xrpl/basics/Slice.h index 75c9b8c7bd..92b777ab98 100644 --- a/include/xrpl/basics/Slice.h +++ b/include/xrpl/basics/Slice.h @@ -208,12 +208,6 @@ operator==(Slice const& lhs, Slice const& rhs) noexcept return std::memcmp(lhs.data(), rhs.data(), lhs.size()) == 0; } -inline bool -operator!=(Slice const& lhs, Slice const& rhs) noexcept -{ - return !(lhs == rhs); -} - inline bool operator<(Slice const& lhs, Slice const& rhs) noexcept { diff --git a/include/xrpl/basics/StringUtilities.h b/include/xrpl/basics/StringUtilities.h index 2b360d2fda..e3b91c2f25 100644 --- a/include/xrpl/basics/StringUtilities.h +++ b/include/xrpl/basics/StringUtilities.h @@ -2,7 +2,6 @@ #include -#include #include #include @@ -125,9 +124,31 @@ struct ParsedUrl bool parseUrl(ParsedUrl& pUrl, std::string const& strUrl); +/** + * Remove leading and trailing ASCII whitespace. + * + * Whitespace is the fixed set " \t\n\v\f\r"; the current locale is not + * consulted, so the result depends only on the input. + * + * @param str The string to trim. + * @return @p str without leading or trailing whitespace. + */ std::string trimWhitespace(std::string str); +/** + * Fold ASCII upper case letters to lower case. + * + * Only 'A' through 'Z' are remapped; every other byte is left alone and the + * current locale is not consulted, so the result depends only on the input. + * + * @param str The string to fold. + * @return @p str with each ASCII upper case letter replaced by its lower case + * equivalent. + */ +std::string +toLower(std::string str); + std::optional toUInt64(std::string const& s); diff --git a/include/xrpl/basics/partitioned_unordered_map.h b/include/xrpl/basics/partitioned_unordered_map.h index e78043e252..c6b0107b93 100644 --- a/include/xrpl/basics/partitioned_unordered_map.h +++ b/include/xrpl/basics/partitioned_unordered_map.h @@ -116,12 +116,6 @@ public: { return lhs.map == rhs.map && lhs.ait == rhs.ait && lhs.mit == rhs.mit; } - - friend bool - operator!=(Iterator const& lhs, Iterator const& rhs) - { - return !(lhs == rhs); - } }; struct ConstIterator @@ -189,12 +183,6 @@ public: { return lhs.map == rhs.map && lhs.ait == rhs.ait && lhs.mit == rhs.mit; } - - friend bool - operator!=(ConstIterator const& lhs, ConstIterator const& rhs) - { - return !(lhs == rhs); - } }; private: diff --git a/include/xrpl/beast/container/detail/aged_ordered_container.h b/include/xrpl/beast/container/detail/aged_ordered_container.h index 5b60ef7e6d..9dd83d466b 100644 --- a/include/xrpl/beast/container/detail/aged_ordered_container.h +++ b/include/xrpl/beast/container/detail/aged_ordered_container.h @@ -1038,25 +1038,6 @@ public: Compare, OtherAllocator> const& other) const; - template < - bool OtherIsMulti, - bool OtherIsMap, - class OtherT, - class OtherDuration, - class OtherAllocator> - bool - operator!=(AgedOrderedContainer< - OtherIsMulti, - OtherIsMap, - Key, - OtherT, - OtherDuration, - Compare, - OtherAllocator> const& other) const - { - return !(this->operator==(other)); - } - template < bool OtherIsMulti, bool OtherIsMap, diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h b/include/xrpl/beast/container/detail/aged_unordered_container.h index c4287b1ca1..ea271feed0 100644 --- a/include/xrpl/beast/container/detail/aged_unordered_container.h +++ b/include/xrpl/beast/container/detail/aged_unordered_container.h @@ -1340,28 +1340,6 @@ public: OtherAllocator> const& other) const requires MaybeMulti; - template < - bool OtherIsMulti, - bool OtherIsMap, - class OtherKey, - class OtherT, - class OtherDuration, - class OtherHash, - class OtherAllocator> - bool - operator!=(AgedUnorderedContainer< - OtherIsMulti, - OtherIsMap, - OtherKey, - OtherT, - OtherDuration, - OtherHash, - KeyEqual, - OtherAllocator> const& other) const - { - return !(this->operator==(other)); - } - private: bool wouldExceed(size_type additional) const diff --git a/include/xrpl/beast/core/List.h b/include/xrpl/beast/core/List.h index b9b6829d31..076ac3028b 100644 --- a/include/xrpl/beast/core/List.h +++ b/include/xrpl/beast/core/List.h @@ -82,13 +82,6 @@ public: return node_ == other.node_; } - template - bool - operator!=(ListIterator const& other) const noexcept - { - return !((*this) == other); - } - reference operator*() const noexcept { diff --git a/include/xrpl/beast/net/IPAddress.h b/include/xrpl/beast/net/IPAddress.h index 7422778ea2..e636a69ce7 100644 --- a/include/xrpl/beast/net/IPAddress.h +++ b/include/xrpl/beast/net/IPAddress.h @@ -103,7 +103,7 @@ namespace boost { template <> struct hash<::beast::ip::Address> { - explicit hash() = default; + hash() = default; std::size_t operator()(::beast::ip::Address const& addr) const diff --git a/include/xrpl/beast/net/IPEndpoint.h b/include/xrpl/beast/net/IPEndpoint.h index d4d3b2ab12..a5fb5b4318 100644 --- a/include/xrpl/beast/net/IPEndpoint.h +++ b/include/xrpl/beast/net/IPEndpoint.h @@ -110,12 +110,6 @@ public: operator==(Endpoint const& lhs, Endpoint const& rhs); friend bool operator<(Endpoint const& lhs, Endpoint const& rhs); - - friend bool - operator!=(Endpoint const& lhs, Endpoint const& rhs) - { - return !(lhs == rhs); - } friend bool operator>(Endpoint const& lhs, Endpoint const& rhs) { diff --git a/include/xrpl/beast/rfc2616.h b/include/xrpl/beast/rfc2616.h index 1986568553..87d63b0260 100644 --- a/include/xrpl/beast/rfc2616.h +++ b/include/xrpl/beast/rfc2616.h @@ -11,6 +11,7 @@ #include #include #include +#include #include namespace beast::rfc2616 { @@ -186,7 +187,7 @@ splitCommas(FwdIt first, FwdIt last) template > Result -splitCommas(boost::beast::string_view const& s) +splitCommas(std::string_view s) { return splitCommas(s.begin(), s.end()); } @@ -229,12 +230,6 @@ public: return other.it_ == it_ && other.end_ == end_ && other.value_.size() == value_.size(); } - bool - operator!=(ListIterator const& other) const - { - return !(*this == other); - } - reference operator*() const { diff --git a/include/xrpl/beast/unit_test/reporter.h b/include/xrpl/beast/unit_test/reporter.h index 0fe77a7862..cbd1c7e70d 100644 --- a/include/xrpl/beast/unit_test/reporter.h +++ b/include/xrpl/beast/unit_test/reporter.h @@ -8,7 +8,6 @@ #include #include -#include #include #include @@ -188,7 +187,7 @@ Reporter::fmtdur(clock_type::duration const& d) using namespace std::chrono; auto const ms = duration_cast(d); if (ms < seconds{1}) - return boost::lexical_cast(ms.count()) + "ms"; + return std::to_string(ms.count()) + "ms"; std::stringstream ss; ss << std::fixed << std::setprecision(1) << (ms.count() / 1000.) << "s"; return ss.str(); diff --git a/include/xrpl/beast/unit_test/suite.h b/include/xrpl/beast/unit_test/suite.h index e24904a87b..2b06fb4e05 100644 --- a/include/xrpl/beast/unit_test/suite.h +++ b/include/xrpl/beast/unit_test/suite.h @@ -6,11 +6,10 @@ #include -#include -#include #include #include +#include #include #include #include @@ -27,10 +26,10 @@ makeReason(String const& reason, char const* file, int line) std::string s(reason); if (!s.empty()) s.append(": "); - namespace fs = boost::filesystem; + namespace fs = std::filesystem; s.append(fs::path{file}.filename().string()); s.append("("); - s.append(boost::lexical_cast(line)); + s.append(std::to_string(line)); s.append(")"); return s; } diff --git a/include/xrpl/beast/utility/temp_dir.h b/include/xrpl/beast/utility/temp_dir.h deleted file mode 100644 index a0ff1e6940..0000000000 --- a/include/xrpl/beast/utility/temp_dir.h +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include - -#include - -namespace beast { - -/** - * RAII temporary directory. - * - * The directory and all its contents are deleted when - * the instance of `temp_dir` is destroyed. - */ -class TempDir -{ - boost::filesystem::path path_; - -public: -#if !GENERATING_DOCS - TempDir(TempDir const&) = delete; - TempDir& - operator=(TempDir const&) = delete; -#endif - - /** - * Construct a temporary directory. - */ - TempDir() - { - auto const dir = boost::filesystem::temp_directory_path(); - do - { - path_ = dir / boost::filesystem::unique_path(); - } while (boost::filesystem::exists(path_)); - boost::filesystem::create_directory(path_); - } - - /** - * Destroy a temporary directory. - */ - ~TempDir() - { - // use non-throwing calls in the destructor - boost::system::error_code ec; - boost::filesystem::remove_all(path_, ec); - // TODO: warn/notify if ec set ? - } - - /** - * Get the native path for the temporary directory - */ - [[nodiscard]] std::string - path() const - { - return path_.string(); - } - - /** - * Get the native path for the a file. - * - * The file does not need to exist. - */ - [[nodiscard]] std::string - file(std::string const& name) const - { - return (path_ / name).string(); - } -}; - -} // namespace beast diff --git a/include/xrpl/conditions/Condition.h b/include/xrpl/conditions/Condition.h index 365a41a087..04e571a028 100644 --- a/include/xrpl/conditions/Condition.h +++ b/include/xrpl/conditions/Condition.h @@ -92,10 +92,4 @@ operator==(Condition const& lhs, Condition const& rhs) lhs.fingerprint == rhs.fingerprint; } -inline bool -operator!=(Condition const& lhs, Condition const& rhs) -{ - return !(lhs == rhs); -} - } // namespace xrpl::cryptoconditions diff --git a/include/xrpl/conditions/Fulfillment.h b/include/xrpl/conditions/Fulfillment.h index 11f3165a58..6fd75aa5a3 100644 --- a/include/xrpl/conditions/Fulfillment.h +++ b/include/xrpl/conditions/Fulfillment.h @@ -93,12 +93,6 @@ operator==(Fulfillment const& lhs, Fulfillment const& rhs) lhs.fingerprint() == rhs.fingerprint(); } -inline bool -operator!=(Fulfillment const& lhs, Fulfillment const& rhs) -{ - return !(lhs == rhs); -} - /** * Determine whether the given fulfillment and condition match */ diff --git a/include/xrpl/config/BasicConfig.h b/include/xrpl/config/BasicConfig.h index 607a0c3e5f..2278a0fa68 100644 --- a/include/xrpl/config/BasicConfig.h +++ b/include/xrpl/config/BasicConfig.h @@ -2,7 +2,6 @@ #include -#include #include #include diff --git a/include/xrpl/config/Constants.h b/include/xrpl/config/Constants.h index 251a9699bc..19f5dc7c5b 100644 --- a/include/xrpl/config/Constants.h +++ b/include/xrpl/config/Constants.h @@ -128,6 +128,7 @@ struct Keys static constexpr auto kMaximumTxnInLedger = "maximum_txn_in_ledger"; static constexpr auto kMaximumTxnPerAccount = "maximum_txn_per_account"; static constexpr auto kMemoryLevel = "memory_level"; + static constexpr auto kMaxWaitingLedgers = "max_waiting_ledgers"; static constexpr auto kMinLedgersToComputeSizeLimit = "min_ledgers_to_compute_size_limit"; static constexpr auto kMinimumEscalationMultiplier = "minimum_escalation_multiplier"; static constexpr auto kMinimumLastLedgerBuffer = "minimum_last_ledger_buffer"; diff --git a/include/xrpl/core/HashRouter.h b/include/xrpl/core/HashRouter.h index 20aafecc5f..25e4df3d0d 100644 --- a/include/xrpl/core/HashRouter.h +++ b/include/xrpl/core/HashRouter.h @@ -34,7 +34,10 @@ enum class HashRouterFlags : std::uint16_t { PRIVATE4 = 0x0800, // Used in EscrowFinish.cpp PRIVATE5 = 0x1000, - PRIVATE6 = 0x2000 + PRIVATE6 = 0x2000, + // Used in apply.cpp + PRIVATE7 = 0x4000, + PRIVATE8 = 0x8000 }; constexpr HashRouterFlags diff --git a/include/xrpl/core/PerfLog.h b/include/xrpl/core/PerfLog.h index f09665e291..dd78a8f9a6 100644 --- a/include/xrpl/core/PerfLog.h +++ b/include/xrpl/core/PerfLog.h @@ -4,10 +4,9 @@ #include #include -#include - #include #include +#include #include #include #include @@ -44,7 +43,7 @@ public: */ struct Setup { - boost::filesystem::path perfLog; + std::filesystem::path perfLog; // log_interval is in milliseconds to support faster testing. milliseconds logInterval{seconds(1)}; }; @@ -149,7 +148,7 @@ public: }; PerfLog::Setup -setupPerfLog(Section const& section, boost::filesystem::path const& configDir); +setupPerfLog(Section const& section, std::filesystem::path const& configDir); std::unique_ptr makePerfLog( diff --git a/include/xrpl/json/Output.h b/include/xrpl/json/Output.h index 53d453c277..f73bd38c77 100644 --- a/include/xrpl/json/Output.h +++ b/include/xrpl/json/Output.h @@ -1,20 +1,19 @@ #pragma once -#include - #include #include +#include namespace json { class Value; -using Output = std::function; +using Output = std::function; inline Output stringOutput(std::string& s) { - return [&](boost::beast::string_view const& b) { s.append(b.data(), b.size()); }; + return [&](std::string_view b) { s.append(b.data(), b.size()); }; } /** diff --git a/include/xrpl/json/json_value.h b/include/xrpl/json/json_value.h index 260917face..57936a774f 100644 --- a/include/xrpl/json/json_value.h +++ b/include/xrpl/json/json_value.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -72,36 +73,18 @@ operator==(StaticString x, StaticString y) return strcmp(x.cStr(), y.cStr()) == 0; } -inline bool -operator!=(StaticString x, StaticString y) -{ - return !(x == y); -} - inline bool operator==(std::string const& x, StaticString y) { return strcmp(x.c_str(), y.cStr()) == 0; } -inline bool -operator!=(std::string const& x, StaticString y) -{ - return !(x == y); -} - inline bool operator==(StaticString x, std::string const& y) { return y == x; } -inline bool -operator!=(StaticString x, std::string const& y) -{ - return !(y == x); -} - /** * @brief Represents a JSON value. * @@ -489,12 +472,6 @@ toJson(xrpl::Number const& number) bool operator==(Value const&, Value const&); -inline bool -operator!=(Value const& x, Value const& y) -{ - return !(x == y); -} - bool operator<(Value const&, Value const&); @@ -548,6 +525,7 @@ public: class ValueIteratorBase { public: + using iterator_category = std::bidirectional_iterator_tag; using size_t = unsigned int; using difference_type = int; using SelfType = ValueIteratorBase; @@ -562,12 +540,6 @@ public: return isEqual(other); } - bool - operator!=(SelfType const& other) const - { - return !isEqual(other); - } - /** * Return either the index or the member name of the referenced value as a * Value. diff --git a/include/xrpl/ledger/BookDirs.h b/include/xrpl/ledger/BookDirs.h index dc4361136d..b9aa87ae52 100644 --- a/include/xrpl/ledger/BookDirs.h +++ b/include/xrpl/ledger/BookDirs.h @@ -49,12 +49,6 @@ public: bool operator==(const_iterator const& other) const; - bool - operator!=(const_iterator const& other) const - { - return !(*this == other); - } - reference operator*() const; diff --git a/include/xrpl/ledger/CanonicalTXSet.h b/include/xrpl/ledger/CanonicalTXSet.h index 11aadf4e92..3fe17d6eef 100644 --- a/include/xrpl/ledger/CanonicalTXSet.h +++ b/include/xrpl/ledger/CanonicalTXSet.h @@ -59,12 +59,6 @@ private: return lhs.txId_ == rhs.txId_; } - friend bool - operator!=(Key const& lhs, Key const& rhs) - { - return !(lhs == rhs); - } - [[nodiscard]] uint256 const& getAccount() const { diff --git a/include/xrpl/ledger/Dir.h b/include/xrpl/ledger/Dir.h index 233719cdeb..eb70b3b6a3 100644 --- a/include/xrpl/ledger/Dir.h +++ b/include/xrpl/ledger/Dir.h @@ -59,12 +59,6 @@ public: bool operator==(ConstIterator const& other) const; - bool - operator!=(ConstIterator const& other) const - { - return !(*this == other); - } - reference operator*() const; diff --git a/include/xrpl/ledger/View.h b/include/xrpl/ledger/View.h index 2b4eeb04c8..1a8f59357e 100644 --- a/include/xrpl/ledger/View.h +++ b/include/xrpl/ledger/View.h @@ -28,6 +28,7 @@ #include #include #include +#include namespace xrpl { @@ -39,6 +40,11 @@ enum class SkipEntry : bool { No = false, Yes }; // //------------------------------------------------------------------------------ +/** + * Whether an expiration check should be inclusive or exclusive. + */ +enum class ExpiryComparison { Inclusive, Exclusive }; + /** * Determines whether the given expiration time has passed. * @@ -58,11 +64,16 @@ enum class SkipEntry : bool { No = false, Yes }; * * @param view The ledger whose parent time is used as the clock. * @param exp The optional expiration time we want to check. + * @param comparison Whether the boundary is inclusive (`now >= exp`, the + * default) or exclusive (`now > exp`). * * @return `true` if `exp` is in the past; `false` otherwise. */ [[nodiscard]] bool -hasExpired(ReadView const& view, std::optional const& exp); +hasExpired( + ReadView const& view, + std::optional const& exp, + ExpiryComparison comparison = ExpiryComparison::Inclusive); // Note, depth parameter is used to limit the recursion depth [[nodiscard]] bool @@ -72,6 +83,13 @@ isVaultPseudoAccountFrozen( MPTIssue const& mptShare, std::uint8_t depth); +[[nodiscard]] bool +isVaultPseudoAccountFrozen( + ReadView const& view, + AccountID const& account, + SLE const& issuanceSle, + std::uint8_t depth); + [[nodiscard]] bool isLPTokenFrozen( ReadView const& view, @@ -79,6 +97,26 @@ isLPTokenFrozen( Asset const& asset, Asset const& asset2); +/** + * Check whether an AMM LPToken may be transferred between @p from and @p to. + * + * @p lpTokenIssuer is the issuer of the LPToken being moved. If it is not an + * AMM account the token is not an LPToken and the transfer is unconditionally + * permitted. Otherwise, for each MPT pool asset of that AMM, canTransfer() must + * permit the transfer (which exempts the MPT issuer). Non-MPT pool assets are + * always transferable by this check, so it is implicitly gated by + * featureMPTokensV2 (MPTs can only be AMM pool assets once V2 is enabled). + * + * @return tesSUCCESS if permitted, otherwise the canTransfer() failure code + * (e.g. tecNO_AUTH) of the first MPT pool asset that disallows it. + */ +[[nodiscard]] TER +canTransferLPToken( + ReadView const& view, + AccountID const& from, + AccountID const& to, + AccountID const& lpTokenIssuer); + // Return the list of enabled amendments [[nodiscard]] std::set getEnabledAmendments(ReadView const& view); @@ -165,7 +203,10 @@ dirLink( * if withdrawing to self. * - If withdrawing to self, succeed. * - If not, checks if the receiver requires deposit authorization, and if - * the sender has it. + * the sender has it (account-based or credential-based). + * - Expects any credentials passed in to already exist in the ledger, and + * returns an internal error otherwise. Validate them beforehand with + * credentials::valid(). * - Checks that the receiver will not exceed the limit (IOU trustline limit * or MPT MaximumAmount). */ @@ -176,7 +217,8 @@ canWithdraw( AccountID const& to, SLE::const_ref toSle, STAmount const& amount, - bool hasDestinationTag); + bool hasDestinationTag, + std::optional> const& credentialIDs = std::nullopt); /** * Checks that can withdraw funds from an object to itself or a destination. @@ -189,7 +231,10 @@ canWithdraw( * if withdrawing to self. * - If withdrawing to self, succeed. * - If not, checks if the receiver requires deposit authorization, and if - * the sender has it. + * the sender has it (account-based or credential-based). + * - Expects any credentials passed in to already exist in the ledger, and + * returns an internal error otherwise. Validate them beforehand with + * credentials::valid(). * - Checks that the receiver will not exceed the limit (IOU trustline limit * or MPT MaximumAmount). */ @@ -199,20 +244,25 @@ canWithdraw( AccountID const& from, AccountID const& to, STAmount const& amount, - bool hasDestinationTag); + bool hasDestinationTag, + std::optional> const& credentialIDs = std::nullopt); /** * Checks that can withdraw funds from an object to itself or a destination. * * The receiver may be either the submitting account (sfAccount) or a different - * destination account (sfDestination). + * destination account (sfDestination). Credentials, if any, are taken from the + * transaction's sfCredentialIDs field. * * - Checks that the receiver account exists. * - If the receiver requires a destination tag, check that one exists, even * if withdrawing to self. * - If withdrawing to self, succeed. * - If not, checks if the receiver requires deposit authorization, and if - * the sender has it. + * the sender has it (account-based or credential-based). + * - Expects any credentials in sfCredentialIDs to already exist in the + * ledger, and returns an internal error otherwise. Validate them + * beforehand with credentials::valid(). * - Checks that the receiver will not exceed the limit (IOU trustline limit * or MPT MaximumAmount). */ diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.h b/include/xrpl/ledger/detail/ReadViewFwdRange.h index 19ac0698c2..bfa2527bbd 100644 --- a/include/xrpl/ledger/detail/ReadViewFwdRange.h +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.h @@ -85,9 +85,6 @@ public: bool operator==(Iterator const& other) const; - bool - operator!=(Iterator const& other) const; - // Can throw reference operator*() const; diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.ipp b/include/xrpl/ledger/detail/ReadViewFwdRange.ipp index c7cbc5ee61..2003280ea6 100644 --- a/include/xrpl/ledger/detail/ReadViewFwdRange.ipp +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.ipp @@ -64,13 +64,6 @@ ReadViewFwdRange::Iterator::operator==(Iterator const& other) const return impl_ == other.impl_; } -template -bool -ReadViewFwdRange::Iterator::operator!=(Iterator const& other) const -{ - return !(*this == other); -} - template auto ReadViewFwdRange::Iterator::operator*() const -> reference diff --git a/include/xrpl/ledger/helpers/AMMHelpers.h b/include/xrpl/ledger/helpers/AMMHelpers.h index 7d41bfce81..a68171c426 100644 --- a/include/xrpl/ledger/helpers/AMMHelpers.h +++ b/include/xrpl/ledger/helpers/AMMHelpers.h @@ -226,7 +226,7 @@ getAMMOfferStartWithTakerGets( auto getAmounts = [&pool, &tfee](Number const& nTakerGetsProposed) { // Round downward to minimize the offer and to maximize the quality. - // This has the most impact when takerGets is XRP. + // This has the most impact when takerGets is integral. auto const takerGets = toAmount(getAsset(pool.out), nTakerGetsProposed, Number::RoundingMode::Downward); return TAmounts{swapAssetOut(pool, takerGets, tfee), takerGets}; @@ -294,7 +294,7 @@ getAMMOfferStartWithTakerPays( auto getAmounts = [&pool, &tfee](Number const& nTakerPaysProposed) { // Round downward to minimize the offer and to maximize the quality. - // This has the most impact when takerPays is XRP. + // This has the most impact when takerPays is integral. auto const takerPays = toAmount(getAsset(pool.in), nTakerPaysProposed, Number::RoundingMode::Downward); return TAmounts{takerPays, swapAssetIn(pool, takerPays, tfee)}; @@ -313,11 +313,11 @@ getAMMOfferStartWithTakerPays( * is equal to LOB quality (in this case AMM offer quality is * better than LOB quality) or AMM offer is equal to LOB quality * (in this case SPQ is better than LOB quality). - * Pre-amendment code calculates takerPays first. If takerGets is XRP, - * it is rounded down, which results in worse offer quality than - * LOB quality, and the offer might fail to generate. - * Post-amendment code calculates the XRP offer side first. The result - * is rounded down, which makes the offer quality better. + * Pre-amendment code calculates takerPays first. If takerGets is the + * economically coarser integral side, it is rounded down, which results in + * worse offer quality than LOB quality, and the offer might fail to generate. + * Post-amendment code calculates the economically coarser integral offer side + * first. The result is rounded down, which makes the offer quality better. * It might not be possible to match either SPQ or AMM offer to LOB * quality. This generally happens at higher fees. * @param pool AMM pool balances @@ -396,10 +396,18 @@ changeSpotPriceQuality( return std::nullopt; } - // Generate the offer starting with XRP side. Return seated offer amounts - // if the offer can be generated, otherwise nullopt. auto amounts = [&]() { - if (isXRP(getAsset(pool.out))) + bool const inIntegral = getAsset(pool.in).integral(); + bool const outIntegral = getAsset(pool.out).integral(); + + // Preserve historical behavior for fractional pairs and XRP/IOU-style + // one-integral-side pairs. For two integral assets, pick the side whose + // minimum unit is economically coarser at this quality. + // + // Quality::rate() is input units per output unit, so one output unit is + // coarser when it costs at least one input unit. Ties use takerGets, + // matching the historical XRP-output behavior. + if (outIntegral && (!inIntegral || Number(quality.rate()) >= 1)) return getAMMOfferStartWithTakerGets(pool, quality, tfee); return getAMMOfferStartWithTakerPays(pool, quality, tfee); }(); diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h b/include/xrpl/ledger/helpers/AccountRootHelpers.h index 350fc6ca85..452d402d14 100644 --- a/include/xrpl/ledger/helpers/AccountRootHelpers.h +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h @@ -15,7 +15,6 @@ #include #include #include -#include #include namespace xrpl { @@ -353,14 +352,14 @@ pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey); * * The list is constructed during initialization and is const after that. * Pseudo-account designator fields MUST be maintained by including the - * SField::sMD_PseudoAccount flag in the SField definition. + * SField::kSmdPseudoAccount flag in the SField definition. */ [[nodiscard]] std::vector const& getPseudoAccountFields(); /** - * Returns true if and only if sleAcct is a pseudo-account or specific - * pseudo-accounts in pseudoFieldFilter. + * Returns true if and only if sleAcct is a pseudo-account of any kind + * (i.e. carries at least one field flagged with SField::kSmdPseudoAccount). * * Returns false if sleAcct is: * - NOT a pseudo-account OR @@ -368,18 +367,15 @@ getPseudoAccountFields(); * - null pointer */ [[nodiscard]] bool -isPseudoAccount(SLE::const_pointer sleAcct, std::set const& pseudoFieldFilter = {}); +isPseudoAccount(SLE::const_pointer sleAcct); /** * Convenience overload that reads the account from the view. */ [[nodiscard]] inline bool -isPseudoAccount( - ReadView const& view, - AccountID const& accountId, - std::set const& pseudoFieldFilter = {}) +isPseudoAccount(ReadView const& view, AccountID const& accountId) { - return isPseudoAccount(view.read(keylet::account(accountId)), pseudoFieldFilter); + return isPseudoAccount(view.read(keylet::account(accountId))); } /** diff --git a/include/xrpl/ledger/helpers/ContractUtils.h b/include/xrpl/ledger/helpers/ContractUtils.h index 831276b623..bbe1e27b8e 100644 --- a/include/xrpl/ledger/helpers/ContractUtils.h +++ b/include/xrpl/ledger/helpers/ContractUtils.h @@ -29,25 +29,39 @@ class ContractEventMap : public std::map namespace contract { -/** The maximum number of data modifications in a single function. */ +/** + * The maximum number of data modifications in a single function. + */ int64_t constexpr maxDataModifications = 1000; -/** The maximum number of bytes the data can occupy. */ +/** + * The maximum number of bytes the data can occupy. + */ int64_t constexpr maxContractDataSize = 1024; -/** The multiplier for contract data size calculations. */ +/** + * The multiplier for contract data size calculations. + */ int64_t constexpr dataByteMultiplier = 512; -/** The cost multiplier of creating a contract in bytes. */ +/** + * The cost multiplier of creating a contract in bytes. + */ int64_t constexpr createByteMultiplier = 500ULL; -/** The value to return when the fee calculation failed. */ +/** + * The value to return when the fee calculation failed. + */ int64_t constexpr feeCalculationFailed = 0x7FFFFFFFFFFFFFFFLL; -/** The maximum number of contract parameters that can be in a transaction. */ +/** + * The maximum number of contract parameters that can be in a transaction. + */ std::size_t constexpr maxContractParams = 8; -/** The maximum number of contract functions that can be in a transaction. */ +/** + * The maximum number of contract functions that can be in a transaction. + */ std::size_t constexpr maxContractFunctions = 32; int64_t diff --git a/include/xrpl/ledger/helpers/CredentialHelpers.h b/include/xrpl/ledger/helpers/CredentialHelpers.h index 8e78a00923..8b1c819bf4 100644 --- a/include/xrpl/ledger/helpers/CredentialHelpers.h +++ b/include/xrpl/ledger/helpers/CredentialHelpers.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -34,7 +35,7 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j); // Amendment and parameters checks for sfCredentialIDs field NotTEC -checkFields(STTx const& tx, beast::Journal j); +checkFields(STTx const& tx, Rules const& rules, beast::Journal j); // Accessing the ledger to check if provided credentials are valid. Do not use // in doApply (only in preclaim) since it does not remove expired credentials. diff --git a/include/xrpl/ledger/helpers/LendingHelpers.h b/include/xrpl/ledger/helpers/LendingHelpers.h index c69efff964..f3fc82eacb 100644 --- a/include/xrpl/ledger/helpers/LendingHelpers.h +++ b/include/xrpl/ledger/helpers/LendingHelpers.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include // IWYU pragma: keep #include @@ -21,6 +22,7 @@ #include #include +#include #include #include @@ -58,6 +60,42 @@ canApplyToBrokerCover( bool checkLendingProtocolDependencies(Rules const& rules, STTx const& tx); +/** + * The accounts and asset that LoanManage::defaultLoan's fixCleanup3_4_0 + * freeze/lock exemption applies to. + * + * `defaultLoan` moves funds from the LoanBroker pseudo-account to the Vault + * pseudo-account via `accountSend`. Since neither is the vault asset's + * issuer, this is a third-party transfer that transits through the issuer in + * two hops (broker -> issuer, issuer -> vault; see + * `directSendNoLimitIOU`/`directSendNoLimitMPT`), so the exemption must cover + * both the issuer/broker and issuer/vault pairs, not a direct broker/vault + * pair. `asset` scopes it further to the vault's own currency/MPT issuance, + * so an unrelated one the same accounts happen to hold is still protected. + */ +struct LoanDefaultFreezeExemptAccounts +{ + AccountID issuer; + AccountID broker; + AccountID vault; + Asset asset; +}; + +/** + * Resolves the accounts and asset a LoanManage default transaction is + * exempt from freeze/lock for. + * + * @param view Ledger view used to resolve the Loan -> LoanBroker -> Vault + * chain. + * @param tx The transaction under invariant review. + * @return The exempt accounts and asset if `tx` is a `ttLOAN_MANAGE` + * transaction with the `tfLoanDefault` flag set, `fixCleanup3_4_0` is + * enabled, and the loan/broker/vault objects it references can all be + * resolved; `std::nullopt` otherwise. + */ +[[nodiscard]] std::optional +getLoanDefaultFreezeExemptAccounts(ReadView const& view, STTx const& tx); + static constexpr std::uint32_t kSecondsInYear = 365 * 24 * 60 * 60; Number @@ -286,6 +324,12 @@ computeFullPaymentInterest( std::uint32_t startDate, TenthBips32 closeInterestRate); +// Returns true if the loan's next payment is late per protocol rules. The +// boundary is amendment-gated: with fixCleanup3_4_0 the due date must be +// strictly in the past, otherwise the exact due-date instant counts as late. +[[nodiscard]] bool +isPaymentLate(ReadView const& view, SLE::const_ref loanSle); + // Deltas applied to Vault.AssetsTotal and LoanBroker.DebtTotal at a single // accounting touch point (origination, payment, impair/unimpair/default). struct AccountingDeltas diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index 5418e5b26a..6d26cf3cbc 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -29,6 +29,9 @@ namespace xrpl { [[nodiscard]] bool isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue); +[[nodiscard]] bool +isGlobalFrozen(SLE const& issuanceSle); + /** * Returns true if @p account's MPToken for @p mptIssue carries the * individual-lock flag (lsfMPTLocked). @@ -40,9 +43,29 @@ isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue); * receive tokens — it combines isIndividualFrozen, isGlobalFrozen, and * isVaultPseudoAccountFrozen into a single complete check. */ + [[nodiscard]] bool isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue); +[[nodiscard]] bool +isIndividualFrozen(SLE const& mptSle); + +/** + * Returns true if @p account cannot send or receive tokens of @p mptIssue + * because a freeze applies. This is the complete check callers should use + * before moving MPT value: it combines @ref isGlobalFrozen (issuance-level + * lock), @ref isIndividualFrozen (per-holder lock bit), and the transitive + * vault pseudo-account check (if @p mptIssue is a vault share, the underlying + * asset is checked, and so on recursively up to @c maxAssetCheckDepth). + * + * The @c SLE overload takes an already-loaded ltMPTOKEN or ltMPTOKEN_ISSUANCE + * ledger entry; for ltMPTOKEN it can skip the per-holder individual-lock lookup. + * @ref isAnyFrozen answers the same question for a set of accounts and returns true + * if the freeze applies to any of them. + * + * @param depth Current recursion depth for the vault-share walk. Callers + * outside this module should leave it at the default. + */ [[nodiscard]] bool isFrozen( ReadView const& view, @@ -50,6 +73,18 @@ isFrozen( MPTIssue const& mptIssue, std::uint8_t depth = 0); +/** + * SLE overload: pass an already-loaded ltMPTOKEN (holder row) or + * ltMPTOKEN_ISSUANCE to reuse it for the freeze checks and avoid re-reading + * the same object. For an ltMPTOKEN, @p sle is used directly for the + * individual-lock check and the issuance is read once for global-freeze and + * vault-pseudo-account. For an ltMPTOKEN_ISSUANCE, @p sle is used directly + * for global-freeze and vault-pseudo-account, and the caller's holder row is + * read for the individual-lock check. + */ +[[nodiscard]] bool +isFrozen(ReadView const& view, AccountID const& account, SLE const& sle, std::uint8_t depth = 0); + [[nodiscard]] bool isAnyFrozen( ReadView const& view, @@ -261,6 +296,14 @@ checkCreateMPT( xrpl::MPTIssue const& mptIssue, xrpl::AccountID const& holder, SLE::ref sponsorSle, + std::uint32_t flags, + beast::Journal j); + +TER +checkCreateMPT( + xrpl::ApplyView& view, + xrpl::MPTIssue const& mptIssue, + xrpl::AccountID const& holder, beast::Journal j); //------------------------------------------------------------------------------ diff --git a/include/xrpl/ledger/helpers/RippleStateHelpers.h b/include/xrpl/ledger/helpers/RippleStateHelpers.h index a0508d074f..1a0f92a94b 100644 --- a/include/xrpl/ledger/helpers/RippleStateHelpers.h +++ b/include/xrpl/ledger/helpers/RippleStateHelpers.h @@ -239,8 +239,13 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc //------------------------------------------------------------------------------ /** - * Any transactors that call addEmptyHolding() in doApply must call - * canAddHolding() in preflight with the same View and Asset + * XRP and the issuer itself are always tesSUCCESS. Otherwise, after + * fixCleanup3_4_0, an existing trust line returns tecDUPLICATE without + * consulting issuer freeze or DefaultRipple; both still apply on the create + * path (DefaultRipple off is terNO_RIPPLE). canAddHolding() ignores existing + * holdings, so transactors that may create a holding in doApply should gate + * their preclaim call on it: after the amendment only when no holding + * exists, before it always. */ [[nodiscard]] TER addEmptyHolding( diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h b/include/xrpl/ledger/helpers/TokenHelpers.h index 501101136a..12fa8a105e 100644 --- a/include/xrpl/ledger/helpers/TokenHelpers.h +++ b/include/xrpl/ledger/helpers/TokenHelpers.h @@ -38,6 +38,12 @@ enum class FreezeHandling { IgnoreFreeze, ZeroIfFrozen }; */ enum class AuthHandling { IgnoreAuth, ZeroIfUnauthorized }; +/** + * Controls whether the recipient owner-reserve check is enforced when + * auto-creating a trustline or MPToken during AMMWithdraw or AMMClawback. + */ +enum class ReserveHandling : bool { EnforceReserve, IgnoreReserve }; + /** * Controls whether to include the account's full spendable balance */ @@ -294,6 +300,14 @@ accountFunds( AuthHandling authHandling, beast::Journal j); +/** + * Returns the transfer fee as Rate based on the type of token + * @param view The ledger view + * @param asset The asset being transferred + */ +[[nodiscard]] Rate +transferRate(ReadView const& view, Asset const& asset); + /** * Returns the transfer fee as Rate based on the type of token * @param view The ledger view @@ -311,6 +325,12 @@ transferRate(ReadView const& view, STAmount const& amount); [[nodiscard]] TER canAddHolding(ReadView const& view, Asset const& asset); +/** + * True if the account already holds this asset (or is the issuer / XRP). + */ +[[nodiscard]] bool +holdingExists(ReadView const& view, AccountID const& account, Asset const& asset); + [[nodiscard]] TER addEmptyHolding( ApplyViewContext ctx, diff --git a/include/xrpl/ledger/helpers/VaultHelpers.h b/include/xrpl/ledger/helpers/VaultHelpers.h index 5681cc57e8..b42f349b95 100644 --- a/include/xrpl/ledger/helpers/VaultHelpers.h +++ b/include/xrpl/ledger/helpers/VaultHelpers.h @@ -1,15 +1,22 @@ #pragma once +#include #include #include +#include #include #include #include +#include +#include +#include #include namespace xrpl { +class STTx; + /** * From the perspective of a vault, return the number of shares to give * depositor when they offer a fixed amount of assets. Note, since shares are @@ -38,6 +45,32 @@ assetsToSharesDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount co [[nodiscard]] std::optional sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& shares); +/** + * Adjusts a requested asset change (`delta`) to match the decimal scale of the + * updated total vault assets. This ensures `sfAssetsTotal`, `sfAssetsAvailable`, + * and the actual asset transfer change by the exact same representable amount. + * + * Rounding strategy: + * - Debits (withdrawals): Rounds down `|delta|` on the new scale to prevent + * paying out more than requested. + * - Credits (deposits): Floors the resulting total asset balance and returns the + * difference from the current total. This prevents crediting the vault with + * more assets than the user deposited. + * + * Key rules: + * - The returned magnitude never exceeds `|delta|`. + * - Returns `tecPRECISION_LOSS` if the change is smaller than 1 ULP of the target scale + * (prevents share operations when totals cannot change). + * - For integer assets (XRP, MPT), rounding is a no-op. + * + * @param vault The vault ledger entry. + * @param delta The requested signed change to sfAssetsTotal. + * @return The rounded, positive magnitude, or `tecPRECISION_LOSS` if the + * change is below representable precision. + */ +[[nodiscard]] std::expected +clampToAssetsTotalScale(SLE::const_ref vault, STAmount const& delta); + /** * Controls whether to truncate shares instead of rounding. */ @@ -52,6 +85,35 @@ enum class TruncateShares : bool { No = false, Yes = true }; */ enum class WaiveUnrealizedLoss : bool { No = false, Yes = true }; +/** + * Returns the assets backing outstanding shares for a withdrawal: + * sfAssetsTotal minus sfLossUnrealized, or sfAssetsTotal alone when the + * unrealized loss is waived. Used by assetsToSharesWithdraw and + * sharesToAssetsWithdraw as the numerator of the share/asset exchange rate. + * + * @param vault The vault SLE. + * @param waive Whether to skip subtracting the unrealized loss. + */ +[[nodiscard]] Number +assetsTotalForWithdrawal(SLE::const_ref vault, WaiveUnrealizedLoss waive); + +/** + * Returns true if debiting `amount` from `total` (the current value of a + * vault's sfAssetsTotal or sfAssetsAvailable) would canonicalize to the + * same STAmount value. This happens when `amount` is non-zero but too small + * to change the stored total at STAmount's precision. Shares would still + * move, so the ValidVault invariant would fail after apply; callers use + * this to reject the transaction upfront instead. + * + * @param asset The vault's underlying asset, used to canonicalize both + * sides the same way the ledger will when the field is stored. + * @param total The field's current value. + * @param amount The amount to debit. Zero always returns false; that case + * is rejected separately. + */ +[[nodiscard]] bool +debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount); + /** * From the perspective of a vault, return the number of shares to demand from * the depositor when they ask to withdraw a fixed amount of assets. Since @@ -123,4 +185,118 @@ isSoleShareholder(ReadView const& view, AccountID const& account, SLE::const_ref [[nodiscard]] VaultVersion getVaultVersion(SLE::const_ref vault); +/** + * Resolves the VaultKind of a vault SLE. Returns VaultKind::ClosedEnded when + * sfVaultKind is present and equal to that value; anything else (including an + * absent field or an unrecognised value) is treated as VaultKind::OpenEnded. + * + * @param vault The vault SLE. + */ +[[nodiscard]] VaultKind +getVaultKind(SLE::const_ref vault); + +/** + * Reads sfVaultKind from a transaction. An absent field resolves to + * VaultKind::OpenEnded (matching the on-ledger default); any unrecognised + * value is also treated as VaultKind::OpenEnded, mirroring the SLE overload. + * Callers that need to reject out-of-range values (e.g. preflight) should + * gate on isValidVaultKind() first. + * + * @param tx The transaction. + */ +[[nodiscard]] VaultKind +getVaultKind(STTx const& tx); + +/** + * Returns true iff sfVaultKind is either absent from @p tx or is present and + * equal to a recognised VaultKind enumerator. Intended for use in preflight + * to reject malformed transactions before decoding with getVaultKind(). + * + * @param tx The transaction. + */ +[[nodiscard]] bool +isValidVaultKind(STTx const& tx); + +/** + * Returns true iff the (SubscriptionDate, RedemptionDate) gap of a + * closed-ended vault satisfies + * kMinInvestmentPeriod <= (red - sub) < kMaxInvestmentPeriod. The arithmetic + * is performed in std::int64_t so that @p sub near UINT32_MAX does not + * overflow. Shared by VaultCreate::preflight and the ValidVault invariant. + * + * @param sub The value of sfSubscriptionDate. + * @param red The value of sfRedemptionDate. + */ +[[nodiscard]] bool +isValidClosedEndedGap(std::uint32_t sub, std::uint32_t red); + +/** + * Returns the current lifecycle phase of a vault. Open-ended + * vaults are always NoPhase. For closed-ended vaults the phase is derived + * from the parent ledger close time and the vault's immutable + * SubscriptionDate and RedemptionDate. + * + * @param view The ledger view whose parent close time is used as the clock. + * @param vault The vault SLE. + */ +[[nodiscard]] VaultPhase +getVaultPhase(ReadView const& view, SLE::const_ref vault); + +/** + * Raw-fields overload of getVaultPhase. Derives the phase from an already + * decomposed vault snapshot: an absent or non-ClosedEnded @p vaultKind + * resolves to VaultPhase::NoPhase; otherwise the phase is computed from + * @p subscriptionDate and @p redemptionDate against the view's parent + * close time using the same boundary semantics as the SLE overload + * (Subscription is inclusive of now == SubscriptionDate; Investment starts + * strictly after). + * + * @param view The ledger view whose parent close time is used as the clock. + * @param vaultKind The value of sfVaultKind, or nullopt if absent. + * @param subscriptionDate The value of sfSubscriptionDate, or nullopt if absent. + * @param redemptionDate The value of sfRedemptionDate, or nullopt if absent. + */ +[[nodiscard]] VaultPhase +getVaultPhase( + ReadView const& view, + std::optional vaultKind, + std::optional subscriptionDate, + std::optional redemptionDate); + +/** + * Controls whether checkVaultDomain reports an expired credential as an + * error. A caller that deletes expired credentials later, in doApply, passes + * Yes and treats the subject as authorized; a caller with no such cleanup + * step must keep the error. + */ +enum class SuppressExpired : bool { No = false, Yes = true }; + +/** + * Checks that subject belongs to the permissioned domain governing a vault's + * shares. + * + * The domain is read from the share issuance rather than from the vault. Vault + * shares are issued by the vault's pseudo-account, which cannot grant an + * authorization explicitly, so domain membership is the only route to being + * authorized: a vault with no domain set has no authorized participants at + * all, and every subject fails with tecNO_AUTH. + * + * Which accounts to check, and whether to check at all, is left to the caller. + * This says nothing about vault privacy or about the roles of the accounts. + * + * @param view The ledger view. + * @param issuance The MPTokenIssuance SLE for the vault's shares. + * @param subject The account whose domain membership is checked. + * @param suppressExpired Whether an expired credential counts as authorized. + * + * @return tesSUCCESS if the subject is a domain member, otherwise the reason + * it is not. + */ +[[nodiscard]] TER +checkVaultDomain( + ReadView const& view, + SLE::const_ref issuance, + AccountID const& subject, + SuppressExpired suppressExpired); + } // namespace xrpl diff --git a/include/xrpl/net/HTTPClientSSLContext.h b/include/xrpl/net/HTTPClientSSLContext.h index 51b50a084c..43467faa89 100644 --- a/include/xrpl/net/HTTPClientSSLContext.h +++ b/include/xrpl/net/HTTPClientSSLContext.h @@ -8,11 +8,11 @@ #include #include #include -#include #include #include +#include #include #include #include @@ -38,8 +38,8 @@ public: if (ec && sslVerifyDir.empty()) { - Throw(boost::str( - boost::format("Failed to set_default_verify_paths: %s") % ec.message())); + Throw( + std::format("Failed to set_default_verify_paths: {}", ec.message())); } } else @@ -54,7 +54,7 @@ public: if (ec) { Throw( - boost::str(boost::format("Failed to add verify path: %s") % ec.message())); + std::format("Failed to add verify path: {}", ec.message())); } } } diff --git a/include/xrpl/proto/xrpl.proto b/include/xrpl/proto/xrpl.proto index b9cb94e668..644e099179 100644 --- a/include/xrpl/proto/xrpl.proto +++ b/include/xrpl/proto/xrpl.proto @@ -1,10 +1,10 @@ syntax = "proto2"; package protocol; -// Unused numbers in the list below may have been used previously. Please don't -// reassign them for reuse unless you are 100% certain that there won't be a -// conflict. Even if you're sure, it's probably best to assign a new type. enum MessageType { + // Previously used - don't reuse. + reserved 0 to 1, 4, 6 to 14, 16 to 29, 36 to 40, 43 to 54, 61 to 62; + mtMANIFESTS = 2; mtPING = 3; mtCLUSTER = 5; @@ -17,7 +17,6 @@ enum MessageType { mtHAVE_SET = 35; mtVALIDATION = 41; mtGET_OBJECTS = 42; - mtVALIDATOR_LIST = 54; mtSQUELCH = 55; mtVALIDATOR_LIST_COLLECTION = 56; mtPROOF_PATH_REQ = 57; @@ -162,14 +161,6 @@ message TMHaveTransactionSet { required bytes hash = 2; } -// Validator list (UNL) -message TMValidatorList { - required bytes manifest = 1; - required bytes blob = 2; - required bytes signature = 3; - required uint32 version = 4; -} - // Validator List v2 message ValidatorBlobInfo { optional bytes manifest = 1; diff --git a/include/xrpl/protocol/AMMCore.h b/include/xrpl/protocol/AMMCore.h index 1e11f6cd8b..3f6b12f460 100644 --- a/include/xrpl/protocol/AMMCore.h +++ b/include/xrpl/protocol/AMMCore.h @@ -91,6 +91,17 @@ getFee(std::uint16_t tfee) return Number{tfee} / kAuctionSlotFeeScaleFactor; } +/** + * Minimum auction slot price: LPTokens * TradingFee / kAuctionSlotMinFeeFraction + * @param lptAMMBalance AMM LP token balance + * @param tradingFee trading fee in {0, 1000} + */ +inline Number +ammAuctionMinSlotPrice(Number const& lptAMMBalance, std::uint16_t tradingFee) +{ + return lptAMMBalance * getFee(tradingFee) / kAuctionSlotMinFeeFraction; +} + /** * Get fee multiplier (1 - tfee) * @tfee trading fee in basis points diff --git a/include/xrpl/protocol/AmountConversions.h b/include/xrpl/protocol/AmountConversions.h index 3bcd80e827..ed68be62fe 100644 --- a/include/xrpl/protocol/AmountConversions.h +++ b/include/xrpl/protocol/AmountConversions.h @@ -154,7 +154,7 @@ T toAmount(Asset const& asset, Number const& n, Number::RoundingMode mode = Number::getround()) { SaveNumberRoundMode const rm(Number::getround()); - if (isXRP(asset)) + if (asset.integral()) Number::setround(mode); if constexpr (std::is_same_v) diff --git a/include/xrpl/protocol/Book.h b/include/xrpl/protocol/Book.h index a83eb41b24..e6ed3729dd 100644 --- a/include/xrpl/protocol/Book.h +++ b/include/xrpl/protocol/Book.h @@ -133,7 +133,7 @@ private: using id_hash_type = boost::base_from_member, 0>; public: - explicit hash() = default; + hash() = default; using value_type = std::size_t; using argument_type = xrpl::MPTIssue; @@ -160,7 +160,7 @@ private: mptissue_hasher mMptissueHasher_; public: - explicit hash() = default; + hash() = default; value_type operator()(argument_type const& asset) const @@ -227,7 +227,7 @@ struct hash : std::hash template <> struct hash : std::hash { - explicit hash() = default; + hash() = default; using Base = std::hash; }; @@ -235,7 +235,7 @@ struct hash : std::hash template <> struct hash : std::hash { - explicit hash() = default; + hash() = default; using Base = std::hash; }; diff --git a/include/xrpl/protocol/Emitable.h b/include/xrpl/protocol/Emitable.h index 6acce0d102..e046ad0e5d 100644 --- a/include/xrpl/protocol/Emitable.h +++ b/include/xrpl/protocol/Emitable.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -28,8 +29,6 @@ enum GranularEmitableType : std::uint32_t { #pragma pop_macro("EMITABLE") }; -enum Emittance { emitable, notEmitable }; - class Emitable { private: diff --git a/include/xrpl/protocol/HashPrefix.h b/include/xrpl/protocol/HashPrefix.h index 9d4471d05c..e77e891b04 100644 --- a/include/xrpl/protocol/HashPrefix.h +++ b/include/xrpl/protocol/HashPrefix.h @@ -92,6 +92,26 @@ enum class HashPrefix : std::uint32_t { * Batch */ Batch = detail::makeHashPrefix('B', 'C', 'H'), + + /** + * inner transaction to sign as the counterparty + */ + CounterpartyTxSign = detail::makeHashPrefix('C', 'P', 'T'), + + /** + * inner transaction to multi-sign as the counterparty + */ + CounterpartyTxMultiSign = detail::makeHashPrefix('C', 'P', 'M'), + + /** + * inner transaction to sign as the sponsor + */ + SponsorTxSign = detail::makeHashPrefix('S', 'P', 'N'), + + /** + * inner transaction to multi-sign as the sponsor + */ + SponsorTxMultiSign = detail::makeHashPrefix('S', 'P', 'M'), }; template diff --git a/include/xrpl/protocol/MPTAmount.h b/include/xrpl/protocol/MPTAmount.h index 462092f7dd..68a7926256 100644 --- a/include/xrpl/protocol/MPTAmount.h +++ b/include/xrpl/protocol/MPTAmount.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -174,4 +175,17 @@ mulRatio(MPTAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundU return MPTAmount(r.convert_to()); } +inline std::optional +tryMulRatio(MPTAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp) +{ + try + { + return mulRatio(amt, num, den, roundUp); + } + catch (std::overflow_error const&) + { + return std::nullopt; + } +} + } // namespace xrpl diff --git a/include/xrpl/protocol/MPTIssue.h b/include/xrpl/protocol/MPTIssue.h index 7f473da6a2..49c1fd63dc 100644 --- a/include/xrpl/protocol/MPTIssue.h +++ b/include/xrpl/protocol/MPTIssue.h @@ -151,7 +151,7 @@ namespace std { template <> struct hash : xrpl::MPTID::hasher { - explicit hash() = default; + hash() = default; }; } // namespace std diff --git a/include/xrpl/protocol/NFTSyntheticSerializer.h b/include/xrpl/protocol/NFTSyntheticSerializer.h deleted file mode 100644 index df4fedb707..0000000000 --- a/include/xrpl/protocol/NFTSyntheticSerializer.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -namespace xrpl::rpc { - -/** - * Adds common synthetic fields to transaction-related JSON responses - */ -/** @{ */ -void -insertNFTSyntheticInJson(json::Value&, std::shared_ptr const&, TxMeta const&); -/** @} */ - -} // namespace xrpl::rpc diff --git a/include/xrpl/protocol/Permissions.h b/include/xrpl/protocol/Permissions.h index 703a0939c9..2a3f561a10 100644 --- a/include/xrpl/protocol/Permissions.h +++ b/include/xrpl/protocol/Permissions.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -38,11 +39,6 @@ enum GranularPermissionType : std::uint32_t { #pragma pop_macro("GRANULAR_PERMISSION") }; -// Injected bare enumerators (xrpl::delegable / xrpl::notDelegable) are required by preprocessor -// tricks in tests and macro-generated code; enum class would break that. -// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) -enum Delegation { Delegable, NotDelegable }; - class Permission { private: @@ -65,7 +61,7 @@ private: struct TxDelegationEntry { uint256 amendment; - Delegation delegable{NotDelegable}; + Delegation delegable{Delegation::NotDelegable}; }; std::unordered_set granularTxTypes_; diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index fbfa75b988..d0de17787c 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -9,8 +9,10 @@ #include #include +#include #include #include +#include namespace xrpl { @@ -327,6 +329,47 @@ enum class VaultVersion : uint8_t { CashBasis, }; +/** + * Vault kind. Distinguishes closed-ended vaults from the default open-ended + * kind. Persisted as sfVaultKind (UINT8); absent means OpenEnded. + */ +enum class VaultKind : std::uint8_t { + OpenEnded = 0, + ClosedEnded = 1, +}; + +/** + * Lifecycle phase of a vault. Open-ended vaults are always NoPhase; the other + * three values are the phases of a closed-ended vault. + */ +enum class VaultPhase : std::uint8_t { + NoPhase = 0, + Subscription, + Investment, + Redemption, +}; + +/** + * Minimum gap between a closed-ended loan's final scheduled payment and the + * vault's RedemptionDate. LoanSet rejects a schedule whose final payment is + * fewer than this many seconds before RedemptionDate. + */ +constexpr std::uint32_t kLoanRedemptionBuffer = std::chrono::seconds{60}.count(); + +/** + * Bounds on the length of a closed-ended vault's Investment phase + * (RedemptionDate - SubscriptionDate). At vault creation the gap must satisfy + * kMinInvestmentPeriod <= gap < kMaxInvestmentPeriod. + * + * 180s is enough to originate a loan that uses the minimum payment interval + * and kLoanRedemptionBuffer after StartDate, which is strictly after + * SubscriptionDate. The interval and buffer need not be equal; only their + * sum plus one second must fit in this floor. + */ +constexpr std::uint32_t kMinInvestmentPeriod = std::chrono::seconds{180}.count(); +// This is 946708560 seconds which 30 x 365.2425 days (the average length of a Gregorian year). +constexpr std::uint32_t kMaxInvestmentPeriod = std::chrono::seconds{std::chrono::years{30}}.count(); + /** * Maximum recursion depth for vault shares being put as an asset inside * another vault; counted from 0 @@ -512,6 +555,11 @@ constexpr std::size_t kEcClawbackProofLength = SECP256K1_COMPACT_CLAWBACK_PROOF_ */ constexpr std::uint32_t kConfidentialFeeMultiplier = 9; +/** + * Maximum value a confidential MPT key epoch may reach. + */ +constexpr std::uint32_t kMaxKeyEpoch = std::numeric_limits::max(); + /** * Compressed EC point prefix for even y-coordinate */ diff --git a/include/xrpl/protocol/Quality.h b/include/xrpl/protocol/Quality.h index 3475efa977..d0d0f10cd2 100644 --- a/include/xrpl/protocol/Quality.h +++ b/include/xrpl/protocol/Quality.h @@ -75,13 +75,6 @@ operator==(TAmounts const& lhs, TAmounts const& rhs) noexcept return lhs.in == rhs.in && lhs.out == rhs.out; } -template -bool -operator!=(TAmounts const& lhs, TAmounts const& rhs) noexcept -{ - return !(lhs == rhs); -} - //------------------------------------------------------------------------------ // XRPL specific constant used for parsing qualities and other things @@ -271,12 +264,6 @@ public: return lhs.value_ == rhs.value_; } - friend bool - operator!=(Quality const& lhs, Quality const& rhs) noexcept - { - return !(lhs == rhs); - } - friend std::ostream& operator<<(std::ostream& os, Quality const& quality) { diff --git a/include/xrpl/protocol/QualityFunction.h b/include/xrpl/protocol/QualityFunction.h index 128b37ce12..4fcc730c42 100644 --- a/include/xrpl/protocol/QualityFunction.h +++ b/include/xrpl/protocol/QualityFunction.h @@ -60,6 +60,15 @@ public: std::optional outFromAvgQ(Quality const& quality); + /** + * Return whether `out` produces at least the requested + * average quality. + * @param quality requested average quality (quality limit) + * @param out output amount to test + */ + [[nodiscard]] bool + satisfiesAvgQ(Quality const& quality, Number const& out) const; + /** * Return true if the quality function is constant */ diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index 2c2136b6e8..d67e0d8654 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -98,9 +98,6 @@ public: */ bool operator==(Rules const&) const; - - bool - operator!=(Rules const& other) const; }; std::optional const& diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index cc80481582..4b2f1cc9fb 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -642,12 +642,6 @@ operator==(STAmount const& lhs, STAmount const& rhs); bool operator<(STAmount const& lhs, STAmount const& rhs); -inline bool -operator!=(STAmount const& lhs, STAmount const& rhs) -{ - return !(lhs == rhs); -} - inline bool operator>(STAmount const& lhs, STAmount const& rhs) { diff --git a/include/xrpl/protocol/STArray.h b/include/xrpl/protocol/STArray.h index 573bb6dad8..e88563fb1a 100644 --- a/include/xrpl/protocol/STArray.h +++ b/include/xrpl/protocol/STArray.h @@ -133,9 +133,6 @@ public: bool operator==(STArray const& s) const; - bool - operator!=(STArray const& s) const; - iterator erase(iterator pos); @@ -283,12 +280,6 @@ STArray::operator==(STArray const& s) const return v_ == s.v_; } -inline bool -STArray::operator!=(STArray const& s) const -{ - return v_ != s.v_; -} - inline STArray::iterator STArray::erase(iterator pos) { diff --git a/include/xrpl/protocol/STBase.h b/include/xrpl/protocol/STBase.h index acc5500a57..a8bda8f614 100644 --- a/include/xrpl/protocol/STBase.h +++ b/include/xrpl/protocol/STBase.h @@ -140,8 +140,6 @@ public: bool operator==(STBase const& t) const; - bool - operator!=(STBase const& t) const; template D& diff --git a/include/xrpl/protocol/STCurrency.h b/include/xrpl/protocol/STCurrency.h index 18642b20cf..933abaedb8 100644 --- a/include/xrpl/protocol/STCurrency.h +++ b/include/xrpl/protocol/STCurrency.h @@ -93,12 +93,6 @@ operator==(STCurrency const& lhs, STCurrency const& rhs) return lhs.currency() == rhs.currency(); } -inline bool -operator!=(STCurrency const& lhs, STCurrency const& rhs) -{ - return !operator==(lhs, rhs); -} - inline bool operator<(STCurrency const& lhs, STCurrency const& rhs) { diff --git a/include/xrpl/protocol/STLedgerEntry.h b/include/xrpl/protocol/STLedgerEntry.h index 8731488adb..7bc369ea37 100644 --- a/include/xrpl/protocol/STLedgerEntry.h +++ b/include/xrpl/protocol/STLedgerEntry.h @@ -19,7 +19,7 @@ namespace xrpl { class Rules; namespace test { -class Invariants_test; +class InvariantsMisc_test; } // namespace test class STLedgerEntry final : public STObject, public CountedObject @@ -83,8 +83,8 @@ private: void setSLEType(); - friend test::Invariants_test; // this test wants access to the private - // type_ + friend test::InvariantsMisc_test; // this test wants access to the + // private type_ STBase* copy(std::size_t n, void* buf) const override; diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h index ac404e6b36..3d448097d2 100644 --- a/include/xrpl/protocol/STObject.h +++ b/include/xrpl/protocol/STObject.h @@ -444,8 +444,6 @@ public: bool operator==(STObject const& o) const; - bool - operator!=(STObject const& o) const; class FieldErr; @@ -679,36 +677,6 @@ public: return !lhs.engaged() || *lhs == *rhs; } - friend bool - operator!=(OptionalProxy const& lhs, std::nullopt_t) noexcept - { - return !(lhs == std::nullopt); - } - - friend bool - operator!=(std::nullopt_t, OptionalProxy const& rhs) noexcept - { - return !(rhs == std::nullopt); - } - - friend bool - operator!=(OptionalProxy const& lhs, optional_type const& rhs) noexcept - { - return !(lhs == rhs); - } - - friend bool - operator!=(optional_type const& lhs, OptionalProxy const& rhs) noexcept - { - return !(lhs == rhs); - } - - friend bool - operator!=(OptionalProxy const& lhs, OptionalProxy const& rhs) noexcept - { - return !(lhs == rhs); - } - // Emulate std::optional::value_or [[nodiscard]] value_type valueOr(value_type val) const; @@ -1214,12 +1182,6 @@ STObject::setFieldH160(SField const& field, BaseUInt<160, Tag> const& v) } } -inline bool -STObject::operator!=(STObject const& o) const -{ - return !(*this == o); -} - template V STObject::getFieldByValue(SField const& field) const diff --git a/include/xrpl/protocol/STPathSet.h b/include/xrpl/protocol/STPathSet.h index d527e2479f..5768721111 100644 --- a/include/xrpl/protocol/STPathSet.h +++ b/include/xrpl/protocol/STPathSet.h @@ -115,9 +115,6 @@ public: bool operator==(STPathElement const& t) const; - bool - operator!=(STPathElement const& t) const; - private: static std::size_t getHash(STPathElement const& element); @@ -432,12 +429,6 @@ STPathElement::operator==(STPathElement const& t) const accountID_ == t.accountID_ && assetID_ == t.assetID_ && issuerID_ == t.issuerID_; } -inline bool -STPathElement::operator!=(STPathElement const& t) const -{ - return !operator==(t); -} - // ------------ STPath ------------ inline STPath::STPath(std::vector p) : path_(std::move(p)) diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index e213d4e0b7..e6291ae950 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include #include @@ -105,14 +107,36 @@ public: [[nodiscard]] json::Value getJson(JsonOptions options, bool binary) const; + /** + * Sign the transaction as its account. + * + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + */ + void + sign(PublicKey const& publicKey, SecretKey const& secretKey); + + /** + * Sign the transaction in one of its signature fields. + * + * The signature is bound to the role that made it, so it cannot be moved + * into another role. + * + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @param role The role signing the transaction. + * @param rules The current ledger rules. + */ void sign( PublicKey const& publicKey, SecretKey const& secretKey, - std::optional> signatureTarget = {}); + SignatureRole role, + Rules const& rules); /** * Check the signature. + * * @param rules The current ledger rules. * @return `true` if valid signature. If invalid, the error message string. */ @@ -120,7 +144,7 @@ public: checkSign(Rules const& rules) const; [[nodiscard]] std::expected - checkBatchSign(Rules const& rules) const; + checkBatchSign() const; // SQL Functions with metadata. static std::string const& @@ -162,28 +186,28 @@ public: private: /** * Check the signature. + * * @param rules The current ledger rules. * @param sigObject Reference to object that contains the signature fields. * Will be *this more often than not. + * @param role The role that made the signature in sigObject. Determines + * the signing prefix, which binds the signature to that role. * @return `true` if valid signature. If invalid, the error message string. */ [[nodiscard]] std::expected - checkSign(Rules const& rules, STObject const& sigObject) const; + checkSign(Rules const& rules, STObject const& sigObject, SignatureRole role) const; [[nodiscard]] std::expected - checkSingleSign(STObject const& sigObject) const; + checkSingleSign(STObject const& sigObject, HashPrefix prefix) const; [[nodiscard]] std::expected - checkMultiSign(Rules const& rules, STObject const& sigObject) const; + checkMultiSign(STObject const& sigObject, HashPrefix prefix) const; [[nodiscard]] std::expected checkBatchSingleSign(STObject const& batchSigner, std::vector const& txIds) const; [[nodiscard]] std::expected - checkBatchMultiSign( - STObject const& batchSigner, - Rules const& rules, - std::vector const& txIds) const; + checkBatchMultiSign(STObject const& batchSigner, std::vector const& txIds) const; void buildBatchTxns(); diff --git a/include/xrpl/protocol/SeqProxy.h b/include/xrpl/protocol/SeqProxy.h index fa72914591..3686d123d6 100644 --- a/include/xrpl/protocol/SeqProxy.h +++ b/include/xrpl/protocol/SeqProxy.h @@ -123,12 +123,6 @@ public: return (lhs.value() == rhs.value()); } - friend constexpr bool - operator!=(SeqProxy lhs, SeqProxy rhs) - { - return !(lhs == rhs); - } - friend constexpr bool operator<(SeqProxy lhs, SeqProxy rhs) { diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h index 73bd9c8289..c1ea5c16ba 100644 --- a/include/xrpl/protocol/Serializer.h +++ b/include/xrpl/protocol/Serializer.h @@ -265,20 +265,10 @@ public: return v == data_; } bool - operator!=(Blob const& v) const - { - return v != data_; - } - bool operator==(Serializer const& v) const { return v.data_ == data_; } - bool - operator!=(Serializer const& v) const - { - return v.data_ != data_; - } static int decodeLengthLength(int b1); diff --git a/include/xrpl/protocol/Sign.h b/include/xrpl/protocol/Sign.h index fad2c35c9e..b7a318eb35 100644 --- a/include/xrpl/protocol/Sign.h +++ b/include/xrpl/protocol/Sign.h @@ -4,13 +4,65 @@ #include #include #include +#include #include #include #include #include +#include + namespace xrpl { +/** + * The signature slots on a transaction. + * + * Each role signs different bytes, so a signature cannot be moved from the + * role that made it into another role. See signingPrefix. + */ +enum class SignatureRole { + /** + * The transaction's own signature, in sfTxnSignature or sfSigners. + */ + Transaction, + /** + * The counterparty's signature, in sfCounterpartySignature. + */ + Counterparty, + /** + * The sponsor's signature, in sfSponsorSignature. + */ + Sponsor +}; + +/** + * The field that holds this role's signature. + * + * @return The signature field, or nullptr for SignatureRole::Transaction, + * whose signature lives at the top level of the transaction. + */ +[[nodiscard]] SField const* +signatureField(SignatureRole role); + +/** + * The role that signs into the given field. + * + * @return The role, or an unseated optional if the field does not hold a + * transaction signature. + */ +[[nodiscard]] std::optional +signatureRole(SField const& sigField); + +/** + * The hash prefix that binds a transaction signature to the role that made it. + * + * @param role The role making the signature. + * @param multiSigning Whether the signature is a multi-signature. + * @param rules The current ledger rules. + */ +[[nodiscard]] HashPrefix +signingPrefix(SignatureRole role, bool multiSigning, Rules const& rules); + /** * Sign an STObject * @@ -49,9 +101,12 @@ verify( /** * Return a Serializer suitable for computing a multisigning TxnSignature. + * + * @param prefix Prefix to insert before the serialized object. Get it from + * signingPrefix, so that the signature is bound to the role making it. */ Serializer -buildMultiSigningData(STObject const& obj, AccountID const& signingID); +buildMultiSigningData(STObject const& obj, AccountID const& signingID, HashPrefix prefix); /** * Break the multi-signing hash computation into 2 parts for optimization. @@ -67,7 +122,7 @@ buildMultiSigningData(STObject const& obj, AccountID const& signingID); * signer's unique data. */ Serializer -startMultiSigningData(STObject const& obj); +startMultiSigningData(STObject const& obj, HashPrefix prefix); inline void finishMultiSigningData(AccountID const& signingID, Serializer& s) diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 879f4da23a..47a8780a41 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -132,7 +132,6 @@ enum TEMcodes : TERUnderlyingType { temBAD_MPT, temBAD_CIPHERTEXT, - temBAD_WASM, temINVALID_BYTECODE, temTEMP_DISABLED, }; diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index e272f25424..b6ac85720a 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -438,8 +438,7 @@ inline constexpr FlagValue tfDepositSubTx = ASF_FLAG(asfDefaultRipple, 8) \ ASF_FLAG(asfDepositAuth, 9) \ ASF_FLAG(asfAuthorizedNFTokenMinter, 10) \ - /* 11 is reserved for Hooks amendment */ \ - /* ASF_FLAG(asfTshCollect, 11) */ \ + /* 11 is unused */ \ ASF_FLAG(asfDisallowIncomingNFTokenOffer, 12) \ ASF_FLAG(asfDisallowIncomingCheck, 13) \ ASF_FLAG(asfDisallowIncomingPayChan, 14) \ diff --git a/include/xrpl/protocol/TxSettings.h b/include/xrpl/protocol/TxSettings.h new file mode 100644 index 0000000000..31a7450e4c --- /dev/null +++ b/include/xrpl/protocol/TxSettings.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include + +#include +#include + +namespace xrpl { + +enum class Delegation { Delegable, NotDelegable }; + +/** + * Whether a smart contract may emit this transaction type. + */ +enum class Emittance { Emitable, NotEmitable }; + +/** + * Operations a transaction is permitted to perform, as a bitfield. + * + * These are declared per-transaction in transactions.macro (via + * TxSettings::privileges) and enforced in InvariantCheck.cpp. + */ +enum class Privilege : std::uint16_t { + NoPriv = 0x0000, // The transaction can not do any of the enumerated operations + CreateAcct = 0x0001, // The transaction can create a new ACCOUNT_ROOT object. + CreatePseudoAcct = 0x0002, // The transaction can create a pseudo account, + // which implies createAcct + MustDeleteAcct = 0x0004, // The transaction must delete an ACCOUNT_ROOT object + MayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT + // object, but does not have to + OverrideFreeze = 0x0010, // The transaction can override some freeze rules + ChangeNftCounts = 0x0020, // The transaction can mint or burn an NFT + CreateMptIssuance = 0x0040, // The transaction can create a new MPT issuance + DestroyMptIssuance = 0x0080, // The transaction can destroy an MPT issuance + MustAuthorizeMpt = 0x0100, // The transaction MUST create or delete an MPT + // object (except by issuer) + MayAuthorizeMpt = 0x0200, // The transaction MAY create or delete an MPT + // object (except by issuer) + MayDeleteMpt = 0x0400, // The transaction MAY delete an MPT object. May not create. + MustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault + MayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault + MayCreateMpt = 0x2000, // The transaction MAY create an MPT object, except for issuer. +}; + +// The inner static_cast is not redundant: the underlying type is narrower than +// `int`, so the operands integer-promote and the result has to be narrowed back. +// safeCast rejects that narrowing, but every input bit is a Privilege bit by +// construction, so the result is always representable. +constexpr Privilege +operator|(Privilege lhs, Privilege rhs) +{ + using Underlying = std::underlying_type_t; + return static_cast( + static_cast(safeCast(lhs) | safeCast(rhs))); +} + +constexpr Privilege +operator&(Privilege lhs, Privilege rhs) +{ + using Underlying = std::underlying_type_t; + return static_cast( + static_cast(safeCast(lhs) & safeCast(rhs))); +} + +/** + * Per-transaction metadata declared in transactions.macro. + * + * Every member has a default, so a transaction only needs to name the settings + * that differ from the common case. See the documentation at the top of + * transactions.macro for the authoring syntax. + * + * This is deliberately not a constexpr-friendly type: amendment identifiers are + * runtime-initialized `extern uint256 const` globals (see Feature.h), so a + * TxSettings can only be built at runtime. + */ +struct TxSettings +{ + /** + * Whether an account may delegate this transaction to another account. + */ + Delegation delegable{Delegation::NotDelegable}; + + /** + * The amendment gating this transaction, or uint256{} if always available. + */ + // The `{}` looks redundant, because BaseUInt's default constructor already + // zeroes the value. It is not: without a default member initializer here, + // every partial designated initializer in transactions.macro trips the + // missing-designated-field-initializers warning, which the build treats as + // an error. + // NOLINTNEXTLINE(readability-redundant-member-init) + uint256 amendment{}; + + /** + * Operations this transaction is permitted to perform. + */ + Privilege privileges{Privilege::NoPriv}; + + /** + * Whether a smart contract may emit this transaction type. + */ + Emittance emittance{Emittance::Emitable}; +}; + +} // namespace xrpl diff --git a/include/xrpl/protocol/Units.h b/include/xrpl/protocol/Units.h index 169ee2c543..94afd72f53 100644 --- a/include/xrpl/protocol/Units.h +++ b/include/xrpl/protocol/Units.h @@ -258,13 +258,6 @@ public: return value_ == other; } - template Other> - constexpr bool - operator!=(ValueUnit const& other) const - { - return !operator==(other); - } - constexpr bool operator<(ValueUnit const& other) const { diff --git a/include/xrpl/protocol/detail/STVar.h b/include/xrpl/protocol/detail/STVar.h index 12026f3d09..56f868b665 100644 --- a/include/xrpl/protocol/detail/STVar.h +++ b/include/xrpl/protocol/detail/STVar.h @@ -152,10 +152,4 @@ operator==(STVar const& lhs, STVar const& rhs) return lhs.get().isEquivalent(rhs.get()); } -inline bool -operator!=(STVar const& lhs, STVar const& rhs) -{ - return !(lhs == rhs); -} - } // namespace xrpl::detail diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index f837f503c6..10464a80b2 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -16,11 +16,14 @@ // Keep it sorted in reverse chronological order. XRPL_FEATURE(SmartContract, Supported::No, VoteBehavior::DefaultNo) -XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo) +XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo) +XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo) +XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo) -XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo) +XRPL_FEATURE(LendingProtocolV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(ConfidentialTransfer, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_3_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index c62450d172..82764875c7 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -416,6 +416,8 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({ {sfReferenceHolding, SoeOptional}, {sfIssuerEncryptionKey, SoeOptional}, {sfAuditorEncryptionKey, SoeOptional}, + {sfIssuerKeyEpoch, SoeOptional}, + {sfAuditorKeyEpoch, SoeOptional}, {sfConfidentialOutstandingAmount, SoeDefault}, })) @@ -514,6 +516,9 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({ {sfWithdrawalPolicy, SoeRequired}, {sfScale, SoeDefault}, {sfLEVersion, SoeDefault}, + {sfVaultKind, SoeDefault}, + {sfSubscriptionDate, SoeOptional}, + {sfRedemptionDate, SoeOptional}, // no SharesTotal ever (use MPTIssuance.sfOutstandingAmount) // no PermissionedDomainID ever (use MPTIssuance.sfDomainID) })) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index b862bf5d74..15673b5300 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -27,6 +27,7 @@ TYPED_SFIELD(sfUNLModifyDisabling, UINT8, 17) TYPED_SFIELD(sfWasLockingChainSend, UINT8, 19) TYPED_SFIELD(sfWithdrawalPolicy, UINT8, 20) TYPED_SFIELD(sfContractResult, UINT8, 21) +TYPED_SFIELD(sfVaultKind, UINT8, 22) // 16-bit integers (common) TYPED_SFIELD(sfLedgerEntryType, UINT16, 1, SField::kSmdNever) @@ -116,12 +117,18 @@ TYPED_SFIELD(sfSponsoringOwnerCount, UINT32, 71) TYPED_SFIELD(sfSponsoringAccountCount, UINT32, 72) TYPED_SFIELD(sfRemainingOwnerCount, UINT32, 73) TYPED_SFIELD(sfSponsorFlags, UINT32, 74) -TYPED_SFIELD(sfGasLimit, UINT32, 75) -TYPED_SFIELD(sfBytecodeSizeLimit, UINT32, 76) -TYPED_SFIELD(sfGasPrice, UINT32, 77) -TYPED_SFIELD(sfGas, UINT32, 78) -TYPED_SFIELD(sfGasUsed, UINT32, 79) -TYPED_SFIELD(sfParameterFlag, UINT32, 80) +TYPED_SFIELD(sfSubscriptionDate, UINT32, 75) +TYPED_SFIELD(sfRedemptionDate, UINT32, 76) +TYPED_SFIELD(sfIssuerKeyEpoch, UINT32, 77) +TYPED_SFIELD(sfAuditorKeyEpoch, UINT32, 78) +TYPED_SFIELD(sfIssuerKeyMirrorEpoch, UINT32, 79) +TYPED_SFIELD(sfAuditorKeyMirrorEpoch, UINT32, 80) +TYPED_SFIELD(sfGasLimit, UINT32, 81) +TYPED_SFIELD(sfBytecodeSizeLimit, UINT32, 82) +TYPED_SFIELD(sfGasPrice, UINT32, 83) +TYPED_SFIELD(sfGas, UINT32, 84) +TYPED_SFIELD(sfGasUsed, UINT32, 85) +TYPED_SFIELD(sfParameterFlag, UINT32, 86) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 485b24323b..0e79acd4e1 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -3,7 +3,7 @@ #endif /** - * TRANSACTION(tag, value, name, delegable, amendments, privileges, emitable, fields) + * TRANSACTION(tag, value, name, settings, fields) * * To ease maintenance, you may replace any unneeded values with "..." * e.g. #define TRANSACTION(tag, value, name, ...) @@ -15,9 +15,35 @@ * # include * #endif * - * The `privileges` parameter of the TRANSACTION macro is a bitfield - * defining which operations the transaction can perform. - * The values are defined and used in InvariantCheck.cpp + * `settings` is a parenthesized brace-init-list for xrpl::TxSettings, declared + * in : + * + * struct TxSettings + * { + * Delegation delegable{Delegation::NotDelegable}; + * uint256 amendment{}; + * Privilege privileges{Privilege::NoPriv}; + * Emittance emittance{Emittance::Emitable}; + * }; + * + * Name only the settings that differ from those defaults, in declaration + * order; use `({})` when none of them do: + * + * ({.delegable = Delegation::Delegable, .amendment = featureFoo}) + * + * You must use designated initializers, as shown above. Positional + * initialization such as `({Delegation::NotDelegable})` is not supported, + * because the code generator reads these settings by member name. + * + * The `privileges` setting is a bitfield defining which operations the + * transaction can perform. The values are defined in TxSettings.h and + * enforced in InvariantCheck.cpp. + * + * The `emittance` setting says whether a smart contract may emit this + * transaction type. It is read by xrpl::Emitable (Emitable.cpp). + * + * A consumer that only needs some of the settings can unwrap the blob with + * `#define UNWRAP(...) __VA_ARGS__` and write `TxSettings UNWRAP settings`. */ /** This transaction type executes a payment. */ @@ -25,10 +51,7 @@ # include #endif TRANSACTION(ttPAYMENT, 0, Payment, - Delegation::Delegable, - uint256{}, - CreateAcct | MayCreateMpt, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .privileges = Privilege::CreateAcct | Privilege::MayCreateMpt}), ({ {sfDestination, SoeRequired}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -45,12 +68,7 @@ TRANSACTION(ttPAYMENT, 0, Payment, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({.delegable = Delegation::Delegable}), ({ {sfDestination, SoeRequired}, {sfDestinationTag, SoeOptional}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -58,19 +76,14 @@ TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, {sfCancelAfter, SoeOptional}, {sfFinishAfter, SoeOptional}, {sfBytecode, SoeOptional}, - {sfData, SoeOptional}, + {sfData, SoeOptional}, })) /** This transaction type completes an existing escrow. */ #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({.delegable = Delegation::Delegable}), ({ {sfOwner, SoeRequired}, {sfOfferSequence, SoeRequired}, {sfFulfillment, SoeOptional}, @@ -85,10 +98,7 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, # include #endif TRANSACTION(ttACCOUNT_SET, 3, AccountSet, - Delegation::NotDelegable, - uint256{}, - NoPriv, - Emittance::emitable, + ({}), ({ {sfEmailHash, SoeOptional}, {sfWalletLocator, SoeOptional}, @@ -106,12 +116,7 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, ({.delegable = Delegation::Delegable}), ({ {sfOwner, SoeRequired}, {sfOfferSequence, SoeRequired}, })) @@ -121,10 +126,7 @@ TRANSACTION(ttESCROW_CANCEL, 4, EscrowCancel, # include #endif TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, - Delegation::NotDelegable, - uint256{}, - NoPriv, - Emittance::notEmitable, + ({.emittance = Emittance::NotEmitable}), ({ {sfRegularKey, SoeOptional}, })) @@ -136,10 +138,7 @@ TRANSACTION(ttREGULAR_KEY_SET, 5, SetRegularKey, # include #endif TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, - Delegation::Delegable, - uint256{}, - MayCreateMpt, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .privileges = Privilege::MayCreateMpt}), ({ {sfTakerPays, SoeRequired, SoeMptSupported}, {sfTakerGets, SoeRequired, SoeMptSupported}, @@ -152,12 +151,7 @@ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, ({.delegable = Delegation::Delegable}), ({ {sfOfferSequence, SoeRequired}, })) @@ -167,12 +161,7 @@ TRANSACTION(ttOFFER_CANCEL, 8, OfferCancel, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, ({.delegable = Delegation::Delegable}), ({ {sfTicketCount, SoeRequired}, })) @@ -185,10 +174,7 @@ TRANSACTION(ttTICKET_CREATE, 10, TicketCreate, # include #endif TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, - Delegation::NotDelegable, - uint256{}, - NoPriv, - Emittance::notEmitable, + ({.emittance = Emittance::NotEmitable}), ({ {sfSignerQuorum, SoeRequired}, {sfSignerEntries, SoeOptional}, @@ -198,12 +184,7 @@ TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({.delegable = Delegation::Delegable}), ({ {sfDestination, SoeRequired}, {sfAmount, SoeRequired}, {sfSettleDelay, SoeRequired}, @@ -216,12 +197,7 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, ({.delegable = Delegation::Delegable}), ({ {sfChannel, SoeRequired}, {sfAmount, SoeRequired}, {sfExpiration, SoeOptional}, @@ -231,12 +207,7 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({.delegable = Delegation::Delegable}), ({ {sfChannel, SoeRequired}, {sfAmount, SoeOptional}, {sfBalance, SoeOptional}, @@ -249,12 +220,7 @@ TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, ({.delegable = Delegation::Delegable}), ({ {sfDestination, SoeRequired}, {sfSendMax, SoeRequired, SoeMptSupported}, {sfExpiration, SoeOptional}, @@ -267,10 +233,7 @@ TRANSACTION(ttCHECK_CREATE, 16, CheckCreate, # include #endif TRANSACTION(ttCHECK_CASH, 17, CheckCash, - Delegation::Delegable, - uint256{}, - MayCreateMpt, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .privileges = Privilege::MayCreateMpt}), ({ {sfCheckID, SoeRequired}, {sfAmount, SoeOptional, SoeMptSupported}, @@ -281,12 +244,7 @@ TRANSACTION(ttCHECK_CASH, 17, CheckCash, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, ({.delegable = Delegation::Delegable}), ({ {sfCheckID, SoeRequired}, })) @@ -294,12 +252,7 @@ TRANSACTION(ttCHECK_CANCEL, 18, CheckCancel, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::notEmitable, - ({ +TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, ({.delegable = Delegation::Delegable, .emittance = Emittance::NotEmitable}), ({ {sfAuthorize, SoeOptional}, {sfUnauthorize, SoeOptional}, {sfAuthorizeCredentials, SoeOptional}, @@ -310,12 +263,7 @@ TRANSACTION(ttDEPOSIT_PREAUTH, 19, DepositPreauth, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttTRUST_SET, 20, TrustSet, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttTRUST_SET, 20, TrustSet, ({.delegable = Delegation::Delegable}), ({ {sfLimitAmount, SoeOptional}, {sfQualityIn, SoeOptional}, {sfQualityOut, SoeOptional}, @@ -326,10 +274,10 @@ TRANSACTION(ttTRUST_SET, 20, TrustSet, # include #endif TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, - Delegation::NotDelegable, - uint256{}, - MustDeleteAcct, - Emittance::notEmitable, + ({ + .privileges = Privilege::MustDeleteAcct, + .emittance = Emittance::NotEmitable, + }), ({ {sfDestination, SoeRequired}, {sfDestinationTag, SoeOptional}, @@ -343,10 +291,7 @@ TRANSACTION(ttACCOUNT_DELETE, 21, AccountDelete, # include #endif TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, - Delegation::Delegable, - uint256{}, - ChangeNftCounts, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .privileges = Privilege::ChangeNftCounts}), ({ {sfNFTokenTaxon, SoeRequired}, {sfTransferFee, SoeOptional}, @@ -362,10 +307,7 @@ TRANSACTION(ttNFTOKEN_MINT, 25, NFTokenMint, # include #endif TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, - Delegation::Delegable, - uint256{}, - ChangeNftCounts, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .privileges = Privilege::ChangeNftCounts}), ({ {sfNFTokenID, SoeRequired}, {sfOwner, SoeOptional}, @@ -375,12 +317,7 @@ TRANSACTION(ttNFTOKEN_BURN, 26, NFTokenBurn, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, ({.delegable = Delegation::Delegable}), ({ {sfNFTokenID, SoeRequired}, {sfAmount, SoeRequired}, {sfDestination, SoeOptional}, @@ -392,12 +329,7 @@ TRANSACTION(ttNFTOKEN_CREATE_OFFER, 27, NFTokenCreateOffer, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, ({.delegable = Delegation::Delegable}), ({ {sfNFTokenOffers, SoeRequired}, })) @@ -405,12 +337,7 @@ TRANSACTION(ttNFTOKEN_CANCEL_OFFER, 28, NFTokenCancelOffer, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, ({.delegable = Delegation::Delegable}), ({ {sfNFTokenBuyOffer, SoeOptional}, {sfNFTokenSellOffer, SoeOptional}, {sfNFTokenBrokerFee, SoeOptional}, @@ -420,12 +347,7 @@ TRANSACTION(ttNFTOKEN_ACCEPT_OFFER, 29, NFTokenAcceptOffer, #if TRANSACTION_INCLUDE # include #endif -TRANSACTION(ttCLAWBACK, 30, Clawback, - Delegation::Delegable, - uint256{}, - NoPriv, - Emittance::emitable, - ({ +TRANSACTION(ttCLAWBACK, 30, Clawback, ({.delegable = Delegation::Delegable}), ({ {sfAmount, SoeRequired, SoeMptSupported}, {sfHolder, SoeOptional}, })) @@ -435,10 +357,12 @@ TRANSACTION(ttCLAWBACK, 30, Clawback, # include #endif TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, - Delegation::Delegable, - featureAMMClawback, - MayDeleteAcct | OverrideFreeze | MayAuthorizeMpt, - Emittance::emitable, + ({ + .delegable = Delegation::Delegable, + .amendment = featureAMMClawback, + .privileges = Privilege::MayDeleteAcct | Privilege::OverrideFreeze | + Privilege::MayAuthorizeMpt, + }), ({ {sfHolder, SoeRequired}, {sfAsset, SoeRequired, SoeMptSupported}, @@ -451,10 +375,11 @@ TRANSACTION(ttAMM_CLAWBACK, 31, AMMClawback, # include #endif TRANSACTION(ttAMM_CREATE, 35, AMMCreate, - Delegation::Delegable, - featureAMM, - CreatePseudoAcct | MayCreateMpt, - Emittance::emitable, + ({ + .delegable = Delegation::Delegable, + .amendment = featureAMM, + .privileges = Privilege::CreatePseudoAcct | Privilege::MayCreateMpt, + }), ({ {sfAmount, SoeRequired, SoeMptSupported}, {sfAmount2, SoeRequired, SoeMptSupported}, @@ -466,10 +391,7 @@ TRANSACTION(ttAMM_CREATE, 35, AMMCreate, # include #endif TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, - Delegation::Delegable, - featureAMM, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureAMM}), ({ {sfAsset, SoeRequired, SoeMptSupported}, {sfAsset2, SoeRequired, SoeMptSupported}, @@ -485,10 +407,11 @@ TRANSACTION(ttAMM_DEPOSIT, 36, AMMDeposit, # include #endif TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, - Delegation::Delegable, - featureAMM, - MayDeleteAcct | MayAuthorizeMpt, - Emittance::emitable, + ({ + .delegable = Delegation::Delegable, + .amendment = featureAMM, + .privileges = Privilege::MayDeleteAcct | Privilege::MayAuthorizeMpt, + }), ({ {sfAsset, SoeRequired, SoeMptSupported}, {sfAsset2, SoeRequired, SoeMptSupported}, @@ -503,10 +426,7 @@ TRANSACTION(ttAMM_WITHDRAW, 37, AMMWithdraw, # include #endif TRANSACTION(ttAMM_VOTE, 38, AMMVote, - Delegation::Delegable, - featureAMM, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureAMM}), ({ {sfAsset, SoeRequired, SoeMptSupported}, {sfAsset2, SoeRequired, SoeMptSupported}, @@ -518,10 +438,7 @@ TRANSACTION(ttAMM_VOTE, 38, AMMVote, # include #endif TRANSACTION(ttAMM_BID, 39, AMMBid, - Delegation::Delegable, - featureAMM, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureAMM}), ({ {sfAsset, SoeRequired, SoeMptSupported}, {sfAsset2, SoeRequired, SoeMptSupported}, @@ -535,10 +452,11 @@ TRANSACTION(ttAMM_BID, 39, AMMBid, # include #endif TRANSACTION(ttAMM_DELETE, 40, AMMDelete, - Delegation::Delegable, - featureAMM, - MustDeleteAcct | MayDeleteMpt, - Emittance::emitable, + ({ + .delegable = Delegation::Delegable, + .amendment = featureAMM, + .privileges = Privilege::MustDeleteAcct | Privilege::MayDeleteMpt, + }), ({ {sfAsset, SoeRequired, SoeMptSupported}, {sfAsset2, SoeRequired, SoeMptSupported}, @@ -549,10 +467,7 @@ TRANSACTION(ttAMM_DELETE, 40, AMMDelete, # include #endif TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, - Delegation::Delegable, - featureXChainBridge, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge}), ({ {sfXChainBridge, SoeRequired}, {sfSignatureReward, SoeRequired}, @@ -561,10 +476,7 @@ TRANSACTION(ttXCHAIN_CREATE_CLAIM_ID, 41, XChainCreateClaimID, /** This transactions initiates a crosschain transaction */ TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, - Delegation::Delegable, - featureXChainBridge, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge}), ({ {sfXChainBridge, SoeRequired}, {sfXChainClaimID, SoeRequired}, @@ -574,10 +486,7 @@ TRANSACTION(ttXCHAIN_COMMIT, 42, XChainCommit, /** This transaction completes a crosschain transaction */ TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, - Delegation::Delegable, - featureXChainBridge, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge}), ({ {sfXChainBridge, SoeRequired}, {sfXChainClaimID, SoeRequired}, @@ -588,10 +497,7 @@ TRANSACTION(ttXCHAIN_CLAIM, 43, XChainClaim, /** This transaction initiates a crosschain account create transaction */ TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, - Delegation::Delegable, - featureXChainBridge, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge}), ({ {sfXChainBridge, SoeRequired}, {sfDestination, SoeRequired}, @@ -601,10 +507,11 @@ TRANSACTION(ttXCHAIN_ACCOUNT_CREATE_COMMIT, 44, XChainAccountCreateCommit, /** This transaction adds an attestation to a claim */ TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, - Delegation::Delegable, - featureXChainBridge, - CreateAcct, - Emittance::emitable, + ({ + .delegable = Delegation::Delegable, + .amendment = featureXChainBridge, + .privileges = Privilege::CreateAcct, + }), ({ {sfXChainBridge, SoeRequired}, @@ -621,12 +528,12 @@ TRANSACTION(ttXCHAIN_ADD_CLAIM_ATTESTATION, 45, XChainAddClaimAttestation, })) /** This transaction adds an attestation to an account */ -TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, - XChainAddAccountCreateAttestation, - Delegation::Delegable, - featureXChainBridge, - CreateAcct, - Emittance::emitable, +TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, XChainAddAccountCreateAttestation, + ({ + .delegable = Delegation::Delegable, + .amendment = featureXChainBridge, + .privileges = Privilege::CreateAcct, + }), ({ {sfXChainBridge, SoeRequired}, @@ -645,10 +552,7 @@ TRANSACTION(ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION, 46, /** This transaction modifies a sidechain */ TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, - Delegation::Delegable, - featureXChainBridge, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge}), ({ {sfXChainBridge, SoeRequired}, {sfSignatureReward, SoeOptional}, @@ -657,10 +561,7 @@ TRANSACTION(ttXCHAIN_MODIFY_BRIDGE, 47, XChainModifyBridge, /** This transactions creates a sidechain */ TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, - Delegation::Delegable, - featureXChainBridge, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureXChainBridge}), ({ {sfXChainBridge, SoeRequired}, {sfSignatureReward, SoeRequired}, @@ -672,10 +573,7 @@ TRANSACTION(ttXCHAIN_CREATE_BRIDGE, 48, XChainCreateBridge, # include #endif TRANSACTION(ttDID_SET, 49, DIDSet, - Delegation::Delegable, - featureDID, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureDID}), ({ {sfDIDDocument, SoeOptional}, {sfURI, SoeOptional}, @@ -687,10 +585,7 @@ TRANSACTION(ttDID_SET, 49, DIDSet, # include #endif TRANSACTION(ttDID_DELETE, 50, DIDDelete, - Delegation::Delegable, - featureDID, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureDID}), ({})) /** This transaction type creates an Oracle instance */ @@ -698,10 +593,7 @@ TRANSACTION(ttDID_DELETE, 50, DIDDelete, # include #endif TRANSACTION(ttORACLE_SET, 51, OracleSet, - Delegation::Delegable, - featurePriceOracle, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featurePriceOracle}), ({ {sfOracleDocumentID, SoeRequired}, {sfProvider, SoeOptional}, @@ -716,10 +608,7 @@ TRANSACTION(ttORACLE_SET, 51, OracleSet, # include #endif TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, - Delegation::Delegable, - featurePriceOracle, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featurePriceOracle}), ({ {sfOracleDocumentID, SoeRequired}, })) @@ -729,10 +618,7 @@ TRANSACTION(ttORACLE_DELETE, 52, OracleDelete, # include #endif TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, - Delegation::Delegable, - fixNFTokenPageLinks, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = fixNFTokenPageLinks}), ({ {sfLedgerFixType, SoeRequired}, {sfOwner, SoeOptional}, @@ -744,10 +630,11 @@ TRANSACTION(ttLEDGER_STATE_FIX, 53, LedgerStateFix, # include #endif TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, - Delegation::Delegable, - featureMPTokensV1, - CreateMptIssuance, - Emittance::emitable, + ({ + .delegable = Delegation::Delegable, + .amendment = featureMPTokensV1, + .privileges = Privilege::CreateMptIssuance, + }), ({ {sfAssetScale, SoeOptional}, {sfTransferFee, SoeOptional}, @@ -762,10 +649,11 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_CREATE, 54, MPTokenIssuanceCreate, # include #endif TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, - Delegation::Delegable, - featureMPTokensV1, - DestroyMptIssuance, - Emittance::emitable, + ({ + .delegable = Delegation::Delegable, + .amendment = featureMPTokensV1, + .privileges = Privilege::DestroyMptIssuance, + }), ({ {sfMPTokenIssuanceID, SoeRequired}, })) @@ -775,10 +663,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_DESTROY, 55, MPTokenIssuanceDestroy, # include #endif TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, - Delegation::Delegable, - featureMPTokensV1, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureMPTokensV1}), ({ {sfMPTokenIssuanceID, SoeRequired}, {sfHolder, SoeOptional}, @@ -795,10 +680,11 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet, # include #endif TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, - Delegation::Delegable, - featureMPTokensV1, - MustAuthorizeMpt, - Emittance::emitable, + ({ + .delegable = Delegation::Delegable, + .amendment = featureMPTokensV1, + .privileges = Privilege::MustAuthorizeMpt, + }), ({ {sfMPTokenIssuanceID, SoeRequired}, {sfHolder, SoeOptional}, @@ -809,10 +695,7 @@ TRANSACTION(ttMPTOKEN_AUTHORIZE, 57, MPTokenAuthorize, # include #endif TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, - Delegation::Delegable, - featureCredentials, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureCredentials}), ({ {sfSubject, SoeRequired}, {sfCredentialType, SoeRequired}, @@ -825,10 +708,7 @@ TRANSACTION(ttCREDENTIAL_CREATE, 58, CredentialCreate, # include #endif TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, - Delegation::Delegable, - featureCredentials, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureCredentials}), ({ {sfIssuer, SoeRequired}, {sfCredentialType, SoeRequired}, @@ -839,10 +719,7 @@ TRANSACTION(ttCREDENTIAL_ACCEPT, 59, CredentialAccept, # include #endif TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, - Delegation::Delegable, - featureCredentials, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureCredentials}), ({ {sfSubject, SoeOptional}, {sfIssuer, SoeOptional}, @@ -854,10 +731,7 @@ TRANSACTION(ttCREDENTIAL_DELETE, 60, CredentialDelete, # include #endif TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, - Delegation::Delegable, - featureDynamicNFT, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureDynamicNFT}), ({ {sfNFTokenID, SoeRequired}, {sfOwner, SoeOptional}, @@ -869,10 +743,7 @@ TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, # include #endif TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, - Delegation::Delegable, - featurePermissionedDomains, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featurePermissionedDomains}), ({ {sfDomainID, SoeOptional}, {sfAcceptedCredentials, SoeRequired}, @@ -883,10 +754,7 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_SET, 62, PermissionedDomainSet, # include #endif TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, - Delegation::Delegable, - featurePermissionedDomains, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featurePermissionedDomains}), ({ {sfDomainID, SoeRequired}, })) @@ -896,10 +764,10 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 63, PermissionedDomainDelete, # include #endif TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, - Delegation::NotDelegable, - featurePermissionDelegationV1_1, - NoPriv, - Emittance::notEmitable, + ({ + .amendment = featurePermissionDelegationV1_1, + .emittance = Emittance::NotEmitable, + }), ({ {sfAuthorize, SoeRequired}, {sfPermissions, SoeRequired}, @@ -910,10 +778,11 @@ TRANSACTION(ttDELEGATE_SET, 64, DelegateSet, # include #endif TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, - Delegation::NotDelegable, - featureSingleAssetVault, - CreatePseudoAcct | CreateMptIssuance | MustModifyVault, - Emittance::emitable, + ({ + .amendment = featureSingleAssetVault, + .privileges = Privilege::CreatePseudoAcct | Privilege::CreateMptIssuance | + Privilege::MustModifyVault, + }), ({ {sfAsset, SoeRequired, SoeMptSupported}, {sfAssetsMaximum, SoeOptional}, @@ -922,6 +791,9 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, {sfWithdrawalPolicy, SoeOptional}, {sfData, SoeOptional}, {sfScale, SoeOptional}, + {sfVaultKind, SoeOptional}, + {sfSubscriptionDate, SoeOptional}, + {sfRedemptionDate, SoeOptional}, })) /** This transaction updates a single asset vault. */ @@ -929,10 +801,10 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate, # include #endif TRANSACTION(ttVAULT_SET, 66, VaultSet, - Delegation::NotDelegable, - featureSingleAssetVault, - MustModifyVault, - Emittance::emitable, + ({ + .amendment = featureSingleAssetVault, + .privileges = Privilege::MustModifyVault, + }), ({ {sfVaultID, SoeRequired}, {sfAssetsMaximum, SoeOptional}, @@ -945,10 +817,11 @@ TRANSACTION(ttVAULT_SET, 66, VaultSet, # include #endif TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, - Delegation::NotDelegable, - featureSingleAssetVault, - MustDeleteAcct | DestroyMptIssuance | MustModifyVault, - Emittance::emitable, + ({ + .amendment = featureSingleAssetVault, + .privileges = Privilege::MustDeleteAcct | Privilege::DestroyMptIssuance | + Privilege::MustModifyVault, + }), ({ {sfVaultID, SoeRequired}, {sfMemoData, SoeOptional}, @@ -959,10 +832,10 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete, # include #endif TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, - Delegation::NotDelegable, - featureSingleAssetVault, - MayAuthorizeMpt | MustModifyVault, - Emittance::emitable, + ({ + .amendment = featureSingleAssetVault, + .privileges = Privilege::MayAuthorizeMpt | Privilege::MustModifyVault, + }), ({ {sfVaultID, SoeRequired}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -973,15 +846,17 @@ TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit, # include #endif TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, - Delegation::NotDelegable, - featureSingleAssetVault, - MayDeleteMpt | MayAuthorizeMpt | MustModifyVault, - Emittance::emitable, + ({ + .amendment = featureSingleAssetVault, + .privileges = Privilege::MayDeleteMpt | Privilege::MayAuthorizeMpt | + Privilege::MustModifyVault, + }), ({ {sfVaultID, SoeRequired}, {sfAmount, SoeRequired, SoeMptSupported}, {sfDestination, SoeOptional}, {sfDestinationTag, SoeOptional}, + {sfCredentialIDs, SoeOptional}, })) /** This transaction claws back tokens from a vault. */ @@ -989,10 +864,10 @@ TRANSACTION(ttVAULT_WITHDRAW, 69, VaultWithdraw, # include #endif TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, - Delegation::NotDelegable, - featureSingleAssetVault, - MayDeleteMpt | MustModifyVault, - Emittance::emitable, + ({ + .amendment = featureSingleAssetVault, + .privileges = Privilege::MayDeleteMpt | Privilege::MustModifyVault, + }), ({ {sfVaultID, SoeRequired}, {sfHolder, SoeRequired}, @@ -1004,10 +879,10 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, # include #endif TRANSACTION(ttBATCH, 71, Batch, - Delegation::NotDelegable, - featureBatchV1_1, - NoPriv, - Emittance::notEmitable, + ({ + .amendment = featureBatchV1_1, + .emittance = Emittance::NotEmitable, + }), ({ {sfRawTransactions, SoeRequired}, {sfBatchSigners, SoeOptional}, @@ -1020,10 +895,10 @@ TRANSACTION(ttBATCH, 71, Batch, # include #endif TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet, - Delegation::NotDelegable, - featureLendingProtocol, - CreatePseudoAcct | MayAuthorizeMpt, - Emittance::emitable, + ({ + .amendment = featureLendingProtocol, + .privileges = Privilege::CreatePseudoAcct | Privilege::MayAuthorizeMpt, + }), ({ {sfVaultID, SoeRequired}, {sfLoanBrokerID, SoeOptional}, @@ -1039,10 +914,10 @@ TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet, # include #endif TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete, - Delegation::NotDelegable, - featureLendingProtocol, - MustDeleteAcct | MayAuthorizeMpt, - Emittance::emitable, + ({ + .amendment = featureLendingProtocol, + .privileges = Privilege::MustDeleteAcct | Privilege::MayAuthorizeMpt, + }), ({ {sfLoanBrokerID, SoeRequired}, })) @@ -1052,10 +927,9 @@ TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete, # include #endif TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit, - Delegation::NotDelegable, - featureLendingProtocol, - NoPriv, - Emittance::emitable, + ({ + .amendment = featureLendingProtocol, + }), ({ {sfLoanBrokerID, SoeRequired}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -1066,15 +940,16 @@ TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit, # include #endif TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, - Delegation::NotDelegable, - featureLendingProtocol, - MayAuthorizeMpt, - Emittance::emitable, + ({ + .amendment = featureLendingProtocol, + .privileges = Privilege::MayAuthorizeMpt, + }), ({ {sfLoanBrokerID, SoeRequired}, {sfAmount, SoeRequired, SoeMptSupported}, {sfDestination, SoeOptional}, {sfDestinationTag, SoeOptional}, + {sfCredentialIDs, SoeOptional}, })) /** This transaction claws back First Loss Capital from a Loan Broker to @@ -1083,10 +958,9 @@ TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw, # include #endif TRANSACTION(ttLOAN_BROKER_COVER_CLAWBACK, 78, LoanBrokerCoverClawback, - Delegation::NotDelegable, - featureLendingProtocol, - NoPriv, - Emittance::emitable, + ({ + .amendment = featureLendingProtocol, + }), ({ {sfLoanBrokerID, SoeOptional}, {sfAmount, SoeOptional, SoeMptSupported}, @@ -1097,10 +971,10 @@ TRANSACTION(ttLOAN_BROKER_COVER_CLAWBACK, 78, LoanBrokerCoverClawback, # include #endif TRANSACTION(ttLOAN_SET, 80, LoanSet, - Delegation::NotDelegable, - featureLendingProtocol, - MayAuthorizeMpt | MustModifyVault, - Emittance::emitable, + ({ + .amendment = featureLendingProtocol, + .privileges = Privilege::MayAuthorizeMpt | Privilege::MustModifyVault, + }), ({ {sfLoanBrokerID, SoeRequired}, {sfData, SoeOptional}, @@ -1126,10 +1000,9 @@ TRANSACTION(ttLOAN_SET, 80, LoanSet, # include #endif TRANSACTION(ttLOAN_DELETE, 81, LoanDelete, - Delegation::NotDelegable, - featureLendingProtocol, - NoPriv, - Emittance::emitable, + ({ + .amendment = featureLendingProtocol, + }), ({ {sfLoanID, SoeRequired}, })) @@ -1139,13 +1012,13 @@ TRANSACTION(ttLOAN_DELETE, 81, LoanDelete, # include #endif TRANSACTION(ttLOAN_MANAGE, 82, LoanManage, - Delegation::NotDelegable, - featureLendingProtocol, - // All of the LoanManage options will modify the vault, but the - // transaction can succeed without options, essentially making it - // a noop. - MayModifyVault, - Emittance::emitable, + ({ + .amendment = featureLendingProtocol, + // All of the LoanManage options will modify the vault, but the + // transaction can succeed without options, essentially making it + // a noop. + .privileges = Privilege::MayModifyVault, + }), ({ {sfLoanID, SoeRequired}, })) @@ -1155,10 +1028,10 @@ TRANSACTION(ttLOAN_MANAGE, 82, LoanManage, # include #endif TRANSACTION(ttLOAN_PAY, 84, LoanPay, - Delegation::NotDelegable, - featureLendingProtocol, - MayAuthorizeMpt | MustModifyVault, - Emittance::emitable, + ({ + .amendment = featureLendingProtocol, + .privileges = Privilege::MayAuthorizeMpt | Privilege::MustModifyVault, + }), ({ {sfLoanID, SoeRequired}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -1169,10 +1042,9 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay, # include #endif TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert, - Delegation::NotDelegable, - featureConfidentialTransfer, - NoPriv, - Emittance::emitable, + ({ + .amendment = featureConfidentialTransfer, + }), ({ {sfMPTokenIssuanceID, SoeRequired}, {sfMPTAmount, SoeRequired}, @@ -1189,10 +1061,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert, # include #endif TRANSACTION(ttCONFIDENTIAL_MPT_MERGE_INBOX, 86, ConfidentialMPTMergeInbox, - Delegation::Delegable, - featureConfidentialTransfer, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureConfidentialTransfer}), ({ {sfMPTokenIssuanceID, SoeRequired}, })) @@ -1202,10 +1071,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_MERGE_INBOX, 86, ConfidentialMPTMergeInbox, # include #endif TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT_BACK, 87, ConfidentialMPTConvertBack, - Delegation::Delegable, - featureConfidentialTransfer, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureConfidentialTransfer}), ({ {sfMPTokenIssuanceID, SoeRequired}, {sfMPTAmount, SoeRequired}, @@ -1221,10 +1087,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT_BACK, 87, ConfidentialMPTConvertBack, # include #endif TRANSACTION(ttCONFIDENTIAL_MPT_SEND, 88, ConfidentialMPTSend, - Delegation::Delegable, - featureConfidentialTransfer, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureConfidentialTransfer}), ({ {sfMPTokenIssuanceID, SoeRequired}, {sfDestination, SoeRequired}, @@ -1243,10 +1106,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_SEND, 88, ConfidentialMPTSend, # include #endif TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback, - Delegation::Delegable, - featureConfidentialTransfer, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureConfidentialTransfer}), ({ {sfMPTokenIssuanceID, SoeRequired}, {sfHolder, SoeRequired}, @@ -1259,10 +1119,9 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback, # include #endif TRANSACTION(ttSPONSORSHIP_TRANSFER, 90, SponsorshipTransfer, - Delegation::NotDelegable, - featureSponsor, - NoPriv, - Emittance::emitable, + ({ + .amendment = featureSponsor, + }), ({ {sfObjectID, SoeOptional}, {sfSponsee, SoeOptional}, @@ -1273,10 +1132,7 @@ TRANSACTION(ttSPONSORSHIP_TRANSFER, 90, SponsorshipTransfer, # include #endif TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet, - Delegation::Delegable, - featureSponsor, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureSponsor}), ({ {sfCounterpartySponsor, SoeOptional}, {sfSponsee, SoeOptional}, @@ -1290,10 +1146,11 @@ TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet, # include #endif TRANSACTION(ttCONTRACT_CREATE, 92, ContractCreate, - Delegation::Delegable, - featureSmartContract, - CreatePseudoAcct, - Emittance::emitable, + ({ + .delegable = Delegation::Delegable, + .amendment = featureSmartContract, + .privileges = Privilege::CreatePseudoAcct, + }), ({ {sfContractCode, SoeOptional}, {sfContractHash, SoeOptional}, @@ -1308,10 +1165,7 @@ TRANSACTION(ttCONTRACT_CREATE, 92, ContractCreate, # include #endif TRANSACTION(ttCONTRACT_MODIFY, 93, ContractModify, - Delegation::Delegable, - featureSmartContract, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureSmartContract}), ({ {sfContractAccount, SoeOptional}, {sfOwner, SoeOptional}, @@ -1328,10 +1182,11 @@ TRANSACTION(ttCONTRACT_MODIFY, 93, ContractModify, # include #endif TRANSACTION(ttCONTRACT_DELETE, 94, ContractDelete, - Delegation::Delegable, - featureSmartContract, - MustDeleteAcct, - Emittance::emitable, + ({ + .delegable = Delegation::Delegable, + .amendment = featureSmartContract, + .privileges = Privilege::MustDeleteAcct, + }), ({ {sfContractAccount, SoeRequired}, })) @@ -1341,10 +1196,7 @@ TRANSACTION(ttCONTRACT_DELETE, 94, ContractDelete, # include #endif TRANSACTION(ttCONTRACT_CLAWBACK, 95, ContractClawback, - Delegation::Delegable, - featureSmartContract, - NoPriv, - Emittance::emitable, + ({.delegable = Delegation::Delegable, .amendment = featureSmartContract}), ({ {sfContractAccount, SoeOptional}, {sfAmount, SoeRequired, SoeMptSupported}, @@ -1355,10 +1207,11 @@ TRANSACTION(ttCONTRACT_CLAWBACK, 95, ContractClawback, # include #endif TRANSACTION(ttCONTRACT_USER_DELETE, 96, ContractUserDelete, - Delegation::Delegable, - featureSmartContract, - NoPriv, - Emittance::notEmitable, + ({ + .delegable = Delegation::Delegable, + .amendment = featureSmartContract, + .emittance = Emittance::NotEmitable, + }), ({ {sfContractAccount, SoeRequired}, {sfGas, SoeRequired}, @@ -1369,10 +1222,11 @@ TRANSACTION(ttCONTRACT_USER_DELETE, 96, ContractUserDelete, # include #endif TRANSACTION(ttCONTRACT_CALL, 97, ContractCall, - Delegation::Delegable, - featureSmartContract, - NoPriv, - Emittance::notEmitable, + ({ + .delegable = Delegation::Delegable, + .amendment = featureSmartContract, + .emittance = Emittance::NotEmitable, + }), ({ {sfContractAccount, SoeRequired}, {sfFunctionName, SoeRequired}, @@ -1388,10 +1242,7 @@ TRANSACTION(ttCONTRACT_CALL, 97, ContractCall, # include #endif TRANSACTION(ttAMENDMENT, 100, EnableAmendment, - Delegation::NotDelegable, - uint256{}, - NoPriv, - Emittance::notEmitable, + ({.emittance = Emittance::NotEmitable}), ({ {sfLedgerSequence, SoeRequired}, {sfAmendment, SoeRequired}, @@ -1401,10 +1252,7 @@ TRANSACTION(ttAMENDMENT, 100, EnableAmendment, For details, see: https://xrpl.org/fee-voting.html */ TRANSACTION(ttFEE, 101, SetFee, - Delegation::NotDelegable, - uint256{}, - NoPriv, - Emittance::notEmitable, + ({.emittance = Emittance::NotEmitable}), ({ {sfLedgerSequence, SoeOptional}, // Old version uses raw numbers @@ -1427,10 +1275,7 @@ TRANSACTION(ttFEE, 101, SetFee, For details, see: https://xrpl.org/negative-unl.html */ TRANSACTION(ttUNL_MODIFY, 102, UNLModify, - Delegation::NotDelegable, - uint256{}, - NoPriv, - Emittance::notEmitable, + ({.emittance = Emittance::NotEmitable}), ({ {sfUNLModifyDisabling, SoeRequired}, {sfLedgerSequence, SoeRequired}, diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index abdc6c0948..664aba8c9b 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -189,7 +189,8 @@ JSS(confidential_balance_inbox); // out: mpt_holders (confidential MPT) JSS(confidential_balance_spending); // out: mpt_holders (confidential MPT) JSS(confidential_balance_version); // out: mpt_holders (confidential MPT) JSS(consensus); // out: NetworkOPs, LedgerConsensus -JSS(contract_account); // out: ContractInfo +JSS(contract_account); // in: LedgerEntry, out: ContractInfo +JSS(contract_hash); // in: LedgerEntry JSS(converge_time); // out: NetworkOPs JSS(converge_time_s); // out: NetworkOPs JSS(cookie); // out: NetworkOPs diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h index 6a2caf52ae..74b5e3c4eb 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h +++ b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h @@ -351,6 +351,54 @@ public: return this->sle_->isFieldPresent(sfAuditorEncryptionKey); } + /** + * @brief Get sfIssuerKeyEpoch (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getIssuerKeyEpoch() const + { + if (hasIssuerKeyEpoch()) + return this->sle_->at(sfIssuerKeyEpoch); + return std::nullopt; + } + + /** + * @brief Check if sfIssuerKeyEpoch is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasIssuerKeyEpoch() const + { + return this->sle_->isFieldPresent(sfIssuerKeyEpoch); + } + + /** + * @brief Get sfAuditorKeyEpoch (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAuditorKeyEpoch() const + { + if (hasAuditorKeyEpoch()) + return this->sle_->at(sfAuditorKeyEpoch); + return std::nullopt; + } + + /** + * @brief Check if sfAuditorKeyEpoch is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorKeyEpoch() const + { + return this->sle_->isFieldPresent(sfAuditorKeyEpoch); + } + /** * @brief Get sfConfidentialOutstandingAmount (SoeDefault) * @return The field value, or std::nullopt if not present. @@ -600,6 +648,28 @@ public: return *this; } + /** + * @brief Set sfIssuerKeyEpoch (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenIssuanceBuilder& + setIssuerKeyEpoch(std::decay_t const& value) + { + object_[sfIssuerKeyEpoch] = value; + return *this; + } + + /** + * @brief Set sfAuditorKeyEpoch (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenIssuanceBuilder& + setAuditorKeyEpoch(std::decay_t const& value) + { + object_[sfAuditorKeyEpoch] = value; + return *this; + } + /** * @brief Set sfConfidentialOutstandingAmount (SoeDefault) * @return Reference to this builder for method chaining. diff --git a/include/xrpl/protocol_autogen/ledger_entries/Vault.h b/include/xrpl/protocol_autogen/ledger_entries/Vault.h index a6ab54cb0a..389ffb4c46 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Vault.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Vault.h @@ -311,6 +311,78 @@ public: { return this->sle_->isFieldPresent(sfLEVersion); } + + /** + * @brief Get sfVaultKind (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getVaultKind() const + { + if (hasVaultKind()) + return this->sle_->at(sfVaultKind); + return std::nullopt; + } + + /** + * @brief Check if sfVaultKind is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasVaultKind() const + { + return this->sle_->isFieldPresent(sfVaultKind); + } + + /** + * @brief Get sfSubscriptionDate (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSubscriptionDate() const + { + if (hasSubscriptionDate()) + return this->sle_->at(sfSubscriptionDate); + return std::nullopt; + } + + /** + * @brief Check if sfSubscriptionDate is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSubscriptionDate() const + { + return this->sle_->isFieldPresent(sfSubscriptionDate); + } + + /** + * @brief Get sfRedemptionDate (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getRedemptionDate() const + { + if (hasRedemptionDate()) + return this->sle_->at(sfRedemptionDate); + return std::nullopt; + } + + /** + * @brief Check if sfRedemptionDate is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasRedemptionDate() const + { + return this->sle_->isFieldPresent(sfRedemptionDate); + } }; /** @@ -543,6 +615,39 @@ public: return *this; } + /** + * @brief Set sfVaultKind (SoeDefault) + * @return Reference to this builder for method chaining. + */ + VaultBuilder& + setVaultKind(std::decay_t const& value) + { + object_[sfVaultKind] = value; + return *this; + } + + /** + * @brief Set sfSubscriptionDate (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultBuilder& + setSubscriptionDate(std::decay_t const& value) + { + object_[sfSubscriptionDate] = value; + return *this; + } + + /** + * @brief Set sfRedemptionDate (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultBuilder& + setRedemptionDate(std::decay_t const& value) + { + object_[sfRedemptionDate] = value; + return *this; + } + /** * @brief Build and return the completed Vault wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/transactions/AMMBid.h b/include/xrpl/protocol_autogen/transactions/AMMBid.h index 30a2b6f2ab..94d0672699 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMBid.h +++ b/include/xrpl/protocol_autogen/transactions/AMMBid.h @@ -21,7 +21,7 @@ class AMMBidBuilder; * Type: ttAMM_BID (39) * Delegable: Delegation::Delegable * Amendment: featureAMM - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use AMMBidBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/AMMClawback.h b/include/xrpl/protocol_autogen/transactions/AMMClawback.h index 38aba892c4..c837b5cee6 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMClawback.h +++ b/include/xrpl/protocol_autogen/transactions/AMMClawback.h @@ -21,7 +21,7 @@ class AMMClawbackBuilder; * Type: ttAMM_CLAWBACK (31) * Delegable: Delegation::Delegable * Amendment: featureAMMClawback - * Privileges: MayDeleteAcct | OverrideFreeze | MayAuthorizeMpt + * Privileges: Privilege::MayDeleteAcct | Privilege::OverrideFreeze | Privilege::MayAuthorizeMpt * * Immutable wrapper around STTx providing type-safe field access. * Use AMMClawbackBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/AMMCreate.h b/include/xrpl/protocol_autogen/transactions/AMMCreate.h index c6ccd4e860..e2e50f87ff 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMCreate.h +++ b/include/xrpl/protocol_autogen/transactions/AMMCreate.h @@ -21,7 +21,7 @@ class AMMCreateBuilder; * Type: ttAMM_CREATE (35) * Delegable: Delegation::Delegable * Amendment: featureAMM - * Privileges: CreatePseudoAcct | MayCreateMpt + * Privileges: Privilege::CreatePseudoAcct | Privilege::MayCreateMpt * * Immutable wrapper around STTx providing type-safe field access. * Use AMMCreateBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/AMMDelete.h b/include/xrpl/protocol_autogen/transactions/AMMDelete.h index 05899a46c8..86e91bf52b 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMDelete.h +++ b/include/xrpl/protocol_autogen/transactions/AMMDelete.h @@ -21,7 +21,7 @@ class AMMDeleteBuilder; * Type: ttAMM_DELETE (40) * Delegable: Delegation::Delegable * Amendment: featureAMM - * Privileges: MustDeleteAcct | MayDeleteMpt + * Privileges: Privilege::MustDeleteAcct | Privilege::MayDeleteMpt * * Immutable wrapper around STTx providing type-safe field access. * Use AMMDeleteBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/AMMDeposit.h b/include/xrpl/protocol_autogen/transactions/AMMDeposit.h index 5416547dab..fed1bd3195 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMDeposit.h +++ b/include/xrpl/protocol_autogen/transactions/AMMDeposit.h @@ -21,7 +21,7 @@ class AMMDepositBuilder; * Type: ttAMM_DEPOSIT (36) * Delegable: Delegation::Delegable * Amendment: featureAMM - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use AMMDepositBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/AMMVote.h b/include/xrpl/protocol_autogen/transactions/AMMVote.h index 7dce3c252f..3fca42a232 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMVote.h +++ b/include/xrpl/protocol_autogen/transactions/AMMVote.h @@ -21,7 +21,7 @@ class AMMVoteBuilder; * Type: ttAMM_VOTE (38) * Delegable: Delegation::Delegable * Amendment: featureAMM - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use AMMVoteBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h b/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h index 81258f22d6..e177011801 100644 --- a/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h +++ b/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h @@ -21,7 +21,7 @@ class AMMWithdrawBuilder; * Type: ttAMM_WITHDRAW (37) * Delegable: Delegation::Delegable * Amendment: featureAMM - * Privileges: MayDeleteAcct | MayAuthorizeMpt + * Privileges: Privilege::MayDeleteAcct | Privilege::MayAuthorizeMpt * * Immutable wrapper around STTx providing type-safe field access. * Use AMMWithdrawBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/AccountDelete.h b/include/xrpl/protocol_autogen/transactions/AccountDelete.h index cf6e97bb63..87ecab0c7b 100644 --- a/include/xrpl/protocol_autogen/transactions/AccountDelete.h +++ b/include/xrpl/protocol_autogen/transactions/AccountDelete.h @@ -21,7 +21,7 @@ class AccountDeleteBuilder; * Type: ttACCOUNT_DELETE (21) * Delegable: Delegation::NotDelegable * Amendment: uint256{} - * Privileges: MustDeleteAcct + * Privileges: Privilege::MustDeleteAcct * * Immutable wrapper around STTx providing type-safe field access. * Use AccountDeleteBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/AccountSet.h b/include/xrpl/protocol_autogen/transactions/AccountSet.h index 55c449e78e..9f85603e22 100644 --- a/include/xrpl/protocol_autogen/transactions/AccountSet.h +++ b/include/xrpl/protocol_autogen/transactions/AccountSet.h @@ -21,7 +21,7 @@ class AccountSetBuilder; * Type: ttACCOUNT_SET (3) * Delegable: Delegation::NotDelegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use AccountSetBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/Batch.h b/include/xrpl/protocol_autogen/transactions/Batch.h index 1a59d2b4c0..f92aaa5348 100644 --- a/include/xrpl/protocol_autogen/transactions/Batch.h +++ b/include/xrpl/protocol_autogen/transactions/Batch.h @@ -21,7 +21,7 @@ class BatchBuilder; * Type: ttBATCH (71) * Delegable: Delegation::NotDelegable * Amendment: featureBatchV1_1 - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use BatchBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/CheckCancel.h b/include/xrpl/protocol_autogen/transactions/CheckCancel.h index b75b717e3f..cf300d3b9b 100644 --- a/include/xrpl/protocol_autogen/transactions/CheckCancel.h +++ b/include/xrpl/protocol_autogen/transactions/CheckCancel.h @@ -21,7 +21,7 @@ class CheckCancelBuilder; * Type: ttCHECK_CANCEL (18) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use CheckCancelBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/CheckCash.h b/include/xrpl/protocol_autogen/transactions/CheckCash.h index c742a15154..b80429875f 100644 --- a/include/xrpl/protocol_autogen/transactions/CheckCash.h +++ b/include/xrpl/protocol_autogen/transactions/CheckCash.h @@ -21,7 +21,7 @@ class CheckCashBuilder; * Type: ttCHECK_CASH (17) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: MayCreateMpt + * Privileges: Privilege::MayCreateMpt * * Immutable wrapper around STTx providing type-safe field access. * Use CheckCashBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/CheckCreate.h b/include/xrpl/protocol_autogen/transactions/CheckCreate.h index 63e55f8604..db51b5eb5f 100644 --- a/include/xrpl/protocol_autogen/transactions/CheckCreate.h +++ b/include/xrpl/protocol_autogen/transactions/CheckCreate.h @@ -21,7 +21,7 @@ class CheckCreateBuilder; * Type: ttCHECK_CREATE (16) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use CheckCreateBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/Clawback.h b/include/xrpl/protocol_autogen/transactions/Clawback.h index 9a3a7f9feb..ad79f1d1fe 100644 --- a/include/xrpl/protocol_autogen/transactions/Clawback.h +++ b/include/xrpl/protocol_autogen/transactions/Clawback.h @@ -21,7 +21,7 @@ class ClawbackBuilder; * Type: ttCLAWBACK (30) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use ClawbackBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h index c80fc81dc5..bf204a35cb 100644 --- a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTClawback.h @@ -21,7 +21,7 @@ class ConfidentialMPTClawbackBuilder; * Type: ttCONFIDENTIAL_MPT_CLAWBACK (89) * Delegable: Delegation::Delegable * Amendment: featureConfidentialTransfer - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use ConfidentialMPTClawbackBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h index 284b7f9e70..d23e6409d9 100644 --- a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvert.h @@ -21,7 +21,7 @@ class ConfidentialMPTConvertBuilder; * Type: ttCONFIDENTIAL_MPT_CONVERT (85) * Delegable: Delegation::NotDelegable * Amendment: featureConfidentialTransfer - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use ConfidentialMPTConvertBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h index 53a8e64125..80ec81e6f3 100644 --- a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTConvertBack.h @@ -21,7 +21,7 @@ class ConfidentialMPTConvertBackBuilder; * Type: ttCONFIDENTIAL_MPT_CONVERT_BACK (87) * Delegable: Delegation::Delegable * Amendment: featureConfidentialTransfer - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use ConfidentialMPTConvertBackBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h index 848da42a41..e3ec886acf 100644 --- a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMergeInbox.h @@ -21,7 +21,7 @@ class ConfidentialMPTMergeInboxBuilder; * Type: ttCONFIDENTIAL_MPT_MERGE_INBOX (86) * Delegable: Delegation::Delegable * Amendment: featureConfidentialTransfer - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use ConfidentialMPTMergeInboxBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h index 806a2586e9..b8aac2bd48 100644 --- a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTSend.h @@ -21,7 +21,7 @@ class ConfidentialMPTSendBuilder; * Type: ttCONFIDENTIAL_MPT_SEND (88) * Delegable: Delegation::Delegable * Amendment: featureConfidentialTransfer - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use ConfidentialMPTSendBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/ContractCall.h b/include/xrpl/protocol_autogen/transactions/ContractCall.h index 5067cd6cf7..73d95a0ebb 100644 --- a/include/xrpl/protocol_autogen/transactions/ContractCall.h +++ b/include/xrpl/protocol_autogen/transactions/ContractCall.h @@ -21,7 +21,7 @@ class ContractCallBuilder; * Type: ttCONTRACT_CALL (97) * Delegable: Delegation::Delegable * Amendment: featureSmartContract - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use ContractCallBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/ContractClawback.h b/include/xrpl/protocol_autogen/transactions/ContractClawback.h index 857b2a821d..8e0f815415 100644 --- a/include/xrpl/protocol_autogen/transactions/ContractClawback.h +++ b/include/xrpl/protocol_autogen/transactions/ContractClawback.h @@ -21,7 +21,7 @@ class ContractClawbackBuilder; * Type: ttCONTRACT_CLAWBACK (95) * Delegable: Delegation::Delegable * Amendment: featureSmartContract - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use ContractClawbackBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/ContractCreate.h b/include/xrpl/protocol_autogen/transactions/ContractCreate.h index 360e216004..703031f6f8 100644 --- a/include/xrpl/protocol_autogen/transactions/ContractCreate.h +++ b/include/xrpl/protocol_autogen/transactions/ContractCreate.h @@ -21,7 +21,7 @@ class ContractCreateBuilder; * Type: ttCONTRACT_CREATE (92) * Delegable: Delegation::Delegable * Amendment: featureSmartContract - * Privileges: CreatePseudoAcct + * Privileges: Privilege::CreatePseudoAcct * * Immutable wrapper around STTx providing type-safe field access. * Use ContractCreateBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/ContractDelete.h b/include/xrpl/protocol_autogen/transactions/ContractDelete.h index db7c4818fc..b0db8b01aa 100644 --- a/include/xrpl/protocol_autogen/transactions/ContractDelete.h +++ b/include/xrpl/protocol_autogen/transactions/ContractDelete.h @@ -21,7 +21,7 @@ class ContractDeleteBuilder; * Type: ttCONTRACT_DELETE (94) * Delegable: Delegation::Delegable * Amendment: featureSmartContract - * Privileges: MustDeleteAcct + * Privileges: Privilege::MustDeleteAcct * * Immutable wrapper around STTx providing type-safe field access. * Use ContractDeleteBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/ContractModify.h b/include/xrpl/protocol_autogen/transactions/ContractModify.h index e2d4001376..737e7ea7fd 100644 --- a/include/xrpl/protocol_autogen/transactions/ContractModify.h +++ b/include/xrpl/protocol_autogen/transactions/ContractModify.h @@ -21,7 +21,7 @@ class ContractModifyBuilder; * Type: ttCONTRACT_MODIFY (93) * Delegable: Delegation::Delegable * Amendment: featureSmartContract - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use ContractModifyBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/ContractUserDelete.h b/include/xrpl/protocol_autogen/transactions/ContractUserDelete.h index d1edaaaa07..e5fa308a27 100644 --- a/include/xrpl/protocol_autogen/transactions/ContractUserDelete.h +++ b/include/xrpl/protocol_autogen/transactions/ContractUserDelete.h @@ -21,7 +21,7 @@ class ContractUserDeleteBuilder; * Type: ttCONTRACT_USER_DELETE (96) * Delegable: Delegation::Delegable * Amendment: featureSmartContract - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use ContractUserDeleteBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/CredentialAccept.h b/include/xrpl/protocol_autogen/transactions/CredentialAccept.h index f2ab546320..7ee2464460 100644 --- a/include/xrpl/protocol_autogen/transactions/CredentialAccept.h +++ b/include/xrpl/protocol_autogen/transactions/CredentialAccept.h @@ -21,7 +21,7 @@ class CredentialAcceptBuilder; * Type: ttCREDENTIAL_ACCEPT (59) * Delegable: Delegation::Delegable * Amendment: featureCredentials - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use CredentialAcceptBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/CredentialCreate.h b/include/xrpl/protocol_autogen/transactions/CredentialCreate.h index 6cf09c852b..6ccc4e3059 100644 --- a/include/xrpl/protocol_autogen/transactions/CredentialCreate.h +++ b/include/xrpl/protocol_autogen/transactions/CredentialCreate.h @@ -21,7 +21,7 @@ class CredentialCreateBuilder; * Type: ttCREDENTIAL_CREATE (58) * Delegable: Delegation::Delegable * Amendment: featureCredentials - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use CredentialCreateBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/CredentialDelete.h b/include/xrpl/protocol_autogen/transactions/CredentialDelete.h index 24a2bfa62a..74039e50bf 100644 --- a/include/xrpl/protocol_autogen/transactions/CredentialDelete.h +++ b/include/xrpl/protocol_autogen/transactions/CredentialDelete.h @@ -21,7 +21,7 @@ class CredentialDeleteBuilder; * Type: ttCREDENTIAL_DELETE (60) * Delegable: Delegation::Delegable * Amendment: featureCredentials - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use CredentialDeleteBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/DIDDelete.h b/include/xrpl/protocol_autogen/transactions/DIDDelete.h index 304287883d..885f84718d 100644 --- a/include/xrpl/protocol_autogen/transactions/DIDDelete.h +++ b/include/xrpl/protocol_autogen/transactions/DIDDelete.h @@ -21,7 +21,7 @@ class DIDDeleteBuilder; * Type: ttDID_DELETE (50) * Delegable: Delegation::Delegable * Amendment: featureDID - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use DIDDeleteBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/DIDSet.h b/include/xrpl/protocol_autogen/transactions/DIDSet.h index 67e5ba23c5..0679170780 100644 --- a/include/xrpl/protocol_autogen/transactions/DIDSet.h +++ b/include/xrpl/protocol_autogen/transactions/DIDSet.h @@ -21,7 +21,7 @@ class DIDSetBuilder; * Type: ttDID_SET (49) * Delegable: Delegation::Delegable * Amendment: featureDID - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use DIDSetBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/DelegateSet.h b/include/xrpl/protocol_autogen/transactions/DelegateSet.h index 592a778952..1d70166920 100644 --- a/include/xrpl/protocol_autogen/transactions/DelegateSet.h +++ b/include/xrpl/protocol_autogen/transactions/DelegateSet.h @@ -21,7 +21,7 @@ class DelegateSetBuilder; * Type: ttDELEGATE_SET (64) * Delegable: Delegation::NotDelegable * Amendment: featurePermissionDelegationV1_1 - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use DelegateSetBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/DepositPreauth.h b/include/xrpl/protocol_autogen/transactions/DepositPreauth.h index b5d575aac5..66c5b390e6 100644 --- a/include/xrpl/protocol_autogen/transactions/DepositPreauth.h +++ b/include/xrpl/protocol_autogen/transactions/DepositPreauth.h @@ -21,7 +21,7 @@ class DepositPreauthBuilder; * Type: ttDEPOSIT_PREAUTH (19) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use DepositPreauthBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/EnableAmendment.h b/include/xrpl/protocol_autogen/transactions/EnableAmendment.h index e811ca16df..08a57540ec 100644 --- a/include/xrpl/protocol_autogen/transactions/EnableAmendment.h +++ b/include/xrpl/protocol_autogen/transactions/EnableAmendment.h @@ -21,7 +21,7 @@ class EnableAmendmentBuilder; * Type: ttAMENDMENT (100) * Delegable: Delegation::NotDelegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use EnableAmendmentBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/EscrowCancel.h b/include/xrpl/protocol_autogen/transactions/EscrowCancel.h index e7e49eca0d..3727bbaa2a 100644 --- a/include/xrpl/protocol_autogen/transactions/EscrowCancel.h +++ b/include/xrpl/protocol_autogen/transactions/EscrowCancel.h @@ -21,7 +21,7 @@ class EscrowCancelBuilder; * Type: ttESCROW_CANCEL (4) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use EscrowCancelBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/EscrowCreate.h b/include/xrpl/protocol_autogen/transactions/EscrowCreate.h index d7621d7505..78f9f00033 100644 --- a/include/xrpl/protocol_autogen/transactions/EscrowCreate.h +++ b/include/xrpl/protocol_autogen/transactions/EscrowCreate.h @@ -21,7 +21,7 @@ class EscrowCreateBuilder; * Type: ttESCROW_CREATE (1) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use EscrowCreateBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/EscrowFinish.h b/include/xrpl/protocol_autogen/transactions/EscrowFinish.h index 8a23fc6752..9c24c7671b 100644 --- a/include/xrpl/protocol_autogen/transactions/EscrowFinish.h +++ b/include/xrpl/protocol_autogen/transactions/EscrowFinish.h @@ -21,7 +21,7 @@ class EscrowFinishBuilder; * Type: ttESCROW_FINISH (2) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use EscrowFinishBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h b/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h index af86dea0b0..4c02989f09 100644 --- a/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h +++ b/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h @@ -21,7 +21,7 @@ class LedgerStateFixBuilder; * Type: ttLEDGER_STATE_FIX (53) * Delegable: Delegation::Delegable * Amendment: fixNFTokenPageLinks - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use LedgerStateFixBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h index 875e0a4c5e..468ce054c2 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h @@ -21,7 +21,7 @@ class LoanBrokerCoverClawbackBuilder; * Type: ttLOAN_BROKER_COVER_CLAWBACK (78) * Delegable: Delegation::NotDelegable * Amendment: featureLendingProtocol - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use LoanBrokerCoverClawbackBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h index 38cc113844..0fe1bd7b91 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h @@ -21,7 +21,7 @@ class LoanBrokerCoverDepositBuilder; * Type: ttLOAN_BROKER_COVER_DEPOSIT (76) * Delegable: Delegation::NotDelegable * Amendment: featureLendingProtocol - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use LoanBrokerCoverDepositBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h index 56a93acbb4..4992fb8bbd 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h @@ -21,7 +21,7 @@ class LoanBrokerCoverWithdrawBuilder; * Type: ttLOAN_BROKER_COVER_WITHDRAW (77) * Delegable: Delegation::NotDelegable * Amendment: featureLendingProtocol - * Privileges: MayAuthorizeMpt + * Privileges: Privilege::MayAuthorizeMpt * * Immutable wrapper around STTx providing type-safe field access. * Use LoanBrokerCoverWithdrawBuilder to construct new transactions. @@ -121,6 +121,32 @@ public: { return this->tx_->isFieldPresent(sfDestinationTag); } + + /** + * @brief Get sfCredentialIDs (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCredentialIDs() const + { + if (hasCredentialIDs()) + { + return this->tx_->at(sfCredentialIDs); + } + return std::nullopt; + } + + /** + * @brief Check if sfCredentialIDs is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCredentialIDs() const + { + return this->tx_->isFieldPresent(sfCredentialIDs); + } }; /** @@ -214,6 +240,17 @@ public: return *this; } + /** + * @brief Set sfCredentialIDs (SoeOptional) + * @return Reference to this builder for method chaining. + */ + LoanBrokerCoverWithdrawBuilder& + setCredentialIDs(std::decay_t const& value) + { + object_[sfCredentialIDs] = value; + return *this; + } + /** * @brief Build and return the LoanBrokerCoverWithdraw wrapper. * @param publicKey The public key for signing. diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h b/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h index 29b3a787fd..c449ebaff0 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h @@ -21,7 +21,7 @@ class LoanBrokerDeleteBuilder; * Type: ttLOAN_BROKER_DELETE (75) * Delegable: Delegation::NotDelegable * Amendment: featureLendingProtocol - * Privileges: MustDeleteAcct | MayAuthorizeMpt + * Privileges: Privilege::MustDeleteAcct | Privilege::MayAuthorizeMpt * * Immutable wrapper around STTx providing type-safe field access. * Use LoanBrokerDeleteBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h b/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h index 41c87c281d..18f14b7a37 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h @@ -21,7 +21,7 @@ class LoanBrokerSetBuilder; * Type: ttLOAN_BROKER_SET (74) * Delegable: Delegation::NotDelegable * Amendment: featureLendingProtocol - * Privileges: CreatePseudoAcct | MayAuthorizeMpt + * Privileges: Privilege::CreatePseudoAcct | Privilege::MayAuthorizeMpt * * Immutable wrapper around STTx providing type-safe field access. * Use LoanBrokerSetBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/LoanDelete.h b/include/xrpl/protocol_autogen/transactions/LoanDelete.h index 8ed537b37a..2696b542da 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanDelete.h +++ b/include/xrpl/protocol_autogen/transactions/LoanDelete.h @@ -21,7 +21,7 @@ class LoanDeleteBuilder; * Type: ttLOAN_DELETE (81) * Delegable: Delegation::NotDelegable * Amendment: featureLendingProtocol - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use LoanDeleteBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/LoanManage.h b/include/xrpl/protocol_autogen/transactions/LoanManage.h index 5eb95d21b1..4a665b372f 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanManage.h +++ b/include/xrpl/protocol_autogen/transactions/LoanManage.h @@ -21,7 +21,7 @@ class LoanManageBuilder; * Type: ttLOAN_MANAGE (82) * Delegable: Delegation::NotDelegable * Amendment: featureLendingProtocol - * Privileges: MayModifyVault + * Privileges: Privilege::MayModifyVault * * Immutable wrapper around STTx providing type-safe field access. * Use LoanManageBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/LoanPay.h b/include/xrpl/protocol_autogen/transactions/LoanPay.h index 8e1faeb981..c9224fd697 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanPay.h +++ b/include/xrpl/protocol_autogen/transactions/LoanPay.h @@ -21,7 +21,7 @@ class LoanPayBuilder; * Type: ttLOAN_PAY (84) * Delegable: Delegation::NotDelegable * Amendment: featureLendingProtocol - * Privileges: MayAuthorizeMpt | MustModifyVault + * Privileges: Privilege::MayAuthorizeMpt | Privilege::MustModifyVault * * Immutable wrapper around STTx providing type-safe field access. * Use LoanPayBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/LoanSet.h b/include/xrpl/protocol_autogen/transactions/LoanSet.h index 2cadebd02e..eb04a468f0 100644 --- a/include/xrpl/protocol_autogen/transactions/LoanSet.h +++ b/include/xrpl/protocol_autogen/transactions/LoanSet.h @@ -21,7 +21,7 @@ class LoanSetBuilder; * Type: ttLOAN_SET (80) * Delegable: Delegation::NotDelegable * Amendment: featureLendingProtocol - * Privileges: MayAuthorizeMpt | MustModifyVault + * Privileges: Privilege::MayAuthorizeMpt | Privilege::MustModifyVault * * Immutable wrapper around STTx providing type-safe field access. * Use LoanSetBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h b/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h index 2fb93eaf35..89d026928d 100644 --- a/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h +++ b/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h @@ -21,7 +21,7 @@ class MPTokenAuthorizeBuilder; * Type: ttMPTOKEN_AUTHORIZE (57) * Delegable: Delegation::Delegable * Amendment: featureMPTokensV1 - * Privileges: MustAuthorizeMpt + * Privileges: Privilege::MustAuthorizeMpt * * Immutable wrapper around STTx providing type-safe field access. * Use MPTokenAuthorizeBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h index 82ffba9996..b83de9d843 100644 --- a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h @@ -21,7 +21,7 @@ class MPTokenIssuanceCreateBuilder; * Type: ttMPTOKEN_ISSUANCE_CREATE (54) * Delegable: Delegation::Delegable * Amendment: featureMPTokensV1 - * Privileges: CreateMptIssuance + * Privileges: Privilege::CreateMptIssuance * * Immutable wrapper around STTx providing type-safe field access. * Use MPTokenIssuanceCreateBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h index cbcd206097..6d1c9b1eaa 100644 --- a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h @@ -21,7 +21,7 @@ class MPTokenIssuanceDestroyBuilder; * Type: ttMPTOKEN_ISSUANCE_DESTROY (55) * Delegable: Delegation::Delegable * Amendment: featureMPTokensV1 - * Privileges: DestroyMptIssuance + * Privileges: Privilege::DestroyMptIssuance * * Immutable wrapper around STTx providing type-safe field access. * Use MPTokenIssuanceDestroyBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h index ed7e1f0f6c..43def05194 100644 --- a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h @@ -21,7 +21,7 @@ class MPTokenIssuanceSetBuilder; * Type: ttMPTOKEN_ISSUANCE_SET (56) * Delegable: Delegation::Delegable * Amendment: featureMPTokensV1 - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use MPTokenIssuanceSetBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h b/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h index 325d2d7fbd..6c858be721 100644 --- a/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h +++ b/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h @@ -21,7 +21,7 @@ class NFTokenAcceptOfferBuilder; * Type: ttNFTOKEN_ACCEPT_OFFER (29) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use NFTokenAcceptOfferBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h b/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h index ec423ea468..ac831bf45e 100644 --- a/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h +++ b/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h @@ -21,7 +21,7 @@ class NFTokenBurnBuilder; * Type: ttNFTOKEN_BURN (26) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: ChangeNftCounts + * Privileges: Privilege::ChangeNftCounts * * Immutable wrapper around STTx providing type-safe field access. * Use NFTokenBurnBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h b/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h index 4c4fb1dc65..81f4f3a848 100644 --- a/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h +++ b/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h @@ -21,7 +21,7 @@ class NFTokenCancelOfferBuilder; * Type: ttNFTOKEN_CANCEL_OFFER (28) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use NFTokenCancelOfferBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h b/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h index a535a578e0..683436f4fd 100644 --- a/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h +++ b/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h @@ -21,7 +21,7 @@ class NFTokenCreateOfferBuilder; * Type: ttNFTOKEN_CREATE_OFFER (27) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use NFTokenCreateOfferBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenMint.h b/include/xrpl/protocol_autogen/transactions/NFTokenMint.h index 5af41eb3dd..5a4e3b5b1c 100644 --- a/include/xrpl/protocol_autogen/transactions/NFTokenMint.h +++ b/include/xrpl/protocol_autogen/transactions/NFTokenMint.h @@ -21,7 +21,7 @@ class NFTokenMintBuilder; * Type: ttNFTOKEN_MINT (25) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: ChangeNftCounts + * Privileges: Privilege::ChangeNftCounts * * Immutable wrapper around STTx providing type-safe field access. * Use NFTokenMintBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenModify.h b/include/xrpl/protocol_autogen/transactions/NFTokenModify.h index 9b9701fed6..84f1e395d4 100644 --- a/include/xrpl/protocol_autogen/transactions/NFTokenModify.h +++ b/include/xrpl/protocol_autogen/transactions/NFTokenModify.h @@ -21,7 +21,7 @@ class NFTokenModifyBuilder; * Type: ttNFTOKEN_MODIFY (61) * Delegable: Delegation::Delegable * Amendment: featureDynamicNFT - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use NFTokenModifyBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/OfferCancel.h b/include/xrpl/protocol_autogen/transactions/OfferCancel.h index 5e6010e0dd..3e52ebf24b 100644 --- a/include/xrpl/protocol_autogen/transactions/OfferCancel.h +++ b/include/xrpl/protocol_autogen/transactions/OfferCancel.h @@ -21,7 +21,7 @@ class OfferCancelBuilder; * Type: ttOFFER_CANCEL (8) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use OfferCancelBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/OfferCreate.h b/include/xrpl/protocol_autogen/transactions/OfferCreate.h index ffc1216297..774921d87a 100644 --- a/include/xrpl/protocol_autogen/transactions/OfferCreate.h +++ b/include/xrpl/protocol_autogen/transactions/OfferCreate.h @@ -21,7 +21,7 @@ class OfferCreateBuilder; * Type: ttOFFER_CREATE (7) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: MayCreateMpt + * Privileges: Privilege::MayCreateMpt * * Immutable wrapper around STTx providing type-safe field access. * Use OfferCreateBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/OracleDelete.h b/include/xrpl/protocol_autogen/transactions/OracleDelete.h index ebdc8fb7e9..e50b6f6b02 100644 --- a/include/xrpl/protocol_autogen/transactions/OracleDelete.h +++ b/include/xrpl/protocol_autogen/transactions/OracleDelete.h @@ -21,7 +21,7 @@ class OracleDeleteBuilder; * Type: ttORACLE_DELETE (52) * Delegable: Delegation::Delegable * Amendment: featurePriceOracle - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use OracleDeleteBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/OracleSet.h b/include/xrpl/protocol_autogen/transactions/OracleSet.h index 0ec6d5cad0..03e4ffc518 100644 --- a/include/xrpl/protocol_autogen/transactions/OracleSet.h +++ b/include/xrpl/protocol_autogen/transactions/OracleSet.h @@ -21,7 +21,7 @@ class OracleSetBuilder; * Type: ttORACLE_SET (51) * Delegable: Delegation::Delegable * Amendment: featurePriceOracle - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use OracleSetBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/Payment.h b/include/xrpl/protocol_autogen/transactions/Payment.h index 389900bf12..cb177a8d08 100644 --- a/include/xrpl/protocol_autogen/transactions/Payment.h +++ b/include/xrpl/protocol_autogen/transactions/Payment.h @@ -21,7 +21,7 @@ class PaymentBuilder; * Type: ttPAYMENT (0) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: CreateAcct | MayCreateMpt + * Privileges: Privilege::CreateAcct | Privilege::MayCreateMpt * * Immutable wrapper around STTx providing type-safe field access. * Use PaymentBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h b/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h index 4c567b13f4..06892955db 100644 --- a/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h @@ -21,7 +21,7 @@ class PaymentChannelClaimBuilder; * Type: ttPAYCHAN_CLAIM (15) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use PaymentChannelClaimBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h b/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h index 0a513d575a..2a3aebca4c 100644 --- a/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h @@ -21,7 +21,7 @@ class PaymentChannelCreateBuilder; * Type: ttPAYCHAN_CREATE (13) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use PaymentChannelCreateBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h b/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h index 51210dd796..9a8c452b0b 100644 --- a/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h @@ -21,7 +21,7 @@ class PaymentChannelFundBuilder; * Type: ttPAYCHAN_FUND (14) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use PaymentChannelFundBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h b/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h index 3db921776c..1b16b13116 100644 --- a/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h +++ b/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h @@ -21,7 +21,7 @@ class PermissionedDomainDeleteBuilder; * Type: ttPERMISSIONED_DOMAIN_DELETE (63) * Delegable: Delegation::Delegable * Amendment: featurePermissionedDomains - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use PermissionedDomainDeleteBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h b/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h index 3e352cad76..30832aec8c 100644 --- a/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h +++ b/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h @@ -21,7 +21,7 @@ class PermissionedDomainSetBuilder; * Type: ttPERMISSIONED_DOMAIN_SET (62) * Delegable: Delegation::Delegable * Amendment: featurePermissionedDomains - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use PermissionedDomainSetBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/SetFee.h b/include/xrpl/protocol_autogen/transactions/SetFee.h index ea3a1fccf7..edcea7b734 100644 --- a/include/xrpl/protocol_autogen/transactions/SetFee.h +++ b/include/xrpl/protocol_autogen/transactions/SetFee.h @@ -21,7 +21,7 @@ class SetFeeBuilder; * Type: ttFEE (101) * Delegable: Delegation::NotDelegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use SetFeeBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/SetRegularKey.h b/include/xrpl/protocol_autogen/transactions/SetRegularKey.h index a943bb0279..042676251b 100644 --- a/include/xrpl/protocol_autogen/transactions/SetRegularKey.h +++ b/include/xrpl/protocol_autogen/transactions/SetRegularKey.h @@ -21,7 +21,7 @@ class SetRegularKeyBuilder; * Type: ttREGULAR_KEY_SET (5) * Delegable: Delegation::NotDelegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use SetRegularKeyBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/SignerListSet.h b/include/xrpl/protocol_autogen/transactions/SignerListSet.h index 6e9d0e41ba..253bcccc1a 100644 --- a/include/xrpl/protocol_autogen/transactions/SignerListSet.h +++ b/include/xrpl/protocol_autogen/transactions/SignerListSet.h @@ -21,7 +21,7 @@ class SignerListSetBuilder; * Type: ttSIGNER_LIST_SET (12) * Delegable: Delegation::NotDelegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use SignerListSetBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h index dfd12a329f..bb3eb2ccf0 100644 --- a/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipSet.h @@ -21,7 +21,7 @@ class SponsorshipSetBuilder; * Type: ttSPONSORSHIP_SET (91) * Delegable: Delegation::Delegable * Amendment: featureSponsor - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use SponsorshipSetBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h index ab26e887e3..5bd5bc1319 100644 --- a/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h +++ b/include/xrpl/protocol_autogen/transactions/SponsorshipTransfer.h @@ -21,7 +21,7 @@ class SponsorshipTransferBuilder; * Type: ttSPONSORSHIP_TRANSFER (90) * Delegable: Delegation::NotDelegable * Amendment: featureSponsor - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use SponsorshipTransferBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/TicketCreate.h b/include/xrpl/protocol_autogen/transactions/TicketCreate.h index 0d8670a76a..4cb109b8f2 100644 --- a/include/xrpl/protocol_autogen/transactions/TicketCreate.h +++ b/include/xrpl/protocol_autogen/transactions/TicketCreate.h @@ -21,7 +21,7 @@ class TicketCreateBuilder; * Type: ttTICKET_CREATE (10) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use TicketCreateBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/TrustSet.h b/include/xrpl/protocol_autogen/transactions/TrustSet.h index 22891b94ec..9d939eb1d0 100644 --- a/include/xrpl/protocol_autogen/transactions/TrustSet.h +++ b/include/xrpl/protocol_autogen/transactions/TrustSet.h @@ -21,7 +21,7 @@ class TrustSetBuilder; * Type: ttTRUST_SET (20) * Delegable: Delegation::Delegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use TrustSetBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/UNLModify.h b/include/xrpl/protocol_autogen/transactions/UNLModify.h index 6569e4bf7d..f5c94071d7 100644 --- a/include/xrpl/protocol_autogen/transactions/UNLModify.h +++ b/include/xrpl/protocol_autogen/transactions/UNLModify.h @@ -21,7 +21,7 @@ class UNLModifyBuilder; * Type: ttUNL_MODIFY (102) * Delegable: Delegation::NotDelegable * Amendment: uint256{} - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use UNLModifyBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/VaultClawback.h b/include/xrpl/protocol_autogen/transactions/VaultClawback.h index 270ccc94bb..d859b4a446 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultClawback.h +++ b/include/xrpl/protocol_autogen/transactions/VaultClawback.h @@ -21,7 +21,7 @@ class VaultClawbackBuilder; * Type: ttVAULT_CLAWBACK (70) * Delegable: Delegation::NotDelegable * Amendment: featureSingleAssetVault - * Privileges: MayDeleteMpt | MustModifyVault + * Privileges: Privilege::MayDeleteMpt | Privilege::MustModifyVault * * Immutable wrapper around STTx providing type-safe field access. * Use VaultClawbackBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/VaultCreate.h b/include/xrpl/protocol_autogen/transactions/VaultCreate.h index b7e1527754..2925302dec 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultCreate.h +++ b/include/xrpl/protocol_autogen/transactions/VaultCreate.h @@ -21,7 +21,7 @@ class VaultCreateBuilder; * Type: ttVAULT_CREATE (65) * Delegable: Delegation::NotDelegable * Amendment: featureSingleAssetVault - * Privileges: CreatePseudoAcct | CreateMptIssuance | MustModifyVault + * Privileges: Privilege::CreatePseudoAcct | Privilege::CreateMptIssuance | Privilege::MustModifyVault * * Immutable wrapper around STTx providing type-safe field access. * Use VaultCreateBuilder to construct new transactions. @@ -214,6 +214,84 @@ public: { return this->tx_->isFieldPresent(sfScale); } + + /** + * @brief Get sfVaultKind (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getVaultKind() const + { + if (hasVaultKind()) + { + return this->tx_->at(sfVaultKind); + } + return std::nullopt; + } + + /** + * @brief Check if sfVaultKind is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasVaultKind() const + { + return this->tx_->isFieldPresent(sfVaultKind); + } + + /** + * @brief Get sfSubscriptionDate (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getSubscriptionDate() const + { + if (hasSubscriptionDate()) + { + return this->tx_->at(sfSubscriptionDate); + } + return std::nullopt; + } + + /** + * @brief Check if sfSubscriptionDate is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasSubscriptionDate() const + { + return this->tx_->isFieldPresent(sfSubscriptionDate); + } + + /** + * @brief Get sfRedemptionDate (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getRedemptionDate() const + { + if (hasRedemptionDate()) + { + return this->tx_->at(sfRedemptionDate); + } + return std::nullopt; + } + + /** + * @brief Check if sfRedemptionDate is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasRedemptionDate() const + { + return this->tx_->isFieldPresent(sfRedemptionDate); + } }; /** @@ -338,6 +416,39 @@ public: return *this; } + /** + * @brief Set sfVaultKind (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultCreateBuilder& + setVaultKind(std::decay_t const& value) + { + object_[sfVaultKind] = value; + return *this; + } + + /** + * @brief Set sfSubscriptionDate (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultCreateBuilder& + setSubscriptionDate(std::decay_t const& value) + { + object_[sfSubscriptionDate] = value; + return *this; + } + + /** + * @brief Set sfRedemptionDate (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultCreateBuilder& + setRedemptionDate(std::decay_t const& value) + { + object_[sfRedemptionDate] = value; + return *this; + } + /** * @brief Build and return the VaultCreate wrapper. * @param publicKey The public key for signing. diff --git a/include/xrpl/protocol_autogen/transactions/VaultDelete.h b/include/xrpl/protocol_autogen/transactions/VaultDelete.h index 67cc32f543..3cef0ce599 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultDelete.h +++ b/include/xrpl/protocol_autogen/transactions/VaultDelete.h @@ -21,7 +21,7 @@ class VaultDeleteBuilder; * Type: ttVAULT_DELETE (67) * Delegable: Delegation::NotDelegable * Amendment: featureSingleAssetVault - * Privileges: MustDeleteAcct | DestroyMptIssuance | MustModifyVault + * Privileges: Privilege::MustDeleteAcct | Privilege::DestroyMptIssuance | Privilege::MustModifyVault * * Immutable wrapper around STTx providing type-safe field access. * Use VaultDeleteBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/VaultDeposit.h b/include/xrpl/protocol_autogen/transactions/VaultDeposit.h index 5bb5362114..099342aa0c 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultDeposit.h +++ b/include/xrpl/protocol_autogen/transactions/VaultDeposit.h @@ -21,7 +21,7 @@ class VaultDepositBuilder; * Type: ttVAULT_DEPOSIT (68) * Delegable: Delegation::NotDelegable * Amendment: featureSingleAssetVault - * Privileges: MayAuthorizeMpt | MustModifyVault + * Privileges: Privilege::MayAuthorizeMpt | Privilege::MustModifyVault * * Immutable wrapper around STTx providing type-safe field access. * Use VaultDepositBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/VaultSet.h b/include/xrpl/protocol_autogen/transactions/VaultSet.h index 14df70f13b..33dfe8bf21 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultSet.h +++ b/include/xrpl/protocol_autogen/transactions/VaultSet.h @@ -21,7 +21,7 @@ class VaultSetBuilder; * Type: ttVAULT_SET (66) * Delegable: Delegation::NotDelegable * Amendment: featureSingleAssetVault - * Privileges: MustModifyVault + * Privileges: Privilege::MustModifyVault * * Immutable wrapper around STTx providing type-safe field access. * Use VaultSetBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h b/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h index 3211524e1f..dfa662f8fd 100644 --- a/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h +++ b/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h @@ -21,7 +21,7 @@ class VaultWithdrawBuilder; * Type: ttVAULT_WITHDRAW (69) * Delegable: Delegation::NotDelegable * Amendment: featureSingleAssetVault - * Privileges: MayDeleteMpt | MayAuthorizeMpt | MustModifyVault + * Privileges: Privilege::MayDeleteMpt | Privilege::MayAuthorizeMpt | Privilege::MustModifyVault * * Immutable wrapper around STTx providing type-safe field access. * Use VaultWithdrawBuilder to construct new transactions. @@ -121,6 +121,32 @@ public: { return this->tx_->isFieldPresent(sfDestinationTag); } + + /** + * @brief Get sfCredentialIDs (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCredentialIDs() const + { + if (hasCredentialIDs()) + { + return this->tx_->at(sfCredentialIDs); + } + return std::nullopt; + } + + /** + * @brief Check if sfCredentialIDs is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCredentialIDs() const + { + return this->tx_->isFieldPresent(sfCredentialIDs); + } }; /** @@ -214,6 +240,17 @@ public: return *this; } + /** + * @brief Set sfCredentialIDs (SoeOptional) + * @return Reference to this builder for method chaining. + */ + VaultWithdrawBuilder& + setCredentialIDs(std::decay_t const& value) + { + object_[sfCredentialIDs] = value; + return *this; + } + /** * @brief Build and return the VaultWithdraw wrapper. * @param publicKey The public key for signing. diff --git a/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h b/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h index b8d551c5e1..a9aa7c2343 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h +++ b/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h @@ -21,7 +21,7 @@ class XChainAccountCreateCommitBuilder; * Type: ttXCHAIN_ACCOUNT_CREATE_COMMIT (44) * Delegable: Delegation::Delegable * Amendment: featureXChainBridge - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use XChainAccountCreateCommitBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h b/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h index 22b57803dc..9cb1f2eaaf 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h +++ b/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h @@ -21,7 +21,7 @@ class XChainAddAccountCreateAttestationBuilder; * Type: ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION (46) * Delegable: Delegation::Delegable * Amendment: featureXChainBridge - * Privileges: CreateAcct + * Privileges: Privilege::CreateAcct * * Immutable wrapper around STTx providing type-safe field access. * Use XChainAddAccountCreateAttestationBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h b/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h index 5e80c05aae..9184c83958 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h +++ b/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h @@ -21,7 +21,7 @@ class XChainAddClaimAttestationBuilder; * Type: ttXCHAIN_ADD_CLAIM_ATTESTATION (45) * Delegable: Delegation::Delegable * Amendment: featureXChainBridge - * Privileges: CreateAcct + * Privileges: Privilege::CreateAcct * * Immutable wrapper around STTx providing type-safe field access. * Use XChainAddClaimAttestationBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/XChainClaim.h b/include/xrpl/protocol_autogen/transactions/XChainClaim.h index ec403b5eb8..e49434c878 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainClaim.h +++ b/include/xrpl/protocol_autogen/transactions/XChainClaim.h @@ -21,7 +21,7 @@ class XChainClaimBuilder; * Type: ttXCHAIN_CLAIM (43) * Delegable: Delegation::Delegable * Amendment: featureXChainBridge - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use XChainClaimBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/XChainCommit.h b/include/xrpl/protocol_autogen/transactions/XChainCommit.h index 48b2263645..471a58dc53 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainCommit.h +++ b/include/xrpl/protocol_autogen/transactions/XChainCommit.h @@ -21,7 +21,7 @@ class XChainCommitBuilder; * Type: ttXCHAIN_COMMIT (42) * Delegable: Delegation::Delegable * Amendment: featureXChainBridge - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use XChainCommitBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h b/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h index 9614b0bd88..ae1269e825 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h +++ b/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h @@ -21,7 +21,7 @@ class XChainCreateBridgeBuilder; * Type: ttXCHAIN_CREATE_BRIDGE (48) * Delegable: Delegation::Delegable * Amendment: featureXChainBridge - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use XChainCreateBridgeBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h b/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h index d17759619f..4c6f98e48f 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h +++ b/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h @@ -21,7 +21,7 @@ class XChainCreateClaimIDBuilder; * Type: ttXCHAIN_CREATE_CLAIM_ID (41) * Delegable: Delegation::Delegable * Amendment: featureXChainBridge - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use XChainCreateClaimIDBuilder to construct new transactions. diff --git a/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h b/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h index e79c9139ce..a3f2930668 100644 --- a/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h +++ b/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h @@ -21,7 +21,7 @@ class XChainModifyBridgeBuilder; * Type: ttXCHAIN_MODIFY_BRIDGE (47) * Delegable: Delegation::Delegable * Amendment: featureXChainBridge - * Privileges: NoPriv + * Privileges: Privilege::NoPriv * * Immutable wrapper around STTx providing type-safe field access. * Use XChainModifyBridgeBuilder to construct new transactions. diff --git a/include/xrpl/rdb/DBInit.h b/include/xrpl/rdb/DBInit.h index 10b04905f2..e6e7e87b6b 100644 --- a/include/xrpl/rdb/DBInit.h +++ b/include/xrpl/rdb/DBInit.h @@ -2,6 +2,9 @@ #include #include +#include +#include +#include namespace xrpl { @@ -9,9 +12,29 @@ namespace xrpl { // These pragmas are built at startup and applied to all database // connections, unless otherwise noted. -inline constexpr char const* kCommonDbPragmaJournal{"PRAGMA journal_mode=%s;"}; -inline constexpr char const* kCommonDbPragmaSync{"PRAGMA synchronous=%s;"}; -inline constexpr char const* kCommonDbPragmaTemp{"PRAGMA temp_store=%s;"}; +// +// They are exposed as functions rather than as format-string constants so +// that the un-substituted template can never reach sqlite: an unrecognized +// pragma value is silently ignored, so forgetting to interpolate would +// leave the setting at its default instead of failing loudly. +[[nodiscard]] inline std::string +commonDbPragmaJournal(std::string_view journalMode) +{ + return std::format("PRAGMA journal_mode={};", journalMode); +} + +[[nodiscard]] inline std::string +commonDbPragmaSync(std::string_view synchronous) +{ + return std::format("PRAGMA synchronous={};", synchronous); +} + +[[nodiscard]] inline std::string +commonDbPragmaTemp(std::string_view tempStore) +{ + return std::format("PRAGMA temp_store={};", tempStore); +} + // A warning will be logged if any lower-safety sqlite tuning settings // are used and at least this much ledger history is configured. This // includes full history nodes. This is because such a large amount of diff --git a/include/xrpl/rdb/DatabaseCon.h b/include/xrpl/rdb/DatabaseCon.h index 90aed04337..5c20f65784 100644 --- a/include/xrpl/rdb/DatabaseCon.h +++ b/include/xrpl/rdb/DatabaseCon.h @@ -6,13 +6,12 @@ #include #include -#include - #include #include #include #include +#include #include #include #include @@ -80,7 +79,7 @@ public: StartUpType startUp = StartUpType::Normal; bool standAlone = false; - boost::filesystem::path dataDir; + std::filesystem::path dataDir; // Indicates whether or not to return the `globalPragma` // from commonPragma() bool useGlobalPragma = false; @@ -143,7 +142,7 @@ public: template DatabaseCon( - boost::filesystem::path const& dataDir, + std::filesystem::path const& dataDir, std::string const& dbName, std::array const& pragma, std::array const& initSQL, @@ -155,7 +154,7 @@ public: // Use this constructor to setup checkpointing template DatabaseCon( - boost::filesystem::path const& dataDir, + std::filesystem::path const& dataDir, std::string const& dbName, std::array const& pragma, std::array const& initSQL, @@ -190,7 +189,7 @@ private: template DatabaseCon( - boost::filesystem::path const& pPath, + std::filesystem::path const& pPath, std::vector const* commonPragma, std::array const& pragma, std::array const& initSQL, diff --git a/include/xrpl/rdb/RelationalDatabase.h b/include/xrpl/rdb/RelationalDatabase.h index e5784c7418..e858f578f8 100644 --- a/include/xrpl/rdb/RelationalDatabase.h +++ b/include/xrpl/rdb/RelationalDatabase.h @@ -14,7 +14,6 @@ #include #include -#include #include #include diff --git a/include/xrpl/server/Manifest.h b/include/xrpl/server/Manifest.h index 786967b057..1b726f2c0c 100644 --- a/include/xrpl/server/Manifest.h +++ b/include/xrpl/server/Manifest.h @@ -306,12 +306,6 @@ operator==(Manifest const& lhs, Manifest const& rhs) lhs.serialized == rhs.serialized; } -inline bool -operator!=(Manifest const& lhs, Manifest const& rhs) -{ - return !(lhs == rhs); -} - struct ValidatorToken { std::string manifest; diff --git a/include/xrpl/server/State.h b/include/xrpl/server/State.h index 8590f6e18f..b79253c12c 100644 --- a/include/xrpl/server/State.h +++ b/include/xrpl/server/State.h @@ -4,8 +4,6 @@ #include #include -#include - #include namespace xrpl { diff --git a/include/xrpl/server/Wallet.h b/include/xrpl/server/Wallet.h index ed8378989f..95486cc468 100644 --- a/include/xrpl/server/Wallet.h +++ b/include/xrpl/server/Wallet.h @@ -10,6 +10,10 @@ #include #include +// boost::optional (not std::optional) appears in the declarations below, +// because SOCI's into()/use() bindings only support boost::optional. +#include + #include #include #include diff --git a/include/xrpl/server/detail/BaseWSPeer.h b/include/xrpl/server/detail/BaseWSPeer.h index b1670865bd..403d7f92ee 100644 --- a/include/xrpl/server/detail/BaseWSPeer.h +++ b/include/xrpl/server/detail/BaseWSPeer.h @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -62,8 +63,7 @@ private: bool pingActive_ = false; boost::beast::websocket::ping_data payload_; error_code ec_; - std::function - controlCallback_; + std::function controlCallback_; public: template @@ -151,7 +151,7 @@ protected: onPing(error_code const& ec); void - onPingPong(boost::beast::websocket::frame_type kind, boost::beast::string_view payload); + onPingPong(boost::beast::websocket::frame_type kind, std::string_view payload); void onTimer(error_code ec); @@ -189,9 +189,9 @@ BaseWSPeer::run() impl().ws_.set_option(port().pmdOptions); // Must manage the control callback memory outside of the `control_callback` // function - controlCallback_ = [this]( - boost::beast::websocket::frame_type kind, - boost::beast::string_view payload) { onPingPong(kind, payload); }; + controlCallback_ = [this](boost::beast::websocket::frame_type kind, std::string_view payload) { + onPingPong(kind, payload); + }; impl().ws_.control_callback(controlCallback_); startTimer(); closeOnTimer_ = true; @@ -430,11 +430,11 @@ template void BaseWSPeer::onPingPong( boost::beast::websocket::frame_type kind, - boost::beast::string_view payload) + std::string_view payload) { if (kind == boost::beast::websocket::frame_type::pong) { - boost::beast::string_view const p(payload_.begin()); + std::string_view const p(payload_.begin(), payload_.size()); if (payload == p) { closeOnTimer_ = false; diff --git a/include/xrpl/shamap/SHAMap.h b/include/xrpl/shamap/SHAMap.h index e198c472fa..05de33ddf3 100644 --- a/include/xrpl/shamap/SHAMap.h +++ b/include/xrpl/shamap/SHAMap.h @@ -484,31 +484,36 @@ private: // returns the first item at or below this node SHAMapLeafNode* - firstBelow(SHAMapTreeNodePtr node, SharedPtrNodeStack& stack, int branch = 0) const; + firstBelow(SHAMapTreeNodePtr node, SharedPtrNodeStack& stack, unsigned int branch = 0u) const; // returns the last item at or below this node SHAMapLeafNode* - lastBelow(SHAMapTreeNodePtr node, SharedPtrNodeStack& stack, int branch = kBranchFactor) const; + lastBelow( + SHAMapTreeNodePtr node, + SharedPtrNodeStack& stack, + unsigned int branch = kBranchFactor) const; + + // direction in which belowHelper scans an inner node's branches + enum class BelowDirection { First, Last }; // helper function for firstBelow and lastBelow SHAMapLeafNode* belowHelper( SHAMapTreeNodePtr node, SharedPtrNodeStack& stack, - int branch, - std::tuple, std::function> const& loopParams) - const; + unsigned int branch, + BelowDirection direction) const; // Simple descent // Get a child of the specified node SHAMapTreeNode* - descend(SHAMapInnerNode*, int branch) const; + descend(SHAMapInnerNode*, unsigned int branch) const; SHAMapTreeNode* - descendThrow(SHAMapInnerNode*, int branch) const; + descendThrow(SHAMapInnerNode*, unsigned int branch) const; SHAMapTreeNodePtr - descend(SHAMapInnerNode&, int branch) const; + descend(SHAMapInnerNode&, unsigned int branch) const; SHAMapTreeNodePtr - descendThrow(SHAMapInnerNode&, int branch) const; + descendThrow(SHAMapInnerNode&, unsigned int branch) const; // Descend with filter // If pending, callback is called as if it called fetchNodeNT @@ -516,7 +521,7 @@ private: SHAMapTreeNode* descendAsync( SHAMapInnerNode* parent, - int branch, + unsigned int branch, SHAMapSyncFilter const* filter, bool& pending, descendCallback&&) const; @@ -525,13 +530,13 @@ private: descend( SHAMapInnerNode* parent, SHAMapNodeID const& parentID, - int branch, + unsigned int branch, SHAMapSyncFilter const* filter) const; // Non-storing // Does not hook the returned node to its parent SHAMapTreeNodePtr - descendNoStore(SHAMapInnerNode&, int branch) const; + descendNoStore(SHAMapInnerNode&, unsigned int branch) const; /** * If there is only one leaf below this node, get its contents @@ -581,8 +586,8 @@ private: using StackEntry = std::tuple< SHAMapInnerNode*, // pointer to the node SHAMapNodeID, // the node's ID - int, // while child we check first - int, // which child we check next + unsigned int, // which child we check first + unsigned int, // which child we check next bool>; // whether we've found any missing children yet // We explicitly choose to specify the use of std::deque here, because @@ -596,7 +601,7 @@ private: using DeferredNode = std::tuple< SHAMapInnerNode*, // parent node SHAMapNodeID, // parent node ID - int, // branch + unsigned int, // branch SHAMapTreeNodePtr>; // node int deferred; @@ -789,12 +794,6 @@ operator==(SHAMap::ConstIterator const& x, SHAMap::ConstIterator const& y) return x.item_ == y.item_; } -inline bool -operator!=(SHAMap::ConstIterator const& x, SHAMap::ConstIterator const& y) -{ - return !(x == y); -} - inline SHAMap::ConstIterator SHAMap::begin() const { diff --git a/include/xrpl/shamap/SHAMapInnerNode.h b/include/xrpl/shamap/SHAMapInnerNode.h index 44d3bd6279..83d039172f 100644 --- a/include/xrpl/shamap/SHAMapInnerNode.h +++ b/include/xrpl/shamap/SHAMapInnerNode.h @@ -62,8 +62,8 @@ private: * * @param i index of the requested child */ - std::optional - getChildIndex(int i) const; + std::optional + getChildIndex(unsigned int i) const; /** * Call the `f` callback for all 16 (branchFactor) branches - even if @@ -125,28 +125,28 @@ public: isEmpty() const; bool - isEmptyBranch(int m) const; + isEmptyBranch(unsigned int branch) const; - int + unsigned int getBranchCount() const; SHAMapHash const& - getChildHash(int m) const; + getChildHash(unsigned int branch) const; void - setChild(int m, SHAMapTreeNodePtr child); + setChild(unsigned int branch, SHAMapTreeNodePtr child); void - shareChild(int m, SHAMapTreeNodePtr const& child); + shareChild(unsigned int branch, SHAMapTreeNodePtr const& child); SHAMapTreeNode* - getChildPointer(int branch); + getChildPointer(unsigned int branch); SHAMapTreeNodePtr - getChild(int branch); + getChild(unsigned int branch); SHAMapTreeNodePtr - canonicalizeChild(int branch, SHAMapTreeNodePtr node); + canonicalizeChild(unsigned int branch, SHAMapTreeNodePtr node); // sync functions bool @@ -190,12 +190,12 @@ SHAMapInnerNode::isEmpty() const } inline bool -SHAMapInnerNode::isEmptyBranch(int m) const +SHAMapInnerNode::isEmptyBranch(unsigned int branch) const { - return (isBranch_ & (1 << m)) == 0; + return (isBranch_ & (1u << branch)) == 0u; } -inline int +inline unsigned int SHAMapInnerNode::getBranchCount() const { return popcnt16(isBranch_); diff --git a/include/xrpl/shamap/SHAMapNodeID.h b/include/xrpl/shamap/SHAMapNodeID.h index 6094892091..f35ba2d2a7 100644 --- a/include/xrpl/shamap/SHAMapNodeID.h +++ b/include/xrpl/shamap/SHAMapNodeID.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -52,7 +53,21 @@ public: } [[nodiscard]] SHAMapNodeID - getChildNodeID(unsigned int m) const; + getChildNodeID(unsigned int branch) const; + + /** + * Test whether this node ID lies on the path to the given leaf key + * + * A node at depth d identifies the tree path spelled by the first d + * nibbles of its key, so any leaf beneath it must agree on that prefix. + * A node ID that fails this test names a different subtree than the one + * it was built for. + * + * @param key the key of a leaf below this node + * @return whether this node ID is a prefix of the leaf key + */ + [[nodiscard]] bool + isPrefixOf(uint256 const& key) const; /** * Create a SHAMapNodeID of a node with the depth of the node and @@ -63,47 +78,34 @@ public: * @return SHAMapNodeID of the node */ static SHAMapNodeID - createID(int depth, uint256 const& key); + createID(unsigned int depth, uint256 const& key); - // FIXME-C++20: use spaceship and operator synthesis /** * Comparison operators + * + * <, >, <= and >= are synthesized from the spaceship. It is written out + * rather than defaulted because the ordering is by depth first, and the + * members are not declared in that order. */ - bool - operator<(SHAMapNodeID const& n) const + std::strong_ordering + operator<=>(SHAMapNodeID const& n) const { - return std::tie(depth_, id_) < std::tie(n.depth_, n.id_); - } - - bool - operator>(SHAMapNodeID const& n) const - { - return n < *this; - } - - bool - operator<=(SHAMapNodeID const& n) const - { - return !(n < *this); - } - - bool - operator>=(SHAMapNodeID const& n) const - { - return !(*this < n); + return std::tie(depth_, id_) <=> std::tie(n.depth_, n.id_); } + /** + * Equality, which the spaceship above does not provide. + * + * Only a *defaulted* operator<=> implicitly declares a defaulted + * operator==; the one above is user-provided, so == has to be written. + * It cannot be defaulted either, because a defaulted == would also compare + * the CountedObject base, which is not equality comparable. + */ bool operator==(SHAMapNodeID const& n) const { return (depth_ == n.depth_) && (id_ == n.id_); } - - bool - operator!=(SHAMapNodeID const& n) const - { - return !(*this == n); - } }; inline std::string diff --git a/include/xrpl/shamap/detail/TaggedPointer.h b/include/xrpl/shamap/detail/TaggedPointer.h index 509e6cc58d..705681be1d 100644 --- a/include/xrpl/shamap/detail/TaggedPointer.h +++ b/include/xrpl/shamap/detail/TaggedPointer.h @@ -219,11 +219,11 @@ public: * * @param i index of the requested child */ - [[nodiscard]] std::optional - getChildIndex(std::uint16_t isBranch, int i) const; + [[nodiscard]] std::optional + getChildIndex(std::uint16_t isBranch, unsigned int i) const; }; -[[nodiscard]] inline int +[[nodiscard]] inline unsigned int popcnt16(std::uint16_t a) { #if __cpp_lib_bitops @@ -234,11 +234,11 @@ popcnt16(std::uint16_t a) // fallback to table lookup static constexpr auto tbl = []() { std::array ret{}; - for (int i = 0; i != 256; ++i) + for (auto i = 0u; i != 256u; ++i) { - for (int j = 0; j != 8; ++j) + for (auto j = 0u; j != 8u; ++j) { - if (i & (1 << j)) + if (i & (1u << j)) ret[i]++; } } diff --git a/include/xrpl/shamap/detail/TaggedPointer.ipp b/include/xrpl/shamap/detail/TaggedPointer.ipp index 9275f3d15a..7db101b3cb 100644 --- a/include/xrpl/shamap/detail/TaggedPointer.ipp +++ b/include/xrpl/shamap/detail/TaggedPointer.ipp @@ -22,6 +22,11 @@ static_assert( static_assert( kBoundaries.back() == SHAMapInnerNode::kBranchFactor, "Last element of boundaries must be number of children in a dense array"); +static_assert( + kBoundaries.front() >= 1, + "TaggedPointer.ipp subtracts 1 from a numAllocated value derived from " + "kBoundaries, as an unsigned quantity, in several places; the smallest " + "boundary must stay non-zero or those subtractions underflow."); // Terminology: A chunk is the memory being allocated from a block. A block // contains multiple chunks. This is the terminology the boost documentation @@ -148,16 +153,16 @@ TaggedPointer::iterChildren(std::uint16_t isBranch, F&& f) const if (numAllocated == SHAMapInnerNode::kBranchFactor) { // dense case - for (int i = 0; i < SHAMapInnerNode::kBranchFactor; ++i) + for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i) f(hashes[i]); } else { // sparse case - int curHashI = 0; - for (int i = 0; i < SHAMapInnerNode::kBranchFactor; ++i) + auto curHashI = 0u; + for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i) { - if ((1 << i) & isBranch) + if ((1u << i) & isBranch) { f(hashes[curHashI++]); } @@ -176,9 +181,9 @@ TaggedPointer::iterNonEmptyChildIndexes(std::uint16_t isBranch, F&& f) const if (capacity() == SHAMapInnerNode::kBranchFactor) { // dense case - for (int i = 0; i < SHAMapInnerNode::kBranchFactor; ++i) + for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i) { - if ((1 << i) & isBranch) + if ((1u << i) & isBranch) { f(i, i); } @@ -187,10 +192,10 @@ TaggedPointer::iterNonEmptyChildIndexes(std::uint16_t isBranch, F&& f) const else { // sparse case - int curHashI = 0; - for (int i = 0; i < SHAMapInnerNode::kBranchFactor; ++i) + auto curHashI = 0u; + for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i) { - if ((1 << i) & isBranch) + if ((1u << i) & isBranch) { f(i, curHashI++); } @@ -216,14 +221,14 @@ TaggedPointer::destroyHashesAndChildren() deallocateArrays(tag, ptr); } -inline std::optional -TaggedPointer::getChildIndex(std::uint16_t isBranch, int i) const +inline std::optional +TaggedPointer::getChildIndex(std::uint16_t isBranch, unsigned int i) const { if (isDense()) return i; // Sparse case - if ((isBranch & (1 << i)) == 0) + if ((isBranch & (1u << i)) == 0u) { // Empty branch. Sparse children do not store empty branches return {}; @@ -273,10 +278,10 @@ inline TaggedPointer::TaggedPointer( *this = std::move(other); auto [srcDstNumAllocated, srcDstHashes, srcDstChildren] = getHashesAndChildren(); bool const srcDstIsDense = isDense(); - int srcDstIndex = 0; - for (int i = 0; i < SHAMapInnerNode::kBranchFactor; ++i) + auto srcDstIndex = 0u; + for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i) { - auto const mask = (1 << i); + auto const mask = (1u << i); bool const inSrc = (srcBranches & mask) != 0; bool const inDst = (dstBranches & mask) != 0; if (inSrc && inDst) @@ -298,13 +303,13 @@ inline TaggedPointer::TaggedPointer( // sparse // need to shift all the elements to the left by // one - for (int c = srcDstIndex; c < srcDstNumAllocated - 1; ++c) + for (auto c = srcDstIndex; c + 1 < srcDstNumAllocated; ++c) { srcDstHashes[c] = srcDstHashes[c + 1]; srcDstChildren[c] = std::move(srcDstChildren[c + 1]); } - srcDstHashes[srcDstNumAllocated - 1].zero(); - srcDstChildren[srcDstNumAllocated - 1].reset(); + srcDstHashes[srcDstNumAllocated - 1u].zero(); + srcDstChildren[srcDstNumAllocated - 1u].reset(); // do not increment the index } } @@ -321,7 +326,7 @@ inline TaggedPointer::TaggedPointer( // sparse // need to create a hole by shifting all the elements to the // right by one - for (int c = srcDstNumAllocated - 1; c > srcDstIndex; --c) + for (auto c = srcDstNumAllocated - 1u; c > srcDstIndex; --c) { srcDstHashes[c] = srcDstHashes[c - 1]; srcDstChildren[c] = std::move(srcDstChildren[c - 1]); @@ -352,10 +357,10 @@ inline TaggedPointer::TaggedPointer( auto [srcNumAllocated, srcHashes, srcChildren] = src.getHashesAndChildren(); bool const srcIsDense = src.isDense(); bool const dstIsDense = dst.isDense(); - int srcIndex = 0, dstIndex = 0; - for (int i = 0; i < SHAMapInnerNode::kBranchFactor; ++i) + auto srcIndex = 0u, dstIndex = 0u; + for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i) { - auto const mask = (1 << i); + auto const mask = (1u << i); bool const inSrc = (srcBranches & mask) != 0; bool const inDst = (dstBranches & mask) != 0; if (inSrc && inDst) @@ -409,7 +414,7 @@ inline TaggedPointer::TaggedPointer( !dstIsDense || dstIndex == dstNumAllocated, "xrpl::TaggedPointer::TaggedPointer(TaggedPointer&& ...) : " "non-sparse or valid sparse"); - for (int i = dstIndex; i < dstNumAllocated; ++i) + for (auto i = dstIndex; i < dstNumAllocated; ++i) { new (&dstHashes[i]) SHAMapHash{}; new (&dstChildren[i]) SHAMapTreeNodePtr{}; @@ -448,9 +453,9 @@ inline TaggedPointer::TaggedPointer( new (&newChildren[branchNum]) SHAMapTreeNodePtr{std::move(oldChildren[indexNum])}; }); // Run the constructors for the remaining elements - for (int i = 0; i < SHAMapInnerNode::kBranchFactor; ++i) + for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i) { - if (((1 << i) & isBranch) != 0) + if (((1u << i) & isBranch) != 0u) continue; new (&newHashes[i]) SHAMapHash{}; new (&newChildren[i]) SHAMapTreeNodePtr{}; @@ -459,7 +464,7 @@ inline TaggedPointer::TaggedPointer( else { // new arrays are sparse, old arrays may be sparse or dense - int curCompressedIndex = 0; + auto curCompressedIndex = 0u; iterNonEmptyChildIndexes(isBranch, [&](auto branchNum, auto indexNum) { new (&newHashes[curCompressedIndex]) SHAMapHash{oldHashes[indexNum]}; new (&newChildren[curCompressedIndex]) @@ -467,7 +472,7 @@ inline TaggedPointer::TaggedPointer( ++curCompressedIndex; }); // Run the constructors for the remaining elements - for (int i = curCompressedIndex; i < newNumAllocated; ++i) + for (auto i = curCompressedIndex; i < newNumAllocated; ++i) { new (&newHashes[i]) SHAMapHash{}; new (&newChildren[i]) SHAMapTreeNodePtr{}; diff --git a/include/xrpl/tx/ApplyContext.h b/include/xrpl/tx/ApplyContext.h index db4574b8fb..e4cd10f36e 100644 --- a/include/xrpl/tx/ApplyContext.h +++ b/include/xrpl/tx/ApplyContext.h @@ -20,7 +20,6 @@ #include #include #include -#include namespace xrpl { @@ -178,16 +177,6 @@ public: view_->rawDestroyXRP(fee); } - /** - * Applies all invariant checkers one by one. - * - * @param result the result generated by processing this transaction. - * @param fee the fee charged for this transaction - * @return the result code that should be returned for this transaction. - */ - TER - checkInvariants(TER const result, XRPAmount const fee); - ApplyViewContext getApplyViewContext() { @@ -198,13 +187,6 @@ public: } private: - static TER - failInvariantCheck(TER const result); - - template - TER - checkInvariantsHelper(TER const result, XRPAmount const fee, std::index_sequence); - OpenViewSandbox base_; ApplyFlags flags_; std::optional view_; diff --git a/include/xrpl/tx/Transactor.h b/include/xrpl/tx/Transactor.h index a71285f70e..aabde69ff9 100644 --- a/include/xrpl/tx/Transactor.h +++ b/include/xrpl/tx/Transactor.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -147,7 +148,7 @@ struct FeePayer FeePayerType type{FeePayerType::Account}; }; -class Transactor +class Transactor : public TxInvariantCheck { protected: ApplyContext& ctx_; @@ -158,7 +159,7 @@ protected: XRPAmount preFeeBalance_{}; // Balance before fees. public: - virtual ~Transactor() = default; + ~Transactor() override = default; Transactor(Transactor const&) = delete; Transactor& operator=(Transactor const&) = delete; @@ -183,20 +184,50 @@ public: return ctx_.view(); } + /** + * Which invariant layers to check. + * + * Full runs the protocol invariants plus the transaction-specific + * check. This is always the scope of the initial pass, even when the + * tentative TER is a tec: a bug or exploit could still mutate ledger + * state, so transaction-specific invariants must run for failed + * transactions too. + * + * ProtocolOnly runs only the protocol invariants and is used + * exclusively for the second invariant pass that follows a + * fee-claim reset — specifically, the reset that + * Transactor::operator() performs when the initial invariant pass + * returns tecINVARIANT_FAILED, rolling the transaction's effects back + * to a fee-claim-only state. In that reduced state the + * transaction-specific post-conditions no longer apply, but the + * protocol invariants must still hold against the fee claim itself. + * ProtocolOnly is not intended for other context discards (e.g. the + * reset used to handle tecOVERSIZE/tecKILLED/etc. in + * processPersistentChanges, or the ctx_.discard() done under + * TapFailHard); those paths do not re-run invariants at all. + */ + enum class InvariantScope { Full, ProtocolOnly }; + /** * Check all invariants for the current transaction. * - * Runs transaction-specific invariants first (visitInvariantEntry + - * finalizeInvariants), then protocol-level invariants. Both layers - * always run; the worst failure code is returned. + * Delegates to the free xrpl::checkInvariants runner. When @p scope is + * InvariantScope::Full, this transactor is passed so both layers + * share a single walk of the modified ledger entries. A failure in + * either layer fails the transaction the same way: tecINVARIANT_FAILED + * on the first pass, which the caller may respond to by rolling the + * transaction back to a fee-claim state and re-invoking this with + * InvariantScope::ProtocolOnly; a failure on that post-reset pass + * escalates to tefINVARIANT_FAILED. * * @param result the tentative TER from transaction processing. * @param fee the fee consumed by the transaction. + * @param scope which invariant layers to check. * * @return the final TER after all invariant checks. */ [[nodiscard]] TER - checkInvariants(TER result, XRPAmount fee); + checkInvariants(TER result, XRPAmount fee, InvariantScope scope); ///////////////////////////////////////////////////// /* @@ -228,6 +259,13 @@ public: static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx, std::uint32_t extraBaseFeeMultiplier); + // Exposed for invariant checks (e.g. ValidVault) that need to know which + // ledger entry actually pays a transaction's fee, distinguishing an + // ordinary sender, a delegate, and pre-funded vs. co-signed fee + // sponsorship. + static FeePayer + getFeePayer(ReadView const& view, STTx const& tx); + /* Do NOT define an invokePreflight function in a derived class. Instead, define: @@ -494,9 +532,6 @@ private: std::pair reset(XRPAmount fee); - static FeePayer - getFeePayer(ReadView const& view, STTx const& tx); - TER consumeSeqProxy(SLE::pointer const& sleAccount); TER @@ -538,20 +573,30 @@ private: preflightUniversal(PreflightContext const& ctx); /** - * Check transaction-specific invariants only. - * - * Walks every modified ledger entry via visitInvariantEntry, then - * calls finalizeInvariants on the derived transactor. Returns - * tecINVARIANT_FAILED if any transaction invariant is violated. - * - * @param result the tentative TER from transaction processing. - * @param fee the fee consumed by the transaction. - * - * @return the original result if all invariants pass, or - * tecINVARIANT_FAILED otherwise. + * Bridges the two-phase TxInvariantCheck interface to this transactor's + * visitInvariantEntry/finalizeInvariants hooks. Declared private (rather + * than protected, like the hooks they forward to) so that neither this + * transactor nor any subclass can call them directly through a + * Transactor& — only through the TxInvariantCheck& that the free + * xrpl::checkInvariants runner holds, which is where the two-phase + * ordering is enforced. */ - [[nodiscard]] TER - checkTransactionInvariants(TER result, XRPAmount fee); + void + visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) final + { + visitInvariantEntry(isDelete, before, after); + } + + [[nodiscard]] bool + finalize( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) final + { + return finalizeInvariants(tx, result, fee, view, j); + } }; inline bool diff --git a/include/xrpl/tx/invariants/FreezeInvariant.h b/include/xrpl/tx/invariants/FreezeInvariant.h index 4b3e9beec4..301e464daf 100644 --- a/include/xrpl/tx/invariants/FreezeInvariant.h +++ b/include/xrpl/tx/invariants/FreezeInvariant.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -11,6 +12,7 @@ #include #include +#include #include namespace xrpl { @@ -69,7 +71,9 @@ private: IssuerChanges const& changes, STTx const& tx, beast::Journal const& j, - bool enforce); + bool enforce, + bool fixOverrideFreeze, + std::optional const& loanDefaultAccounts); static bool validateFrozenState( @@ -78,7 +82,9 @@ private: STTx const& tx, beast::Journal const& j, bool enforce, - bool globalFreeze); + bool globalFreeze, + bool fixOverrideFreeze, + std::optional const& loanDefaultAccounts); }; } // namespace xrpl diff --git a/include/xrpl/tx/invariants/InvariantCheckPrivilege.h b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h index b2f1c62a54..ca9755ea1c 100644 --- a/include/xrpl/tx/invariants/InvariantCheckPrivilege.h +++ b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h @@ -1,9 +1,7 @@ #pragma once -#include #include - -#include +#include // IWYU pragma: export namespace xrpl { @@ -26,37 +24,8 @@ not have the relevant amendments enabled_. It's intentionally a pain in the neck so that bad code gets caught and fixed as early as possible. */ -// Bitwise flags, 86 files, used in macros files -// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) -enum Privilege { - NoPriv = 0x0000, // The transaction can not do any of the enumerated operations - CreateAcct = 0x0001, // The transaction can create a new ACCOUNT_ROOT object. - CreatePseudoAcct = 0x0002, // The transaction can create a pseudo account, - // which implies createAcct - MustDeleteAcct = 0x0004, // The transaction must delete an ACCOUNT_ROOT object - MayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT - // object, but does not have to - OverrideFreeze = 0x0010, // The transaction can override some freeze rules - ChangeNftCounts = 0x0020, // The transaction can mint or burn an NFT - CreateMptIssuance = 0x0040, // The transaction can create a new MPT issuance - DestroyMptIssuance = 0x0080, // The transaction can destroy an MPT issuance - MustAuthorizeMpt = 0x0100, // The transaction MUST create or delete an MPT - // object (except by issuer) - MayAuthorizeMpt = 0x0200, // The transaction MAY create or delete an MPT - // object (except by issuer) - MayDeleteMpt = 0x0400, // The transaction MAY delete an MPT object. May not create. - MustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault - MayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault - MayCreateMpt = 0x2000, // The transaction MAY create an MPT object, except for issuer. -}; - -constexpr Privilege -operator|(Privilege lhs, Privilege rhs) -{ - return safeCast( - safeCast>(lhs) | - safeCast>(rhs)); -} +// `enum Privilege` and its `operator|` live in , +// alongside the TxSettings struct that carries them out of transactions.macro. bool hasPrivilege(STTx const& tx, Privilege priv); diff --git a/include/xrpl/tx/invariants/InvariantRunner.h b/include/xrpl/tx/invariants/InvariantRunner.h new file mode 100644 index 0000000000..29a9dc09b2 --- /dev/null +++ b/include/xrpl/tx/invariants/InvariantRunner.h @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * @brief Runtime interface for a transaction-specific invariant check. + * + * The free checkInvariants runner drives two layers of checks over a single + * walk of the modified ledger entries: + * + * - Protocol checks are the concrete types in InvariantChecks, held in a + * std::tuple and dispatched statically by a compile-time fold (no + * virtual calls). They are duck-typed against the two-phase contract + * described below; see InvariantChecker_PROTOTYPE in InvariantCheck.h. + * - The transaction-specific check is injected at runtime through this + * interface, so the runner can call it without depending on the concrete + * transactor type. Transactor implements this interface directly (see + * Transactor.h) so that the interface's access can stay narrower than + * Transactor's own public surface: calling through a TxInvariantCheck& + * (all the runner ever holds) is public, but calling through a + * Transactor& is not, since Transactor overrides these as private + * (forwarding to its own protected visitInvariantEntry/finalizeInvariants). + * + * Both layers honour the same two-phase protocol: + * + * Phase 1 — state collection (visitEntry). Called once for each ledger + * entry created, modified, or deleted by the transaction. Implementations + * accumulate whatever state they need to evaluate their post-conditions. + * Must not throw. + * + * Phase 2 — condition evaluation (finalize). Called once after every + * modified entry has been visited. Returns true if all post-conditions + * hold, false to fail the transaction. + * + * Rule: invariants must run regardless of transaction result. finalize + * MUST perform meaningful checks even when the transaction has failed + * (when result is not tesSUCCESS). A bug or exploit could cause a failed + * transaction to mutate ledger state in unexpected ways; invariants are the + * last line of defense. + * + * The typical pattern: an invariant that expects a domain-specific state + * change (e.g. a Vault being created) should expect that change only when + * the transaction succeeded. A failed VaultCreate must not have created a + * Vault. + * + * Rule: privilege-gated checks apply to failed transactions too. Failed + * transactions carry no privileges. Any privilege-gated assertion must + * therefore also be enforced for failed transactions. + */ +class TxInvariantCheck +{ +public: + virtual ~TxInvariantCheck() = default; + + /** + * @brief Called for each ledger entry modified by the transaction. + * + * @param isDelete true if the SLE is being deleted. + * @param before the entry's state before the transaction (nullptr for + * newly created entries). + * @param after the entry's state after the transaction. For deletions + * this is the SLE being erased; use @p isDelete rather than + * a null @p after to detect deletions. @p after is + * never null. + */ + virtual void + visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) = 0; + + /** + * @brief Called after all entries have been visited. + * + * @param tx the transaction being applied. + * @param result the tentative TER result of the transaction. + * @param fee the fee consumed by the transaction. + * @param view read-only view of the ledger after the transaction. + * @param j journal for logging invariant failures. + * @return true if all invariants hold; false to fail with + * tecINVARIANT_FAILED / tefINVARIANT_FAILED. + */ + [[nodiscard]] virtual bool + finalize( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) = 0; +}; + +/** + * @brief Run all protocol invariant checks plus the transaction-specific check + * in a single pass over the modified entries. + * + * Both layers share one walk of the modified-entry set: @p txCheck's + * visitEntry accumulates state on the same traversal that drives the + * protocol checkers, then both layers' finalize run on the complete state. + * + * Any failure (a finalize returning false or an exception anywhere in the + * check) returns failInvariantCheck(result). On the first pass that yields + * tecINVARIANT_FAILED, which the transactor treats as a signal to roll the + * transaction's effects back to a fee-claim-only state and re-run this + * runner against the reduced state (see Transactor::InvariantScope). If + * that second pass also fails, the result escalates to tefINVARIANT_FAILED, + * which excludes the transaction from the ledger entirely. + * + * The whole traversal — both layers' visitEntry calls and both layers' + * finalize calls — runs under a single try/catch. There is no per-layer + * isolation: an exception anywhere aborts the remaining traversal and + * finalize calls and fails the transaction. + * + * @param ctx the apply context for the current transaction. + * @param result the tentative TER from transaction processing. + * @param fee the fee consumed by the transaction. + * @param txCheck the transaction-specific invariant check. + * @return the final TER after all invariant checks. + */ +[[nodiscard]] TER +checkInvariants( + ApplyContext& ctx, + TER result, + XRPAmount fee, + std::optional> txCheck); + +[[nodiscard]] inline TER +checkInvariants(ApplyContext& ctx, TER result, XRPAmount fee) +{ + return checkInvariants(ctx, result, fee, std::nullopt); +} + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/LoanBrokerInvariant.h b/include/xrpl/tx/invariants/LoanBrokerInvariant.h index 979f57de35..2b8713fb90 100644 --- a/include/xrpl/tx/invariants/LoanBrokerInvariant.h +++ b/include/xrpl/tx/invariants/LoanBrokerInvariant.h @@ -19,6 +19,11 @@ namespace xrpl { * 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one * node (the root), which will only hold entries for `RippleState` or * `MPToken` objects. + * 2. Under featureLendingProtocolV1_1, an `ltLOAN_BROKER` may only be deleted + * by a `ttLOAN_BROKER_DELETE` transaction, and only when its pre-state + * `OwnerCount` is zero and its pre-state `DebtTotal` rounds to zero at the + * vault's `AssetsTotal` scale, as `LoanBrokerDelete::preclaim` requires. + * 3. At most one `ltLOAN_BROKER` may be deleted in a single transaction. * */ class ValidLoanBroker @@ -36,6 +41,15 @@ class ValidLoanBroker // pseudo-accounts. Key is the brokerID / index. It will be used to find the // LoanBroker object if brokerBefore and brokerAfter are nullptr std::map brokers_; + // The broker whose ledger entry was deleted by this transaction, if any. + // Only ttLOAN_BROKER_DELETE removes a broker, and it removes exactly one. + // This is the pre-transaction state, which is what LoanBrokerDelete::preclaim + // reads when it decides whether the broker may be deleted, so the deletion invariants inspect + // the same DebtTotal and OwnerCount that the transactor did. + SLE::const_pointer deletedBroker_ = nullptr; + // Set if visitEntry observes more than one ltLOAN_BROKER deletion in the + // same transaction. Enforced as its own invariant in finalize. + bool multipleBrokerDeletions_ = false; // Collect all the modified trust lines. Their high and low accounts will be // loaded to look for LoanBroker pseudo-accounts. std::vector lines_; diff --git a/include/xrpl/tx/invariants/LoanInvariant.h b/include/xrpl/tx/invariants/LoanInvariant.h index 0648881423..34ce1a4dc2 100644 --- a/include/xrpl/tx/invariants/LoanInvariant.h +++ b/include/xrpl/tx/invariants/LoanInvariant.h @@ -15,7 +15,33 @@ namespace xrpl { /** * @brief Invariants: Loans are internally consistent * - * 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0` + * 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`. + * 2. A newly-created Loan against a closed-ended vault must satisfy + * `StartDate + PaymentInterval * PaymentRemaining < Vault.RedemptionDate`. + * 3. An `ltLOAN` may only be created by a `ttLOAN_SET` transaction. + * 4. Prior to `featureLendingProtocolV1_1`, the `lsfLoanOverpayment` flag on a + * Loan must not change. From `featureLendingProtocolV1_1` onward the same + * rule is enforced by `NoModifiedUnmodifiableFields`. + * 5. Under `featureLendingProtocolV1_1`: + * a. An `ltLOAN` may only be deleted by a `ttLOAN_DELETE` transaction. + * b. If `Loan.PaymentRemaining = 0` then `Loan.NextPaymentDueDate = 0`. + * c. The `lsfLoanImpaired` flag may only change through a `ttLOAN_MANAGE` + * or `ttLOAN_PAY` transaction. + * d. The `lsfLoanDefault` flag may only change through a `ttLOAN_MANAGE` + * transaction. Combined with `NoModifiedUnmodifiableFields`, which + * rejects any clearing of `lsfLoanDefault`, this makes the flag + * write-once: `ttLOAN_MANAGE` may set it, and no transaction may + * clear it. + * e. Interest due, computed as `TotalValueOutstanding - + * PrincipalOutstanding - ManagementFeeOutstanding`, must not be + * negative. + * f. A Loan must reference a live `ltLOAN_BROKER`, and that broker must + * reference a live `ltVAULT`. + * g. Post-conditions for the Loan paid down by a successful `ttLOAN_PAY`: + * `PaymentRemaining > 0` after: `PrincipalOutstanding` and + * `PaymentRemaining` strictly decrease; `NextPaymentDueDate` + * advances by N * `PaymentInterval`, N > 0. + * `PaymentRemaining == 0` after: pinned by checks 1 and 5b. * */ class ValidLoan @@ -23,6 +49,9 @@ class ValidLoan // Pair is . After is used for most of the checks, except // those that check changed values. std::vector> loans_; + // Loans removed from the ledger, in the same form as loans_. + // Note that `after` holds the erased entry, so it is not null. + std::vector> deletedLoans_; public: void diff --git a/include/xrpl/tx/invariants/MPTInvariant.h b/include/xrpl/tx/invariants/MPTInvariant.h index 5740cd5be2..ddda348d2e 100644 --- a/include/xrpl/tx/invariants/MPTInvariant.h +++ b/include/xrpl/tx/invariants/MPTInvariant.h @@ -215,6 +215,13 @@ class ValidMPTTransfer // Deleted MPToken // MPToken key: true if MPTAuthorized is set hash_map deletedAuthorized_; + // Every touched AccountRoot (not only pseudos): + // AccountID -> whether it was a pseudo-account BEFORE this transaction + // applied. Needed because a transaction may erase a pseudo-account and + // move MPT out of it in the same transaction; by finalize() time the + // view no longer shows it as a pseudo-account (or as existing at all). + // False entries freeze the pre-tx classification for touched non-pseudos. + hash_map pseudoAccountsBefore_; public: /** diff --git a/include/xrpl/tx/invariants/VaultInvariant.h b/include/xrpl/tx/invariants/VaultInvariant.h index 136c6c4a25..ee52f4edb3 100644 --- a/include/xrpl/tx/invariants/VaultInvariant.h +++ b/include/xrpl/tx/invariants/VaultInvariant.h @@ -38,7 +38,20 @@ namespace xrpl { * - vault set must not alter the vault assets or shares balance * - no vault transaction can change loss unrealized (it's updated by loan * transactions) + * - a created closed-ended vault must satisfy + * MIN_INVESTMENT_PERIOD <= RedemptionDate - SubscriptionDate < + * MAX_INVESTMENT_PERIOD + * - vault deposit may only succeed when the vault phase is NoPhase or + * Subscription + * - vault withdrawal may not succeed when the vault phase is Investment + * - closed-ended loan origination (ttLOAN_SET) may only succeed when the + * vault phase is Investment * + * Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced + * by NoModifiedUnmodifiableFields (see InvariantCheck.cpp). From + * featureLendingProtocolV1_1 onwards, immutability of the vault's Asset, + * pseudo-account and ShareMPTID is likewise enforced by + * NoModifiedUnmodifiableFields; prior to that amendment it is checked here. */ class ValidVault { @@ -55,6 +68,9 @@ class ValidVault Number assetsAvailable = 0; Number assetsMaximum = 0; Number lossUnrealized = 0; + std::optional vaultKind; + std::optional subscriptionDate; + std::optional redemptionDate; Vault static make(SLE const&); }; @@ -115,20 +131,57 @@ private: deltaAssets(AccountID const& id) const; /** - * @brief Return the vault-asset delta for the transaction's sending - * account, adjusted for the fee. + * @brief Return the AccountRoot whose XRP balance actually absorbed a + * transaction's fee, if any. * - * Calls @c deltaAssets for @c tx[sfAccount] and, for non-delegated XRP - * transactions, adds the consumed fee back so the invariant sees the net - * asset movement rather than the fee-reduced balance change. + * Mirrors @c Transactor::getFeePayer, but resolves to @c std::nullopt for + * a pre-funded sponsorship: that fee is drawn from the @c ltSponsorship + * object's @c sfFeeAmount, never from the sponsor's own AccountRoot, so + * there is no balance to add back there. * - * @param tx The transaction being applied. - * @param fee Fee charged by this transaction. + * @param view Read-only view of the ledger after the transaction. + * @param tx The transaction being applied. + * @return The fee-paying AccountRoot's id, or @c std::nullopt when the + * fee was not drawn from any AccountRoot balance. + */ + [[nodiscard]] static std::optional + feePayerAccountRoot(ReadView const& view, STTx const& tx); + + /** + * @brief Return the vault-asset delta for a party inspected as a + * withdrawal/deposit counterparty, adjusted for the fee. + * + * Calls @c deltaAssets for @p id and, for XRP transactions, adds the + * consumed fee back only when @p id is the AccountRoot that actually + * paid it (per @c feePayerAccountRoot) -- so the invariant sees the net + * asset movement rather than a fee-reduced balance change, regardless of + * whether @p id is the sender, a distinct destination, a delegate, or a + * co-signed fee sponsor. Post-@c fixCleanup3_4_0, any resulting + * economically-zero delta is always normalized to absence. + * + * Pre-@c fixCleanup3_4_0 this replicates the legacy behaviour exactly: + * only @c tx[sfAccount] could ever receive a fee correction (and only + * when it was itself, per @c STTx::getFeePayerID, the fee payer). After + * that sender-only correction a zero delta is collapsed to absence; if + * the correction does not apply, a present-zero delta is kept as-is. + * + * @param view Read-only view of the ledger after the transaction. + * @param id Account being inspected as sender or destination. + * @param tx The transaction being applied. + * @param fee Fee charged by this transaction. + * @param fix340Enabled Whether @c fixCleanup3_4_0 is enabled, as already + * determined once by @c finalize. * @return The fee-adjusted delta, or @c std::nullopt if the net delta is - * zero or the account entry was not touched. + * zero (always post-amendment; pre-amendment only after the + * sender-only fee correction) or the entry was not touched. */ [[nodiscard]] std::optional - deltaAssetsTxAccount(STTx const& tx, XRPAmount fee) const; + deltaAssetsForParty( + ReadView const& view, + AccountID const& id, + STTx const& tx, + XRPAmount fee, + bool fix340Enabled) const; /** * @brief Return the vault-share balance-change delta for an account. @@ -153,6 +206,17 @@ private: [[nodiscard]] static bool isVaultEmpty(Vault const& vault); + /** + * @brief Invariant check for @c ttLOAN_SET. + * + * For a closed-ended vault, a loan may only be originated while the vault is in the Investment + * phase (strictly past @c SubscriptionDate and before @c RedemptionDate). Open-ended vaults (@c + * NoPhase) are unaffected. The complementary maturity bound (final payment precedes @c + * RedemptionDate by at least @c kLoanRedemptionBuffer) is enforced by @c ValidLoan. + */ + [[nodiscard]] bool + finalizeLoanSet(ReadView const& view, beast::Journal const& j) const; + public: // Compute the coarsest scale required to represent all numbers [[nodiscard]] static std::int32_t diff --git a/include/xrpl/tx/paths/detail/Steps.h b/include/xrpl/tx/paths/detail/Steps.h index 8ee37c026c..1d68860adc 100644 --- a/include/xrpl/tx/paths/detail/Steps.h +++ b/include/xrpl/tx/paths/detail/Steps.h @@ -274,19 +274,6 @@ public: return lhs.equal(rhs); } - /** - * Return true if lhs != rhs. - * - * @param lhs Step to compare. - * @param rhs Step to compare. - * @return true if lhs != rhs. - */ - friend bool - operator!=(Step const& lhs, Step const& rhs) - { - return !(lhs == rhs); - } - /** * Streaming operator for a Step. */ diff --git a/include/xrpl/tx/paths/detail/StrandFlow.h b/include/xrpl/tx/paths/detail/StrandFlow.h index c932c49cca..fcca97ecfc 100644 --- a/include/xrpl/tx/paths/detail/StrandFlow.h +++ b/include/xrpl/tx/paths/detail/StrandFlow.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -373,7 +374,7 @@ qualityUpperBound(ReadView const& v, Strand const& strand) * increases quality of AMM steps, increasing the strand's composite * quality as the result. */ -template +template inline TOutAmt limitOut( ReadView const& v, @@ -411,21 +412,29 @@ limitOut( auto const out = qf->outFromAvgQ(limitQuality); if (!out) return remainingOut; - if constexpr (std::is_same_v) + if constexpr (std::is_same_v || std::is_same_v) { - return XRPAmount{*out}; + auto const roundedOut = TOutAmt{*out}; + // Integral outputs that round above the continuous target can + // realize worse average quality than the requested limit. Keep the + // default rounded value when it still satisfies the limit, since it + // is the largest matching offer; otherwise round down. + if (v.rules().enabled(featureMPTokensV2) && roundedOut > *out && + !qf->satisfiesAvgQ(limitQuality, roundedOut)) + { + NumberRoundModeGuard const g(Number::RoundingMode::Downward); + return TOutAmt{*out}; + } + return roundedOut; } else if constexpr (std::is_same_v) { return IOUAmount{*out}; } - else if constexpr (std::is_same_v) - { - return MPTAmount{*out}; - } else { - return STAmount{remainingOut.asset(), out->mantissa(), out->exponent()}; + static constexpr bool kAlwaysFalse = !std::is_same_v; + static_assert(kAlwaysFalse, "Unhandled StepAmount type"); } }(); // A tiny difference could be due to the round off diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h b/include/xrpl/tx/transactors/dex/AMMWithdraw.h index 7004dd57c1..ea0ad3b253 100644 --- a/include/xrpl/tx/transactors/dex/AMMWithdraw.h +++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h @@ -109,6 +109,11 @@ public: * @param lpTokens current LPT balance * @param lpTokensWithdraw amount of tokens to withdraw * @param tfee trading fee in basis points + * @param freezeHandling whether a frozen balance is reported as zero + * @param authHandling whether an unauthorized MPT balance is reported as + * zero + * @param reserveHandling whether the recipient owner-reserve check is + * enforced when a trustline or MPToken has to be auto-created * @param withdrawAll if withdrawing all lptokens * @param priorBalance balance before fees * @return @@ -118,6 +123,7 @@ public: Sandbox& view, SLE const& ammSle, AccountID const account, + std::optional const& clawbackIssuer, AccountID const& ammAccount, STAmount const& amountBalance, STAmount const& amount2Balance, @@ -127,6 +133,7 @@ public: std::uint16_t tfee, FreezeHandling freezeHandling, AuthHandling authHandling, + ReserveHandling reserveHandling, WithdrawAll withdrawAll, XRPAmount const& priorBalance, beast::Journal const& journal); @@ -138,12 +145,22 @@ public: * @param view * @param ammSle AMM ledger entry * @param ammAccount AMM account + * @param clawbackIssuer when set (AMMClawback path), the issuer performing + * the clawback. A recreated MPToken is only auto-authorized when the + * asset's issuer matches this account, so a clawback cannot grant + * authorization on behalf of a different (paired-asset) issuer. + * @param account LP account * @param amountBalance current LP asset1 balance * @param amountWithdraw asset1 withdraw amount * @param amount2Withdraw asset2 withdraw amount * @param lpTokensAMMBalance current AMM LPT balance * @param lpTokensWithdraw amount of lptokens to withdraw * @param tfee trading fee in basis points + * @param freezeHandling whether a frozen balance is reported as zero + * @param authHandling whether an unauthorized MPT balance is reported as + * zero + * @param reserveHandling whether the recipient owner-reserve check is + * enforced when a trustline or MPToken has to be auto-created * @param withdrawAll if withdraw all lptokens * @param priorBalance balance before fees * @return @@ -153,6 +170,7 @@ public: Sandbox& view, SLE const& ammSle, AccountID const& ammAccount, + std::optional const& clawbackIssuer, AccountID const& account, STAmount const& amountBalance, STAmount const& amountWithdraw, @@ -162,6 +180,7 @@ public: std::uint16_t tfee, FreezeHandling freezeHandling, AuthHandling authHandling, + ReserveHandling reserveHandling, WithdrawAll withdrawAll, XRPAmount const& priorBalance, beast::Journal const& journal); diff --git a/include/xrpl/tx/transactors/vault/VaultWithdraw.h b/include/xrpl/tx/transactors/vault/VaultWithdraw.h index 22ad39d26d..b61af8b323 100644 --- a/include/xrpl/tx/transactors/vault/VaultWithdraw.h +++ b/include/xrpl/tx/transactors/vault/VaultWithdraw.h @@ -20,6 +20,9 @@ public: { } + static bool + checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); diff --git a/include/xrpl/tx/wasm/HostContext.h b/include/xrpl/tx/wasm/HostContext.h new file mode 100644 index 0000000000..cea74f145b --- /dev/null +++ b/include/xrpl/tx/wasm/HostContext.h @@ -0,0 +1,430 @@ +#pragma once + +#include + +#include + +namespace xrpl { +// `xrpl::HostFunctions` is forward-declared rather than included: this header is +// `include!()`d by the cxxbridge-generated translation unit, whose target gets only the +// project's `include/` directory - not the Boost paths that HostFunc.h -> Slice.h -> +// strHex.h transitively need. A reference member and declarations alone do not require a +// complete type; HostContext.cpp, compiled into libxrpl, includes the real header. +class HostFunctions; + +// Defined by the cxx bridge, which emits it into `xrpl_wasm_vm_ffi_cxxbridge/lib.h` from the +// declaration in `crates/xrpl-wasm-vm-ffi` - so the data types and their wire values are +// written once, in Rust, rather than kept in step with a copy here. +// +// Forward-declared for the reason `HostFunctions` above is: that generated header includes +// this one, so naming its definition here would be circular. A scoped enum with a fixed +// underlying type needs no definition to appear in a signature; `HostContext.cpp` includes +// the generated header for the `switch`. +enum class TraceDataType : std::int32_t; + +// The host handed to the Rust wasm engine: one method per entry in the wasm host ABI, +// each forwarding to `xrpl::HostFunctions` - the single source of truth for ledger +// access - and lowering its typed `std::expected` result onto the ABI's wire form. +// +// Every method is `noexcept`, and every body catches everything: a C++ exception +// unwinding into the Rust frames that called it would be undefined behaviour, so a caught +// one leaves here as `HostFunctionError::InternalFatal`, which the engine reads as a fatal +// error and reports as `tecINTERNAL`. +// +// Not an owner: it borrows the `HostFunctions` it is built over for the length of one run. +class HostContext +{ + // Non-const so a host function that mutates (`cacheLedgerObj`, `updateData`) can be + // reached from the `const` methods below: constness of the reference is not + // constness of the referent. + HostFunctions& hostFunctions_; + +public: + HostContext(HostFunctions& hostFunctions); + + // A byte-producing call is handed `out` - a slice aliasing either guest linear + // memory or the engine's output buffer - writes the value only if the whole of it + // fits, and returns the value's *true* length, which may exceed `out`. That is how a + // guest learns the size to ask for, and it is why these methods never need to know + // the guest's capacity: the engine owns the buffer-fit, field-cap and transfer-budget + // rules and derives all three from the length returned here. + // + // A negative return is a `HostFunctionError` code. + [[nodiscard]] std::int32_t + getLedgerSqn(rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + getParentLedgerTime(rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + getParentLedgerHash(rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + getBaseFee(rust::Slice out) const noexcept; + + // The amendment is either a 32-byte id or a name; a 32-byte input is tried as an + // id first and falls back to a name lookup. Answers 1 or 0, or a negative + // `HostFunctionError` code. + [[nodiscard]] std::int32_t + isAmendmentEnabled(rust::Slice amendment) const noexcept; + + // The object id must be a 32-byte uint256, else `InvalidParams`. `cacheIdx` selects + // the slot (0 = pick a free one). Answers the slot used, or a negative + // `HostFunctionError` code. + [[nodiscard]] std::int32_t + cacheLedgerObj(rust::Slice objId, std::int32_t cacheIdx) const noexcept; + + [[nodiscard]] std::int32_t + getTxField(std::int32_t field, rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + getCurrentLedgerObjField(std::int32_t field, rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + getLedgerObjField(std::int32_t cacheIdx, std::int32_t field, rust::Slice out) + const noexcept; + + // The locator is a path of little-endian i32 steps, so its byte length must be a + // non-zero multiple of 4, else `LocatorMalformed`. + [[nodiscard]] std::int32_t + getTxNestedField(rust::Slice locator, rust::Slice out) + const noexcept; + + [[nodiscard]] std::int32_t + getCurrentLedgerObjNestedField( + rust::Slice locator, + rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + getLedgerObjNestedField( + std::int32_t cacheIdx, + rust::Slice locator, + rust::Slice out) const noexcept; + + // Answers the array's element count directly, or a negative `HostFunctionError` + // code (`NoArray` if the field is not an array). + [[nodiscard]] std::int32_t + getTxArrayLen(std::int32_t field) const noexcept; + + [[nodiscard]] std::int32_t + getCurrentLedgerObjArrayLen(std::int32_t field) const noexcept; + + [[nodiscard]] std::int32_t + getLedgerObjArrayLen(std::int32_t cacheIdx, std::int32_t field) const noexcept; + + [[nodiscard]] std::int32_t + getTxNestedArrayLen(rust::Slice locator) const noexcept; + + [[nodiscard]] std::int32_t + getCurrentLedgerObjNestedArrayLen(rust::Slice locator) const noexcept; + + [[nodiscard]] std::int32_t + getLedgerObjNestedArrayLen(std::int32_t cacheIdx, rust::Slice locator) + const noexcept; + + // Answers 1/0 for a valid/invalid signature, or a negative `HostFunctionError`. + [[nodiscard]] std::int32_t + checkSignature( + rust::Slice message, + rust::Slice signature, + rust::Slice pubkey) const noexcept; + + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + accountKeylet(rust::Slice account, rust::Slice out) + const noexcept; + + // Each asset is decoded by length (24 = MPT, 20 = XRP, 40 = issue), else + // `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + ammKeylet( + rust::Slice asset1, + rust::Slice asset2, + rust::Slice out) const noexcept; + + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + checkKeylet( + rust::Slice account, + std::uint32_t seq, + rust::Slice out) const noexcept; + + // Subject and issuer must each be 20 bytes, else `InvalidParams`. Writes the + // 32-byte keylet. + [[nodiscard]] std::int32_t + credentialKeylet( + rust::Slice subject, + rust::Slice issuer, + rust::Slice credentialType, + rust::Slice out) const noexcept; + + // Both accounts must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + delegateKeylet( + rust::Slice account, + rust::Slice authorize, + rust::Slice out) const noexcept; + + // Both accounts must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + depositPreauthKeylet( + rust::Slice account, + rust::Slice authorize, + rust::Slice out) const noexcept; + + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + didKeylet(rust::Slice account, rust::Slice out) + const noexcept; + + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + escrowKeylet( + rust::Slice account, + std::uint32_t seq, + rust::Slice out) const noexcept; + + // Both accounts and the currency must each be 20 bytes, else `InvalidParams`. + // Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + trustLineKeylet( + rust::Slice account1, + rust::Slice account2, + rust::Slice currency, + rust::Slice out) const noexcept; + + // The issuer id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + mptokenIssuanceKeylet( + rust::Slice issuer, + std::uint32_t seq, + rust::Slice out) const noexcept; + + // The MPT id must be 24 bytes and the holder 20, else `InvalidParams`. Writes the + // 32-byte keylet. + [[nodiscard]] std::int32_t + mptokenKeylet( + rust::Slice mptid, + rust::Slice holder, + rust::Slice out) const noexcept; + + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + nftokenOfferKeylet( + rust::Slice account, + std::uint32_t seq, + rust::Slice out) const noexcept; + + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + offerKeylet( + rust::Slice account, + std::uint32_t seq, + rust::Slice out) const noexcept; + + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + oracleKeylet( + rust::Slice account, + std::uint32_t docId, + rust::Slice out) const noexcept; + + // Both account ids must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + paychannelKeylet( + rust::Slice account, + rust::Slice destination, + std::uint32_t seq, + rust::Slice out) const noexcept; + + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + permissionedDomainKeylet( + rust::Slice account, + std::uint32_t seq, + rust::Slice out) const noexcept; + + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + signerListKeylet(rust::Slice account, rust::Slice out) + const noexcept; + + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + ticketKeylet( + rust::Slice account, + std::uint32_t seq, + rust::Slice out) const noexcept; + + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + vaultKeylet( + rust::Slice account, + std::uint32_t seq, + rust::Slice out) const noexcept; + + // Both accounts must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + sponsorshipKeylet( + rust::Slice sponsor, + rust::Slice sponsee, + rust::Slice out) const noexcept; + + // The account id must be 20 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + loanBrokerKeylet( + rust::Slice owner, + std::uint32_t seq, + rust::Slice out) const noexcept; + + // The loan broker id must be 32 bytes, else `InvalidParams`. Writes the 32-byte keylet. + [[nodiscard]] std::int32_t + loanKeylet( + rust::Slice loanBrokerID, + std::uint32_t loanSeq, + rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + sha512Half(rust::Slice data, rust::Slice out) const noexcept; + + // Renders `data` as `dataType` says, and hands the text to `HostFunctions::trace`, which + // is what puts it in this node's log. + // + // The one call that answers nothing: the guest's wasm function has no result, and this + // node's own log is the only thing a trace touches, so a buffer that does not hold what + // it claims is logged here and dropped rather than reported to a contract. + void + trace(rust::Str msg, rust::Slice data, TraceDataType dataType) + const noexcept; + + // Stores `data` as the current object's data field and returns the number of bytes + // stored, or a negative `HostFunctionError` code. + [[nodiscard]] std::int32_t + updateData(rust::Slice data) const noexcept; + + // The account id must be 20 bytes and the nft id 32 bytes, else `InvalidParams`. + // Writes the token's URI bytes. + [[nodiscard]] std::int32_t + getNFT( + rust::Slice account, + rust::Slice nftId, + rust::Slice out) const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Writes the 20-byte issuer + // account encoded in the id. + [[nodiscard]] std::int32_t + getNFTIssuer(rust::Slice nftId, rust::Slice out) + const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Writes the taxon as its four + // little-endian bytes. + [[nodiscard]] std::int32_t + getNFTTaxon(rust::Slice nftId, rust::Slice out) + const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Returns the flags, or a + // negative `HostFunctionError` code. + [[nodiscard]] std::int32_t + getNFTFlags(rust::Slice nftId) const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Returns the transfer fee, or a + // negative `HostFunctionError` code. + [[nodiscard]] std::int32_t + getNFTTransferFee(rust::Slice nftId) const noexcept; + + // The nft id must be 32 bytes, else `InvalidParams`. Writes the sequence number as + // its four little-endian bytes. + [[nodiscard]] std::int32_t + getNFTSequence(rust::Slice nftId, rust::Slice out) + const noexcept; + + // Float / number arithmetic. A float is an XRPL `Number` in serialized form; + // `mode` is a rounding mode. Each writes the result float bytes unless noted. + + [[nodiscard]] std::int32_t + floatFromInt(std::int64_t x, std::int32_t mode, rust::Slice out) const noexcept; + + // The integer region must be eight bytes, else `InvalidParams`. + [[nodiscard]] std::int32_t + floatFromUint( + rust::Slice x, + std::int32_t mode, + rust::Slice out) const noexcept; + + // `amount` must be a serialized `STAmount`, else `InvalidParams`. + [[nodiscard]] std::int32_t + floatFromSTAmount( + rust::Slice amount, + std::int32_t mode, + rust::Slice out) const noexcept; + + // `number` must be a serialized `STNumber`, else `InvalidParams`. + [[nodiscard]] std::int32_t + floatFromSTNumber( + rust::Slice number, + std::int32_t mode, + rust::Slice out) const noexcept; + + // Rounds the float to an integer, written as its eight little-endian bytes. + [[nodiscard]] std::int32_t + floatToInt(rust::Slice x, std::int32_t mode, rust::Slice out) + const noexcept; + + // Writes the mantissa (eight little-endian bytes) and the exponent (four little- + // endian bytes) to two output regions; returns their total size. + [[nodiscard]] std::int32_t + floatToMantExp( + rust::Slice x, + rust::Slice mantissaOut, + rust::Slice exponentOut) const noexcept; + + [[nodiscard]] std::int32_t + floatFromMantExp( + std::int64_t mantissa, + std::int32_t exponent, + std::int32_t mode, + rust::Slice out) const noexcept; + + // Returns a negative, zero, or positive scalar as `x` is less than, equal to, or + // greater than `y`, or a negative `HostFunctionError` code on failure. + [[nodiscard]] std::int32_t + floatCompare(rust::Slice x, rust::Slice y) + const noexcept; + + [[nodiscard]] std::int32_t + floatAdd( + rust::Slice x, + rust::Slice y, + std::int32_t mode, + rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + floatSubtract( + rust::Slice x, + rust::Slice y, + std::int32_t mode, + rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + floatMultiply( + rust::Slice x, + rust::Slice y, + std::int32_t mode, + rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + floatDivide( + rust::Slice x, + rust::Slice y, + std::int32_t mode, + rust::Slice out) const noexcept; + + [[nodiscard]] std::int32_t + floatPower( + rust::Slice x, + std::int32_t n, + std::int32_t mode, + rust::Slice out) const noexcept; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/wasm/HostFunc.h b/include/xrpl/tx/wasm/HostFunc.h index d02b21dce6..ee6daeffee 100644 --- a/include/xrpl/tx/wasm/HostFunc.h +++ b/include/xrpl/tx/wasm/HostFunc.h @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -16,9 +15,6 @@ #include #include -#include -#include -#include #include #include @@ -65,9 +61,6 @@ floatMultiplyImpl(Slice const& x, Slice const& y, int32_t mode); std::expected floatDivideImpl(Slice const& x, Slice const& y, int32_t mode); -std::expected -floatRootImpl(Slice const& x, int32_t n, int32_t mode); - std::expected floatPowerImpl(Slice const& x, int32_t n, int32_t mode); @@ -77,7 +70,6 @@ floatPowerImpl(Slice const& x, int32_t n, int32_t mode); class HostFunctions { protected: - RTOptRef rt_; beast::Journal j_; public: @@ -85,26 +77,6 @@ public: { } - void - setRT(WasmRuntimeWrapper& rt) - { - rt_ = rt; - } - - void - resetRT() - { - rt_ = std::nullopt; - } - - [[nodiscard]] WasmRuntimeWrapper& - getRT() const - { - if (!rt_) - Throw("Wasm runtime not set"); - return rt_->get(); - } - [[nodiscard]] beast::Journal getJournal() const { @@ -368,7 +340,25 @@ public: return std::unexpected(HostFunctionError::Unimplemented); } - [[nodiscard]] virtual std::expected + [[nodiscard]] [[nodiscard]] virtual std::expected + sponsorshipKeylet(AccountID const& sponsor, AccountID const& sponsee) const + { + return std::unexpected(HostFunctionError::Unimplemented); + } + + [[nodiscard]] [[nodiscard]] virtual std::expected + loanBrokerKeylet(AccountID const& owner, std::uint32_t seq) const + { + return std::unexpected(HostFunctionError::Unimplemented); + } + + [[nodiscard]] [[nodiscard]] virtual std::expected + loanKeylet(uint256 const& loanBrokerID, std::uint32_t loanSeq) const + { + return std::unexpected(HostFunctionError::Unimplemented); + } + + [[nodiscard]] [[nodiscard]] virtual std::expected getNFT(AccountID const& account, uint256 const& nftId) const { return std::unexpected(HostFunctionError::Unimplemented); @@ -483,13 +473,7 @@ public: return std::unexpected(HostFunctionError::Unimplemented); } - [[nodiscard]] virtual std::expected - floatRoot(Slice const& x, int32_t n, int32_t mode) const - { - return std::unexpected(HostFunctionError::Unimplemented); - } - - [[nodiscard]] virtual std::expected + [[nodiscard]] [[nodiscard]] virtual std::expected floatPower(Slice const& x, int32_t n, int32_t mode) const { return std::unexpected(HostFunctionError::Unimplemented); @@ -550,8 +534,8 @@ public: virtual std::expected setDataNestedObjectField( AccountID const& account, - std::string_view const& nestedKey, std::string_view const& key, + std::string_view const& nestedKey, STJson::Value const& value) { return std::unexpected(HostFunctionError::Unimplemented); @@ -612,6 +596,4 @@ public: // LCOV_EXCL_STOP }; -using HFRef = std::reference_wrapper; - } // namespace xrpl diff --git a/include/xrpl/tx/wasm/HostFuncImpl.h b/include/xrpl/tx/wasm/HostFuncImpl.h index 569b151e29..d7d999005f 100644 --- a/include/xrpl/tx/wasm/HostFuncImpl.h +++ b/include/xrpl/tx/wasm/HostFuncImpl.h @@ -223,6 +223,15 @@ public: std::expected vaultKeylet(AccountID const& account, std::uint32_t seq) const override; + std::expected + sponsorshipKeylet(AccountID const& sponsor, AccountID const& sponsee) const override; + + std::expected + loanBrokerKeylet(AccountID const& owner, std::uint32_t seq) const override; + + std::expected + loanKeylet(uint256 const& loanBrokerID, std::uint32_t loanSeq) const override; + std::expected getNFT(AccountID const& account, uint256 const& nftId) const override; @@ -280,9 +289,6 @@ public: std::expected floatDivide(Slice const& x, Slice const& y, int32_t mode) const override; - std::expected - floatRoot(Slice const& x, int32_t n, int32_t mode) const override; - std::expected floatPower(Slice const& x, int32_t n, int32_t mode) const override; }; diff --git a/include/xrpl/tx/wasm/HostFuncWrapper.h b/include/xrpl/tx/wasm/HostFuncWrapper.h deleted file mode 100644 index b0829542df..0000000000 --- a/include/xrpl/tx/wasm/HostFuncWrapper.h +++ /dev/null @@ -1,315 +0,0 @@ -#pragma once - -#include - -#include - -#include - -namespace xrpl { - -#define WASM_CB_PARAMS_LIST void *env, wasm_val_vec_t const *params, wasm_val_vec_t *results -#define WASM_SECONDARY_CB_PARAMS_LIST \ - HostFunctions &hf, wasm_val_vec_t const *params, wasm_val_vec_t *results - -wasm_trap_t* HostFuncMain_wrap(WASM_CB_PARAMS_LIST); - -using getLedgerSqn_proto = int32_t(uint8_t*, int32_t); -wasm_trap_t* getLedgerSqn_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getParentLedgerTime_proto = int32_t(uint8_t*, int32_t); -wasm_trap_t* getParentLedgerTime_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getParentLedgerHash_proto = int32_t(uint8_t*, int32_t); -wasm_trap_t* getParentLedgerHash_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getBaseFee_proto = int32_t(uint8_t*, int32_t); -wasm_trap_t* getBaseFee_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using isAmendmentEnabled_proto = int32_t(uint8_t const*, int32_t); -wasm_trap_t* isAmendmentEnabled_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using cacheLedgerObj_proto = int32_t(uint8_t const*, int32_t, int32_t); -wasm_trap_t* cacheLedgerObj_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getTxField_proto = int32_t(int32_t, uint8_t*, int32_t); -wasm_trap_t* getTxField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getCurrentLedgerObjField_proto = int32_t(int32_t, uint8_t*, int32_t); -wasm_trap_t* getCurrentLedgerObjField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getLedgerObjField_proto = int32_t(int32_t, int32_t, uint8_t*, int32_t); -wasm_trap_t* getLedgerObjField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getTxNestedField_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* getTxNestedField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getCurrentLedgerObjNestedField_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* getCurrentLedgerObjNestedField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getLedgerObjNestedField_proto = int32_t(int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* getLedgerObjNestedField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getTxArrayLen_proto = int32_t(int32_t); -wasm_trap_t* getTxArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getCurrentLedgerObjArrayLen_proto = int32_t(int32_t); -wasm_trap_t* getCurrentLedgerObjArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getLedgerObjArrayLen_proto = int32_t(int32_t, int32_t); -wasm_trap_t* getLedgerObjArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getTxNestedArrayLen_proto = int32_t(uint8_t const*, int32_t); -wasm_trap_t* getTxNestedArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getCurrentLedgerObjNestedArrayLen_proto = int32_t(uint8_t const*, int32_t); -wasm_trap_t* getCurrentLedgerObjNestedArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getLedgerObjNestedArrayLen_proto = int32_t(int32_t, uint8_t const*, int32_t); -wasm_trap_t* getLedgerObjNestedArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using updateData_proto = int32_t(uint8_t const*, int32_t); -wasm_trap_t* updateData_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using checkSignature_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t const*, int32_t); -wasm_trap_t* checkSignature_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using computeSha512HalfHash_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* computeSha512HalfHash_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using accountKeylet_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* accountKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using ammKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* ammKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using checkKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* checkKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using credentialKeylet_proto = int32_t( - uint8_t const*, - int32_t, - uint8_t const*, - int32_t, - uint8_t const*, - int32_t, - uint8_t*, - int32_t); -wasm_trap_t* credentialKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using delegateKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* delegateKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using depositPreauthKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* depositPreauthKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using didKeylet_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* didKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using escrowKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* escrowKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using trustLineKeylet_proto = int32_t( - uint8_t const*, - int32_t, - uint8_t const*, - int32_t, - uint8_t const*, - int32_t, - uint8_t*, - int32_t); -wasm_trap_t* trustLineKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using mptokenIssuanceKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* mptokenIssuanceKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using mptokenKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* mptokenKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using nftokenOfferKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* nftokenOfferKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using offerKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* offerKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using oracleKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* oracleKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using paychannelKeylet_proto = int32_t( - uint8_t const*, - int32_t, - uint8_t const*, - int32_t, - uint8_t const*, - int32_t, - uint8_t*, - int32_t); -wasm_trap_t* paychannelKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using permissionedDomainKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* permissionedDomainKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using signerListKeylet_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* signerListKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using ticketKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* ticketKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using vaultKeylet_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* vaultKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getNFT_proto = int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* getNFT_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getNFTIssuer_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* getNFTIssuer_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getNFTTaxon_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* getNFTTaxon_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getNFTFlags_proto = int32_t(uint8_t const*, int32_t); -wasm_trap_t* getNFTFlags_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getNFTTransferFee_proto = int32_t(uint8_t const*, int32_t); -wasm_trap_t* getNFTTransferFee_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getNFTSequence_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* getNFTSequence_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -// trace(msg_ptr, msg_len, data_type, data_ptr, data_len); data_type is a -// TraceDataType. -using trace_proto = void(uint8_t const*, int32_t, int32_t, uint8_t const*, int32_t); -wasm_trap_t* trace_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatFromInt_proto = int32_t(int64_t, uint8_t*, int32_t, int32_t); -wasm_trap_t* floatFromInt_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatFromUint_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t, int32_t); -wasm_trap_t* floatFromUint_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatFromSTAmount_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t, int32_t); -wasm_trap_t* floatFromSTAmount_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatFromSTNumber_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t, int32_t); -wasm_trap_t* floatFromSTNumber_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatToInt_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t, int32_t); -wasm_trap_t* floatToInt_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatToMantExp_proto = int32_t(uint8_t const*, int32_t, uint8_t*, int32_t, uint8_t*, int32_t); -wasm_trap_t* floatToMantExp_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatFromMantExp_proto = int32_t(int64_t, int32_t, uint8_t*, int32_t, int32_t); -wasm_trap_t* floatFromMantExp_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatCompare_proto = int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t); -wasm_trap_t* floatCompare_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatAdd_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t, int32_t); -wasm_trap_t* floatAdd_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatSubtract_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t, int32_t); -wasm_trap_t* floatSubtract_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatMultiply_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t, int32_t); -wasm_trap_t* floatMultiply_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatDivide_proto = - int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t, int32_t); -wasm_trap_t* floatDivide_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatRoot_proto = int32_t(uint8_t const*, int32_t, int32_t, uint8_t*, int32_t, int32_t); -wasm_trap_t* floatRoot_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using floatPower_proto = int32_t(uint8_t const*, int32_t, int32_t, uint8_t*, int32_t, int32_t); -wasm_trap_t* floatPower_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -// Contract-specific host function wrappers - -using instanceParam_proto = int32_t(int32_t, int32_t, uint8_t*, int32_t); -wasm_trap_t* instanceParam_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using functionParam_proto = int32_t(int32_t, int32_t, uint8_t*, int32_t); -wasm_trap_t* functionParam_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getDataObjectField_proto = - int32_t(uint8_t*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* getDataObjectField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getDataNestedObjectField_proto = - int32_t(uint8_t*, int32_t, uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* getDataNestedObjectField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using setDataObjectField_proto = - int32_t(uint8_t*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* setDataObjectField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using setDataNestedObjectField_proto = - int32_t(uint8_t*, int32_t, uint8_t const*, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* setDataNestedObjectField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getDataArrayElementField_proto = - int32_t(uint8_t*, int32_t, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* getDataArrayElementField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using getDataNestedArrayElementField_proto = int32_t( - uint8_t*, - int32_t, - uint8_t const*, - int32_t, - int32_t, - uint8_t const*, - int32_t, - uint8_t*, - int32_t); -wasm_trap_t* getDataNestedArrayElementField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using setDataArrayElementField_proto = - int32_t(uint8_t*, int32_t, int32_t, uint8_t const*, int32_t, uint8_t*, int32_t); -wasm_trap_t* setDataArrayElementField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using setDataNestedArrayElementField_proto = int32_t( - uint8_t*, - int32_t, - uint8_t const*, - int32_t, - int32_t, - uint8_t const*, - int32_t, - uint8_t*, - int32_t); -wasm_trap_t* setDataNestedArrayElementField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using buildTxn_proto = int32_t(int32_t); -wasm_trap_t* buildTxn_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using addTxnField_proto = int32_t(int32_t, int32_t, uint8_t const*, int32_t); -wasm_trap_t* addTxnField_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using emitBuiltTxn_proto = int32_t(int32_t); -wasm_trap_t* emitBuiltTxn_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using emitTxn_proto = int32_t(uint8_t const*, int32_t); -wasm_trap_t* emitTxn_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -using emitEvent_proto = int32_t(uint8_t const*, int32_t, uint8_t const*, int32_t); -wasm_trap_t* emitEvent_wrap(WASM_SECONDARY_CB_PARAMS_LIST); - -} // namespace xrpl diff --git a/include/xrpl/tx/wasm/README.md b/include/xrpl/tx/wasm/README.md index 04958b663a..6b94abc873 100644 --- a/include/xrpl/tx/wasm/README.md +++ b/include/xrpl/tx/wasm/README.md @@ -1,189 +1,41 @@ # WASM Module for Programmable Escrows -This module provides WebAssembly (WASM) execution capabilities for programmable -escrows on the XRP Ledger. When an escrow is finished, the WASM code runs to -determine whether the escrow conditions are met, enabling custom programmable -logic for escrow release conditions. - -For the full specification, see +WebAssembly execution for programmable escrows. When an escrow is finished, its contract +runs to decide whether the release conditions are met. Specification: [XLS-0102: WASM VM](https://xls.xrpl.org/xls/XLS-0102-wasm-vm.html). -## Architecture +The engine itself is Rust (`crates/xrpl-wasm-vm`, over wasmi), reached through a cxx +bridge. -The module follows a layered architecture: +## What is in this directory -``` -┌─────────────────────────────────────────────────────────────┐ -│ WasmEngine (WasmVM.h) │ -│ runEscrowWasm(), preflightEscrowWasm() │ -│ Host function registration │ -├─────────────────────────────────────────────────────────────┤ -│ WasmiEngine (WasmiVM.h) │ -│ Low-level wasmi interpreter integration │ -├─────────────────────────────────────────────────────────────┤ -│ HostFuncWrapper │ HostFuncImpl │ -│ C-style WASM bridges │ C++ implementations │ -├─────────────────────────────────────────────────────────────┤ -│ HostFunc (Interface) │ -│ Abstract base class for host functions │ -└─────────────────────────────────────────────────────────────┘ -``` +- **`WasmVM.h`** — the entry points xrpld calls: `runEscrowWasm` (execute a contract, + returning a result and its gas cost, or a `WasmTER`) and `preflightEscrowWasm` (screen a + module with no host and no execution). Both own their TER maps. +- **`HostFunc.h`** — the `HostFunctions` interface: one virtual per host function, each + defaulting to `Unimplemented`, returning `std::expected`. +- **`HostFuncImpl.h`** — `WasmHostFunctionsImpl`, the implementation over an + `ApplyContext&`. Bodies are split across `HostFuncImpl*.cpp` by category. +- **`HostContext.h`** — the bridge's C++ half: an ABI-shaped, `noexcept` view of + `HostFunctions` that the engine calls back into. Nothing may unwind into Rust, so every + method catches everything — through `guarded()`, except `trace`, which answers the guest + nothing and so has its own catch that only logs. +- **`WasmCommon.h`** — the shared vocabulary: `HostFunctionError` (the codes a contract + sees), `Bytes`, `FieldLocator`, `WasmTER`, `adjustWasmEndianess`, which is where the + boundary's byte order is decided, and `guarded()`, the catch that turns a throwing host + body into a code the engine can read. -### Key Components +## Host functions -- **`WasmVM.h` / `detail/WasmVM.cpp`** - High-level facade providing: - - `WasmEngine` singleton that wraps the underlying WASM interpreter - - `runEscrowWasm()` - Execute WASM code for escrow finish - - `preflightEscrowWasm()` - Validate WASM code during preflight - - `createWasmImport()` - Register all host functions +Grouped by what they reach: ledger information; transaction and ledger-object field access; +keylet construction; cryptography; float arithmetic; NFT queries; tracing. -- **`WasmiVM.h` / `detail/WasmiVM.cpp`** - Low-level integration with the - [wasmi](https://github.com/wasmi-labs/wasmi) WebAssembly interpreter: - - `WasmiEngine` - Manages WASM modules, instances, and execution - - Memory management and gas metering - - Function invocation and result handling +The wire names and per-call gas costs are declared in `crates/xrpl-host-functions` — +one `host_functions!` block that generates the ABI trait and the spec table. That +declaration is the single source of truth; `HostFunc.h` is the C++ side of it. -- **`HostFunc.h`** - Abstract `HostFunctions` base class defining the interface - for all callable host functions. Each method returns - `std::expected`. +## Entry point -- **`HostFuncImpl.h` / `detail/HostFuncImpl*.cpp`** - Concrete - `WasmHostFunctionsImpl` class that implements host functions with access to - `ApplyContext` for ledger state queries. Implementation split across files: - - `HostFuncImpl.cpp` - Core utilities (updateData, checkSignature, etc.) - - `HostFuncImplFloat.cpp` - Float/number arithmetic operations - - `HostFuncImplGetter.cpp` - Field access (transaction, ledger objects) - - `HostFuncImplKeylet.cpp` - Keylet construction functions - - `HostFuncImplLedgerHeader.cpp` - Ledger header info access - - `HostFuncImplNFT.cpp` - NFT-related queries - - `HostFuncImplTrace.cpp` - Debugging/tracing functions - -- **`HostFuncWrapper.h` / `detail/HostFuncWrapper.cpp`** - C-style wrapper - functions that bridge WASM calls to C++ `HostFunctions` methods. Each host - function has: - - A `_proto` type alias defining the function signature - - A `_wrap` function that extracts parameters and calls the implementation - -- **`ParamsHelper.h`** - Utilities for WASM parameter handling: - - `WASM_IMPORT_FUNC` / `WASM_IMPORT_FUNC2` macros for registration - - `wasmParams()` helper for building parameter vectors - - Type conversion between WASM and C++ types - -## Host Functions - -Host functions allow WASM code to interact with the XRP Ledger. They are -organized into categories: - -- **Ledger Information** - Access ledger sequence, timestamps, hashes, fees -- **Transaction & Ledger Object Access** - Read fields from the transaction - and ledger objects (including the current escrow object) -- **Keylet Construction** - Build keylets to look up various ledger object types -- **Cryptography** - Signature verification and hashing -- **Float Arithmetic** - Mathematical operations for amount calculations -- **NFT Operations** - Query NFT properties -- **Tracing/Debugging** - Log messages for debugging - -For the complete list of available host functions, their WASM names, and gas -costs, see the [XLS-0102 specification](https://xls.xrpl.org/xls/XLS-0102-wasm-vm.html) -or `detail/WasmVM.cpp` where they are registered via `WASM_IMPORT_FUNC2` macros. -For method signatures, see `HostFunc.h`. - -## Gas Model - -Each host function has an associated gas cost. The gas cost is specified when -registering the function in `detail/WasmVM.cpp`: - -```cpp -WASM_IMPORT_FUNC2(i, getLedgerSqn, "get_ledger_sqn", hfs, 60); -// ^^ gas cost -``` - -WASM execution is metered, and if the gas limit is exceeded, execution fails. - -## Entry Point - -The WASM module must export a function with the name defined by -`escrowFunctionName` (currently `"escrow_finish"`). This function: - -- Takes no parameters (or parameters passed via host function calls) -- Returns an `int32_t`: - - `1` (or positive): Escrow conditions are met, allow finish - - `0` (or negative): Escrow conditions are not met, reject finish - -## Adding a New Host Function - -To add a new host function, follow these steps: - -### 1. Add to HostFunc.h (Base Class) - -Add a virtual method declaration with a default implementation that returns an -error: - -```cpp -virtual std::expected -myNewFunction(ParamType1 param1, ParamType2 param2) -{ - return std::unexpected(HostFunctionError::INTERNAL); -} -``` - -### 2. Add to HostFuncImpl.h (Declaration) - -Add the method override declaration in `WasmHostFunctionsImpl`: - -```cpp -std::expected -myNewFunction(ParamType1 param1, ParamType2 param2) override; -``` - -### 3. Implement in detail/HostFuncImpl\*.cpp - -Add the implementation in the appropriate file: - -```cpp -std::expected -WasmHostFunctionsImpl::myNewFunction(ParamType1 param1, ParamType2 param2) -{ - // Implementation using ctx (ApplyContext) for ledger access - return result; -} -``` - -### 4. Add Wrapper to HostFuncWrapper.h - -Add the prototype and wrapper declaration: - -```cpp -using myNewFunction_proto = int32_t(uint8_t const*, int32_t, ...); -wasm_trap_t* -myNewFunction_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results); -``` - -### 5. Implement Wrapper in detail/HostFuncWrapper.cpp - -Implement the C-style wrapper that bridges WASM to C++: - -```cpp -wasm_trap_t* -myNewFunction_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results) -{ - // Extract parameters from params - // Call hfs->myNewFunction(...) - // Set results and return -} -``` - -### 6. Register in WasmVM.cpp - -Add the function registration in `setCommonHostFunctions()` or -`createWasmImport()`: - -```cpp -WASM_IMPORT_FUNC2(i, myNewFunction, "my_new_function", hfs, 100); -// ^^ WASM name ^^ gas cost -``` - -> [!IMPORTANT] -> New host functions MUST be amendment-gated in `WasmVM.cpp`. -> Wrap the registration in an amendment check to ensure the function is only -> available after the corresponding amendment is enabled on the network. +A module must export `escrow_finish` (`escrowFunctionName`) taking no parameters and +returning `int32_t`: positive means the conditions are met, zero or negative rejects the +finish. Everything the contract needs it asks for through a host call. diff --git a/include/xrpl/tx/wasm/WasmCommon.h b/include/xrpl/tx/wasm/WasmCommon.h index e2e5193709..d3a3d2df3f 100644 --- a/include/xrpl/tx/wasm/WasmCommon.h +++ b/include/xrpl/tx/wasm/WasmCommon.h @@ -1,16 +1,19 @@ #pragma once +#include #include #include +#include #include #include #include #include -#include +#include +#include #include +#include #include -#include #include #include #include @@ -21,32 +24,7 @@ using Bytes = std::vector; using Hash = xrpl::uint256; using FloatPair = std::pair; -// Error signals that cross the wasm boundary as trap messages (the C API has no -// trap code). WasmiEngine::call maps them to TER: hfErrInternal -> tecINTERNAL, -// hfErrOutOfGas / wasmi's OutOfFuel -> tecOUT_OF_GAS, anything else -> -// tecFAILED_PROCESSING. -// -// Matched as substrings, not by equality: the C API returns the Rust Debug form -// of the error, e.g. `Error { kind: Message("HfInternal") }` or -// `Error { kind: TrapCode(OutOfFuel) }`. -std::string_view inline constexpr hfErrInternal = "HfInternal"; -std::string_view inline constexpr hfErrOutOfGas = "HfOutOfGas"; -std::string_view inline constexpr wasmiTrapOutOfFuel = "OutOfFuel"; - -// Guest ABI, mirrored in the wasm stdlib: append only, never renumber. Starts at -// 1 so a zeroed data_type is rejected rather than treated as Int64. -enum class TraceDataType : std::int32_t { - Int64 = 1, - Uint64, - Xfloat, - Account, - Amount, - AsHex, // raw bytes, hex-encoded by the host before printing - AsText, // bytes printed verbatim as text -}; - enum class HostFunctionError : int32_t { - Success = 0, Unimplemented = -1, FieldNotFound = -2, BufferTooSmall = -3, @@ -69,19 +47,15 @@ enum class HostFunctionError : int32_t { FloatComputationError = -20, SubmitTxnFailure = -21, InvalidState = -22, -}; -enum class WasmTypes { WtI32, WtI64 }; - -struct Wmem -{ - std::uint8_t* p = nullptr; - std::size_t s = 0; - - Wmem() = default; - Wmem(void* ptr, std::size_t size) : p(reinterpret_cast(ptr)), s(size) - { - } + // The call was not served at all, so the engine stops the run and the transaction is + // tecINTERNAL rather than the contract being handed a code to interpret. `guarded` + // answers it for a host body that throws. + // + // The only entry outside the -1 ..= -22 range a contract reads: it needs no number + // there, and INT32_MIN cannot collide with a code appended above. Negative so that a + // reader treating it as an ordinary failure is still right. + InternalFatal = std::numeric_limits::min(), }; template @@ -151,71 +125,6 @@ public: } }; -class WasmRuntimeWrapper -{ -public: - virtual ~WasmRuntimeWrapper() = default; - - virtual Wmem - getMem() = 0; - - virtual std::int64_t - getGas() = 0; - - virtual std::int64_t - setGas(std::int64_t gas) = 0; - - virtual std::int64_t - getTransferLimit() = 0; - - virtual std::int64_t - setTransferLimit(std::int64_t transferLimit) = 0; -}; -using RTOptRef = std::optional>; - -struct WasmParam -{ - // We are not supporting float/double - - WasmTypes type = WasmTypes::WtI32; - union - { - std::int32_t i32; - std::int64_t i64 = 0; - } of; -}; - -template -inline void -wasmParamsHlp(std::vector& v, std::int32_t p, Types&&... args) -{ - v.push_back({.type = WasmTypes::WtI32, .of = {.i32 = p}}); - wasmParamsHlp(v, std::forward(args)...); -} - -template -inline void -wasmParamsHlp(std::vector& v, std::int64_t p, Types&&... args) -{ - v.push_back({.type = WasmTypes::WtI64, .of = {.i64 = p}}); - wasmParamsHlp(v, std::forward(args)...); -} - -inline void -wasmParamsHlp(std::vector& v) -{ -} - -template -inline std::vector -wasmParams(Types&&... args) -{ - std::vector v; - v.reserve(sizeof...(args)); - wasmParamsHlp(v, std::forward(args)...); - return v; -} - template constexpr T adjustWasmEndianessHlp(T x) @@ -253,4 +162,28 @@ hfErrorToInt(HostFunctionError e) return static_cast(e); } +template +std::invoke_result_t +guarded( + beast::Journal journal, + std::invoke_result_t onThrow, + Body&& body, + std::source_location const location = std::source_location::current()) noexcept +{ + try + { + return body(); + } + catch (std::exception const& e) + { + JLOG(journal.error()) << "wasm: " << location.function_name() << " threw: " << e.what(); + } + catch (...) + { + JLOG(journal.error()) << "wasm: " << location.function_name() << " threw"; + } + + return onThrow; +} + } // namespace xrpl diff --git a/include/xrpl/tx/wasm/WasmImportsHelper.h b/include/xrpl/tx/wasm/WasmImportsHelper.h deleted file mode 100644 index 0c31e969c1..0000000000 --- a/include/xrpl/tx/wasm/WasmImportsHelper.h +++ /dev/null @@ -1,126 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include - -namespace bft = boost::function_types; - -namespace xrpl { - -using wasmSecondaryCbFuncType = - wasm_trap_t*(HostFunctions&, wasm_val_vec_t const*, wasm_val_vec_t*); - -struct WasmImportFunc -{ - std::string_view name; - std::optional result; - std::vector params; - - wasmSecondaryCbFuncType* wrap = nullptr; - uint32_t gas = 0; -}; - -using WasmUserData = std::pair; -// string - import function name -using ImportVec = std::unordered_map; - -template -void -WasmImpArgs(WasmImportFunc& e) -{ - if constexpr (N < C) - { - using at = boost::mpl::at_c::type; - if constexpr (std::is_pointer_v || std::is_same_v) - { - e.params.push_back(WasmTypes::WtI32); - } - else if constexpr (std::is_same_v) - { - e.params.push_back(WasmTypes::WtI64); - } - else - { - static_assert(std::is_pointer_v, "Unsupported argument type"); - } - - return WasmImpArgs(e); - } -} - -template -inline constexpr bool wasmDependentFalse = false; - -template -void -WasmImpRet(WasmImportFunc& e) -{ - if constexpr (std::is_pointer_v || std::is_same_v) - { - e.result = WasmTypes::WtI32; - } - else if constexpr (std::is_same_v) - { - e.result = WasmTypes::WtI64; - } - else if constexpr (std::is_void_v) - { - e.result.reset(); - } - else - { - static_assert(wasmDependentFalse, "Unsupported return type"); - } -} - -template -void -WasmImpFuncHelper(WasmImportFunc& e) -{ - using rt = bft::result_type::type; - using pt = bft::parameter_types::type; - // typename boost::mpl::at_c::type - - WasmImpRet(e); - WasmImpArgs<0, bft::function_arity::value, pt>(e); - // WasmImpWrap(e, std::forward(f)); -} - -// imp_name - string literal, must have static lifetime -template -void -WasmImpFunc( - ImportVec& v, - std::string_view impName, - wasmSecondaryCbFuncType* fWrap, - HostFunctions& hf, - uint32_t gas = 0) -{ - WasmImportFunc e; - e.name = impName; - e.wrap = fWrap; - e.gas = gas; - WasmImpFuncHelper(e); - v.emplace(impName, std::make_pair(HFRef(hf), std::move(e))); -} - -#define WASM_IMPORT_FUNC(v, f, ...) WasmImpFunc(v, #f, &f##_wrap, ##__VA_ARGS__) - -// n - string literal name, must have static lifetime -#define WASM_IMPORT_FUNC2(v, f, n, ...) WasmImpFunc(v, n, &f##_wrap, ##__VA_ARGS__) - -} // namespace xrpl diff --git a/include/xrpl/tx/wasm/WasmVM.h b/include/xrpl/tx/wasm/WasmVM.h index e20488de00..51ea24dce1 100644 --- a/include/xrpl/tx/wasm/WasmVM.h +++ b/include/xrpl/tx/wasm/WasmVM.h @@ -4,94 +4,47 @@ #include #include #include -#include #include #include -#include -#include #include -#include namespace xrpl { -std::string_view inline constexpr wEnv = "env"; -std::string_view inline constexpr wHostLib = "host_lib"; -std::string_view inline constexpr wMem = "memory"; -std::string_view inline constexpr wStore = "store"; -std::string_view inline constexpr wLoad = "load"; -std::string_view inline constexpr wSize = "size"; -std::string_view inline constexpr wAlloc = "allocate"; -std::string_view inline constexpr wDealloc = "deallocate"; -std::string_view inline constexpr wProcExit = "proc_exit"; - +// The export a programmable escrow's contract is run through. std::string_view inline constexpr escrowFunctionName = "escrow_finish"; -uint32_t inline constexpr maxPages = 128; // 8MB = 64KB*128 - -class WasmiEngine; - -class WasmEngine -{ - std::unique_ptr const impl_; - - WasmEngine(); - -public: - WasmEngine(WasmEngine const&) = delete; - WasmEngine(WasmEngine&&) = delete; - WasmEngine& - operator=(WasmEngine const&) = delete; - WasmEngine& - operator=(WasmEngine&&) = delete; - - static WasmEngine& - instance(); - - std::expected, WasmTER> - run(Bytes const& wasmCode, - HostFunctions& hfs, - int64_t gasLimit, - std::string_view funcName = {}, - std::vector const& params = {}, - ImportVec const& imports = {}, - beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); - - NotTEC - check( - Bytes const& wasmCode, - HostFunctions& hfs, - std::string_view funcName, - std::vector const& params = {}, - ImportVec const& imports = {}, - beast::Journal j = beast::Journal{beast::Journal::getNullSink()}); - - // Host functions helper functionality - void* - newTrap(std::string const& txt = std::string()); - - [[nodiscard]] beast::Journal - getJournal() const; -}; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -ImportVec -createWasmImport(HostFunctions& hfs); - +// Run `wasmCode`'s `funcName` export with `gasLimit` gas, servicing its host calls +// through `hfs`. +// +// On success the result is what the contract returned - positive means the escrow may +// finish - together with the gas it consumed. On failure it is the TER to apply and, +// when the number means anything, the gas to write to transaction metadata: a contract +// that traps or exhausts its budget is charged for what it burned, while a `tecINTERNAL` +// reports no cost because the fault is the node's rather than the transaction's. std::expected runEscrowWasm( Bytes const& wasmCode, HostFunctions& hfs, - int64_t gasLimit, - std::string_view funcName = escrowFunctionName, - std::vector const& params = {}); + std::int64_t gasLimit, + std::string_view funcName = escrowFunctionName) noexcept; +// Screen `wasmCode`: whether `runEscrowWasm` would refuse it before the contract's +// first instruction. Compiles the module and reads its imports and exports; runs +// nothing. +// +// Takes no `HostFunctions`, because the verdict comes from the compiled module alone. +// That is what makes this callable from a transactor's `preflight`, which has no view +// to build a host over. +// +// `temINVALID_BYTECODE` for every fault in the module - the transaction carries something this +// engine cannot run, so it is refused before it can reach the ledger. +// `telFAILED_PROCESSING` if the engine itself failed: nothing was learned about the +// module, and a defect here is not evidence that the transaction is malformed. NotTEC preflightEscrowWasm( Bytes const& wasmCode, - HostFunctions& hfs, - std::string_view funcName = escrowFunctionName, - std::vector const& params = {}); + beast::Journal j, + std::string_view funcName = escrowFunctionName) noexcept; } // namespace xrpl diff --git a/include/xrpl/tx/wasm/WasmiVM.h b/include/xrpl/tx/wasm/WasmiVM.h deleted file mode 100644 index 5a72cd35f6..0000000000 --- a/include/xrpl/tx/wasm/WasmiVM.h +++ /dev/null @@ -1,462 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace xrpl { - -template -class WasmVec -{ - using TD = std::remove_pointer_t; - T vec_; - -public: - WasmVec(size_t s = 0) : vec_ WASM_EMPTY_VEC - { - if (s > 0) - Create(&vec_, s); // zeroes memory - } - - ~WasmVec() - { - clear(); - } - - WasmVec(WasmVec const&) = delete; - WasmVec& - operator=(WasmVec const&) = delete; - - WasmVec(WasmVec&& other) noexcept : vec_ WASM_EMPTY_VEC - { - *this = std::move(other); - } - - WasmVec& - operator=(WasmVec&& other) noexcept - { - if (this != &other) - { - clear(); - vec_ = other.vec_; - other.vec_ = WASM_EMPTY_VEC; - } - return *this; - } - - void - clear() - { - Destroy(&vec_); // call destructor for every elements too - vec_ = WASM_EMPTY_VEC; - } - - T - release() - { - T result = vec_; - vec_ = WASM_EMPTY_VEC; - return result; - } - - T* - get() - { - return &vec_; - } - - [[nodiscard]] T const* - get() const - { - return &vec_; - } - - TD& - operator[](size_t i) - { - if (i >= vec_.size) - Throw("Out of bound"); - return vec_.data[i]; - } - - TD const& - operator[](size_t i) const - { - if (i >= vec_.size) - Throw("Out of bound"); - return vec_.data[i]; - } - - [[nodiscard]] size_t - size() const - { - return vec_.size; - } - - [[nodiscard]] bool - empty() const - { - return vec_.size == 0u; - } -}; - -using WasmValtypeVec = - WasmVec; -using WasmValVec = WasmVec; -using WasmExternVec = - WasmVec; -using WasmExporttypeVec = WasmVec< - wasm_exporttype_vec_t, - &wasm_exporttype_vec_new_uninitialized, - &wasm_exporttype_vec_delete>; -using WasmImporttypeVec = WasmVec< - wasm_importtype_vec_t, - &wasm_importtype_vec_new_uninitialized, - &wasm_importtype_vec_delete>; - -struct WasmiResult -{ - WasmValVec r; - // Set iff the call trapped. Holds the TER the trap was classified into - // (tecINTERNAL / tecOUT_OF_GAS / tecFAILED_PROCESSING); see - // WasmiEngine::call. std::nullopt means the call returned normally. - std::optional ter; - - WasmiResult(unsigned n = 0) : r(n) - { - } - - WasmiResult() = delete; - ~WasmiResult() = default; - WasmiResult(WasmiResult&& o) = default; - WasmiResult& - operator=(WasmiResult&& o) = default; -}; - -using ModulePtr = std::unique_ptr; -using InstancePtr = std::unique_ptr; -using EnginePtr = std::unique_ptr; -using StorePtr = std::unique_ptr; - -using FuncInfo = std::pair; - -class InstanceWrapper -{ - wasm_store_t* store_ = nullptr; - WasmExternVec exports_; - mutable int memIdx_ = -1; - InstancePtr instance_; - beast::Journal j_ = beast::Journal(beast::Journal::getNullSink()); - std::int64_t transferLimit_ = kWasmTransferLimit; - -private: - static InstancePtr - init( - StorePtr& s, - ModulePtr& m, - WasmExternVec& expt, - WasmExternVec const& imports, - beast::Journal j); - -public: - InstanceWrapper() : instance_(nullptr, &wasm_instance_delete) {}; - - InstanceWrapper(InstanceWrapper const&) = delete; - - InstanceWrapper(InstanceWrapper&& o) : instance_(nullptr, &wasm_instance_delete) - { - *this = std::move(o); // LCOV_EXCL_LINE - } - - InstanceWrapper(StorePtr& s, ModulePtr& m, WasmExternVec const& imports, beast::Journal j) - : store_(s.get()), instance_(init(s, m, exports_, imports, j)), j_(j) - { - } - - InstanceWrapper& - operator=(InstanceWrapper&& o); - - InstanceWrapper& - operator=(InstanceWrapper const&) = delete; - - operator bool() const - { - return static_cast(instance_); - } - - FuncInfo - getFunc(std::string_view funcName, WasmExporttypeVec const& exportTypes) const; - - Wmem - getMem() const; - - std::int64_t - getGas() const; - - std::int64_t - setGas(std::int64_t) const; - - std::int64_t - getTransferLimit() const; - - std::int64_t - setTransferLimit(std::int64_t); -}; - -class ModuleWrapper -{ - ModulePtr module_; - InstanceWrapper instanceWrap_; - WasmExporttypeVec exportTypes_; - beast::Journal j_ = beast::Journal(beast::Journal::getNullSink()); - -public: - // LCOV_EXCL_START - ModuleWrapper() : module_(nullptr, &wasm_module_delete) - { - } - - ModuleWrapper(ModuleWrapper&& o) : module_(nullptr, &wasm_module_delete) - { - *this = std::move(o); - } - // LCOV_EXCL_STOP - - ModuleWrapper& - operator=(ModuleWrapper&& o); - ModuleWrapper( - StorePtr& s, - Bytes const& wasmBin, - bool instantiate, - ImportVec const& imports, - beast::Journal j); - ~ModuleWrapper() = default; - - operator bool() const - { - return instanceWrap_; - } - - FuncInfo - getFunc(std::string_view funcName) const - { - return instanceWrap_.getFunc(funcName, exportTypes_); - } - - wasm_functype_t const* - getFuncType(std::string_view funcName) const; - - Wmem - getMem() const - { - return instanceWrap_.getMem(); - } - - InstanceWrapper& - getInstance(int i = 0) - { - return instanceWrap_; - } - - InstanceWrapper const& - getInstance(int i = 0) const - { - return instanceWrap_; - } - - int - addInstance(StorePtr& s, WasmExternVec const& imports) - { - instanceWrap_ = {s, module_, imports, j_}; - return 0; - } - - std::int64_t - getGas() const - { - return instanceWrap_ ? instanceWrap_.getGas() : -1; - } - -private: - static ModulePtr - init(StorePtr& s, Bytes const& wasmBin, beast::Journal j); - - WasmExternVec - buildImports(StorePtr& s, ImportVec const& imports) const; -}; - -class WasmiEngine -{ - EnginePtr engine_; - StorePtr store_; - std::unique_ptr moduleWrap_; - beast::Journal j_ = beast::Journal(beast::Journal::getNullSink()); - - std::mutex m_; // 1 instance mutex - -public: - WasmiEngine() : engine_(init()), store_(nullptr, &wasm_store_delete) - { - } - - ~WasmiEngine() = default; - - static EnginePtr - init(); - - std::expected, WasmTER> - run(Bytes const& wasmCode, - HostFunctions& hfs, - int64_t gas, - std::string_view funcName, - std::vector const& params, - ImportVec const& imports, - beast::Journal j); - - NotTEC - check( - Bytes const& wasmCode, - HostFunctions& hfs, - std::string_view funcName, - std::vector const& params, - ImportVec const& imports, - beast::Journal j); - - [[nodiscard]] std::int64_t - getGas() const - { - return moduleWrap_ ? moduleWrap_->getGas() : -1; // LCOV_EXCL_LINE - } - - // Host functions helper functionality - wasm_trap_t* - newTrap(std::string const& msg); - - // LCOV_EXCL_START - [[nodiscard]] beast::Journal - getJournal() const - { - return j_; - } - // LCOV_EXCL_STOP - -private: - [[nodiscard]] InstanceWrapper& - getRT(int m = 0, int i = 0) const - { - if (!moduleWrap_) - Throw("no module"); - return moduleWrap_->getInstance(i); - } - - [[nodiscard]] Wmem - getMem() const - { - return moduleWrap_ ? moduleWrap_->getMem() : Wmem(); - } - - std::expected, WasmTER> - runHlp( - Bytes const& wasmCode, - HostFunctions& hfs, - int64_t gas, - std::string_view funcName, - std::vector const& params, - ImportVec const& imports, - beast::Journal j); - - NotTEC - checkHlp( - Bytes const& wasmCode, - HostFunctions& hfs, - std::string_view funcName, - std::vector const& params, - ImportVec const& imports, - beast::Journal j); - - int - addModule(Bytes const& wasmCode, bool instantiate, ImportVec const& imports, int64_t gas); - void - clearModules(); - - // int addInstance(); - - int32_t - runFunc(std::string_view const funcName, int32_t p); - - int32_t - makeModule(Bytes const& wasmCode, WasmExternVec const& imports = {}); - - [[nodiscard]] FuncInfo - getFunc(std::string_view funcName) const - { - return moduleWrap_->getFunc(funcName); - } - - static std::vector - convertParams(std::vector const& params); - - static int - compareParamTypes(wasm_valtype_vec_t const* ftp, std::vector const& p); - - static void - addParam(std::vector& in, int32_t p); - static void - addParam(std::vector& in, int64_t p); - - template - inline WasmiResult - call(std::string_view func, Types&&... args); - - template - inline WasmiResult - call(FuncInfo const& f, Types&&... args); - - template - inline WasmiResult - call(FuncInfo const& f, std::vector& in); - - template - inline WasmiResult - call(FuncInfo const& f, std::vector& in, std::int32_t p, Types&&... args); - - template - inline WasmiResult - call(FuncInfo const& f, std::vector& in, std::int64_t p, Types&&... args); - - template - inline WasmiResult - call( - FuncInfo const& f, - std::vector& in, - uint8_t const* d, - int32_t sz, - Types&&... args); - - template - inline WasmiResult - call(FuncInfo const& f, std::vector& in, Bytes const& p, Types&&... args); -}; - -} // namespace xrpl diff --git a/nix/check-tools/README.md b/nix/check-tools/README.md index 5b7538f2ca..f23b2dcc21 100644 --- a/nix/check-tools/README.md +++ b/nix/check-tools/README.md @@ -1,7 +1,8 @@ # check-tools snapshots These files capture the output of [`bin/check-tools.sh`](../../bin/check-tools.sh) -— the versions of the development tooling — in each Nix environment: +— the version and resolved store path of each development tool — in each Nix +environment: | File | Environment | | ---------------------- | ------------------------------------ | @@ -17,9 +18,13 @@ So if you change the environment (bump the image tag in and commit the affected snapshots. Each snapshot is `check-tools.sh` stdout with the git-clone connectivity check -skipped (`CHECK_TOOLS_SKIP_CLONE=1`), so it contains only deterministic version -data. On macOS the dev-shell greeting that `nix develop` prints first is dropped -with `sed -n '/^Detected OS:/,$p'`. +skipped (`CHECK_TOOLS_SKIP_CLONE=1`), so it is deterministic for a given +environment. On macOS the dev-shell greeting that `nix develop` prints first is +dropped with `sed -n '/^Detected OS:/,$p'`. + +The store paths carry their derivation hash, so they change whenever a tool is +rebuilt — a `flake.lock` update generally rewrites most of them even when no +version moves. That is deliberate: it makes tooling changes visible in review. ## Regenerating diff --git a/nix/check-tools/macos.txt b/nix/check-tools/macos.txt index 93cc926181..d2dee651f1 100644 --- a/nix/check-tools/macos.txt +++ b/nix/check-tools/macos.txt @@ -1,47 +1,146 @@ Detected OS: macos (Darwin arm64) Core build tools: - [ ok ] cmake cmake version 4.1.2 - [ ok ] conan Conan version 2.28.1 - [ ok ] git git version 2.54.0 - [ ok ] python3 Python 3.13.13 + ✅ cmake + cmake version 4.1.2 + /nix/store/gvabsb4yqb5xsqzqph54rijnn4zpihnp-cmake-4.1.2/bin/cmake + ✅ conan + Conan version 2.28.1 + /nix/store/9jiyxmkpwmn6dcqs0765s83riw3l5ail-conan-2.28.1/bin/conan + ✅ git + git version 2.54.0 + /nix/store/a14yxcqvv9x2l9mllgpirzhvz93pgprg-git-2.54.0/bin/git + ✅ python3 + Python 3.13.13 + /nix/store/ygxqin6ydzjfawywqpp5pal8wv6sf5bh-python3-3.13.13/bin/python3.13 Development tooling: - [ ok ] ccache ccache version 4.13.6 - [ ok ] clang clang version 22.1.7 - [ ok ] clang++ clang version 22.1.7 - [ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0 - [ ok ] curl curl 8.20.0 (aarch64-apple-darwin25.3.0) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1 - [ ok ] file file-5.47 - [ ok ] less less 692 (PCRE2 regular expressions) - [ ok ] make GNU Make 4.4.1 - [ ok ] netstat present - [ ok ] ninja 1.13.2 - [ ok ] perl v5.42.0 - [ ok ] pkg-config 0.29.2 - [ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00) - [ ok ] zip Zip 3.0 - [ ok ] clang-format clang-format version 22.1.7 - [ ok ] dot dot - graphviz version 12.2.1 (0) - [ ok ] doxygen 1.16.1 - [ ok ] gcovr gcovr 8.4 - [ ok ] gh gh version 2.94.0 (nixpkgs) - [ ok ] git-cliff git-cliff 2.13.1 - [ ok ] git-lfs git-lfs/3.7.1 (3.7.1; darwin arm64; go 1.26.3) - [ ok ] gpg gpg (GnuPG) 2.4.9 - [ ok ] pre-commit pre-commit 4.5.1 - [ ok ] run-clang-tidy usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers] + ✅ ccache + ccache version 4.13.6 + /nix/store/57davyvs6p6dkrl3svzwg1ph18wsy4cz-ccache-4.13.6/bin/ccache + ✅ clang + clang version 22.1.7 + /nix/store/192glrb2cldvziyf3378mzjqbzx3ih4g-clang-wrapper-22.1.7/bin/clang + ✅ clang-22 + clang version 22.1.7 + /nix/store/rbap7zqq7mw00fyqa02p5rj7gqjp4w5i-clang-22/bin/clang-22 + ✅ clang++ + clang version 22.1.7 + /nix/store/192glrb2cldvziyf3378mzjqbzx3ih4g-clang-wrapper-22.1.7/bin/clang++ + ✅ clang++-22 + clang version 22.1.7 + /nix/store/v9haf787f7bcz0mq1sad4bpyx21pj6li-clang++-22/bin/clang++-22 + ✅ ClangBuildAnalyzer + ClangBuildAnalyzer 1.6.0 + /nix/store/4l50ds9fa2mkvh7wg8qzrlbmjs12sb8l-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer + ✅ curl + curl 8.20.0 (aarch64-apple-darwin25.3.0) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1 + /nix/store/kclq0czaxvsgh4ym9ld7b6iwy50l1snk-curl-8.20.0-bin/bin/curl + ✅ file + file-5.47 + /nix/store/dax63li7wwcbqxxkkgzc4g2rx7d4w86x-file-5.47/bin/file + ✅ less + less 692 (PCRE2 regular expressions) + /nix/store/lvr16y75r1pdxpdv0aph5ak2yd0hkvqm-less-692/bin/less + ✅ make + GNU Make 4.4.1 + /nix/store/8wwiw8pwyhrkzyq28hqzxfl4z84lks81-gnumake-4.4.1/bin/make + ✅ netstat + present + /nix/store/qsd1kzqb0ahrk433vmyl245gp623j19s-network_cmds-730.80.3/bin/netstat + ✅ ninja + 1.13.2 + /nix/store/bqykhrblarkj4fl0hz2mf8ngwfv6x6bz-ninja-1.13.2/bin/ninja + ✅ perl + v5.42.0 + /nix/store/js13ri9fvm0ajk1fpd3acigys2a9whdv-perl-5.42.0/bin/perl + ✅ pkg-config + 0.29.2 + /nix/store/lzrwr375jqhhbca116kja96xf1md83l8-pkg-config-wrapper-0.29.2/bin/pkg-config + ✅ vim + VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00) + /nix/store/6vbkykg92w603c0sw3mkk7p7mfaawbns-vim-9.2.0389/bin/vim + ✅ zip + Zip 3.0 + /nix/store/z6ph729vcakbvz3wh8ln1wk6mi06w487-zip-3.0/bin/zip + ✅ clang-apply-replacements + clang-apply-replacements version 22.1.7 + /nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-apply-replacements + ✅ clang-apply-replacements-22 + clang-apply-replacements version 22.1.7 + /nix/store/m3ii69rca4077lf4wlk7m3jcag1fs577-clang-apply-replacements-22/bin/clang-apply-replacements-22 + ✅ clang-format + clang-format version 22.1.7 + /nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-format + ✅ clang-format-22 + clang-format version 22.1.7 + /nix/store/4fawqy6ngqcsqd2ygyyzm93q0xy3f5gs-clang-format-22/bin/clang-format-22 + ✅ clang-tidy + LLVM version 22.1.7 + /nix/store/vzyyjf3cm1hbj9wcr2qcb66x6j98zpy7-clang-tools-22.1.7/bin/clang-tidy + ✅ clang-tidy-22 + LLVM version 22.1.7 + /nix/store/jqw4280saixaxxihwdba9ldm2fsm6dr3-clang-tidy-22/bin/clang-tidy-22 + ✅ dot + dot - graphviz version 12.2.1 (0) + /nix/store/ijb4fbnqa6wzlpqnhb6q9knqpf7qqn5z-graphviz-12.2.1/bin/dot + ✅ doxygen + 1.16.1 + /nix/store/kbryjdpq9jizjb0ws0nzbf2h2ymbdiwm-doxygen-1.16.1/bin/doxygen + ✅ gcovr + gcovr 8.4 + /nix/store/wn8jiyh9p0bybs96s4163qp3k8vfmczx-python3.13-gcovr-8.4/bin/gcovr + ✅ gh + gh version 2.94.0 (nixpkgs) + /nix/store/fhnpw0hs0gjms1ha6ap02jq7rx13gkbp-gh-2.94.0/bin/gh + ✅ git-cliff + git-cliff 2.13.1 + /nix/store/cy0wwhgxa7yvrz97zydbq6sqmixc90fq-git-cliff-2.13.1/bin/git-cliff + ✅ git-lfs + git-lfs/3.7.1 (3.7.1; darwin arm64; go 1.26.3) + /nix/store/k9r7zjfjplqa4d5s71cqvf2iv73jd9mc-git-lfs-3.7.1/bin/git-lfs + ✅ gpg + gpg (GnuPG) 2.4.9 + /nix/store/cgh6iwzz5jgx9z5whka4vgj210i6npc6-gnupg-2.4.9/bin/gpg + ✅ pre-commit + pre-commit 4.5.1 + /nix/store/z3cca68620w0w10f090szgzdnmh1waf2-pre-commit-4.5.1/bin/pre-commit + ✅ run-clang-tidy + usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers] + /nix/store/4x28x911z2f9y7adqlh3qspp4a16dig7-run-clang-tidy/bin/run-clang-tidy + ✅ run-clang-tidy-22 + usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers] + /nix/store/x8iymrh76sk5q91ryg5pa7i32s6gfh34-run-clang-tidy-22/bin/run-clang-tidy-22 Rust toolchain: - [ ok ] cargo cargo 1.95.0 (f2d3ce0bd 2026-03-21) - [ ok ] cargo-audit cargo-audit-audit 0.22.1 - [ ok ] cargo-llvm-cov cargo-llvm-cov 0.8.5 - [ ok ] cargo-nextest cargo-nextest 0.9.137 - [ ok ] clippy clippy 0.1.95 (59807616e1 2026-04-14) - [ ok ] rust-analyzer rust-analyzer 1.95.0 (59807616 2026-04-14) - [ ok ] rustc rustc 1.95.0 (59807616e 2026-04-14) - [ ok ] rustfmt rustfmt 1.9.0-stable (59807616e1 2026-04-14) + ✅ cargo + cargo 1.97.1 (c980f4866 2026-06-30) + /nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/cargo + ✅ cargo-audit + cargo-audit-audit 0.22.1 + /nix/store/snwkga2f5gyf404h7mmp9wriwxb8v65f-cargo-audit-0.22.1/bin/cargo-audit + ✅ cargo-llvm-cov + cargo-llvm-cov 0.8.5 + /nix/store/fpiqdh91gwyxalqp409ynm0s0g086w7w-cargo-llvm-cov-0.8.5/bin/cargo-llvm-cov + ✅ cargo-nextest + cargo-nextest 0.9.137 + /nix/store/ylz7m947mhkgsp6i7611id3s3gcd58nq-cargo-nextest-0.9.137/bin/cargo-nextest + ✅ clippy-driver + clippy 0.1.97 (8bab26f4f6 2026-07-14) + /nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/clippy-driver + ✅ rust-analyzer + rust-analyzer 1.97.1 (8bab26f4 2026-07-14) + /nix/store/j6apc5pmd0giy15da9p650r8zklslmvi-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer + ✅ rust-nightly + rustc 1.99.0-nightly (87e5904f5 2026-07-20) + /nix/store/fqpjz4l0nsnji8b2pz57mnj0akbp6hcl-rust-nightly/bin/rust-nightly + ✅ rustc + rustc 1.97.1 (8bab26f4f 2026-07-14) + /nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/rustc + ✅ rustfmt + rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14) + /nix/store/5ymwgr9jqjz7zzbmj0j5vqbwcd3kp0vm-rustfmt-preview-1.97.1-aarch64-apple-darwin/bin/rustfmt Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set). -All 36 checked tools are present and runnable. +✅ All 45 checked tools are present and runnable. diff --git a/nix/check-tools/nix-ubuntu-amd64.txt b/nix/check-tools/nix-ubuntu-amd64.txt index b922cca4a8..ba5d5e65b1 100644 --- a/nix/check-tools/nix-ubuntu-amd64.txt +++ b/nix/check-tools/nix-ubuntu-amd64.txt @@ -1,55 +1,174 @@ Detected OS: linux (Linux x86_64) Core build tools: - [ ok ] cmake cmake version 4.1.2 - [ ok ] conan Conan version 2.28.1 - [ ok ] git git version 2.54.0 - [ ok ] python3 Python 3.13.13 + ✅ cmake + cmake version 4.1.2 + /nix/store/r9941n32g4wyvggz2703dlplbdq8a6rd-cmake-4.1.2/bin/cmake + ✅ conan + Conan version 2.28.1 + /nix/store/lxny9y4jvjdws7hgz1mygvb7hjrpmna5-conan-2.28.1/bin/conan + ✅ git + git version 2.54.0 + /nix/store/bcnisk3ydfgv26v2gw3zlky24g00yww2-git-2.54.0/bin/git + ✅ python3 + Python 3.13.13 + /nix/store/60m4rxhg2fldqaak400c0lry96ijrzqn-python3-3.13.13/bin/python3.13 Development tooling: - [ ok ] ccache ccache version 4.13.6 - [ ok ] clang clang version 22.1.7 - [ ok ] clang++ clang version 22.1.7 - [ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0 - [ ok ] curl curl 8.20.0 (x86_64-pc-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1 - [ ok ] file file-5.47 - [ ok ] less less 692 (PCRE2 regular expressions) - [ ok ] make GNU Make 4.4.1 - [ ok ] netstat net-tools 2.10 - [ ok ] ninja 1.13.2 - [ ok ] perl v5.42.0 - [ ok ] pkg-config 0.29.2 - [ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00) - [ ok ] zip Zip 3.0 - [ ok ] clang-format clang-format version 22.1.7 - [ ok ] dot dot - graphviz version 12.2.1 (0) - [ ok ] doxygen 1.16.1 - [ ok ] gcovr gcovr 8.4 - [ ok ] gh gh version 2.94.0 (nixpkgs) - [ ok ] git-cliff git-cliff 2.13.1 - [ ok ] git-lfs git-lfs/3.7.1 (3.7.1; linux amd64; go 1.26.3) - [ ok ] gpg gpg (GnuPG) 2.4.9 - [ ok ] pre-commit pre-commit 4.5.1 - [ ok ] run-clang-tidy usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers] + ✅ ccache + ccache version 4.13.6 + /nix/store/c9wwl7s5i6rsfwvf4v0xbbmzx5m6jgfr-ccache-4.13.6/bin/ccache + ✅ clang + clang version 22.1.7 + /nix/store/ff0hrp9r9i3pa5arkdw0sgmzp8d576qi-clang-wrapper-22.1.7/bin/clang + ✅ clang-22 + clang version 22.1.7 + /nix/store/dagc2rq44gfbr7w7yvvqca3yqpc9gqbq-clang-22/bin/clang-22 + ✅ clang++ + clang version 22.1.7 + /nix/store/ff0hrp9r9i3pa5arkdw0sgmzp8d576qi-clang-wrapper-22.1.7/bin/clang++ + ✅ clang++-22 + clang version 22.1.7 + /nix/store/l5m8clin1npl605wdkd8mr18ggxww3z4-clang++-22/bin/clang++-22 + ✅ ClangBuildAnalyzer + ClangBuildAnalyzer 1.6.0 + /nix/store/bshlmn8fqw55nsnm581xqlfbahfkykxx-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer + ✅ curl + curl 8.20.0 (x86_64-pc-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1 + /nix/store/zbwymrp4lcfjc4kkk0n4779v0kjjz58z-curl-8.20.0-bin/bin/curl + ✅ file + file-5.47 + /nix/store/bizyfqdw0h67wzqmp10knmf9s2pqahdb-file-5.47/bin/file + ✅ less + less 692 (PCRE2 regular expressions) + /nix/store/c6bacbn93qg4a7g9n4czww8rg24dvysr-less-692/bin/less + ✅ make + GNU Make 4.4.1 + /nix/store/d3bwqm6bymhy3pdgbvf7vxjqfp31m3j1-gnumake-4.4.1/bin/make + ✅ netstat + net-tools 2.10 + /nix/store/jmyzqvgflnswmws7rnxx6g3zbj680xvd-net-tools-2.10/bin/netstat + ✅ ninja + 1.13.2 + /nix/store/7a235m7crqbb4h49sak20fqxpw3n7hr0-ninja-1.13.2/bin/ninja + ✅ perl + v5.42.0 + /nix/store/6plwsm6pkq79yjv4xvy8csk2pd4hzr67-perl-5.42.0/bin/perl + ✅ pkg-config + 0.29.2 + /nix/store/1m05k7xgfnw6jc21xxk5681ni3ar97wf-pkg-config-wrapper-0.29.2/bin/pkg-config + ✅ vim + VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00) + /nix/store/hvyqx52g4g2fxhgpans3fksjj6lmlyaw-vim-9.2.0389/bin/vim + ✅ zip + Zip 3.0 + /nix/store/qnd2ag67hrjj0b6vbmisdshf50r6s72n-zip-3.0/bin/zip + ✅ clang-apply-replacements + clang-apply-replacements version 22.1.7 + /nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-apply-replacements + ✅ clang-apply-replacements-22 + clang-apply-replacements version 22.1.7 + /nix/store/py2wihg0a96qcppv4hjmww547xabr0fb-clang-apply-replacements-22/bin/clang-apply-replacements-22 + ✅ clang-format + clang-format version 22.1.7 + /nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-format + ✅ clang-format-22 + clang-format version 22.1.7 + /nix/store/kz820ccifjlwqnwqjsx7kbiajrgsmbrh-clang-format-22/bin/clang-format-22 + ✅ clang-tidy + LLVM version 22.1.7 + /nix/store/4zp1rjpj2xijrv4kqpwsy3ixwb2r6nlk-clang-tools-22.1.7/bin/clang-tidy + ✅ clang-tidy-22 + LLVM version 22.1.7 + /nix/store/gdrkvpw846lkyzh8y9p3zx50g6ml2v84-clang-tidy-22/bin/clang-tidy-22 + ✅ dot + dot - graphviz version 12.2.1 (0) + /nix/store/12rgns2296s4qcja778gvcbx61z77rc4-graphviz-12.2.1/bin/dot + ✅ doxygen + 1.16.1 + /nix/store/k0vzr5lvgq1byraknzwvk51wcgpnsrkh-doxygen-1.16.1/bin/doxygen + ✅ gcovr + gcovr 8.4 + /nix/store/iyzi7fpyclqrha054adnizvif02lg49x-python3.13-gcovr-8.4/bin/gcovr + ✅ gh + gh version 2.94.0 (nixpkgs) + /nix/store/pidh15szlsb1vc41xdsa3xbdghdazvby-gh-2.94.0/bin/gh + ✅ git-cliff + git-cliff 2.13.1 + /nix/store/1q851fs62shgjhc03fxxdkpzxdjg7k11-git-cliff-2.13.1/bin/git-cliff + ✅ git-lfs + git-lfs/3.7.1 (3.7.1; linux amd64; go 1.26.3) + /nix/store/6ljwpal7b1756708m33vj0crpral7mvl-git-lfs-3.7.1/bin/git-lfs + ✅ gpg + gpg (GnuPG) 2.4.9 + /nix/store/wx7vk8babxkgy813r70yc67vcwnmagbx-gnupg-2.4.9/bin/gpg + ✅ pre-commit + pre-commit 4.5.1 + /nix/store/bj6i9vl34cij5h0r165y40hrjqak0bmz-pre-commit-4.5.1/bin/pre-commit + ✅ run-clang-tidy + usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers] + /nix/store/sbg911hs9dbclrzlp04br3iyfpgnaj6r-run-clang-tidy/bin/run-clang-tidy + ✅ run-clang-tidy-22 + usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers] + /nix/store/n8yak1ap308gvi7gmrniw0ybsx80fjws-run-clang-tidy-22/bin/run-clang-tidy-22 Rust toolchain: - [ ok ] cargo cargo 1.95.0 (f2d3ce0bd 2026-03-21) - [ ok ] cargo-audit cargo-audit-audit 0.22.1 - [ ok ] cargo-llvm-cov cargo-llvm-cov 0.8.5 - [ ok ] cargo-nextest cargo-nextest 0.9.137 - [ ok ] clippy clippy 0.1.95 (59807616e1 2026-04-14) - [ ok ] rust-analyzer rust-analyzer 1.95.0 (5980761 2026-04-14) - [ ok ] rustc rustc 1.95.0 (59807616e 2026-04-14) - [ ok ] rustfmt rustfmt 1.9.0-stable (59807616e1 2026-04-14) + ✅ cargo + cargo 1.97.1 (c980f4866 2026-06-30) + /nix/store/88abzp43ywyzql1rhf8jh5aj5n5j7xzr-cargo-1.97.1-x86_64-unknown-linux-gnu/bin/cargo + ✅ cargo-audit + cargo-audit-audit 0.22.1 + /nix/store/2w9if868piw98xz057sz97jnjvf7hnvf-cargo-audit-0.22.1/bin/cargo-audit + ✅ cargo-llvm-cov + cargo-llvm-cov 0.8.5 + /nix/store/jjpdf1l6izz6607a346ykra9sndzaw7h-cargo-llvm-cov-0.8.5/bin/cargo-llvm-cov + ✅ cargo-nextest + cargo-nextest 0.9.137 + /nix/store/jhkr7gwyrchkml33gyns9cy0yn7b57qc-cargo-nextest-0.9.137/bin/cargo-nextest + ✅ clippy-driver + clippy 0.1.97 (8bab26f4f6 2026-07-14) + /nix/store/40d3mzka7r1ps71l0yv2fs6616nbw85m-rust-minimal-1.97.1/bin/clippy-driver + ✅ rust-analyzer + rust-analyzer 1.97.1 (8bab26f 2026-07-14) + /nix/store/lr3m97p3hx1k22a7c44pb0wa7rbayhfi-rust-analyzer-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rust-analyzer + ✅ rust-nightly + rustc 1.99.0-nightly (87e5904f5 2026-07-20) + /nix/store/j7kf7a5h4xypzp6x1skg4dsdx2k4fwb3-rust-nightly/bin/rust-nightly + ✅ rustc + rustc 1.97.1 (8bab26f4f 2026-07-14) + /nix/store/40d3mzka7r1ps71l0yv2fs6616nbw85m-rust-minimal-1.97.1/bin/rustc + ✅ rustfmt + rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14) + /nix/store/6f1icmb2za20kxn30pgmbv5jq9fnbf4z-rustfmt-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rustfmt GCC toolchain: - [ ok ] gcc gcc (GCC) 15.2.0 - [ ok ] g++ g++ (GCC) 15.2.0 - [ ok ] gcov gcov (GCC) 15.2.0 + ✅ gcc + gcc (GCC) 15.2.0 + /nix/store/3dd6y3pq00i3r85l45jvz63wjya403nl-gcc-wrapper-15.2.0/bin/gcc + ✅ gcc-15 + gcc (GCC) 15.2.0 + /nix/store/d6iri2s6bzqq5ac3fg25j6hgnn1lz44f-gcc-15/bin/gcc-15 + ✅ g++ + g++ (GCC) 15.2.0 + /nix/store/3dd6y3pq00i3r85l45jvz63wjya403nl-gcc-wrapper-15.2.0/bin/g++ + ✅ g++-15 + g++ (GCC) 15.2.0 + /nix/store/gm3msmmxq055lm9gprkfjj9d2gdz1mpg-g++-15/bin/g++-15 + ✅ cpp + cpp (GCC) 15.2.0 + /nix/store/3dd6y3pq00i3r85l45jvz63wjya403nl-gcc-wrapper-15.2.0/bin/cpp + ✅ cpp-15 + cpp (GCC) 15.2.0 + /nix/store/bn3gmn0m7g4gn2i0yml46fljc7mghiq5-cpp-15/bin/cpp-15 + ✅ gcov + gcov (GCC) 15.2.0 + /nix/store/xvv5sm5i8x0ks6ypfkzl7c4j9srnxz7k-gcc-15.2.0/bin/gcov Mold: - [ ok ] mold mold 2.41.0 (compatible with GNU ld) + ✅ mold + mold 2.41.0 (compatible with GNU ld) + /nix/store/2w6fpgxjzzyqmd25wzplm23dfa49a0p2-mold-unwrapped-wrapper-2.41.0/bin/mold Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set). -All 40 checked tools are present and runnable. +✅ All 53 checked tools are present and runnable. diff --git a/nix/check-tools/nix-ubuntu-arm64.txt b/nix/check-tools/nix-ubuntu-arm64.txt index 5267839682..2b45230327 100644 --- a/nix/check-tools/nix-ubuntu-arm64.txt +++ b/nix/check-tools/nix-ubuntu-arm64.txt @@ -1,55 +1,174 @@ Detected OS: linux (Linux aarch64) Core build tools: - [ ok ] cmake cmake version 4.1.2 - [ ok ] conan Conan version 2.28.1 - [ ok ] git git version 2.54.0 - [ ok ] python3 Python 3.13.13 + ✅ cmake + cmake version 4.1.2 + /nix/store/nkcpxjifkambzlrwh27a8igvhnbchibg-cmake-4.1.2/bin/cmake + ✅ conan + Conan version 2.28.1 + /nix/store/8i2gyqgc00xvxg9xm6y7n0ilncdv8imw-conan-2.28.1/bin/conan + ✅ git + git version 2.54.0 + /nix/store/ixp98f9avf8ikpdrmp40cj33g0dazyp9-git-2.54.0/bin/git + ✅ python3 + Python 3.13.13 + /nix/store/lqn6mbgzzdrqq2qkwddcmxj9z6amdd86-python3-3.13.13/bin/python3.13 Development tooling: - [ ok ] ccache ccache version 4.13.6 - [ ok ] clang clang version 22.1.7 - [ ok ] clang++ clang version 22.1.7 - [ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0 - [ ok ] curl curl 8.20.0 (aarch64-unknown-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1 - [ ok ] file file-5.47 - [ ok ] less less 692 (PCRE2 regular expressions) - [ ok ] make GNU Make 4.4.1 - [ ok ] netstat net-tools 2.10 - [ ok ] ninja 1.13.2 - [ ok ] perl v5.42.0 - [ ok ] pkg-config 0.29.2 - [ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00) - [ ok ] zip Zip 3.0 - [ ok ] clang-format clang-format version 22.1.7 - [ ok ] dot dot - graphviz version 12.2.1 (0) - [ ok ] doxygen 1.16.1 - [ ok ] gcovr gcovr 8.4 - [ ok ] gh gh version 2.94.0 (nixpkgs) - [ ok ] git-cliff git-cliff 2.13.1 - [ ok ] git-lfs git-lfs/3.7.1 (3.7.1; linux arm64; go 1.26.3) - [ ok ] gpg gpg (GnuPG) 2.4.9 - [ ok ] pre-commit pre-commit 4.5.1 - [ ok ] run-clang-tidy usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers] + ✅ ccache + ccache version 4.13.6 + /nix/store/2q39xi2kbi04ibga7635f2sl148d1mzv-ccache-4.13.6/bin/ccache + ✅ clang + clang version 22.1.7 + /nix/store/xjqffrq9i7la058s9865ig71l9sp1ys5-clang-wrapper-22.1.7/bin/clang + ✅ clang-22 + clang version 22.1.7 + /nix/store/vcf6ilfwn57828hwzyp6zlyr24j9j6yw-clang-22/bin/clang-22 + ✅ clang++ + clang version 22.1.7 + /nix/store/xjqffrq9i7la058s9865ig71l9sp1ys5-clang-wrapper-22.1.7/bin/clang++ + ✅ clang++-22 + clang version 22.1.7 + /nix/store/xby0f6gamr7m27zp5cndsvghbp9lgb3c-clang++-22/bin/clang++-22 + ✅ ClangBuildAnalyzer + ClangBuildAnalyzer 1.6.0 + /nix/store/h893hd4q1bb6ily2lby5dzyfrrzd2nvj-clangbuildanalyzer-1.6.0/bin/ClangBuildAnalyzer + ✅ curl + curl 8.20.0 (aarch64-unknown-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1 + /nix/store/i1s0lqwlrmjd2dxzgy2p84cxqqsb0bmk-curl-8.20.0-bin/bin/curl + ✅ file + file-5.47 + /nix/store/dx973zg9km2w9albsib2vw9wyvacfrlw-file-5.47/bin/file + ✅ less + less 692 (PCRE2 regular expressions) + /nix/store/1blb3s7hhsr77wqi598m6k1qkfp3ms0w-less-692/bin/less + ✅ make + GNU Make 4.4.1 + /nix/store/9ngw1ippk25jjj5fjxv36xbp6iq7rxdx-gnumake-4.4.1/bin/make + ✅ netstat + net-tools 2.10 + /nix/store/7vdsz21f0s499s5yyqzp5s4676q4yxdd-net-tools-2.10/bin/netstat + ✅ ninja + 1.13.2 + /nix/store/8ksx98gsbn5lmlizcmw57yd4sg0k2p58-ninja-1.13.2/bin/ninja + ✅ perl + v5.42.0 + /nix/store/5wnly69vv1i3y97al4v3xrqymf9hlzgq-perl-5.42.0/bin/perl + ✅ pkg-config + 0.29.2 + /nix/store/c7vwy0gl1q0agl2h22gi0m9dg7xxad2l-pkg-config-wrapper-0.29.2/bin/pkg-config + ✅ vim + VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00) + /nix/store/v8c7pvx26irvy9k5sbwd183cyvckzzb3-vim-9.2.0389/bin/vim + ✅ zip + Zip 3.0 + /nix/store/5mh19mvbv9ym2sm9vymyyaac5l2cj2jq-zip-3.0/bin/zip + ✅ clang-apply-replacements + clang-apply-replacements version 22.1.7 + /nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-apply-replacements + ✅ clang-apply-replacements-22 + clang-apply-replacements version 22.1.7 + /nix/store/bg4kn8z81hk7b9284rjqvr51wpfjqc24-clang-apply-replacements-22/bin/clang-apply-replacements-22 + ✅ clang-format + clang-format version 22.1.7 + /nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-format + ✅ clang-format-22 + clang-format version 22.1.7 + /nix/store/79v57mzcw8ng8kl7p961ck08ymhp31v7-clang-format-22/bin/clang-format-22 + ✅ clang-tidy + LLVM version 22.1.7 + /nix/store/s53p2m776iqaz7acgr5csgpsd18w15h7-clang-tools-22.1.7/bin/clang-tidy + ✅ clang-tidy-22 + LLVM version 22.1.7 + /nix/store/wdyd6cb9z1lyi37lbzvwldgcc7yv1n5c-clang-tidy-22/bin/clang-tidy-22 + ✅ dot + dot - graphviz version 12.2.1 (0) + /nix/store/58rrk4yzwpmyxvl8cqm18h3dhv24zf00-graphviz-12.2.1/bin/dot + ✅ doxygen + 1.16.1 + /nix/store/hq32kzwpl89wgr49iq0gmqn9r5n072zq-doxygen-1.16.1/bin/doxygen + ✅ gcovr + gcovr 8.4 + /nix/store/sml3xbbfhhlhk6h7jnlg19pdbx9b764b-python3.13-gcovr-8.4/bin/gcovr + ✅ gh + gh version 2.94.0 (nixpkgs) + /nix/store/7hh2qi0gj2ifbxbl56cjzbiyfc379bji-gh-2.94.0/bin/gh + ✅ git-cliff + git-cliff 2.13.1 + /nix/store/bidn3pz53yd6qlg711917xx0q10hqmqv-git-cliff-2.13.1/bin/git-cliff + ✅ git-lfs + git-lfs/3.7.1 (3.7.1; linux arm64; go 1.26.3) + /nix/store/4rsklvkbac5bayy0zv12kxyvspi4sshd-git-lfs-3.7.1/bin/git-lfs + ✅ gpg + gpg (GnuPG) 2.4.9 + /nix/store/ka4i8zz5ni3rzqnzcxbfvwr95fk8pn6q-gnupg-2.4.9/bin/gpg + ✅ pre-commit + pre-commit 4.5.1 + /nix/store/n981w6hjfar2l81kxbxs2wxl64vwa5kj-pre-commit-4.5.1/bin/pre-commit + ✅ run-clang-tidy + usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers] + /nix/store/4z2fyklg78klallr7x9j02kz92hnxp4m-run-clang-tidy/bin/run-clang-tidy + ✅ run-clang-tidy-22 + usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers] + /nix/store/f8m0p9ad40brp9ahy4i0h27kqjkya1j9-run-clang-tidy-22/bin/run-clang-tidy-22 Rust toolchain: - [ ok ] cargo cargo 1.95.0 (f2d3ce0bd 2026-03-21) - [ ok ] cargo-audit cargo-audit-audit 0.22.1 - [ ok ] cargo-llvm-cov cargo-llvm-cov 0.8.5 - [ ok ] cargo-nextest cargo-nextest 0.9.137 - [ ok ] clippy clippy 0.1.95 (59807616e1 2026-04-14) - [ ok ] rust-analyzer rust-analyzer 1.95.0 (5980761 2026-04-14) - [ ok ] rustc rustc 1.95.0 (59807616e 2026-04-14) - [ ok ] rustfmt rustfmt 1.9.0-stable (59807616e1 2026-04-14) + ✅ cargo + cargo 1.97.1 (c980f4866 2026-06-30) + /nix/store/6hch2qrr86n2sa2m90lrpxrfxxwbkayl-cargo-1.97.1-aarch64-unknown-linux-gnu/bin/cargo + ✅ cargo-audit + cargo-audit-audit 0.22.1 + /nix/store/9rxbrn9aa2r1z96186s69pc7vzizyfch-cargo-audit-0.22.1/bin/cargo-audit + ✅ cargo-llvm-cov + cargo-llvm-cov 0.8.5 + /nix/store/vwjsi159n89szrx4yh5pc3jlf2gp4fld-cargo-llvm-cov-0.8.5/bin/cargo-llvm-cov + ✅ cargo-nextest + cargo-nextest 0.9.137 + /nix/store/qb6bcg2fjvm3r9s9j98nmffmf9xwh45s-cargo-nextest-0.9.137/bin/cargo-nextest + ✅ clippy-driver + clippy 0.1.97 (8bab26f4f6 2026-07-14) + /nix/store/a6p27cg6b8szfixfyvkssx6l0c345zw8-rust-minimal-1.97.1/bin/clippy-driver + ✅ rust-analyzer + rust-analyzer 1.97.1 (8bab26f 2026-07-14) + /nix/store/262830dlw2517lnagfx7i7agqgl4fmsd-rust-analyzer-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rust-analyzer + ✅ rust-nightly + rustc 1.99.0-nightly (87e5904f5 2026-07-20) + /nix/store/c59pxk1yikdlf129qwyg4fplmxcrha0k-rust-nightly/bin/rust-nightly + ✅ rustc + rustc 1.97.1 (8bab26f4f 2026-07-14) + /nix/store/a6p27cg6b8szfixfyvkssx6l0c345zw8-rust-minimal-1.97.1/bin/rustc + ✅ rustfmt + rustfmt 1.9.0-stable (8bab26f4f6 2026-07-14) + /nix/store/nd8g81wv1smnvdpy4whpcyv2siwjmaan-rustfmt-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rustfmt GCC toolchain: - [ ok ] gcc gcc (GCC) 15.2.0 - [ ok ] g++ g++ (GCC) 15.2.0 - [ ok ] gcov gcov (GCC) 15.2.0 + ✅ gcc + gcc (GCC) 15.2.0 + /nix/store/rn6svg593xsmn8qcjzk8x9pa1i62c4kb-gcc-wrapper-15.2.0/bin/gcc + ✅ gcc-15 + gcc (GCC) 15.2.0 + /nix/store/h489d1rmjisfbxh5kmsb0a7c35j8qsdf-gcc-15/bin/gcc-15 + ✅ g++ + g++ (GCC) 15.2.0 + /nix/store/rn6svg593xsmn8qcjzk8x9pa1i62c4kb-gcc-wrapper-15.2.0/bin/g++ + ✅ g++-15 + g++ (GCC) 15.2.0 + /nix/store/9ywmhz8bmzknrn3pn84g46z8hj3vrmw5-g++-15/bin/g++-15 + ✅ cpp + cpp (GCC) 15.2.0 + /nix/store/rn6svg593xsmn8qcjzk8x9pa1i62c4kb-gcc-wrapper-15.2.0/bin/cpp + ✅ cpp-15 + cpp (GCC) 15.2.0 + /nix/store/vmjilh1b830qz9yh0a1jj5ads0jxizdk-cpp-15/bin/cpp-15 + ✅ gcov + gcov (GCC) 15.2.0 + /nix/store/rmwf5hpi1y2m1wpnfvlxmrhksm4djk2j-gcc-15.2.0/bin/gcov Mold: - [ ok ] mold mold 2.41.0 (compatible with GNU ld) + ✅ mold + mold 2.41.0 (compatible with GNU ld) + /nix/store/f5qh5a0bx1dslmnf5n5gx0s6aljbswq3-mold-unwrapped-wrapper-2.41.0/bin/mold Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set). -All 40 checked tools are present and runnable. +✅ All 53 checked tools are present and runnable. diff --git a/nix/ci-env.nix b/nix/ci-env.nix index 787b94406e..779b5b7230 100644 --- a/nix/ci-env.nix +++ b/nix/ci-env.nix @@ -1,67 +1,39 @@ +# The environment CI builds in: every tool on PATH, no Nix stdenv setup hooks. +# Baked into the `nix-*` Docker images on Linux (see nix/docker), built on the +# runner on macOS (see .github/actions/setup-nix-env). { pkgs, customGlibc, ... }: let - inherit (import ./packages.nix { inherit pkgs; }) - commonPackages - gccVersion - llvmVersion - mkVersionedToolLinks - ; + inherit (import ./packages.nix { inherit pkgs; }) commonPackages; - # Custom-glibc toolchain, shared with the Linux dev shell (see compilers.nix). - inherit (import ./compilers.nix { inherit pkgs customGlibc; }) - customGcc - customClang - customBinutils - customGcov - ; + # Each forces something absent on the other platform, so both stay lazy. + linux = import ./linux.nix { inherit pkgs customGlibc; }; + darwin = import ./darwin.nix { inherit pkgs; }; - # Strip the generic cc/c++/cpp symlinks from the clang wrapper so it can - # coexist with the gcc wrapper in buildEnv. gcc remains the default - # compiler (cc/c++/cpp); clang is invoked explicitly as clang/clang++. - customClangForCiEnv = pkgs.symlinkJoin { - name = "clang-wrapper-custom-for-ci-env"; - paths = [ customClang ]; - postBuild = '' - rm -f $out/bin/cc $out/bin/c++ $out/bin/cpp - ''; - }; + # What a buildEnv cannot express: environment variables. $GITHUB_ENV format; + # `set -a; . env; set +a` loads it in a shell. + darwinEnv = pkgs.writeTextDir "share/xrpld-ci-env/env" ( + pkgs.lib.concatStrings ( + pkgs.lib.mapAttrsToList (name: value: "${name}=${value}\n") (darwin.sdkEnv // darwin.libresolvEnv) + ) + ); + toolchain = if pkgs.stdenv.isLinux then linux.toolchain else (darwin.toolchain ++ [ darwinEnv ]); in { default = pkgs.buildEnv { name = "xrpld-ci-env"; - paths = commonPackages ++ [ - customGcc - customGcov - customClangForCiEnv - customBinutils - (mkVersionedToolLinks { - name = "gcc"; - package = customGcc; - version = gccVersion; - tools = [ - "gcc" - "g++" - "cpp" - ]; - }) - (mkVersionedToolLinks { - name = "clang"; - package = customClang; - version = llvmVersion; - tools = [ - "clang" - "clang++" - ]; - }) - # CA certificate bundle so HTTPS clients (git, curl, conan) can verify - # TLS connections without ca-certificates being installed in the system. - pkgs.cacert - ]; + paths = + commonPackages + ++ toolchain + ++ [ + # CA certificate bundle so HTTPS clients (git, curl, conan) can verify + # TLS connections without ca-certificates being installed in the system. + pkgs.cacert + ]; pathsToLink = [ "/bin" "/etc/ssl/certs" diff --git a/nix/darwin.nix b/nix/darwin.nix new file mode 100644 index 0000000000..837752fc6a --- /dev/null +++ b/nix/darwin.nix @@ -0,0 +1,80 @@ +# The darwin toolchain, counterpart to linux.nix. Split by consumer: a dev +# shell's stdenv provides the SDK variables, nothing provides libresolv. +# +# darwin only - `libresolv` does not exist on Linux. +{ pkgs }: +let + inherit (import ./packages.nix { inherit pkgs; }) + llvmVersion + llvmPackages + mkVersionedToolLinks + ; + + # nixpkgs keeps libresolv out of the macOS SDK, so neither c-ares' `-lresolv` + # nor grpc's resolves. Headers can come from nixpkgs; the + # library cannot, or its store path lands in xrpld - hence this copy. + libresolvSystemStub = + pkgs.runCommand "libresolv-system-stub" + { + nativeBuildInputs = [ llvmPackages.bintools ]; + } + '' + mkdir -p "$out/lib" + cp ${pkgs.darwin.libresolv}/lib/libresolv.9.dylib "$out/lib/" + chmod +w "$out/lib/libresolv.9.dylib" + llvm-install-name-tool -id /usr/lib/libresolv.9.dylib "$out/lib/libresolv.9.dylib" + ln -s libresolv.9.dylib "$out/lib/libresolv.dylib" + ''; +in +{ + # For an environment that only puts binaries on PATH. + toolchain = [ + llvmPackages.clang + # The wrappers re-export only part of cctools; a bare env has no stdenv to + # supply the rest, and without `dsymutil` even `clang -g` cannot link. One + # by one, because buildEnv rejects any name a wrapper owns (notably `ld`). + (pkgs.linkFarm "cctools-extra" ( + map + (tool: { + name = "bin/${tool}"; + path = "${llvmPackages.clang.bintools.bintools}/bin/${tool}"; + }) + [ + "codesign_allocate" + "dsymutil" + "dwarfdump" + "install_name_tool" + "lipo" + "otool" + ] + )) + (mkVersionedToolLinks { + name = "clang"; + package = llvmPackages.clang; + version = llvmVersion; + tools = [ + "clang" + "clang++" + ]; + }) + ]; + + # Without these CMake asks `xcrun` and gets the Command Line Tools SDK, whose + # headers clash with the Nix libc++ ones. + sdkEnv = { + DEVELOPER_DIR = "${pkgs.apple-sdk}"; + SDKROOT = "${pkgs.apple-sdk}/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk"; + }; + + # Salted names: the wrappers only read plain NIX_CFLAGS_COMPILE / NIX_LDFLAGS + # through role variables a Nix stdenv would set. The salt is the target + # platform, so this fits the gcc wrapper too. + # + # No space after -isystem: these are written one per line as KEY=VALUE, and a + # shell sourcing that reads the space as the end of the assignment. + libresolvEnv = { + "NIX_CFLAGS_COMPILE_${llvmPackages.clang.suffixSalt}" = + "-isystem${pkgs.darwin.libresolv.dev}/include"; + "NIX_LDFLAGS_${llvmPackages.clang.bintools.suffixSalt}" = "-L${libresolvSystemStub}/lib"; + }; +} diff --git a/nix/devshell.nix b/nix/devshell.nix index cb4a99c76a..07f7143c5b 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -14,26 +14,55 @@ let plainGccStdenv = pkgs."gcc${toString gccVersion}Stdenv"; plainClangStdenv = llvmPackages.stdenv; - # Custom-glibc stdenvs, matching the CI environment (see compilers.nix). The - # pinned glibc snapshot only builds on Linux, so on darwin these fall back to - # the plain stdenvs; the `if isLinux` guard keeps `customGlibc` from being - # forced (and erroring) on macOS. - customCompilers = import ./compilers.nix { inherit pkgs customGlibc; }; - customGccStdenv = if pkgs.stdenv.isLinux then customCompilers.customStdenv else plainGccStdenv; - customClangStdenv = - if pkgs.stdenv.isLinux then customCompilers.customClangStdenv else plainClangStdenv; + # Each forces something absent on the other platform, so both stay lazy. + linux = import ./linux.nix { inherit pkgs customGlibc; }; + darwin = import ./darwin.nix { inherit pkgs; }; + + # Custom-glibc stdenvs, matching the CI environment. darwin has no custom + # glibc, so there they fall back to the plain nixpkgs stdenvs. + customGccStdenv = if pkgs.stdenv.isLinux then linux.gccStdenv else plainGccStdenv; + customClangStdenv = if pkgs.stdenv.isLinux then linux.clangStdenv else plainClangStdenv; # gcov matching each gcc shell, so `-Dcoverage=ON` builds work in the shell. plainGcov = mkGcov { name = "plain"; cc = gccPackage.cc; }; - customGccGcov = if pkgs.stdenv.isLinux then customCompilers.customGcov else plainGcov; + customGccGcov = if pkgs.stdenv.isLinux then linux.gcov else plainGcov; + + # Whole directory: init.sh locates the profiles relative to itself. + conanDir = ../conan; + + # Own Conan home, so Nix-built packages never share a cache with a system + # Conan. The stamp holds a content-addressed store path, so init.sh re-runs + # only when something in conan/ changes. + conanHook = '' + export CONAN_HOME=~/.conan2-nix + _xrpl_conan_stamp="$CONAN_HOME/.xrpld-devshell" + if [ "$(cat "$_xrpl_conan_stamp" 2>/dev/null)" != "${conanDir}" ]; then + if ${conanDir}/init.sh; then + printf '%s' "${conanDir}" >"$_xrpl_conan_stamp" + else + echo "⚠️ Conan setup failed - run ./conan/init.sh from the repository root to retry." + fi + fi + unset _xrpl_conan_stamp + ''; + + # Not sdkEnv: a shell's stdenv already sets that up. Prepended so the stub + # beats the nixpkgs libresolv this shell's tooling drags in. + darwinLibresolvHook = pkgs.lib.optionalString pkgs.stdenv.isDarwin ( + pkgs.lib.concatLines ( + pkgs.lib.mapAttrsToList ( + name: value: ''export ${name}="${value} ''${${name}:-}"'' + ) darwin.libresolvEnv + ) + ); # Shown when entering a *-plain shell. These exist only on Linux (see below), # where the stock toolchain diverges from CI. plainWarningHook = '' - echo "⚠️ WARNING: this is the stock nixpkgs toolchain and does not match CI's glibc. Prefer 'nix develop .#gcc' / '.#clang' unless you need to skip the custom-glibc build." + echo "⚠️ WARNING: this is the stock nixpkgs toolchain and does not match CI's glibc. Prefer 'nix develop .#gcc' / '.#clang' unless you need to skip the custom-glibc build." ''; # Tools to expose under version-suffixed names (see mkVersionedToolLinks). @@ -87,6 +116,8 @@ let shellHook = '' echo "Welcome to xrpld development shell"; ${compilerVersionHook} + ${darwinLibresolvHook} + ${conanHook} ${warningHook} ''; } diff --git a/nix/docker/Dockerfile b/nix/docker/Dockerfile index 74c630cb61..7eae693ad2 100644 --- a/nix/docker/Dockerfile +++ b/nix/docker/Dockerfile @@ -8,8 +8,10 @@ RUN mkdir -p ~/.config/nix && \ # Copy our source and setup our working dir. COPY nix/ci-env.nix /tmp/build/nix/ci-env.nix -COPY nix/compilers.nix /tmp/build/nix/compilers.nix +COPY nix/linux.nix /tmp/build/nix/linux.nix COPY nix/packages.nix /tmp/build/nix/packages.nix +COPY nix/rust-nightly.sh /tmp/build/nix/rust-nightly.sh +COPY nix/rust.nix /tmp/build/nix/rust.nix COPY nix/utils.nix /tmp/build/nix/utils.nix COPY flake.nix /tmp/build/ COPY flake.lock /tmp/build/ diff --git a/nix/compilers.nix b/nix/linux.nix similarity index 75% rename from nix/compilers.nix rename to nix/linux.nix index 90856afacc..ea808fbf50 100644 --- a/nix/compilers.nix +++ b/nix/linux.nix @@ -1,7 +1,9 @@ -# Custom-glibc compiler toolchain shared by the CI environment (ci-env.nix) and -# the Linux dev shell (devshell.nix): gcc / clang / binutils rebuilt to target -# the pinned custom glibc. Linux only — the pinned glibc snapshot does not build -# on darwin, so callers must not evaluate this on macOS. +# The Linux toolchain: gcc / clang / binutils rebuilt to target the pinned +# custom glibc, shared by the CI environment (ci-env.nix) and the dev shell +# (devshell.nix). The counterpart to darwin.nix. +# +# Linux only — the pinned glibc snapshot does not build on darwin, so callers +# must not evaluate this on macOS. { pkgs, customGlibc, @@ -9,9 +11,11 @@ let inherit (import ./packages.nix { inherit pkgs; }) gccPackage + gccVersion llvmPackages llvmVersion mkGcov + mkVersionedToolLinks ; # binutils wrapped to emit binaries that reference the custom glibc @@ -103,15 +107,46 @@ let echo "-isystem ${customCompilerRt.dev}/include" >> $out/nix-support/cc-cflags ''; }; + # Strip the generic cc/c++/cpp symlinks from the clang wrapper so it can + # coexist with the gcc wrapper in buildEnv. gcc remains the default + # compiler (cc/c++/cpp); clang is invoked explicitly as clang/clang++. + customClangForCiEnv = pkgs.symlinkJoin { + name = "clang-wrapper-custom-for-ci-env"; + paths = [ customClang ]; + postBuild = '' + rm -f $out/bin/cc $out/bin/c++ $out/bin/cpp + ''; + }; in { - inherit + # For an environment that only puts binaries on PATH. + toolchain = [ customGcc - customClang - customBinutils - customStdenv customGcov - ; + customClangForCiEnv + customBinutils + (mkVersionedToolLinks { + name = "gcc"; + package = customGcc; + version = gccVersion; + tools = [ + "gcc" + "g++" + "cpp" + ]; + }) + (mkVersionedToolLinks { + name = "clang"; + package = customClang; + version = llvmVersion; + tools = [ + "clang" + "clang++" + ]; + }) + ]; - customClangStdenv = pkgs.stdenvAdapters.overrideCC pkgs.stdenv customClang; + gccStdenv = customStdenv; + clangStdenv = pkgs.stdenvAdapters.overrideCC pkgs.stdenv customClang; + gcov = customGcov; } diff --git a/nix/packages.nix b/nix/packages.nix index 01ab2ecf9a..9af230097d 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -16,23 +16,7 @@ let exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@" ''; - # rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so - # cargo has a linker). That default may be different from the clang we pin here, - # so it shadows our clang and the build can silently use a different compiler - # version. Drop that cc from every propagation channel instead of pinning a - # replacement: the toolchain then carries no compiler and cargo just uses the - # active shell's stdenv cc. Must cover all channels — rust-overlay uses both - # propagatedBuildInputs and depsHostHostPropagated. - rustToolchainBase = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml; - rustToolchain = - let - defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv - withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath); - in - rustToolchainBase.overrideAttrs (old: { - propagatedBuildInputs = withoutDefaultCc (old.propagatedBuildInputs or [ ]); - depsHostHostPropagated = withoutDefaultCc (old.depsHostHostPropagated or [ ]); - }); + rust = import ./rust.nix { inherit pkgs; }; # Nix wraps its toolchain so that binaries are exposed only under unsuffixed # names (gcc, g++, clang-tidy, ...). Several tools probe for a @@ -50,6 +34,9 @@ let # environment (the plain stdenv compiler in the dev shell, the custom-glibc # wrappers in ci-env.nix), so those callers pass their own `package`; the # clang tooling is environment-independent and is linked in commonPackages. + # + # Exec wrappers, not symlinks: the nixpkgs clang-tools wrapper dispatches on + # `$(basename $0)-unwrapped`, which a suffixed symlink turns into a dead path. mkVersionedToolLinks = { name, @@ -57,12 +44,15 @@ let version, tools, }: - pkgs.linkFarm "${name}-${toString version}-versioned-links" ( - map (tool: { - name = "bin/${tool}-${toString version}"; - path = "${package}/bin/${tool}"; - }) tools - ); + pkgs.symlinkJoin { + name = "${name}-${toString version}-versioned-links"; + paths = map ( + tool: + pkgs.writeShellScriptBin "${tool}-${toString version}" '' + exec "${package}/bin/${tool}" "$@" + '' + ) tools; + }; # The cc-wrapper doesn't re-export gcov, but coverage tooling (gcovr) needs a # gcov that exactly matches the compiler. Surface it from a gcc `cc` output. @@ -102,51 +92,38 @@ in mkGcov ; - commonPackages = with pkgs; [ - clangToolLinks - runClangTidyLink - ccache - clangbuildanalyzer - clangTools - cmake - conan - curlMinimal # needed for codecov/codecov-action - doxygen - file # needed for cpack in Clio - gcovr - gh - git - git-cliff - git-lfs - gnumake - gnupg # needed for signing commits & codecov/codecov-action - graphviz - less # needed for git diff - mold - nettools # provides netstat, used to debug failures in CI - ninja - patchelf - perl # needed for openssl - pkg-config - pre-commit - # protoc generates the Go gRPC bindings and embeds its own version string into every committed - # .pb.go file. To allow CI to verify those files with a plain `git diff`, we pin the version to - # `protobuf_34` rather than the rolling `protobuf` to keep regeneration reproducible across the - # Nix frequently changing unstable channel. The protoc-gen-go* plugins have no versioned - # attributes in nixpkgs; protoc-gen-go's version is in turn constrained by the go.mod require - # on google.golang.org/protobuf. - protobuf_34 # provides protoc - protoc-gen-go # protoc plugin for the Go message bindings - protoc-gen-go-grpc # protoc plugin for the Go gRPC service stubs - python3 - runClangTidy - vim - zip - # Rust packages - cargo-audit - cargo-llvm-cov - cargo-nextest - corrosion - rustToolchain - ]; + commonPackages = + (with pkgs; [ + clangToolLinks + runClangTidyLink + ccache + clangbuildanalyzer + clangTools + cmake + conan + curlMinimal # needed for codecov/codecov-action + doxygen + file # needed for cpack in Clio + gcovr + gh + git + git-cliff + git-lfs + gnumake + gnupg # needed for signing commits & codecov/codecov-action + graphviz + less # needed for git diff + mold + nettools # provides netstat, used to debug failures in CI + ninja + patchelf + perl # needed for openssl + pkg-config + pre-commit + python3 + runClangTidy + vim + zip + ]) + ++ rust.packages; } diff --git a/nix/rust-nightly.sh b/nix/rust-nightly.sh new file mode 100644 index 0000000000..263e647d8e --- /dev/null +++ b/nix/rust-nightly.sh @@ -0,0 +1,23 @@ +#!@runtimeShell@ +# Reaches the nightly Rust toolchain, which is deliberately kept off PATH. +# Packaged by nix/rust.nix, which explains why. + +set -euo pipefail + +usage() { + echo "usage: rust-nightly (path | run ...)" >&2 + exit 2 +} + +case "${1-}" in + path) printf '%s\n' "@rustNightlyBin@" ;; + run) + shift + if [[ $# -eq 0 ]]; then + usage + fi + export PATH="@rustNightlyBin@:${PATH}" + exec "$@" + ;; + *) usage ;; +esac diff --git a/nix/rust.nix b/nix/rust.nix new file mode 100644 index 0000000000..8be48dcad0 --- /dev/null +++ b/nix/rust.nix @@ -0,0 +1,84 @@ +# The Rust half of the tool set shared by the CI environment and the dev shell: +# the stable toolchain pinned by rust-toolchain.toml, the nightly the Rust +# coverage job needs, and the cargo plugins. Consumed by packages.nix. +{ pkgs }: +let + # rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so + # cargo has a linker). That default may be different from the clang we pin + # elsewhere, so it shadows our clang and the build can silently use a different + # compiler version. Drop that cc from every propagation channel instead of + # pinning a replacement: the toolchain then carries no compiler and cargo just + # uses the active shell's stdenv cc. + # + # The channel list is every list mkDerivation propagates to a dependent's + # environment (including the two legacy aliases). rust-overlay currently only + # uses propagatedBuildInputs and depsHostHostPropagated, but covering all of + # them means an upstream switch to another channel cannot quietly put the + # compiler back on PATH. + dropDefaultCc = + toolchain: + let + defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv + withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath); + in + toolchain.overrideAttrs ( + old: + pkgs.lib.genAttrs [ + "depsBuildBuildPropagated" + "propagatedNativeBuildInputs" # alias of depsBuildHostPropagated + "depsBuildTargetPropagated" + "depsHostHostPropagated" + "propagatedBuildInputs" # alias of depsHostTargetPropagated + "depsTargetTargetPropagated" + ] (channel: withoutDefaultCc (old.${channel} or [ ])) + ); + + rustToolchain = dropDefaultCc (pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml); + + # cargo-llvm-cov honours the #[coverage(off)] that keeps unit tests out of the + # coverage report only under a nightly rustc, and looks for llvm-profdata and + # llvm-cov in that same toolchain's sysroot — hence llvm-tools-preview. + # + # Not every nightly ships every component, so `nightly.latest` breaks on the + # days llvm-tools-preview is absent; selectLatestNightlyWith walks back to the + # newest one that has it. The result is the newest such nightly *known to the + # locked rust-overlay*, which means updating flake.lock moves the compiler that + # produces the coverage numbers — and with it the rustc version recorded in + # nix/check-tools/*.txt, so those snapshots need regenerating alongside. + rustNightly = dropDefaultCc ( + pkgs.rust-bin.selectLatestNightlyWith ( + toolchain: toolchain.minimal.override { extensions = [ "llvm-tools-preview" ]; } + ) + ); + + # A second toolchain cannot go on PATH: its cargo and rustc would collide with + # the pinned stable's in the ci-env buildEnv, which resolves collisions by + # picking one silently. Reaching the nightly only through this wrapper keeps it + # in the image closure (the Docker build copies the whole closure, not just + # what is linked into /bin) while leaving it inactive everywhere that does not + # ask for it. + # + # The script's `path` subcommand exists for scopes wider than one command — a + # CI job appending to $GITHUB_PATH, so that the cargo cache action's own + # `rustc -vV` probe, which runs in a step of its own, agrees with the toolchain + # the build will use. + rustNightlyScript = pkgs.replaceVarsWith { + name = "rust-nightly"; + src = ./rust-nightly.sh; + dir = "bin"; + isExecutable = true; + replacements = { + inherit (pkgs) runtimeShell; + rustNightlyBin = "${rustNightly}/bin"; + }; + }; +in +{ + packages = [ + pkgs.cargo-audit + pkgs.cargo-llvm-cov + pkgs.cargo-nextest + rustNightlyScript + rustToolchain + ]; +} diff --git a/package/Dockerfile b/package/Dockerfile deleted file mode 100644 index 6cb2a09933..0000000000 --- a/package/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -ARG BASE_IMAGE=debian:bookworm - -FROM ${BASE_IMAGE} - -# Packaging runs in a vanilla distro image, so the tooling has to come -# from the distro's archive: debhelper for deb, rpm-build (and the -# systemd / find-debuginfo macros it depends on) for rpm. -# The container also uses git (real history) for -# build_pkg.sh's SOURCE_DATE_EPOCH; otherwise it falls back to a tarball -# download and the timestamp comes from wall-clock time. - -COPY package/install-packaging-tools.sh /tmp/install-packaging-tools.sh - -RUN /tmp/install-packaging-tools.sh diff --git a/package/README.md b/package/README.md index 887509b60b..6e88309ecd 100644 --- a/package/README.md +++ b/package/README.md @@ -8,10 +8,14 @@ a build configured with `-Dvalidator_keys=ON`. ``` package/ - build_pkg.sh Staging and build script (called by the CMake `package` target and CI) + build_pkg.py Staging and build script (called by the CMake `package` target and CI) + sign_rpm.py Signs the built RPMs (called by CI when publishing) + docker/ + Dockerfile Packaging image, built by `build-packaging-images.yml`; installs its tooling with `bin/install-packaging-tools.sh` + publish_pkg.py Uploads built packages to the XRPLF Nexus repositories (called by CI, and shipped in that image) rpm/ xrpld.spec RPM spec - debian/ Debian control files (control, rules, copyright, xrpld.docs, xrpld.links, source/format) + debian/ Debian control files (control, rules, copyright, xrpld.docs, xrpld.links, xrpld.lintian-overrides, source/format) shared/ xrpld.service systemd unit file (used by both RPM and DEB) xrpld.sysusers sysusers.d config (used by both RPM and DEB) @@ -21,18 +25,19 @@ package/ ## Prerequisites -Packaging targets and their container images are declared in -[`.github/scripts/strategy-matrix/linux.json`](../.github/scripts/strategy-matrix/linux.json) -under `package_configs`, one entry per distro. Today only `linux/amd64` is -emitted. Each entry pins its full container image in an `image` field; to move -to a new image, edit that field and both CI and local builds pick it up. The -package format (deb or rpm) is inferred at build time from the container's -package manager (`apt-get` -> deb, `dnf`/`yum` -> rpm). +Packaging is declared on the build configs themselves, in +[`.github/scripts/strategy-matrix/linux.json`](../.github/scripts/strategy-matrix/linux.json): +a config that is also packaged carries a `package` map, so its binaries and its +packaging job cannot drift apart. Today only `linux/amd64` is emitted. The map +pins the full container image in `image` — edit that field to move to a new +image and both CI and local builds pick it up — and names the format that image +builds in `type`, which CI passes to `build_pkg.py` as `--package-type`; the two +have to stay in step. -| Package type | Image (`package_configs.[].image` in `linux.json`) | Tools required | -| ------------ | ---------------------------------------------------------- | --------------------------------------------------- | -| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-` | `rpmbuild` | -| DEB | `ghcr.io/xrplf/xrpld/packaging-debian:sha-` | `dpkg-buildpackage`, debhelper with compat level 13 | +| Package type | Image (`configs.[].package.image` in `linux.json`) | Tools required | +| ------------ | ---------------------------------------------------------- | -------------------------------------------------------------- | +| RPM | `ghcr.io/xrplf/xrpld/packaging-rhel:sha-` | `rpmbuild`, `rpmsign` | +| DEB | `ghcr.io/xrplf/xrpld/packaging-debian:sha-` | `dpkg-buildpackage`, debhelper with compat level 13, `lintian` | To print the full packaging matrix (artifact names and images) for the current `linux.json`: @@ -46,20 +51,27 @@ To print the full packaging matrix (artifact names and images) for the current ### Via CI Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call -`reusable-package.yml`. That workflow generates its own packaging matrix from -`package_configs` in `linux.json` (via `generate.py --packaging`) and fans out -one job per distro. Each job downloads the pre-built `xrpld` and `validator-keys` -binary artifacts and runs in that distro's container, so the package format -follows from the container's package manager. The packaging script derives the -package version from the downloaded binary's `xrpld --version` output; no CMake -configure or build step is needed inside the packaging job. +`reusable-package.yml`, which runs in three stages: -The binaries come from the `debian` and `rhel` build configurations in -`linux.json`'s `configs` section, which pass `-Dvalidator_keys=ON` so that the +1. `package` fans out one job per config carrying a `package` map, building and + signing in that config's container, and uploading `-pkg` alongside + `-pkg-debug` for the much larger debug symbols. +2. `test-install` installs `-pkg` in the container of every distro the + packages target and runs the binaries there, so one that cannot be installed + never reaches Nexus. +3. `publish` uploads both artifacts, or lists what it would upload. + +The packaging script derives the package version from the downloaded binary's +`xrpld --version` output; no CMake configure or build step is needed inside the +packaging job. + +The binaries come from the `debian` and `rhel` build configs themselves — the +ones carrying the `package` map — which pass `-Dvalidator_keys=ON` so that the build job produces `validator-keys` next to `xrpld` and uploads it as the -`validator-keys-` artifact. The packaging entry for a distro names -both artifacts (`xrpld_artifact_name` and `validator_keys_artifact_name`), so a -packaged configuration must keep `-Dvalidator_keys=ON`. +`validator-keys-` artifact. The packaging matrix names both +artifacts (`xrpld_artifact_name` and `validator_keys_artifact_name`) after that +same config, so a packaged config must keep `-Dvalidator_keys=ON`. Those configs +are not `minimal`, so `on-pr.yml` only packages once a PR runs the full matrix. `validator-keys` is fetched from an exact commit pinned in [`cmake/XrplValidatorKeys.cmake`](../cmake/XrplValidatorKeys.cmake), so a given @@ -72,11 +84,10 @@ With `xrpld` and `validator-keys` binaries already built at `build/xrpld` and The image tag is derived from `linux.json` so you don't need to hardcode a SHA. ```bash -# From the repo root. Each distro's container image is the `image` field of its -# package_configs entry in linux.json; the package format is inferred from the -# container's package manager. Example for the rpm-producing image (use -# .package_configs.debian[0].image for the deb image): -IMAGE=$(jq -r '.package_configs.rhel[0].image' .github/scripts/strategy-matrix/linux.json) +# From the repo root. Each distro's container image is the `package.image` field +# of its config in linux.json. Example for the rpm-producing image (use +# .configs.debian[0].package.image and --package-type deb for the other one): +IMAGE=$(jq -r '.configs.rhel[0].package.image' .github/scripts/strategy-matrix/linux.json) PKG_RELEASE=1 @@ -84,10 +95,12 @@ docker run --rm \ -v "$(pwd):/src" \ -w /src \ "${IMAGE}" \ - ./package/build_pkg.sh --pkg-release "${PKG_RELEASE}" + ./package/build_pkg.py \ + --package-type rpm \ + --pkg-release "${PKG_RELEASE}" \ + --channel UNRELEASED -# Output: -# build/debbuild/*.deb (DEB + dbgsym .ddeb) +# Output (the deb image writes build/debbuild/*.deb instead): # build/rpmbuild/RPMS/x86_64/*.rpm ``` @@ -111,29 +124,94 @@ cmake --build . --target package # deb on Debian/Ubuntu, rpm on RHEL The `cmake/XrplPackaging.cmake` module defines the `package` target only if at least one of `rpmbuild` / `dpkg-buildpackage` is present and both the `xrpld` and `validator-keys` targets exist (`-Dxrpld=ON -Dvalidator_keys=ON`); the target -builds both binaries before packaging. `build_pkg.sh` then infers the package -format from the host's package manager. The packaging script installs to -FHS-standard paths (`/usr/bin`, `/etc/xrpld`, etc.) regardless of -`CMAKE_INSTALL_PREFIX`. +builds both binaries before packaging, passing `--package-type deb` when +`dpkg-buildpackage` is present and `rpm` otherwise, and `--channel UNRELEASED`. +The packaging script installs to FHS-standard paths (`/usr/bin`, `/etc/xrpld`, +etc.) regardless of `CMAKE_INSTALL_PREFIX`. -The package version is not a CMake input on this path: `build_pkg.sh` derives it +The package version is not a CMake input on this path: `build_pkg.py` derives it from the just-built `xrpld` binary's `xrpld --version` output. The package release defaults to 1 and is overridable with `-Dpkg_release=N`. -## How `build_pkg.sh` works +## Publishing packages -`build_pkg.sh` derives the `xrpld` software version from +Packages are published to the XRPLF repositories on Sonatype Nexus at +`https://packages.xrplf.org`. The `release-info` action decides the channel from +the event, and `publish_pkg.py` maps that channel to its repositories: + +| Event | Version | Channel | DEB repository | RPM upload repository | +| ------------------------ | ----------------- | --------- | -------------- | --------------------- | +| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` | +| tag | `X.Y.Z-rcN` | `rc` | `deb-rc` | `rpm-rc-hosted` | +| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` | +| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` | +| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` | + +Only a tag names a channel — do not extend that to `develop`, where +`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final +version during a release cycle, which would send develop builds into `stable`. +Versions sort in row order, so moving to a more mature channel never downgrades. + +The action decides the package release number on the same split: a tag's version +is unique, so its packages are release 1, while develop repeats the same version +and takes `.git`, e.g. +`857.20260826gitb6a8995` — the leading run number keeps each push superseding +the last, and the date and hash say which commit a package on +`packages.xrplf.org` came from. Both reach the packaging scripts as arguments, +so neither script derives anything itself. + +Publishing is its own job, gated behind `test-install`, uploading from the same +image that built the packages with the `publish_pkg.py` shipped in it — the +same copy other repositories run. Without `publish: true` the job is a +`--dry-run`, listing the uploads it would make without needing credentials, so +any run that builds packages also exercises the upload routing. `on-trigger.yml` +passes `publish: true` for develop pushes in `XRPLF/rippled` and `on-tag.yml` +for tags in any `XRPLF` repository, both authenticating with the +`NEXUS_REMOTE_USERNAME` / `NEXUS_REMOTE_PASSWORD` secrets already used for the +Conan remote; `on-pr.yml` never publishes. + +Nexus owns the repository metadata; nothing here indexes anything. Worth knowing: + +- Each apt-hosted repository needs a distribution (ours use `any`) and a PGP + signing keypair configured in Nexus, which rejects one created without a + keypair. Nexus signs the apt metadata with it, never the packages. +- Hosted yum repositories cannot be signed by Nexus, so each `rpm--hosted` + repository sits behind a `rpm-` yum group repository whose metadata + Nexus signs. Uploads go to the hosted repository; clients point at the group + and verify the metadata with `repo_gpgcheck=1`. Nexus never signs the RPMs + themselves, so `sign_rpm.py` signs them before they are uploaded, and clients + verify them with `gpgcheck=1`. +- yum metadata is rebuilt asynchronously, so a successful publish is not + immediately installable. +- Each job uploads only what it built, and uploads are not transactional, so a + failure can leave one format published alone. Re-running is safe: both the apt + POST and the yum PUT replace an existing asset. +- The `develop` repositories gain a package per push, so they need a cleanup + policy to stay bounded; tagged channels publish each version once. + +### Publishing from other repositories + +`publish_pkg.py` knows nothing about `xrpld`, so the packaging image +installs it at `/usr/local/bin/publish_pkg.py` for other XRPLF repositories that +build their packages elsewhere. + +## How `build_pkg.py` works + +`build_pkg.py` derives the `xrpld` software version from `${BUILD_DIR}/xrpld --version` in both package formats. The binary's version is already SemVer-validated by `BuildInfo`. -`build_pkg.sh` converts pre-release versions such as `3.2.0-b1` or +`build_pkg.py` converts pre-release versions such as `3.2.0-b1` or `3.2.0-rc1` from `-` to `~` for package metadata so pre-releases sort before the final release. If that normalized package version still contains `-`, packaging fails because RPM forbids `-` in `Version`, and Debian uses `-` as the upstream/revision separator. +> [!NOTE] +> Debug and sanitizer builds are not packaged yet. + `pkg_version` is the normalized package metadata version derived inside -`build_pkg.sh` from the binary-reported `xrpld` version (`-` pre-release +`build_pkg.py` from the binary-reported `xrpld` version (`-` pre-release separator converted to `~`). It is not a separate user input. `PKG_RELEASE` is a different value: the package release iteration for that @@ -151,32 +229,41 @@ With `PKG_RELEASE=1`, the package metadata becomes: | `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` | | `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` | -The Debian changelog entry carries the repository component: final releases use -`stable`, `b0` builds, including `b0+metadata`, use `develop`, and `bN`/`rcN` -pre-releases use `unstable`. -Build metadata on a final release, such as `3.2.0+abc123`, is rejected. +`build_pkg.py` defines `dist` as `.el9` rather than letting rpmbuild take it +from the build host, so the RHEL image can track a newer release without +changing what the packages claim to target. + +The Debian changelog entry carries the channel passed as `--channel`, which +only accepts the channels in the table above plus `UNRELEASED`, the Debian +convention for a build that targets no channel at all — what local and CMake +builds pass, since nothing publishes them. An unsupported pre-release, and +build metadata on a final release such as `3.2.0+abc123`, are both rejected. The RPM path intentionally uses `~` in `Version`, matching the Debian pre-release ordering convention, so RPM filenames/NVRs begin with forms like `xrpld-3.2.0~b1-...` and `xrpld-3.2.0~rc1-...` instead of encoding pre-releases with an older `0..` RPM `Release` value. -The package format (`deb` or `rpm`) is inferred from the host's package -manager (`apt-get` -> deb, `dnf`/`yum` -> rpm). Hosts without one of those -fail early. +The package format is `--package-type`, either `deb` or `rpm`. It is required, +so a job never silently builds the wrong format for the image it runs in; the +matching build tool still has to be on PATH. -Flags are for explicit invocation; environment variables are intended for -CMake/CI integration. The CI workflow and the CMake `package` target both invoke -`build_pkg.sh` with no flags; CMake supplies `SRC_DIR`, `BUILD_DIR`, and -`PKG_RELEASE` via env, while CI supplies `BUILD_DIR` and `PKG_RELEASE` via env -and lets the script use defaults for the rest. +Every input is a named argument, and every argument but `--build-dir` and +`--pkg-release` is required. The repository root is not an argument +at all: the script reads it from its own location. Only secrets stay in the +environment, so they never reach the process list -- `PKG_SIGNING_KEY` for +`sign_rpm.py`, and `NEXUS_USERNAME` / `NEXUS_PASSWORD` for `publish_pkg.py`. -It resolves `SRC_DIR` and `BUILD_DIR` to absolute paths, then calls +Signing is not part of this script. `sign_rpm.py` does it in a separate CI step +that only runs when publishing, so a published RPM is always signed and a local +build never needs a key. + +It resolves the build directory to an absolute path, then calls `stage_common()` to copy the `xrpld` and `validator-keys` binaries, config files, and shared support files into the staging area, and invokes the platform build -tool. Both binaries must be present in `BUILD_DIR` and must run in the packaging -environment; a missing or non-runnable one fails early. That runtime check is -what catches a binary still linked against the Nix store's ELF loader (see +tool. Both binaries must be present in the build directory and must run in the +packaging environment; a missing or non-runnable one fails early. That runtime +check is what catches a binary still linked against the Nix store's ELF loader (see `patch_nix_binary` in `cmake/PatchNixBinary.cmake`). ### RPM @@ -186,14 +273,9 @@ what catches a binary still linked against the Nix store's ELF loader (see 3. Runs `rpmbuild -bb`, passing the normalized package metadata version as the `pkg_version` RPM macro and `PKG_RELEASE` as the `pkg_release` RPM macro. The spec uses manual `install` commands to place files, disables `dwz`, and - writes uncompressed RPM payloads while generating debuginfo packages. + generates debuginfo packages. 4. Output: `rpmbuild/RPMS/x86_64/xrpld-*.rpm` -The uncompressed RPM payload setting is intentionally unconditional for -generated RPMs. It trades larger RPM artifacts for much shorter package -build/validation time, which keeps RPM package validation in the same rough time -class as Debian package validation. - RPM upgrades intentionally do not restart a running `xrpld` service. The spec uses `%systemd_postun`, matching Debian's `dh_installsystemd --no-stop-on-upgrade` behavior; operators pick up the new binary on the next @@ -205,35 +287,45 @@ service restart. 2. Stages the binaries, configs, `README.md`, `LICENSE.md`, and `validator-keys-LICENSE`. 3. Copies `package/debian/` control files into `debbuild/source/debian/`. -4. Copies shared service/sysusers/tmpfiles into `debian/` where `dh_installsystemd`, `dh_installsysusers`, and `dh_installtmpfiles` pick them up automatically. +4. Copies shared service/sysusers/tmpfiles/logrotate into `debian/` where `dh_installsystemd`, `dh_installsysusers`, `dh_installtmpfiles` and `dh_installlogrotate` pick them up automatically. 5. Generates a minimal `debian/changelog` using `${pkg_version}-${PKG_RELEASE}`, where `pkg_version` is derived from the binary-reported `xrpld` version. 6. Runs `dpkg-buildpackage -b --no-sign -d` (`-d` skips the build-dependency check, since the binary is already built). `debian/rules` uses manual `install` commands. -7. Output: `debbuild/*.deb` and `debbuild/*.ddeb` (dbgsym package) + + It also rewrites the `libc6` bound to `LIBC_MIN` in `debian/rules`, the glibc + the Nix toolchain builds against. `dpkg-shlibdeps` would otherwise derive it + from the build host's symbols file — on trixie that yields `libc6 (>= 2.34)` + because of `sysconf`, locking out distros the binaries run on. A check fails + the build if either binary outgrows `LIBC_MIN`. + +7. Output: `debbuild/*.deb`, the binary package and the `-dbgsym` package. + Debian gives dbgsym packages a `.deb` extension; only Ubuntu uses `.ddeb`. ## Post-build verification ```bash -# DEB -dpkg-deb -c debbuild/*.deb | grep -E 'systemd|sysusers|tmpfiles' +# DEB (one invocation per package: the dbgsym package is a .deb too) +for deb in debbuild/*.deb; do dpkg-deb -c "${deb}"; done | grep -E 'systemd|sysusers|tmpfiles' lintian -I debbuild/*.deb # RPM rpm -qlp rpmbuild/RPMS/x86_64/*.rpm ``` +`lintian` still reports `embedded-library zlib`, `no-manual-page` and +`initial-upload-closes-no-bugs`; only the `/usr/local` tags are overridden. + ## Reproducibility -`build_pkg.sh` already defaults `SOURCE_DATE_EPOCH` to the latest git commit -time, or the current time outside a git tree, and exports it (override with -`--source-date-epoch` / `SOURCE_DATE_EPOCH`); the RPM spec clamps file -modification times to it via `%build_mtime_policy`. The remaining variables -below further improve reproducibility but are _not_ set by the script — export -them yourself if needed: +Both formats build reproducibly as they are: the same binaries at the same +commit give byte-identical packages on a rebuild, and nothing has to be +exported by hand. -```bash -export TZ=UTC -export LC_ALL=C.UTF-8 -export GZIP=-n -export DEB_BUILD_OPTIONS="noautodbgsym reproducible=+fixfilepath" -``` +`build_pkg.py` sets `SOURCE_DATE_EPOCH` from the latest git commit time. +`dpkg-buildpackage` honours it on its own; the RPM spec sets three macros: + +- `%clamp_mtime_to_source_date_epoch` — file modification times, from + `SOURCE_DATE_EPOCH`. +- `%use_source_date_epoch_as_buildtime` — the `BUILDTIME` header, from the + same. +- `%_buildhost` — pinned, so the builder's hostname stays out of the header. diff --git a/package/build_pkg.py b/package/build_pkg.py new file mode 100755 index 0000000000..1aaf53d5ff --- /dev/null +++ b/package/build_pkg.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +"""Build an RPM or Debian package from the pre-built xrpld and validator-keys binaries. + +The build tool for the chosen format has to be on PATH, so this runs in the +vanilla distro image that matches it. +""" + +from __future__ import annotations + +import argparse +import os +import re +import shutil +import subprocess +import textwrap +from datetime import datetime, timezone +from pathlib import Path + +# This script lives in the repository it packages. +SRC_DIR = Path(__file__).resolve().parents[1] + +PRE_RELEASE = re.compile(r"^(b|rc)(0|[1-9][0-9]*)(\+.*)?$") + +# Files both packaging systems consume, staged under the same names. +STAGED_FROM_BUILD = ("xrpld", "validator-keys", "validator-keys-LICENSE") +STAGED_FROM_SRC = { + "cfg/xrpld-example.cfg": "xrpld.cfg", + "cfg/validators-example.txt": "validators.txt", + "LICENSE.md": "LICENSE.md", + "README.md": "README.md", +} +STAGED_UNITS = ("xrpld.service", "xrpld.sysusers", "xrpld.tmpfiles", "xrpld.logrotate") + + +def run(*command: object, cwd: Path | None = None) -> None: + """Echo a command and run it.""" + argv = [str(part) for part in command] + print("+ " + " ".join(argv), flush=True) + subprocess.run(argv, check=True, cwd=cwd) + + +def capture(*command: object) -> str: + """Run a command and return its stdout, stripped.""" + argv = [str(part) for part in command] + # stderr is left alone so a failing command explains itself. + return subprocess.run( + argv, stdout=subprocess.PIPE, text=True, check=True + ).stdout.strip() + + +def package_version(reported: str) -> str: + """Normalise a reported version into one the package formats accept. + + A pre-release switches to '~' (3.2.0-b1 -> 3.2.0~b1), which also sorts before + the final 3.2.0; a no-op for a final release. + """ + base, _, pre_release = reported.partition("-") + version = f"{base}~{pre_release}" if pre_release else base + + # BuildInfo already SemVer-validates the version. Packaging adds one narrower + # constraint: after normalisation the version must not contain '-', because + # RPM forbids it in Version and Debian reads it as the revision separator. + assert "-" not in version, ( + f"unsupported version {reported!r}: {version!r} cannot contain '-'. " + "Use a single-token pre-release like 3.2.0-b1 or 3.2.0-rc2." + ) + assert pre_release or "+" not in reported, ( + f"unsupported version {reported!r}: " + "build metadata is only supported on bN/rcN pre-releases." + ) + assert not pre_release or PRE_RELEASE.match(pre_release), ( + f"unsupported pre-release {pre_release!r}: use bN or rcN, " + "e.g. 3.2.0-b1 or 3.2.0-rc2." + ) + return version + + +def read_version(xrpld: Path) -> str: + """Read the version from the binary that is about to be packaged.""" + fields = capture(xrpld, "--version").partition("\n")[0].split() + assert len(fields) >= 3, f"cannot read a version from {xrpld} --version" + return fields[2] + + +def check_binaries(build_dir: Path) -> None: + """Fail unless the binaries and their notices are present and runnable.""" + missing = [ + name + for name in ("xrpld", "validator-keys") + if not os.access(build_dir / name, os.X_OK) + ] + assert not missing, ( + f"missing or not executable in {build_dir}: {' '.join(missing)}. " + "Both binaries come from a single CMake build directory configured with " + "-Dxrpld=ON -Dvalidator_keys=ON." + ) + + # No package goes out without the attribution. + notice = build_dir / "validator-keys-LICENSE" + assert notice.is_file(), ( + f"missing {notice}. cmake/XrplValidatorKeys.cmake copies it out of the " + "fetched validator-keys-tool source, so reconfigure with -Dvalidator_keys=ON." + ) + + # Catches a binary still pointing at the Nix store's ELF loader, since + # packaging runs in a vanilla distro container. + capture(build_dir / "validator-keys", "--version") + + +def source_date_epoch() -> int: + """The last commit's timestamp.""" + # git refuses to read a checkout owned by another user, which is what a CI + # container or a bind mount hands it. + return int( + capture( + "git", + "-c", + f"safe.directory={SRC_DIR}", + "-C", + SRC_DIR, + "log", + "-1", + "--format=%ct", + ) + ) + + +def stage_common(build_dir: Path, dest: Path) -> None: + """Copy everything both packaging systems consume into dest.""" + dest.mkdir(parents=True, exist_ok=True) + + for name in STAGED_FROM_BUILD: + shutil.copy2(build_dir / name, dest / name) + for source, name in STAGED_FROM_SRC.items(): + shutil.copy2(SRC_DIR / source, dest / name) + + +def stage_units(dest: Path) -> None: + """Copy the systemd, sysusers, tmpfiles and logrotate files into dest. + + Each format wants them somewhere else: rpmbuild reads them from SOURCES, + debhelper from debian/. + """ + for name in STAGED_UNITS: + shutil.copy2(SRC_DIR / "package" / "shared" / name, dest / name) + + +def build_rpm(build_dir: Path, *, version: str, pkg_release: str) -> None: + """Stage the spec and its sources, then build the binary RPMs.""" + topdir = build_dir / "rpmbuild" + for name in ("BUILD", "BUILDROOT", "RPMS", "SOURCES", "SPECS", "SRPMS"): + (topdir / name).mkdir(parents=True, exist_ok=True) + + spec = topdir / "SPECS" / "xrpld.spec" + shutil.copy2(SRC_DIR / "package" / "rpm" / "xrpld.spec", spec) + stage_common(build_dir, topdir / "SOURCES") + stage_units(topdir / "SOURCES") + + run( + "rpmbuild", + "-bb", + "--define", + f"_topdir {topdir}", + "--define", + f"pkg_version {version}", + "--define", + f"pkg_release {pkg_release}", + # The image tracks the newest distro, but the packages target el9. + "--define", + "dist .el9", + spec, + ) + + +def build_deb( + build_dir: Path, + *, + version: str, + reported: str, + pkg_release: str, + channel: str, + epoch: int, +) -> None: + """Stage the debian directory and its sources, then build the binary DEBs.""" + staging = build_dir / "debbuild" / "source" + stage_common(build_dir, staging) + shutil.copytree(SRC_DIR / "package" / "debian", staging / "debian") + + # debhelper picks these up from debian/ automatically. + stage_units(staging / "debian") + + date = datetime.fromtimestamp(epoch, timezone.utc).strftime( + "%a, %d %b %Y %H:%M:%S %z" + ) + # The leading spaces are significant to dpkg. + changelog = textwrap.dedent(f"""\ + xrpld ({version}-{pkg_release}) {channel}; urgency=medium + * Release {reported}. + + -- XRPL Foundation {date} + """) + (staging / "debian" / "changelog").write_text(changelog) + + run("dpkg-buildpackage", "-b", "--no-sign", "-d", cwd=staging) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--package-type", + required=True, + choices=("deb", "rpm"), + help="the package format to build", + ) + parser.add_argument( + "--build-dir", + type=Path, + default=Path("build"), + help="directory holding the xrpld and validator-keys binaries (default: %(default)s)", + ) + parser.add_argument( + "--pkg-release", + default="1", + help="package release iteration (default: %(default)s)", + ) + parser.add_argument( + "--channel", + required=True, + choices=("stable", "rc", "beta", "develop", "private", "UNRELEASED"), + help="release channel, written to debian/changelog", + ) + args = parser.parse_args() + package_type: str = args.package_type + build_dir: Path = args.build_dir.resolve() + pkg_release: str = args.pkg_release + channel: str = args.channel + + assert build_dir.is_dir(), ( + f"build directory not found: {build_dir}. Build the binaries before " + "packaging, or point --build-dir at the directory holding them." + ) + + check_binaries(build_dir) + reported = read_version(build_dir / "xrpld") + version = package_version(reported) + epoch = source_date_epoch() + + # rpmbuild and dpkg-buildpackage both honour this for file timestamps. + os.environ["SOURCE_DATE_EPOCH"] = str(epoch) + + # Remove both build trees, because a package left from an earlier build would + # otherwise be picked up and published alongside this one. + for tree in ("debbuild", "rpmbuild"): + shutil.rmtree(build_dir / tree, ignore_errors=True) + + if package_type == "deb": + build_deb( + build_dir, + version=version, + reported=reported, + pkg_release=pkg_release, + channel=channel, + epoch=epoch, + ) + else: + build_rpm(build_dir, version=version, pkg_release=pkg_release) + + +if __name__ == "__main__": + main() diff --git a/package/build_pkg.sh b/package/build_pkg.sh deleted file mode 100755 index d853bf95b7..0000000000 --- a/package/build_pkg.sh +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Build an RPM or Debian package from the pre-built xrpld and validator-keys -# binaries. -# -# Flags override env vars; env vars override defaults. - -usage() { - cat <<'EOF' -Usage: build_pkg.sh [options] - -Options (each can also be set via the env var shown): - --src-dir DIR repo root [SRC_DIR; default: ${PWD}] - --build-dir DIR directory holding the - xrpld and validator-keys - binaries [BUILD_DIR; default: ${PWD}/build] - --pkg-release N package release iteration [PKG_RELEASE; default: 1] - --source-date-epoch SECS reproducibility timestamp [SOURCE_DATE_EPOCH; latest git ctime; fallback: current time] - -h, --help show this help and exit -EOF -} - -need_arg() { - if [[ $# -lt 2 || "$2" == --* ]]; then - echo "Missing value for $1" >&2 - exit 2 - fi -} - -# Seed from env. CLI parsing below overrides these directly. -SRC_DIR="${SRC_DIR:-}" -BUILD_DIR="${BUILD_DIR:-}" -PKG_RELEASE="${PKG_RELEASE:-1}" -SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-}" - -while [[ $# -gt 0 ]]; do - case "$1" in - --src-dir) - need_arg "$@" - SRC_DIR="$2" - shift 2 - ;; - --build-dir) - need_arg "$@" - BUILD_DIR="$2" - shift 2 - ;; - --pkg-release) - need_arg "$@" - PKG_RELEASE="$2" - shift 2 - ;; - --source-date-epoch) - need_arg "$@" - SOURCE_DATE_EPOCH="$2" - shift 2 - ;; - -h | --help) - usage - exit 0 - ;; - *) - echo "Unknown argument: $1" >&2 - usage >&2 - exit 2 - ;; - esac -done - -SRC_DIR="$(cd "${SRC_DIR:-${PWD}}" && pwd)" -BUILD_DIR="${BUILD_DIR:-${PWD}/build}" -if [[ ! -d "${BUILD_DIR}" ]]; then - echo "build_pkg.sh: build directory not found: ${BUILD_DIR}" >&2 - echo "Build the binaries before packaging, or set BUILD_DIR to the directory containing them." >&2 - exit 1 -fi -BUILD_DIR="$(cd "${BUILD_DIR}" && pwd)" - -xrpld_binary="${BUILD_DIR}/xrpld" -validator_keys_binary="${BUILD_DIR}/validator-keys" - -# Report both binaries at once: they share a single BUILD_DIR, so telling the -# reader to point it at one of them in isolation is advice they cannot follow. -missing=() -[[ -x "${xrpld_binary}" ]] || missing+=(xrpld) -[[ -x "${validator_keys_binary}" ]] || missing+=(validator-keys) - -if [[ ${#missing[@]} -gt 0 ]]; then - echo "build_pkg.sh: missing or not executable in ${BUILD_DIR}: ${missing[*]}" >&2 - echo "Both binaries come from a single CMake build directory configured with" >&2 - echo "-Dxrpld=ON -Dvalidator_keys=ON. Build them, then point BUILD_DIR at that" >&2 - echo "directory." >&2 - exit 1 -fi - -# Shipping validator-keys means shipping its notice, so treat it as required -# rather than letting a package go out without the attribution. -validator_keys_license="${BUILD_DIR}/validator-keys-LICENSE" -if [[ ! -f "${validator_keys_license}" ]]; then - echo "build_pkg.sh: missing ${validator_keys_license}." >&2 - echo "cmake/XrplValidatorKeys.cmake copies it out of the fetched" >&2 - echo "validator-keys-tool source, so reconfigure with -Dvalidator_keys=ON." >&2 - exit 1 -fi - -# The binary must also *run* here. Packaging happens in a vanilla distro -# container, so this is what catches a binary still pointing at the Nix store's -# ELF loader (see patch_nix_binary in cmake/PatchNixBinary.cmake); xrpld is -# covered implicitly by the version query below. -if ! "${validator_keys_binary}" --version >/dev/null; then - echo "build_pkg.sh: ${validator_keys_binary} exists but does not run here." >&2 - exit 1 -fi - -xrpld_version="$("${xrpld_binary}" --version | awk 'NR == 1 { print $3 }')" - -if [[ -z "${xrpld_version}" ]]; then - echo "build_pkg.sh: unable to derive xrpld version from ${xrpld_binary} --version." >&2 - exit 1 -fi - -# The version as the package formats consume it: identical to xrpld_version -# except a pre-release uses '~' (3.2.0-b1 -> 3.2.0~b1), which also sorts before -# the final 3.2.0; a no-op for a final release. Lowercase = derived internally, -# not an input (cf. pkg_type). -pkg_version="${xrpld_version}" -pre_release="" -if [[ "${xrpld_version}" == *-* ]]; then - pre_release="${xrpld_version#*-}" - pkg_version="${xrpld_version%%-*}~${pre_release}" -fi - -# BuildInfo already SemVer-validates the binary's version. Packaging adds one -# narrower constraint: after pre-release normalization, the package version must -# not contain '-' because RPM forbids it in Version and Debian uses it as the -# upstream/revision separator. -if [[ "${pkg_version}" == *-* ]]; then - echo "build_pkg.sh: unsupported xrpld version '${xrpld_version}'." >&2 - echo "Package version '${pkg_version}' cannot contain '-'." >&2 - echo "Use a single-token pre-release like 3.2.0-b1 or 3.2.0-rc2." >&2 - exit 1 -fi - -if [[ -z "${pre_release}" && "${xrpld_version}" == *+* ]]; then - echo "build_pkg.sh: unsupported xrpld version '${xrpld_version}'." >&2 - echo "Build metadata is only supported on bN/rcN pre-releases." >&2 - exit 1 -fi - -if [[ -n "${pre_release}" && ! "${pre_release}" =~ ^(b0|b[1-9][0-9]*|rc[0-9]+)(\+.*)?$ ]]; then - echo "build_pkg.sh: unsupported xrpld pre-release '${pre_release}'." >&2 - echo "Use bN or rcN, e.g. 3.2.0-b1 or 3.2.0-rc2." >&2 - exit 1 -fi - -if command -v apt-get >/dev/null 2>&1; then - pkg_type=deb -elif command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then - pkg_type=rpm -else - echo "Cannot infer pkg_type: no apt-get, dnf, or yum on PATH." >&2 - exit 1 -fi - -if [[ -z "${SOURCE_DATE_EPOCH}" ]]; then - if git -C "${SRC_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - SOURCE_DATE_EPOCH="$(git -C "${SRC_DIR}" log -1 --format=%ct)" - else - SOURCE_DATE_EPOCH="$(date +%s)" - fi -fi - -export SOURCE_DATE_EPOCH -CHANGELOG_DATE="$(date -u -R -d "@${SOURCE_DATE_EPOCH}")" - -SHARED="${SRC_DIR}/package/shared" -DEBIAN_DIR="${SRC_DIR}/package/debian" - -# Stage files that both packaging systems consume using the same filenames. -stage_common() { - local dest="$1" - mkdir -p "${dest}" - - cp "${xrpld_binary}" "${dest}/xrpld" - cp "${validator_keys_binary}" "${dest}/validator-keys" - cp "${validator_keys_license}" "${dest}/validator-keys-LICENSE" - cp "${SRC_DIR}/cfg/xrpld-example.cfg" "${dest}/xrpld.cfg" - cp "${SRC_DIR}/cfg/validators-example.txt" "${dest}/validators.txt" - cp "${SRC_DIR}/LICENSE.md" "${dest}/LICENSE.md" - cp "${SRC_DIR}/README.md" "${dest}/README.md" - - cp "${SHARED}/xrpld.service" "${dest}/xrpld.service" - cp "${SHARED}/xrpld.sysusers" "${dest}/xrpld.sysusers" - cp "${SHARED}/xrpld.tmpfiles" "${dest}/xrpld.tmpfiles" - cp "${SHARED}/xrpld.logrotate" "${dest}/xrpld.logrotate" -} - -build_rpm() { - local topdir="${BUILD_DIR}/rpmbuild" - rm -rf "${topdir}" - mkdir -p "${topdir}"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} - - cp "${SRC_DIR}/package/rpm/xrpld.spec" "${topdir}/SPECS/xrpld.spec" - stage_common "${topdir}/SOURCES" - - set -x - rpmbuild -bb \ - --define "_topdir ${topdir}" \ - --define "pkg_version ${pkg_version}" \ - --define "pkg_release ${PKG_RELEASE}" \ - "${topdir}/SPECS/xrpld.spec" -} - -build_deb() { - local staging="${BUILD_DIR}/debbuild/source" - rm -rf "${staging}" - mkdir -p "${staging}" - - stage_common "${staging}" - cp -r "${DEBIAN_DIR}" "${staging}/debian" - - cp "${staging}/xrpld.service" "${staging}/debian/xrpld.service" - cp "${staging}/xrpld.sysusers" "${staging}/debian/xrpld.sysusers" - cp "${staging}/xrpld.tmpfiles" "${staging}/debian/xrpld.tmpfiles" - cp "${staging}/xrpld.logrotate" "${staging}/debian/xrpld.logrotate" - - # Choose the Debian repository component for this package. - # 3.2.0 -> stable, *-b0[+metadata] -> develop, - # bN/rcN pre-releases -> unstable. - local deb_component - if [[ -z "${pre_release}" ]]; then - deb_component="stable" - elif [[ "${pre_release}" =~ ^b0(\+.*)?$ ]]; then - deb_component="develop" - elif [[ "${pre_release}" =~ ^(b[1-9][0-9]*|rc[0-9]+)(\+.*)?$ ]]; then - deb_component="unstable" - else - echo "build_pkg.sh: unsupported xrpld pre-release '${pre_release}'." >&2 - echo "Use bN or rcN, e.g. 3.2.0-b1 or 3.2.0-rc2." >&2 - exit 1 - fi - - # Debian version is [~
]-.
-    cat >"${staging}/debian/changelog" <  ${CHANGELOG_DATE}
-EOF
-
-    chmod +x "${staging}/debian/rules"
-
-    set -x
-    (cd "${staging}" && dpkg-buildpackage -b --no-sign -d)
-}
-
-"build_${pkg_type}"
diff --git a/package/debian/control b/package/debian/control
index 62e5d79ef1..359f39f770 100644
--- a/package/debian/control
+++ b/package/debian/control
@@ -4,6 +4,7 @@ Priority: optional
 Maintainer: XRPL Foundation 
 Rules-Requires-Root: no
 Build-Depends:
+ binutils,
  debhelper-compat (= 13)
 Standards-Version: 4.7.0
 Homepage: https://github.com/XRPLF/rippled
@@ -11,8 +12,6 @@ Vcs-Git: https://github.com/XRPLF/rippled.git
 Vcs-Browser: https://github.com/XRPLF/rippled
 
 Package: xrpld
-Section: net
-Priority: optional
 Architecture: any
 Depends:
  ${shlibs:Depends},
diff --git a/package/debian/copyright b/package/debian/copyright
index 2cf673854a..baaa12e13c 100644
--- a/package/debian/copyright
+++ b/package/debian/copyright
@@ -1,5 +1,5 @@
 Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
-Upstream-Name: rippled
+Upstream-Name: xrpld
 Source: https://github.com/XRPLF/rippled
 
 Files: *
@@ -15,7 +15,7 @@ Copyright: 2016, Ripple Labs Inc.
  2009-2010, Satoshi Nakamoto
  2011, The Bitcoin developers
  2003-2005, Tom Wu
-License: ISC
+License: ISC and BSL-1.0 and MIT and Tom-Wu
 Comment: Built from https://github.com/ripple/validator-keys-tool at the commit
  pinned in cmake/XrplValidatorKeys.cmake. Besides ISC-licensed code it
  incorporates work under the Boost Software License 1.0 (ASIO), the MIT/X11
@@ -35,3 +35,74 @@ License: ISC
  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+License: BSL-1.0
+ Boost Software License - Version 1.0 - August 17th, 2003
+ .
+ Permission is hereby granted, free of charge, to any person or organization
+ obtaining a copy of the software and accompanying documentation covered by
+ this license (the "Software") to use, reproduce, display, distribute,
+ execute, and transmit the Software, and to prepare derivative works of the
+ Software, and to permit third-parties to whom the Software is furnished to
+ do so, all subject to the following:
+ .
+ The copyright notices in the Software and this entire statement, including
+ the above license grant, this restriction and the following disclaimer,
+ must be included in all copies of the Software, in whole or in part, and
+ all derivative works of the Software, unless such copies or derivative
+ works are solely in the form of machine-executable object code generated by
+ a source language processor.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+ SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+ FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
+License: Tom-Wu
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+ .
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY
+ WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
+ .
+ IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL,
+ INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER
+ RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF
+ THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT
+ OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ .
+ In addition, the following condition applies:
+ .
+ All redistributions must retain an intact copy of this copyright notice
+ and disclaimer.
diff --git a/package/debian/rules b/package/debian/rules
old mode 100644
new mode 100755
index 8f880b8192..dd6d1e66b9
--- a/package/debian/rules
+++ b/package/debian/rules
@@ -2,6 +2,12 @@
 
 export DH_VERBOSE = 1
 
+# The glibc the Nix toolchain builds against, and so the real floor for the
+# binaries. dpkg-shlibdeps would instead derive libc6 (>= 2.34) from the build
+# host's symbols file, where sysconf carries that minver, locking out distros
+# the binaries actually run on.
+LIBC_MIN = 2.31
+
 %:
 	dh $@
 
@@ -11,6 +17,8 @@ override_dh_auto_configure override_dh_auto_build override_dh_auto_test:
 override_dh_installsystemd:
 	dh_installsystemd --no-stop-on-upgrade xrpld.service
 
+# The tmpfiles snippet sets ownership to the xrpld user, so the sysusers snippet
+# has to be emitted first: run it early and make its own sequence slot a no-op.
 execute_before_dh_installtmpfiles:
 	dh_installsysusers
 
@@ -22,5 +30,23 @@ override_dh_install:
 	install -D -m 0644 xrpld.cfg        debian/xrpld/etc/xrpld/xrpld.cfg
 	install -D -m 0644 validators.txt   debian/xrpld/etc/xrpld/validators.txt
 
+override_dh_shlibdeps:
+	dh_shlibdeps
+	# Guards against the toolchain moving past LIBC_MIN and the packages then
+	# claiming a floor they do not meet.
+	for binary in xrpld validator-keys; do \
+		needed=$$(readelf --dyn-syms --wide $$binary \
+			| grep -o 'GLIBC_[0-9.]*' | sed 's/GLIBC_//' | sort -uV | tail -1); \
+		if [ -z "$$needed" ]; then \
+			echo "$$binary: no GLIBC_ symbol versions read, cannot check LIBC_MIN" >&2; \
+			exit 1; \
+		fi; \
+		if dpkg --compare-versions "$$needed" gt "$(LIBC_MIN)"; then \
+			echo "$$binary needs glibc $$needed, above LIBC_MIN $(LIBC_MIN)" >&2; \
+			exit 1; \
+		fi; \
+	done
+	sed -i 's/libc6 (>= [0-9.]*)/libc6 (>= $(LIBC_MIN))/' debian/xrpld.substvars
+
 override_dh_dwz:
 	@:
diff --git a/package/debian/xrpld.docs b/package/debian/xrpld.docs
index 77681ddc6e..97325dfcf5 100644
--- a/package/debian/xrpld.docs
+++ b/package/debian/xrpld.docs
@@ -1,2 +1,3 @@
 README.md
+LICENSE.md
 validator-keys-LICENSE
diff --git a/package/debian/xrpld.links b/package/debian/xrpld.links
index 10d34f5b8c..6dea4f28f3 100644
--- a/package/debian/xrpld.links
+++ b/package/debian/xrpld.links
@@ -1,2 +1,3 @@
-# Legacy compat symlinks (remove next major release)
+# Legacy compatibility for pre-FHS package layouts.
+# TODO: remove after rippled fully deprecated.
 usr/bin/xrpld          usr/local/bin/rippled
diff --git a/package/debian/xrpld.lintian-overrides b/package/debian/xrpld.lintian-overrides
new file mode 100644
index 0000000000..a0b3f583ed
--- /dev/null
+++ b/package/debian/xrpld.lintian-overrides
@@ -0,0 +1,6 @@
+# The /usr/local/bin/rippled symlink is deliberate compatibility for pre-FHS
+# layouts, so the Policy 9.1.2 tags it raises are expected.
+# TODO: remove alongside debian/xrpld.links after rippled fully deprecated.
+xrpld: dir-in-usr-local [usr/local/bin/]
+xrpld: file-in-usr-local [usr/local/bin/rippled]
+xrpld: file-in-unusual-dir [usr/local/bin/rippled]
diff --git a/package/docker/Dockerfile b/package/docker/Dockerfile
new file mode 100644
index 0000000000..adf372b6fa
--- /dev/null
+++ b/package/docker/Dockerfile
@@ -0,0 +1,10 @@
+ARG BASE_IMAGE=debian:trixie
+
+FROM ${BASE_IMAGE}
+
+# Bind-mounted rather than copied in, so the installer never lands in a layer.
+RUN --mount=type=bind,source=bin/install-packaging-tools.sh,target=/install-packaging-tools.sh \
+    /install-packaging-tools.sh
+
+# See ../README.md, "Publishing from other repositories".
+COPY package/docker/publish_pkg.py /usr/local/bin/publish_pkg.py
diff --git a/package/docker/publish_pkg.py b/package/docker/publish_pkg.py
new file mode 100755
index 0000000000..84a0448e7b
--- /dev/null
+++ b/package/docker/publish_pkg.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python3
+"""Publish built DEB and RPM packages to the XRPLF repositories on Nexus.
+
+Knows nothing about what it uploads beyond the channel, so it publishes whatever
+built the packages; see package/README.md, "Publishing from other repositories".
+
+RPMs are uploaded to the hosted repository, but yum clients install from the
+'rpm-' group repository in front of it, which serves signed metadata.
+
+NEXUS_USERNAME and NEXUS_PASSWORD are read from the environment, so the
+credentials never reach the process list.
+"""
+
+import argparse
+import base64
+import os
+import time
+import urllib.error
+import urllib.request
+from pathlib import Path
+
+SUFFIXES = (".deb", ".ddeb", ".rpm")
+
+# No progress for this long ends an attempt. urlopen applies the timeout per
+# socket operation, so a stalled transfer fails while a merely slow one carries
+# on -- the debuginfo package is large enough for that distinction to matter.
+STALL_TIMEOUT = 300
+
+ATTEMPTS = 4
+RETRY_DELAY = 5
+
+# 429 is Nexus asking to slow down, not a rejection, so it retries like a 5xx.
+RETRYABLE_STATUSES = (429,)
+
+
+def build_opener() -> urllib.request.OpenerDirector:
+    """An opener with no redirect handler, so a 3xx raises instead of being followed.
+
+    A redirected upload is silently downgraded to a GET, turning it into a no-op
+    that still answers 200.
+    """
+    opener = urllib.request.OpenerDirector()
+    opener.add_handler(urllib.request.HTTPHandler())
+    opener.add_handler(urllib.request.HTTPSHandler())
+    opener.add_handler(urllib.request.HTTPErrorProcessor())
+    opener.add_handler(urllib.request.HTTPDefaultErrorHandler())
+    return opener
+
+
+def upload(url: str, method: str, headers: dict[str, str], package: Path) -> None:
+    """Send one package, retrying only what is worth retrying.
+
+    A 4xx other than 429 is a deterministic rejection, so it is reported at once
+    rather than re-sending the whole body three more times. Nexus explains what
+    it rejected in the response body, so that body is always surfaced.
+    """
+    opener = build_opener()
+
+    for attempt in range(1, ATTEMPTS + 1):
+        try:
+            with package.open("rb") as body:
+                request = urllib.request.Request(
+                    url,
+                    data=body,
+                    method=method,
+                    headers={**headers, "Content-Length": str(package.stat().st_size)},
+                )
+                opener.open(request, timeout=STALL_TIMEOUT)
+            return
+        except urllib.error.HTTPError as error:
+            detail = error.read().decode(errors="replace").strip()
+            reason = f"HTTP {error.code}: {detail}"
+            retryable = error.code >= 500 or error.code in RETRYABLE_STATUSES
+        except (urllib.error.URLError, OSError) as error:
+            reason = str(error)
+            retryable = True
+
+        assert (
+            retryable and attempt < ATTEMPTS
+        ), f"upload of {package.name} failed: {reason}"
+        print(f"    attempt {attempt} failed ({reason}), retrying")
+        time.sleep(RETRY_DELAY)
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        "--channel",
+        required=True,
+        choices=("stable", "rc", "beta", "develop", "private"),
+        help="release channel, selecting the deb- and rpm--hosted repositories",
+    )
+    parser.add_argument(
+        "--package-dir",
+        type=Path,
+        default=Path("build"),
+        help=f"searched recursively for {', '.join(SUFFIXES)} (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--nexus-url",
+        default="https://packages.xrplf.org",
+        help="the Nexus instance to publish to (default: %(default)s)",
+    )
+    parser.add_argument(
+        "--dry-run",
+        action="store_true",
+        help="list the uploads without performing them",
+    )
+    args = parser.parse_args()
+    channel: str = args.channel
+    package_dir: Path = args.package_dir
+    nexus_url: str = args.nexus_url
+    dry_run: bool = args.dry_run
+
+    nexus = nexus_url.rstrip("/")
+    deb_repo = f"deb-{channel}"
+    rpm_repo = f"rpm-{channel}-hosted"
+
+    auth: dict[str, str] = {}
+    if not dry_run:
+        username = os.environ.get("NEXUS_USERNAME")
+        password = os.environ.get("NEXUS_PASSWORD")
+        assert username and password, "NEXUS_USERNAME and NEXUS_PASSWORD are required"
+        token = base64.b64encode(f"{username}:{password}".encode()).decode()
+        auth = {"Authorization": f"Basic {token}"}
+
+    # Deliberately not shared with sign_rpm.py: this script ships standalone in
+    # the packaging image for other repositories to run.
+    packages = sorted(
+        path
+        for path in package_dir.rglob("*")
+        if path.is_file() and path.suffix in SUFFIXES
+    )
+    # Uploading nothing would otherwise look like a successful publish.
+    assert packages, f"no packages found in {package_dir}"
+
+    print(f"Publishing {package_dir} to {deb_repo} and {rpm_repo} on {nexus}:")
+    for package in packages:
+        if package.suffix == ".rpm":
+            # yum repositories are addressed by path, and the arch comes from
+            # the name, e.g. xrpld-3.4.0-1.el9.x86_64.rpm.
+            destination = f"{rpm_repo}/{package.stem.rsplit('.', 1)[-1]}"
+            url = f"{nexus}/repository/{destination}/{package.name}"
+            method, content_type = "PUT", "application/octet-stream"
+        else:
+            # A raw body with a multipart Content-Type, POSTed to the repository
+            # root, is the documented upload for a hosted apt repository:
+            # https://help.sonatype.com/en/apt-repositories.html#deploying-packages-to-hosted-apt-repositories
+            destination = deb_repo
+            url = f"{nexus}/repository/{destination}/"
+            method, content_type = "POST", "multipart/form-data"
+
+        print(f"  {package.name} -> {destination}")
+        if not dry_run:
+            upload(url, method, {"Content-Type": content_type, **auth}, package)
+
+    verb = "would be published" if dry_run else "published"
+    print(f"{len(packages)} package(s) {verb}.")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/package/rpm/xrpld.spec b/package/rpm/xrpld.spec
index 0e3ee2a968..5139cd54e5 100644
--- a/package/rpm/xrpld.spec
+++ b/package/rpm/xrpld.spec
@@ -17,17 +17,27 @@ URL:      https://github.com/XRPLF/rippled
 ExclusiveArch: x86_64 aarch64
 BuildRequires: systemd-rpm-macros
 
-%undefine _debugsource_packages
-%debug_package
-# Intentionally trade larger RPM artifacts for faster package validation.
-%global _binary_payload w.ufdio
-%global _find_debuginfo_dwz_opts %{nil}
-
-%build_mtime_policy clamp_to_source_date_epoch
-
+# These have to precede %%debug_package: it opens the debuginfo subpackage, and
+# any tag after it is silently dropped from the main package.
 %{?systemd_requires}
 %{?sysusers_requires_compat}
 
+%undefine _debugsource_packages
+%debug_package
+# Level 3 rather than the el9 default of 19: it shrinks the multi-gigabyte
+# debuginfo package roughly fourfold in about a second, where 19 would spend
+# minutes on it.
+%global _binary_payload w3.zstdio
+%global _find_debuginfo_dwz_opts %{nil}
+
+# Reproducibility: the first two take their value from the SOURCE_DATE_EPOCH
+# build_pkg.py exports. Without these the header records the wall clock and the
+# build container's hostname, so two builds of the same commit differ.
+%global clamp_mtime_to_source_date_epoch 1
+%global use_source_date_epoch_as_buildtime 1
+%global _buildhost xrplf.org
+
+
 %description
 xrpld is the reference implementation of the XRP Ledger protocol. It
 participates in the peer-to-peer XRP Ledger network, processes
@@ -51,7 +61,7 @@ install -Dm0644 %{_sourcedir}/validators.txt       %{buildroot}%{_sysconfdir}/%{
 install -Dm0644 %{_sourcedir}/xrpld.service        %{buildroot}%{_unitdir}/xrpld.service
 install -Dm0644 %{_sourcedir}/xrpld.sysusers       %{buildroot}%{_sysusersdir}/xrpld.conf
 install -Dm0644 %{_sourcedir}/xrpld.tmpfiles       %{buildroot}%{_tmpfilesdir}/xrpld.conf
-install -Dm0644 /dev/null %{buildroot}%{_presetdir}/50-xrpld.preset
+install -d %{buildroot}%{_presetdir}
 cat >%{buildroot}%{_presetdir}/50-xrpld.preset <<'EOF'
 enable xrpld.service
 EOF
@@ -74,7 +84,7 @@ ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
 %sysusers_create_package %{name} %{_sourcedir}/xrpld.sysusers
 
 %post
-systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
+%tmpfiles_create_package %{name} %{_sourcedir}/xrpld.tmpfiles
 %systemd_post xrpld.service
 
 %preun
@@ -84,11 +94,12 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
 %systemd_postun xrpld.service
 
 %files
+%attr(0755,root,root) %dir %{_docdir}/%{name}
 %license %{_docdir}/%{name}/LICENSE.md
 %license %{_docdir}/%{name}/validator-keys-LICENSE
 %doc %{_docdir}/%{name}/README.md
 
-%dir %{_sysconfdir}/%{name}
+%attr(0755,root,root) %dir %{_sysconfdir}/%{name}
 
 %{_bindir}/%{name}
 %{_bindir}/validator-keys
@@ -99,7 +110,7 @@ systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
 
 
 %{_unitdir}/xrpld.service
-%{_presetdir}/50-xrpld.preset
+%attr(0644,root,root) %{_presetdir}/50-xrpld.preset
 %{_sysusersdir}/xrpld.conf
 %{_tmpfilesdir}/xrpld.conf
 %ghost %dir /var/lib/xrpld
diff --git a/package/shared/xrpld.service b/package/shared/xrpld.service
index f54e47aa14..27dd6a5a3a 100644
--- a/package/shared/xrpld.service
+++ b/package/shared/xrpld.service
@@ -17,6 +17,8 @@ ProtectHome=true
 PrivateTmp=true
 User=xrpld
 Group=xrpld
+# xrpld.tmpfiles creates these at install and boot; these recreate them on
+# every start, so a removed directory does not stop the service.
 StateDirectory=xrpld
 StateDirectoryMode=0750
 LogsDirectory=xrpld
@@ -24,9 +26,5 @@ LogsDirectoryMode=0750
 LimitNOFILE=65536
 SystemCallArchitectures=native
 
-# Uncomment both lines to allow xrpld to bind to privileged ports (<1024)
-#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
-#AmbientCapabilities=CAP_NET_BIND_SERVICE
-
 [Install]
 WantedBy=multi-user.target
diff --git a/package/sign_rpm.py b/package/sign_rpm.py
new file mode 100755
index 0000000000..07bda9f392
--- /dev/null
+++ b/package/sign_rpm.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+"""Sign the RPMs built by build_pkg.py.
+
+Nexus signs the yum repository metadata (via the 'rpm-' group
+repository), but never the packages themselves, so they carry their own
+signature. Clients verify the packages with gpgcheck=1 and the metadata with
+repo_gpgcheck=1.
+
+The DEBs are deliberately not signed: embedded DEB signatures exist (debsigs),
+but apt does not verify them by default and trusts the repository metadata,
+which Nexus signs, instead.
+
+PKG_SIGNING_KEY is read from the environment, so the key never reaches the
+process list.
+"""
+
+from __future__ import annotations
+
+import argparse
+import os
+import subprocess
+import tempfile
+from pathlib import Path
+
+# An RSA signature lands in the RSAHEADER tag, a DSA or EdDSA one in DSAHEADER,
+# so both are queried; checking only the first would reject a signed package.
+SIGNATURE_QUERY = "%{RSAHEADER:pgpsig}%{DSAHEADER:pgpsig}"
+UNSIGNED = "(none)(none)"
+
+
+def gpg(gnupghome: Path, *args: str, stdin: str | None = None) -> str:
+    """Run gpg against a throwaway keyring and return its stdout."""
+    return subprocess.run(
+        ["gpg", "--batch", "--quiet", *args],
+        input=stdin,
+        # stderr is left alone so a failing gpg explains itself.
+        stdout=subprocess.PIPE,
+        text=True,
+        check=True,
+        env={**os.environ, "GNUPGHOME": str(gnupghome)},
+    ).stdout
+
+
+def import_key(gnupghome: Path, key: str) -> str:
+    """Import the armoured private key and return its fingerprint."""
+    gpg(gnupghome, "--import", stdin=key)
+
+    records = [
+        line.split(":")
+        for line in gpg(gnupghome, "--list-secret-keys", "--with-colons").splitlines()
+    ]
+    # Exactly one, so the fingerprint picked below is not a guess.
+    secrets = [record for record in records if record[0] == "sec"]
+    assert (
+        len(secrets) == 1
+    ), f"PKG_SIGNING_KEY must hold exactly one secret key, found {len(secrets)}"
+
+    # The first fingerprint belongs to the primary key; subkeys follow.
+    fingerprints = [record[9] for record in records if record[0] == "fpr"]
+    assert fingerprints, "PKG_SIGNING_KEY holds a secret key with no fingerprint"
+    return fingerprints[0]
+
+
+def sign(gnupghome: Path, rpms: list[Path], fingerprint: str) -> None:
+    """Attach a signature to every RPM in one rpmsign invocation."""
+    subprocess.run(
+        [
+            "rpmsign",
+            "--define",
+            f"_gpg_name {fingerprint}",
+            # Loopback pinentry: the key is unattended, so there is no tty to
+            # prompt on.
+            "--define",
+            "_gpg_sign_cmd_extra_args --pinentry-mode loopback --batch --yes",
+            "--addsign",
+            *(str(rpm) for rpm in rpms),
+        ],
+        check=True,
+        env={**os.environ, "GNUPGHOME": str(gnupghome)},
+    )
+
+
+def verify(rpms: list[Path]) -> None:
+    """Fail unless every RPM now carries a signature.
+
+    rpmsign can exit 0 having attached nothing, and an unsigned package is only
+    rejected later, on the installing machine.
+    """
+    for rpm in rpms:
+        signature = subprocess.run(
+            ["rpm", "--query", "--queryformat", SIGNATURE_QUERY, "--package", str(rpm)],
+            stdout=subprocess.PIPE,
+            text=True,
+            check=True,
+        ).stdout.strip()
+        assert signature != UNSIGNED, f"{rpm} is unsigned after rpmsign"
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument(
+        "--package-dir",
+        type=Path,
+        default=Path("build"),
+        help="searched recursively for *.rpm (default: %(default)s)",
+    )
+    args = parser.parse_args()
+    package_dir: Path = args.package_dir
+
+    # Deliberately not shared with publish_pkg.py, which ships standalone in the
+    # packaging image.
+    rpms = sorted(path for path in package_dir.rglob("*.rpm") if path.is_file())
+    # Signing nothing would otherwise look like a successful signing.
+    assert rpms, f"no RPMs found in {package_dir}"
+
+    key = os.environ.get("PKG_SIGNING_KEY")
+    assert key, "PKG_SIGNING_KEY is required"
+
+    # The keyring holds an unencrypted private key, so it goes even if signing
+    # fails.
+    with tempfile.TemporaryDirectory() as tmp:
+        gnupghome = Path(tmp)
+        fingerprint = import_key(gnupghome, key)
+        print(f"Signing {len(rpms)} RPM(s) with {fingerprint}.")
+        sign(gnupghome, rpms, fingerprint)
+        verify(rpms)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
index a82b4734d8..dd5e1fe438 100644
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,4 +1,4 @@
 [toolchain]
-channel = "1.95"
-components = ["rustfmt", "clippy", "rust-analyzer", "llvm-tools-preview"]
+channel = "1.97.1"
+components = ["rustfmt", "clippy", "rust-analyzer", "llvm-tools-preview", "rust-src"]
 profile = "minimal"
diff --git a/src/benchmarks/libxrpl/CMakeLists.txt b/src/benchmarks/libxrpl/CMakeLists.txt
index ac751a0413..ab3e68b87b 100644
--- a/src/benchmarks/libxrpl/CMakeLists.txt
+++ b/src/benchmarks/libxrpl/CMakeLists.txt
@@ -20,3 +20,20 @@ target_link_libraries(
 xrpl_add_benchmark(nodestore)
 target_link_libraries(xrpl.bench.nodestore PRIVATE xrpl.imports.bench)
 add_dependencies(xrpl.benchmarks xrpl.bench.nodestore)
+
+# Gas calibration for the wasm host functions. The ledger and real host come from
+# `xrpl.testkit.wasm` (built with the tests, but framework-free), so this target links
+# no GTest and no GMock.
+if(TARGET xrpl.testkit.wasm)
+    xrpl_add_benchmark(wasm)
+    target_link_libraries(
+        xrpl.bench.wasm
+        PRIVATE xrpl.imports.bench xrpl.testkit.wasm
+    )
+    add_dependencies(xrpl.benchmarks xrpl.bench.wasm)
+else()
+    message(
+        STATUS
+        "xrpl.testkit.wasm not built (tests disabled); skipping xrpl.bench.wasm."
+    )
+endif()
diff --git a/src/benchmarks/libxrpl/nodestore/Backend.cpp b/src/benchmarks/libxrpl/nodestore/Backend.cpp
index cd3e15bd65..9d5937f869 100644
--- a/src/benchmarks/libxrpl/nodestore/Backend.cpp
+++ b/src/benchmarks/libxrpl/nodestore/Backend.cpp
@@ -41,10 +41,11 @@ struct RunState
     release()
     {
         harness.reset();
-        Batch{}.swap(present);
-        Batch{}.swap(recent);
-        std::vector{}.swap(missing);
-        std::vector{}.swap(shuffle);
+        present = Batch{};
+        recent = Batch{};
+        missing = std::vector{};
+        shuffle = std::vector{};
+        avgPayload = 0;
     }
 };
 
@@ -239,9 +240,13 @@ registerWorkload(BackendConfig const& bc, Workload const& w)
     if (!w.pinToPool)
     {
         auto rs = std::make_shared();
-        auto* b = benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs));
-        b->RangeMultiplier(10)->Range(kPoolSizes.front(), kPoolSizes.back());
-        b->Threads(1)->Threads(4)->Threads(8)->UseRealTime();
+        benchmark::RegisterBenchmark(name, makeRunner(w, cfg, rs))
+            ->RangeMultiplier(10)
+            ->Range(kPoolSizes.front(), kPoolSizes.back())
+            ->Threads(1)
+            ->Threads(4)
+            ->Threads(8)
+            ->UseRealTime();
 
         return;
     }
diff --git a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h
index debdc5d47a..a90207f26a 100644
--- a/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h
+++ b/src/benchmarks/libxrpl/nodestore/NodeStoreBench.h
@@ -2,10 +2,10 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -227,7 +227,7 @@ sliceFixedBatches(Batch const& pool, std::size_t batchSize)
  */
 struct BackendHarness
 {
-    beast::TempDir tempDir;  ///< Declared first so it is destroyed last
+    TempDir tempDir;  ///< Declared first so it is destroyed last
     DummyScheduler scheduler;
     beast::Journal journal{beast::Journal::getNullSink()};
     std::unique_ptr backend;
@@ -257,7 +257,7 @@ struct BackendHarness
  */
 struct DatabaseHarness
 {
-    beast::TempDir tempDir;
+    TempDir tempDir;
     DummyScheduler scheduler;
     beast::Journal journal{beast::Journal::getNullSink()};
     std::unique_ptr db;
@@ -297,12 +297,11 @@ struct BackendConfig
 inline std::vector const&
 backendConfigs()
 {
+    // Use factory settings for each DB
     static std::vector const kConfigs = {
         {.name = "nudb", .config = "type=nudb"},
 #if XRPL_ROCKSDB_AVAILABLE
-        {.name = "rocksdb",
-         .config = "type=rocksdb,open_files=2000,filter_bits=12,cache_mb=256,"
-                   "file_size_mb=8,file_size_mult=2"},
+        {.name = "rocksdb", .config = "type=rocksdb"},
 #endif
     };
     return kConfigs;
diff --git a/src/benchmarks/libxrpl/wasm/BenchFixtures.cpp b/src/benchmarks/libxrpl/wasm/BenchFixtures.cpp
new file mode 100644
index 0000000000..3eb9c7e35b
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/BenchFixtures.cpp
@@ -0,0 +1,175 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test::bench {
+
+Fixtures::Fixtures()
+    : alice_{ledger_.fund("benchAlice")}
+    , bob_{ledger_.fund("benchBob")}
+    , signerListOwner_{ledger_.fund("benchSigners")}
+    , escrow_{keylet::account(AccountID{})}
+    , signedMessage_{signMessage("the quick brown fox jumps over the lazy dog")}
+    , nftId_{NftIds::makeNftId(alice_.id())}
+{
+    ledger_.makeSignerList(signerListOwner_, 2, {{alice_, 1}, {bob_, 1}});
+
+    // The escrow has to be submitted after the accounts exist, which is why it is built here
+    // rather than in the initializer list: its keylet depends on the owner's sequence number at
+    // submission time.
+    auto const ownerSeq = ledger_.ledger.getAccountRoot(alice_.id()).getSequence();
+    auto const created = ledger_.ledger.submit(
+        transactions::EscrowCreateBuilder{alice_.id(), bob_.id(), XRP(100)}.setFinishAfter(
+            900'000'000),
+        alice_);
+    if (created.ter != tesSUCCESS)
+    {
+        fixtureFailed(std::string{"creating the escrow: "} + transToken(created.ter));
+    }
+    ledger_.ledger.close();
+    escrow_ = keylet::escrow(alice_.id(), SeqProxy::rawSequence(ownerSeq));
+}
+
+Account const&
+Fixtures::alice() const
+{
+    return alice_;
+}
+
+Account const&
+Fixtures::bob() const
+{
+    return bob_;
+}
+
+TxAssembler
+Fixtures::memoTx()
+{
+    auto assembler = escrowFinishTx(ledger_.ledger, alice_);
+    assembler.build = [inner = std::move(assembler.build)](STObject& obj) {
+        inner(obj);
+        auto memos = STArray{};
+        memos.push_back(makeMemo(WasmLedger::toBytes("hello")));
+        memos.push_back(makeMemo(WasmLedger::toBytes("world")));
+        obj.setFieldArray(sfMemos, memos);
+    };
+    return assembler;
+}
+
+FieldLocator
+Fixtures::memoLocator()
+{
+    return FieldLocator{{sfMemos.getCode(), 0, sfMemoData.getCode()}};
+}
+
+WasmHost
+Fixtures::host()
+{
+    auto assembler = memoTx();
+    return ledger_.makeHost(
+        keylet::account(alice_.id()), assembler.type, std::move(assembler.build));
+}
+
+WasmHost
+Fixtures::cachedHost()
+{
+    auto wasmHost = host();
+    if (!wasmHost->cacheLedgerObj(keylet::account(alice_.id()).key, 1).has_value())
+    {
+        fixtureFailed("caching the account root into slot 1");
+    }
+    return wasmHost;
+}
+
+WasmHost
+Fixtures::signerListHost()
+{
+    auto assembler = bareTx();
+    return ledger_.makeHost(
+        keylet::signerList(signerListOwner_.id()), assembler.type, std::move(assembler.build));
+}
+
+WasmHost
+Fixtures::cachedSignerListHost()
+{
+    auto assembler = bareTx();
+    auto wasmHost =
+        ledger_.makeHost(keylet::account(AccountID{}), assembler.type, std::move(assembler.build));
+    if (!wasmHost->cacheLedgerObj(keylet::signerList(signerListOwner_.id()).key, 1).has_value())
+    {
+        fixtureFailed("caching the signer list into slot 1");
+    }
+    return wasmHost;
+}
+
+WasmHost
+Fixtures::tracingHost()
+{
+    return ledger_.makeTracingHost();
+}
+
+Keylet const&
+Fixtures::escrow() const
+{
+    return escrow_;
+}
+
+WasmHost
+Fixtures::escrowHost()
+{
+    return ledger_.makeHost(escrow_);
+}
+
+Slice
+Fixtures::floatX()
+{
+    return FloatConstants::slice(FloatConstants::kPi);
+}
+
+Slice
+Fixtures::floatY()
+{
+    return FloatConstants::slice(FloatConstants::kTwo);
+}
+
+SignedMessage const&
+Fixtures::signedMessage() const
+{
+    return signedMessage_;
+}
+
+uint256 const&
+Fixtures::nftId() const
+{
+    return nftId_;
+}
+
+Fixtures&
+Fixtures::instance()
+{
+    static Fixtures kValue;
+    return kValue;
+}
+
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/BenchFixtures.h b/src/benchmarks/libxrpl/wasm/BenchFixtures.h
new file mode 100644
index 0000000000..8693a13dfd
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/BenchFixtures.h
@@ -0,0 +1,111 @@
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+
+// The ledger and canned inputs every `*.bench.cpp` measures against.
+class Fixtures
+{
+public:
+    // The one set of fixtures every benchmark shares, built on first use.
+    static Fixtures&
+    instance();
+
+    // A sequence number for the keylets that take one. Arbitrary — a keylet hashes whatever it
+    // is given, so the value cannot change the cost.
+    static constexpr std::uint32_t kSeq = 42;
+
+    // Rounding mode 0 throughout the float family: modes select a tie-breaking rule, not a
+    // different algorithm, so they do not move the cost, and pinning one keeps the fourteen
+    // comparable.
+    static constexpr std::int32_t kRoundingMode = 0;
+
+    // Two funded accounts, enough for every keylet shape and every object below.
+    [[nodiscard]] Account const&
+    alice() const;
+    [[nodiscard]] Account const&
+    bob() const;
+
+    // The default host: its transaction carries a two-element memo array (something for the
+    // nested getters to walk to and the array-length getters to count) and its current object is
+    // Alice's account root.
+    [[nodiscard]] WasmHost
+    host();
+
+    // The same, with Alice's account root pinned to slot 1, for the `le_*` getters that read
+    // through a cache slot rather than the current object.
+    [[nodiscard]] WasmHost
+    cachedHost();
+
+    // An account root has no arrays, so the array-length getters that read a *ledger object*
+    // need a different one. A signer list has `sfSignerEntries`; without it those calls would
+    // answer `FieldNotFound` and the benchmark would time the rejection instead of the work.
+    [[nodiscard]] WasmHost
+    signerListHost();
+    [[nodiscard]] WasmHost
+    cachedSignerListHost();
+
+    // A host whose `trace` output is captured rather than dropped, so the log-enabled path can
+    // be measured against the log-disabled one that `host()` gives.
+    [[nodiscard]] WasmHost
+    tracingHost();
+
+    // A real escrow, created through the real transactor — the current object for
+    // `home_le_field`, the one getter whose cost depends on the object it reads rather than on
+    // its arguments.
+    [[nodiscard]] Keylet const&
+    escrow() const;
+    [[nodiscard]] WasmHost
+    escrowHost();
+
+    // `sfMemos[0].sfMemoData` — a two-step locator path, the shape the nested getters are priced
+    // for.
+    [[nodiscard]] static FieldLocator
+    memoLocator();
+
+    // Canonical float operands. Zeroed bytes decode as a non-canonical float and would be
+    // refused before any arithmetic ran, so the whole family shares these two known-good values.
+    [[nodiscard]] static Slice
+    floatX();
+    [[nodiscard]] static Slice
+    floatY();
+
+    // A signed message for `check_sig`. Signing is far more expensive than the verification
+    // being measured, so it happens once here rather than inside a timed loop.
+    [[nodiscard]] SignedMessage const&
+    signedMessage() const;
+
+    // A well-formed NFToken id with the fixture's known taxon, flags, fee and sequence baked in,
+    // so the id-extractor getters have real fields to pull out rather than zeros.
+    [[nodiscard]] uint256 const&
+    nftId() const;
+
+private:
+    // Order matters, and that is the reason this is a constructor rather than a pile of lazy
+    // statics: the accounts have to be funded before the signer list and the escrow can be built
+    // on them.
+    Fixtures();
+
+    // The transaction the default host runs, carrying the memo array.
+    [[nodiscard]] TxAssembler
+    memoTx();
+
+    WasmLedger ledger_;
+    Account alice_;
+    Account bob_;
+    Account signerListOwner_;
+    Keylet escrow_;
+    SignedMessage signedMessage_;
+    uint256 nftId_;
+};
+
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/Crossing.cpp b/src/benchmarks/libxrpl/wasm/Crossing.cpp
new file mode 100644
index 0000000000..d764d051b7
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/Crossing.cpp
@@ -0,0 +1,44 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+// The harness checking itself.
+//
+// This file holds no host function — it belongs to the wasm directory rather than
+// `host_functions/` because it measures the two reference points every per-function number is read
+// against, and neither is a host call.
+//
+// `GuestInstruction` runs a contract whose "host call" is a couple of guest instructions. Its
+// `implied_gas` and `charged_gas` are then two independent measurements of the same quantity — one
+// from wall time via `secondsPerGas`, one from the engine's own fuel meter — and they should agree
+// closely. When they diverge, `secondsPerGas` has measured something other than a guest
+// instruction and no other number in the run is trustworthy. Read this first.
+//
+// The crossing floor is the other reference point, and it lives in `host_functions/LedgerSqn`:
+// `ldgr_index` takes no input and answers from a header already in hand, so its impl is as close
+// to nothing as a host function gets, and whatever its `ThroughVm` case costs above its `Impl`
+// case is the price of leaving the guest — paid by every one of the 61 functions before any of
+// them does any work.
+//
+// So the reading order across the suite is: this file, then `LedgerSqn`'s pair for the floor, then
+// a function's own `Impl` number. Those three should account for its `ThroughVm` number; where
+// they do not, the gap is size-dependent copying, which the swept cases (`Sha512Half`,
+// `UpdateData`) expose.
+
+void
+guestInstruction(benchmark::State& state)
+{
+    static constexpr std::string_view kBody = "(i32.add (local.get $r) (i32.const 1))";
+    // Empty import name: this case prices no host function, so there is nothing to look a
+    // declaration up for and it reports no `suggested_gas`.
+    benchmarkThroughVm(state, "", "", "", kBody, [] { return Fixtures::instance().host(); });
+}
+BENCHMARK(guestInstruction)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/README.md b/src/benchmarks/libxrpl/wasm/README.md
new file mode 100644
index 0000000000..27700b937f
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/README.md
@@ -0,0 +1,182 @@
+# WASM host functions — gas calibration
+
+These are **not tests**: nothing asserts, and a number moving is not a build failure. They answer
+the question the tests cannot — whether each `#[gas = N]` in
+`crates/xrpl-host-functions/src/lib.rs` matches what the function actually costs.
+
+```bash
+cmake --build build --target xrpl.bench.wasm
+./build/xrpl.bench.wasm # everything (~2 min)
+./build/xrpl.bench.wasm --benchmark_filter=sha512Half
+./build/xrpl.bench.wasm --benchmark_repetitions=25 --benchmark_report_aggregates_only=true
+```
+
+**Build Release.** Debug inflates the crossing far more than the impls: `Impl`-to-`Impl` ratios
+survive it, `suggested_gas` does not.
+
+## Reading the output
+
+| Counter             | Meaning                                                                            |
+| ------------------- | ---------------------------------------------------------------------------------- |
+| `suggested_gas`     | **the answer** — what this function should be priced at                            |
+| `host_function_gas` | what `lib.rs` says today, read through the `wasm_testkit` bridge so it can't drift |
+| `price_ratio`       | `host_function_gas / suggested_gas`. **1.0 is correct; below 1 is underpriced**    |
+| `rel_error`         | relative uncertainty of `suggested_gas`. **Quote this one** — ~1.2% when quiet     |
+| `unreliable`        | `1` means do not act on this row                                                   |
+| `implied_gas`       | the raw measurement, before the crossing is added back                             |
+| `charged_gas`       | what the engine actually billed; confirms the right call was measured              |
+| `ns_per_call`       | raw wall time, for debugging a suspicious ratio                                    |
+
+Sort by `price_ratio`. **Below 1 is the direction that matters** — an underpriced call is one a
+contract can buy too cheaply, a denial-of-service vector rather than a rounding error:
+
+```bash
+./build/xrpl.bench.wasm --benchmark_format=json |
+    jq -r '.benchmarks[] | select(.price_ratio) | [.price_ratio, .name] | @tsv' | sort -n
+```
+
+`unreliable=1` when `rel_error` exceeds 25%, or when `suggested_gas` falls below the crossing floor
+— a call whose own cost is small next to the crossing is read off the difference of two nearly
+equal numbers.
+
+### With `--benchmark_repetitions`
+
+Adds `_mean` / `_median` / `_stddev` / `_cv` rows. One trap worth knowing:
+
+**`cv` on `suggested_gas` for an `Impl` case is not an error bar.** The crossing floor is measured
+once per process, so repetitions never resample it — and it is most of a cheap `Impl` case's value.
+Over 25 repetitions on an idle machine, `Impl` `cv` reads **1.3%** against `ThroughVm`'s 2.3%, while
+`rel_error` is 1.2% for both. The lowest number in the output is the least trustworthy one.
+
+Use repetitions to confirm the machine is quiet and to get a median; quote `rel_error`. Under load
+the `Impl` cases inflate first (3.0% against 1.8% at load ~10), which makes the gap between the two
+families a usable load detector.
+
+## How `suggested_gas` is measured
+
+Gas is wasmi fuel — `set_fuel(gas)` meters guest instructions and host charges from one pool — so
+one gas is about one guest instruction and the question becomes a ratio. Every step is a
+subtraction, so fixed costs cancel:
+
+```
+secondsPerGas  = (time_busy − time_idle) / (fuel_busy − fuel_idle)   # pure-wasm loop, N vs 0
+implied_gas    = secondsPerCall / secondsPerGas
+crossing_floor = (ldgr_index ThroughVm − ldgr_index Impl) / secondsPerGas
+
+suggested_gas  = implied_gas                     # ThroughVm — the guest already paid the crossing
+suggested_gas  = implied_gas + crossing_floor    # Impl — a guest cannot call without paying it
+price_ratio    = host_function_gas / suggested_gas
+
+rel_error = sqrt( ( sqrt((implied·caseErr)² + (floor·floorErr)²) / suggested )² + perGasErr² )
+```
+
+`secondsPerCall` is itself a subtraction: a `ThroughVm` case runs a contract making N host calls
+against a **byte-identical** one making none, so compilation, instantiation and the guest's own loop
+cancel. An `Impl` case times `kCallsPerRun` direct calls and divides.
+
+`secondsPerGas` is measured **once** and shared by every case, deliberately — two routes to the same
+price must divide by the same constant, and per-case calibration was tried and swung 6x between
+runs. Its uncertainty is propagated arithmetically instead. See the comments in `WasmBench.cpp` for
+why the three terms in `rel_error` do not all combine in quadrature.
+
+**`guestInstruction` is the self-test — read it first.** It runs the calibration's own loop body, so
+its `implied_gas` (wall time) and `charged_gas` (the fuel meter) are two independent measurements of
+one quantity. Quiet Release machine: **≈13.7 against 13.007, ~5% high.** A persistent gap much
+beyond that means every other number in the run shares it. It has already caught an estimator
+mismatch worth +40%, and the memory leak below.
+
+**`suggested_gas` for an `Impl`-only case is a lower bound** — the crossing floor is measured on a
+call with no input, so a function that moves bytes pays more; `Sha512Half` and `UpdateData` sweep
+that per-byte term. `ThroughVm` cases are deliberately one per crossing _shape_, not one per
+function.
+
+## VM overhead (`Vm.cpp`)
+
+Everything above prices what a contract _asks for_. `Vm.cpp` prices getting it running at all. Of
+the seven stages `runEscrowWasm` performs — compile, store, linker, instantiate, entry-point lookup,
+call, fuel read — **only the call is metered**; the rest is wall time no transaction pays for.
+
+These cases report `ns_per_op` and `gas_equivalent` (that time over the same `secondsPerGas`, so an
+unpriced stage reads on the same axis as a priced one), plus `module_bytes` and `gas_per_byte` on
+size sweeps.
+
+| case                       | measures                                                                                                                     |
+| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
+| `preflightMinimal`         | compile plus the import/export walk. The narrowest view of compilation reachable from C++: no host, no instance              |
+| `preflightRejects`         | the same on a module refused at the import walk. Against `preflightMinimal`, what refusing costs versus accepting            |
+| `runMinimal`               | a whole run of a do-nothing contract — every stage. Minus `preflightMinimal`, the stages that are not compilation            |
+| `compileScaling/N`         | compilation against module size: `N` unreachable filler functions, preflight only                                            |
+| `runScaling/N`             | the same modules through a whole run. Paired with `compileScaling` per size, separates size-dependent stages from fixed ones |
+| `instantiateScaling/pages` | declared memory at constant module size, so what moves is the host allocating and zeroing pages                              |
+
+Two of those pairings are the point of the file. `runMinimal − preflightMinimal` gives the fixed
+cost of everything that is not compilation; `runScaling` against `compileScaling` shows compilation
+appearing a second time, because the transactor validates and executes with no module cache between
+them. The size sweeps matter more than the floor: a fixed cost is only a griefing concern if it is
+large, but a slope against attacker-chosen module size is one at any height.
+
+Two caveats when reading a sweep. `gas_per_byte` is an **average** carrying the case's fixed cost,
+not a marginal rate — it overestimates, and falls toward the true slope as the module grows, so read
+the convergence rather than any single row. And the `/4096` points get few iterations and go noisy
+first; compare `rel_error` across the sweep before quoting the largest one.
+
+The sweep stops at 4096 functions for want of a real cap to stop at — no maximum contract size is
+enforced anywhere yet, the transactor not being wired.
+
+The linker rebuild and the fuel-metering overhead are **not** separable from here — C++ sees only
+`runEscrowWasm` and `preflightEscrowWasm`. Both need benchmarks inside `xrpl-wasm-vm`, where
+`compile` and `wasm_engine` are `pub(crate)`.
+
+### Compiling leaks — pin your iteration counts
+
+`wasm_engine()` is a process-global `LazyLock`, and what `Module::new` adds to it is never
+released. Repeatedly preflighting one **60-byte** module:
+
+| `--benchmark_repetitions` | peak RSS |
+| ------------------------- | -------- |
+| 1                         | 0.41 GB  |
+| 5                         | 1.46 GB  |
+| 15                        | 4.20 GB  |
+
+Linear, at roughly **800 bytes per compile**. Within the suite this is why every `Vm.cpp` case pins
+`->Iterations(...)`: automatic sizing ran `preflightMinimal` ~348k times per repetition, reaching
+7.9 GB at 25 repetitions, after which every later case in the binary failed to compile — 720 errored
+rows, all blaming cases that were innocent.
+
+**Outside the suite it is worth a look.** A validator compiles once to screen an `EscrowCreate`
+and again for every `EscrowFinish` that runs the contract — with no module cache between them, and
+once per apply attempt rather than once per transaction — all against that same static engine.
+Whether that is unbounded growth in production depends on wasmi internals not checked here (wasmi
+2.0.0, wasmparser 0.228): this is the C++-visible symptom, not a diagnosis.
+
+## Gotchas, each of which has already cost someone an afternoon
+
+- **The wasm ABI is not the trait's argument order.** `float_add(x, y, mode, out)` in Rust is
+  `(x_ptr, x_len, y_ptr, y_len, out_ptr, out_len, mode)` on the wire — scalars move _after_ the
+  output region. Check `register.rs`, not `lib.rs`, when writing WAT.
+- **A soft host error still "succeeds".** The run completes and gas is charged _before_ the body, so
+  a wrong-argument case reports a plausible, confidently wrong number. The harness requires the
+  contract's result to be `>= 0`; the tell is a `ThroughVm` case coming out _faster_ than its `Impl`.
+- **A host serves exactly one run** (`checkSelf` in `WasmVM.cpp`), so a benchmark builds a fresh host
+  per run and cannot pre-cache a slot.
+- **`MAX_FIELD_BYTES` is 1024** — nothing crosses the boundary above 1 KiB, so size sweeps stop there.
+- **Do not compare timings across builds at the cheap end.** Two builds whose timed regions were
+  byte-identical measured 2.18 ns and 2.52 ns for the same case — ~15% apart, from code layout alone.
+  Use `price_ratio` within one run, and reason from the code when judging whether a change costs
+  anything.
+- **Every case pins `->Iterations(...)`, for two different reasons.** Host-function cases must
+  because with `UseManualTime` automatic sizing reads only the tiny reported residue and would ask
+  for millions of iterations; `Vm.cpp` cases must because compiling leaks.
+
+## Layout
+
+One `.cpp` per host function under `host_functions/`, mirroring
+`src/tests/libxrpl/tx/wasm/host_functions/`, so adding a host function is a two-file checklist
+rather than a judgement call. `Crossing.cpp` holds the harness's own reference points, `Vm.cpp` the
+per-run overhead around them, `WasmBench.*` the measurement machinery, `BenchFixtures.*` the shared
+ledger (one ledger, funded once, for the whole binary).
+
+The ledger and real host come from `xrpl.testkit.wasm` — a framework-free library built alongside
+the tests — so this target links **no GTest and no GMock**. See
+`src/tests/libxrpl/tx/wasm/README.md` for how that library is split, and why its setup steps throw
+rather than using `EXPECT_`.
diff --git a/src/benchmarks/libxrpl/wasm/Vm.cpp b/src/benchmarks/libxrpl/wasm/Vm.cpp
new file mode 100644
index 0000000000..dd5ec2e420
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/Vm.cpp
@@ -0,0 +1,149 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+// What a run costs *around* the contract, rather than what its host calls cost. Only the guest's
+// own execution is metered, so every stage measured here is wall time no transaction pays for.
+// ../README.md has what each case measures and how to read `gas_equivalent`.
+//
+// **Every case must pin `->Iterations(...)`.** Compiling a module allocates against the
+// process-global engine and is never reclaimed — roughly 800 bytes per compile — so Google
+// Benchmark's automatic sizing, which targets a wall-clock budget rather than a compile count,
+// reaches gigabytes resident and the whole binary stops being able to compile anything.
+
+// The smallest module the engine accepts. Everything a run does to it is overhead by construction.
+std::string
+minimalWat()
+{
+    return R"wat((module
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (i32.const 1)))
+)wat";
+}
+
+// `count` unreachable functions on top of the minimal module: bigger without doing more.
+//
+// Each body is seeded with its own index so no two are identical and none folds to a constant the
+// translator can drop — either would break the link between function count and byte count.
+// Unreachable is fine: validation and translation visit every function a module declares, and that
+// visit is exactly the cost being swept.
+std::string
+fillerWat(size_t count)
+{
+    auto out = std::string{"(module\n  (memory (export \"memory\") 1)\n"};
+    for (auto i = 0uz; i < count; ++i)
+    {
+        out += std::format(
+            "  (func $f{0} (param i32) (result i32)\n"
+            "    (i32.add (i32.mul (local.get 0) (i32.const {0})) (i32.const {0})))\n",
+            i);
+    }
+    out += "  (func (export \"escrow_finish\") (result i32)\n    (i32.const 1)))\n";
+    return out;
+}
+
+// The minimal module asking for `pages` of initial memory. `MAX_MEMORY_PAGES` is 128 (8 MiB), and a
+// contract declares this for free — the host allocates and zeroes it before the first instruction.
+std::string
+pagesWat(size_t pages)
+{
+    return std::format(
+        "(module\n"
+        "  (memory (export \"memory\") {})\n"
+        "  (func (export \"escrow_finish\") (result i32)\n"
+        "    (i32.const 1)))\n",
+        pages);
+}
+
+// Compiles cleanly and is then refused: `env::malloc` is not a namespace the engine serves, so
+// `check_imports` stops at it.
+std::string
+rejectedWat()
+{
+    return R"wat((module
+  (import "env" "malloc" (func $malloc (param i32) (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (i32.const 1)))
+)wat";
+}
+
+void
+preflightMinimal(benchmark::State& state)
+{
+    static auto const kWasm = assembleWat(minimalWat());
+    benchmarkPreflight(state, kWasm);
+}
+BENCHMARK(preflightMinimal)->UseManualTime()->Iterations(kBenchIterations);
+
+void
+preflightRejects(benchmark::State& state)
+{
+    static auto const kWasm = assembleWat(rejectedWat());
+    benchmarkPreflight(state, kWasm, /*expectAccepted*/ false);
+}
+BENCHMARK(preflightRejects)->UseManualTime()->Iterations(kBenchIterations);
+
+void
+runMinimal(benchmark::State& state)
+{
+    static auto const kWasm = assembleWat(minimalWat());
+    benchmarkRun(state, kWasm, [] { return Fixtures::instance().host(); });
+}
+BENCHMARK(runMinimal)->UseManualTime()->Iterations(kBenchIterations);
+
+void
+compileScaling(benchmark::State& state)
+{
+    auto const wasm = assembleWat(fillerWat(static_cast(state.range(0))));
+    benchmarkPreflight(state, wasm, /*expectAccepted*/ true, /*sizeSweep*/ true);
+}
+BENCHMARK(compileScaling)
+    ->UseManualTime()
+    ->Iterations(kBenchIterations)
+    ->Arg(1)
+    ->Arg(8)
+    ->Arg(64)
+    ->Arg(512)
+    ->Arg(4096);
+
+void
+runScaling(benchmark::State& state)
+{
+    auto const wasm = assembleWat(fillerWat(static_cast(state.range(0))));
+    benchmarkRun(state, wasm, [] { return Fixtures::instance().host(); }, /*sizeSweep*/ true);
+}
+BENCHMARK(runScaling)
+    ->UseManualTime()
+    ->Iterations(kBenchIterations)
+    ->Arg(1)
+    ->Arg(8)
+    ->Arg(64)
+    ->Arg(512)
+    ->Arg(4096);
+
+void
+instantiateScaling(benchmark::State& state)
+{
+    auto const wasm = assembleWat(pagesWat(static_cast(state.range(0))));
+    benchmarkRun(state, wasm, [] { return Fixtures::instance().host(); });
+}
+BENCHMARK(instantiateScaling)
+    ->UseManualTime()
+    ->Iterations(kBenchIterations)
+    ->Arg(1)
+    ->Arg(8)
+    ->Arg(32)
+    ->Arg(128);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/WasmBench.cpp b/src/benchmarks/libxrpl/wasm/WasmBench.cpp
new file mode 100644
index 0000000000..e5975f2403
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/WasmBench.cpp
@@ -0,0 +1,397 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test::bench {
+
+int
+callsWithinTransferBudget(std::int64_t bytesWrittenPerCall)
+{
+    // Writes nothing back to the guest, so the budget does not apply.
+    if (bytesWrittenPerCall <= 0)
+    {
+        return kCallsPerRun;
+    }
+
+    auto const affordable = kTransferLimitBytes / bytesWrittenPerCall;
+    if (affordable < 1)
+    {
+        fixtureFailed("a single call would exceed the run's transfer budget");
+    }
+    return static_cast(std::min(affordable, kCallsPerRun));
+}
+
+std::string
+dataSegment(int offset, std::span bytes)
+{
+    return std::format("  (data (i32.const {}) \"{}\")\n", offset, watEscaped(bytes));
+}
+
+std::string
+dataSegment(int offset, Bytes const& bytes)
+{
+    return dataSegment(offset, std::span{bytes.data(), bytes.size()});
+}
+
+std::string
+makeLoopWat(std::string_view imports, std::string_view data, std::string_view body, int count)
+{
+    static constexpr auto kTemplate = R"wat((module
+{}
+  (memory (export "memory") 1)
+{}
+  (func (export "escrow_finish") (result i32)
+    (local $i i32)
+    (local $r i32)
+    (local.set $i (i32.const {}))
+    (block $done
+      (loop $again
+        (br_if $done (i32.eqz (local.get $i)))
+        (local.set $r {})
+        (local.set $i (i32.sub (local.get $i) (i32.const 1)))
+        (br $again)))
+    (local.get $r)))
+)wat";
+
+    return std::format(kTemplate, imports, data, count, body);
+}
+
+Timing
+timeRun(HostFunctions& host, Bytes const& wasm)
+{
+    auto const start = std::chrono::steady_clock::now();
+    auto outcome = runEscrowWasm(wasm, host, kBenchGas);
+    auto const elapsed = std::chrono::steady_clock::now() - start;
+
+    benchmark::DoNotOptimize(outcome);
+    return {
+        .seconds = std::chrono::duration(elapsed).count(),
+        .gas = outcome.has_value() ? outcome->cost : std::int64_t{0}};
+}
+
+StageTimer::StageTimer(benchmark::State& state, std::int64_t moduleBytes)
+    : state_{state}, moduleBytes_{moduleBytes}
+{
+}
+
+void
+StageTimer::add(double seconds)
+{
+    state_.SetIterationTime(seconds);
+    total_ += seconds;
+    sumSquares_ += seconds * seconds;
+    ++rounds_;
+}
+
+void
+StageTimer::report()
+{
+    if (rounds_ == 0)
+    {
+        return;
+    }
+
+    auto const count = static_cast(rounds_);
+    auto const mean = total_ / count;
+    auto const variance = std::max(0.0, (sumSquares_ / count) - (mean * mean));
+    auto const spread = mean > 0.0 ? std::sqrt(variance) / mean : 0.0;
+
+    auto const& calibration = Calibration::instance();
+    auto const perGas = calibration.secondsPerGas();
+    auto const equivalent = perGas > 0.0 ? mean / perGas : 0.0;
+
+    state_.counters["ns_per_op"] = mean * 1e9;
+    // What this stage would cost if it were charged at the rate the guest inside it is charged at.
+    // It is not charged, which is the point: this puts an unpriced stage in the host-function
+    // table's units.
+    state_.counters["gas_equivalent"] = equivalent;
+
+    if (moduleBytes_ > 0)
+    {
+        state_.counters["module_bytes"] = static_cast(moduleBytes_);
+        // An average over the whole operation, not the marginal rate: it carries the case's fixed
+        // cost, so it overestimates and falls toward the true slope as the sweep grows.
+        state_.counters["gas_per_byte"] = equivalent / static_cast(moduleBytes_);
+    }
+
+    // `gas_equivalent` divides by `secondsPerGas`, so the divisor's uncertainty is in every number
+    // here too.
+    auto const caseStdErr = spread / std::sqrt(count);
+    auto const perGasErr = calibration.secondsPerGasRelStdErr();
+    auto const totalErr = std::sqrt((caseStdErr * caseStdErr) + (perGasErr * perGasErr));
+    state_.counters["rel_error"] = totalErr;
+    state_.counters["unreliable"] = totalErr > kMaxRelativeSpread ? 1 : 0;
+}
+
+void
+benchmarkPreflight(benchmark::State& state, Bytes const& wasm, bool expectAccepted, bool sizeSweep)
+{
+    (void)Calibration::instance();
+
+    // Discarded: the reject case refuses on every iteration, and a real sink would put string
+    // formatting and I/O inside the measurement.
+    auto const journal = beast::Journal{beast::Journal::getNullSink()};
+
+    if (isTesSuccess(preflightEscrowWasm(wasm, journal)) != expectAccepted)
+    {
+        state.SkipWithError(
+            expectAccepted
+                ? "the module was refused; the case would be measuring the reject path"
+                : "the module was accepted; the case would be measuring the accept path");
+        return;
+    }
+
+    StageTimer timer{state, sizeSweep ? static_cast(wasm.size()) : 0};
+    for (auto _ : state)
+    {
+        auto const start = std::chrono::steady_clock::now();
+        auto verdict = preflightEscrowWasm(wasm, journal);
+        auto const elapsed = std::chrono::steady_clock::now() - start;
+
+        benchmark::DoNotOptimize(verdict);
+        timer.add(std::chrono::duration(elapsed).count());
+    }
+    timer.report();
+}
+
+namespace {
+
+// Seconds of wall time one unit of gas buys on this machine.
+//
+// The estimator must be the *same* one the cases use — a mean, with the same clamp at zero. Since
+// `implied_gas = secondsPerCall / secondsPerGas`, any difference between how divisor and dividend
+// are estimated lands in every reported number: calibrating with a best-of while measuring with a
+// mean once biased the whole report +40%.
+//
+// `guestInstruction` in Crossing.cpp is the check that this holds — it runs this exact loop body.
+double
+measureSecondsPerGas(double& relativeStandardError)
+{
+    // A couple of guest instructions, no memory traffic, nothing the engine can fold away.
+    static constexpr auto kBody = std::string_view{"(i32.add (local.get $r) (i32.const 1))"};
+    auto const busy = assembleWat(makeLoopWat("", "", kBody, kCallsPerRun));
+    auto const idle = assembleWat(makeLoopWat("", "", kBody, 0));
+
+    auto fixture = WasmLedger{};
+
+    // Warm up, so the first-run penalty does not land on one side of the subtraction.
+    for (auto i = 0U; i < 8; ++i)
+    {
+        timeRun(*fixture.makeHost(), busy);
+        timeRun(*fixture.makeHost(), idle);
+    }
+
+    auto total = 0.0;
+    auto sumSquares = 0.0;
+    // Fuel is exact and deterministic, so any pair gives the same delta.
+    auto gasDelta = std::int64_t{1};
+    for (auto i = 0; i < kCalibrationPairs; ++i)
+    {
+        auto hotHost = fixture.makeHost();
+        auto const hot = timeRun(*hotHost, busy);
+        auto coldHost = fixture.makeHost();
+        auto const cold = timeRun(*coldHost, idle);
+
+        auto const delta = std::max(0.0, hot.seconds - cold.seconds);
+        total += delta;
+        sumSquares += delta * delta;
+        gasDelta = std::max(std::int64_t{1}, hot.gas - cold.gas);
+    }
+
+    auto const mean = total / kCalibrationPairs;
+    auto const variance = std::max(0.0, (sumSquares / kCalibrationPairs) - (mean * mean));
+    relativeStandardError = mean > 0.0 ? std::sqrt(variance / kCalibrationPairs) / mean : 0.0;
+
+    return mean / static_cast(gasDelta);
+}
+
+// The crossing, in gas: `ldgr_index` through the VM minus `ldgr_index` called directly.
+//
+// Both halves are means, for the reason above: the VM half has to match `benchmarkThroughVm`'s
+// estimator and the impl half `benchmarkImpl`'s. `secondsPerGas` is passed in rather than
+// re-measured so it comes from the same snapshot.
+double
+measureCrossingFloorGas(double secondsPerGas, double& relativeStandardError)
+{
+    static constexpr std::string_view kImport =
+        R"(  (import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
+)";
+    static constexpr std::string_view kBody = "(call $ldgr_index (i32.const 0) (i32.const 4))";
+
+    auto const loaded = assembleWat(makeLoopWat(kImport, "", kBody, kCallsPerRun));
+    auto const baseline = assembleWat(makeLoopWat(kImport, "", kBody, 0));
+
+    auto fixture = WasmLedger{};
+
+    auto vmTotal = 0.0;
+    auto vmSquares = 0.0;
+    auto guestOverheadGas = 0.0;
+    for (auto i = 0; i < kBenchIterations; ++i)
+    {
+        auto hotHost = fixture.makeHost();
+        auto const hot = timeRun(*hotHost, loaded);
+        auto coldHost = fixture.makeHost();
+        auto const cold = timeRun(*coldHost, baseline);
+
+        auto const perCall = std::max(0.0, hot.seconds - cold.seconds) / kCallsPerRun;
+        vmTotal += perCall;
+        vmSquares += perCall * perCall;
+        // Exact, from the fuel meter: what the guest burned per call beyond the call itself.
+        guestOverheadGas =
+            (static_cast(hot.gas - cold.gas) / kCallsPerRun) - declaredGas("ldgr_index");
+    }
+    auto const vmSeconds = vmTotal / kBenchIterations;
+
+    // The impl side is the same call without the VM. Subtracting it leaves the crossing.
+    auto implTotal = 0.0;
+    auto implSquares = 0.0;
+    auto host = fixture.makeHost();
+    for (auto i = 0; i < kBenchIterations; ++i)
+    {
+        auto const start = std::chrono::steady_clock::now();
+        for (auto c = 0U; c < kCallsPerRun; ++c)
+        {
+            auto result = host->getLedgerSqn();
+            benchmark::DoNotOptimize(result);
+        }
+        auto const elapsed = std::chrono::steady_clock::now() - start;
+
+        auto const perCall = std::chrono::duration(elapsed).count() / kCallsPerRun;
+        implTotal += perCall;
+        implSquares += perCall * perCall;
+    }
+    auto const implSeconds = implTotal / kBenchIterations;
+
+    if (secondsPerGas <= 0.0)
+    {
+        return 0.0;
+    }
+    // Take the guest's loop bookkeeping off here too: `report` removes it from every `ThroughVm`
+    // number, so leaving it in would make the two routes to one price disagree by that amount.
+    auto const crossing = std::max(0.0, vmSeconds - implSeconds) / secondsPerGas;
+    auto const floor = std::max(0.0, crossing - std::max(0.0, guestOverheadGas));
+
+    // Relative to the *difference*, not to either half: both contribute their error, and the
+    // denominator is what survives the subtraction. Not divided by `secondsPerGas` — that error is
+    // common-mode with the rest of `suggested_gas` and is applied once, to the sum, in `report`.
+    auto const vmVariance = std::max(0.0, (vmSquares / kBenchIterations) - (vmSeconds * vmSeconds));
+    auto const implVariance =
+        std::max(0.0, (implSquares / kBenchIterations) - (implSeconds * implSeconds));
+    auto const vmStdErr = std::sqrt(vmVariance / kBenchIterations);
+    auto const implStdErr = std::sqrt(implVariance / kBenchIterations);
+
+    auto const crossingSeconds = vmSeconds - implSeconds;
+    auto const crossingStdErr = std::sqrt((vmStdErr * vmStdErr) + (implStdErr * implStdErr));
+    relativeStandardError = crossingSeconds > 0.0 ? crossingStdErr / crossingSeconds : 0.0;
+
+    return floor;
+}
+
+}  // namespace
+
+Calibration const&
+Calibration::instance()
+{
+    static Calibration const kValue;
+    return kValue;
+}
+
+Calibration::Calibration()
+    : secondsPerGas_{measureSecondsPerGas(secondsPerGasRelStdErr_)}
+    , crossingFloorGas_{measureCrossingFloorGas(secondsPerGas_, crossingFloorRelStdErr_)}
+{
+}
+
+double
+declaredGas(std::string_view wasmName)
+{
+    return static_cast(
+        rs::wasm_testkit::host_function_gas(rust::Str{wasmName.data(), wasmName.size()}));
+}
+
+void
+report(
+    benchmark::State& state,
+    double secondsPerCall,
+    double chargedGas,
+    double guestOverheadGas,
+    double relativeSpread,
+    std::int64_t rounds,
+    std::string_view wasmName,
+    bool throughVm)
+{
+    auto const& calibration = Calibration::instance();
+    auto const perGas = calibration.secondsPerGas();
+    auto const measured = perGas > 0.0 ? secondsPerCall / perGas : 0.0;
+
+    // The timed number covers the host call *and* whatever the guest ran around it.
+    // `guestOverheadGas` is exact, so taking it off removes a bias rather than trading estimates.
+    auto const implied = std::max(0.0, measured - guestOverheadGas);
+    auto const suggested = throughVm ? implied : implied + calibration.crossingFloorGas();
+
+    state.counters["implied_gas"] = implied;
+    state.counters["ns_per_call"] = secondsPerCall * 1e9;
+    state.counters["charged_gas"] = chargedGas;
+
+    if (wasmName.empty())
+    {
+        return;
+    }
+
+    auto const declared = declaredGas(wasmName);
+    state.counters["host_function_gas"] = declared;
+    state.counters["suggested_gas"] = suggested;
+    // Below 1 is the direction that matters: an underpriced call is one a contract buys too
+    // cheaply.
+    state.counters["price_ratio"] = suggested > 0.0 ? declared / suggested : 0.0;
+
+    // Uncertainty **of `suggested_gas`**, not of `implied_gas` — different numbers once the floor
+    // is added. `implied` and the floor are independent timings, so their absolute errors add in
+    // quadrature over the sum; but both divide by `secondsPerGas`, so that error is common-mode and
+    // applies once to the total. Adding it per-term would count it twice.
+    //
+    // For `ThroughVm` the floor term is zero and `suggested == implied`, so this reduces exactly to
+    // the plain `sqrt(caseErr² + perGasErr²)`. Only `Impl` cases move — and for a cheap one the
+    // floor is most of `suggested_gas`, so an error bar describing `implied` alone described
+    // little.
+    auto const caseStdErr =
+        rounds > 0 ? relativeSpread / std::sqrt(static_cast(rounds)) : relativeSpread;
+
+    auto const impliedErr = implied * caseStdErr;
+    auto const floorErr =
+        throughVm ? 0.0 : calibration.crossingFloorGas() * calibration.crossingFloorRelStdErr();
+    auto const independentErr = suggested > 0.0
+        ? std::sqrt((impliedErr * impliedErr) + (floorErr * floorErr)) / suggested
+        : caseStdErr;
+
+    auto const perGasErr = calibration.secondsPerGasRelStdErr();
+    auto const totalErr = std::sqrt((independentErr * independentErr) + (perGasErr * perGasErr));
+    state.counters["rel_error"] = totalErr;
+
+    // A call whose own cost is small next to the crossing is read off the difference of two nearly
+    // equal numbers, so its `suggested_gas` is scatter rather than signal.
+    auto const floor = calibration.crossingFloorGas();
+    state.counters["unreliable"] =
+        (totalErr > kMaxRelativeSpread || (floor > 0.0 && suggested < floor)) ? 1 : 0;
+}
+
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/WasmBench.h b/src/benchmarks/libxrpl/wasm/WasmBench.h
new file mode 100644
index 0000000000..98f0921be7
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/WasmBench.h
@@ -0,0 +1,369 @@
+#pragma once
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+// The gas-calibration harness. What the numbers mean and how to read a report are in ../README.md.
+
+namespace xrpl::test::bench {
+
+// Enough that a benchmark loop never ends early; running out would measure something shorter.
+inline constexpr std::int64_t kBenchGas = 2'000'000'000;
+
+// Host calls per run: enough that the per-call cost dominates the baseline subtraction's residue.
+inline constexpr std::int32_t kCallsPerRun = 1000;
+
+// Timed iterations per case. Pinned because automatic sizing cannot work here: a case reports a
+// residue of nanoseconds while the iteration producing it ran two contracts and cost milliseconds,
+// so sizing would ask for millions.
+inline constexpr std::int32_t kBenchIterations = 50;
+
+// Pairs the one-off calibration averages. Higher than `kBenchIterations` because `secondsPerGas`
+// divides every reported number, and a bare wasm loop is cheap to repeat.
+inline constexpr std::int32_t kCalibrationPairs = 400;
+
+// Above this relative uncertainty, `suggested_gas` is reported as unreliable.
+inline constexpr double kMaxRelativeSpread = 0.25;
+
+// `TRANSFER_LIMIT_BYTES` in crates/xrpl-wasm-vm/src/vm.rs: what a run may write into guest memory
+// before `charge_transfer` starts refusing calls.
+inline constexpr std::int64_t kTransferLimitBytes = 1 << 20;
+
+// How many calls a run can afford, given the bytes each has the host **write into guest memory**.
+// Output direction only — what the guest passes in is borrowed, and costs nothing against the
+// budget. Never raises the count to meet a floor; a case that cannot afford one call fails loudly.
+int
+callsWithinTransferBudget(std::int64_t bytesWrittenPerCall);
+
+// One run of a contract: how long it took, and what the engine charged it.
+struct Timing
+{
+    double seconds{};
+    std::int64_t gas{};
+};
+
+// A `(data ...)` segment placing `bytes` at `offset` in guest memory, so the timed loop measures
+// the host call rather than the guest arranging its arguments. `watEscaped` in WasmRun.h says why
+// zeroed memory will not do.
+std::string
+dataSegment(int offset, std::span bytes);
+
+std::string
+dataSegment(int offset, Bytes const& bytes);
+
+// A contract that runs `body` `count` times and returns the last result.
+std::string
+makeLoopWat(std::string_view imports, std::string_view data, std::string_view body, int count);
+
+// Run pre-assembled `wasm` once through the real VM, reporting wall time and gas.
+Timing
+timeRun(HostFunctions& host, Bytes const& wasm);
+
+// What this machine costs, measured once and shared by every case: two cases pricing one function
+// two ways (`Impl` + crossing floor, versus `ThroughVm`) must divide by the *same* `secondsPerGas`
+// or they disagree for reasons unrelated to the function.
+class Calibration
+{
+public:
+    static Calibration const&
+    instance();
+
+    // Seconds of wall time one unit of gas buys here.
+    [[nodiscard]] double
+    secondsPerGas() const
+    {
+        return secondsPerGas_;
+    }
+
+    // The gas a host call costs before it does anything: region decode, bounds checks, the cxx hop.
+    [[nodiscard]] double
+    crossingFloorGas() const
+    {
+        return crossingFloorGas_;
+    }
+
+    // Propagated into every case's `rel_error`, which is what lets one snapshot stay shared:
+    // `--benchmark_repetitions` resamples per-case timings but never this divisor.
+    [[nodiscard]] double
+    secondsPerGasRelStdErr() const
+    {
+        return secondsPerGasRelStdErr_;
+    }
+
+    // An additive term in every `Impl` case's `suggested_gas`, and most of the cheap ones.
+    [[nodiscard]] double
+    crossingFloorRelStdErr() const
+    {
+        return crossingFloorRelStdErr_;
+    }
+
+private:
+    Calibration();
+
+    double secondsPerGasRelStdErr_{};
+    double crossingFloorRelStdErr_{};
+    double secondsPerGas_{};
+    double crossingFloorGas_{};
+};
+
+// What the gas table declares for a host function, by guest import name, read through the
+// `wasm_testkit` bridge so it cannot drift.
+double
+declaredGas(std::string_view wasmName);
+
+// Attach the calibration counters to a finished case.
+//
+// `guestOverheadGas` is fuel the guest burned around the call — loop bookkeeping and the
+// `i32.const`s pushing arguments. From the fuel meter, so it is exact. Zero for `Impl` cases.
+void
+report(
+    benchmark::State& state,
+    double secondsPerCall,
+    double chargedGas,
+    double guestOverheadGas,
+    double relativeSpread,
+    std::int64_t rounds,
+    std::string_view wasmName,
+    bool throughVm);
+
+// Accumulates one whole-operation case and turns it into counters.
+//
+// The two harnesses below subtract setup away, amortizing over `kCallsPerRun` calls against a
+// baseline. Here that is inverted: setup is the subject, timed whole, nothing amortized.
+class StageTimer
+{
+public:
+    // Pass zero for `moduleBytes` unless the case belongs to a sweep that *varies* module size —
+    // the per-byte counter it enables is an average over the whole operation, meaningless where the
+    // module is constant.
+    StageTimer(benchmark::State& state, std::int64_t moduleBytes);
+
+    void
+    add(double seconds);
+
+    // Attach the counters. Call once, after the loop.
+    void
+    report();
+
+private:
+    benchmark::State& state_;
+    std::int64_t moduleBytes_{};
+    double total_{};
+    double sumSquares_{};
+    std::int64_t rounds_{};
+};
+
+// Measure `preflightEscrowWasm`: compile, then the walk over the module's imports and exports.
+//
+// `expectAccepted` is what the module *should* do. A refused module stops at the first fault and is
+// far cheaper, so a case that silently flipped verdict would report a confident number for an
+// operation it never performed.
+void
+benchmarkPreflight(
+    benchmark::State& state,
+    Bytes const& wasm,
+    bool expectAccepted = true,
+    bool sizeSweep = false);
+
+// Measure a whole `runEscrowWasm` — every stage, unamortized.
+template 
+void
+benchmarkRun(benchmark::State& state, Bytes const& wasm, SetUp&& setUp, bool sizeSweep = false)
+{
+    // Force the calibration and the engine's lazy construction before the clock starts, so the
+    // first case does not absorb them into its own first iteration.
+    [[maybe_unused]] auto const& calibration = Calibration::instance();
+
+    auto probe = setUp();
+    if (auto const check = runEscrowWasm(wasm, *probe, kBenchGas); !check.has_value())
+    {
+        state.SkipWithError("the benchmarked contract did not run to completion");
+        return;
+    }
+
+    auto timer = StageTimer{state, sizeSweep ? static_cast(wasm.size()) : 0};
+    for (auto _ : state)
+    {
+        auto host = setUp();
+        timer.add(timeRun(*host, wasm).seconds);
+    }
+    timer.report();
+}
+
+// Measure a host function through the whole stack — guest, VM, marshalling, real impl, real ledger
+// — with everything but the host calls subtracted away.
+template 
+void
+benchmarkThroughVm(
+    benchmark::State& state,
+    std::string_view wasmName,
+    std::string_view imports,
+    std::string_view data,
+    std::string_view body,
+    SetUp&& setUp,
+    int calls = kCallsPerRun)
+{
+    auto const loaded = assembleWat(makeLoopWat(imports, data, body, calls));
+    auto const baseline = assembleWat(makeLoopWat(imports, data, body, 0));
+
+    // A host serves exactly one run — `runEscrowWasm` asserts it was handed a clean one
+    // (`checkSelf` in WasmVM.cpp). Hence `setUp` being a factory rather than a host.
+    auto probe = setUp();
+
+    // A soft host error still completes the run, so require both that it completed and that the
+    // last host call returned a non-negative result, which every body leaves in `$r`.
+    auto const check = runEscrowWasm(loaded, *probe, kBenchGas);
+    if (!check.has_value())
+    {
+        state.SkipWithError("the benchmarked contract did not run to completion");
+        return;
+    }
+    if (check->result < 0)
+    {
+        state.SkipWithError(
+            "the benchmarked host call returned error code " + std::to_string(check->result) +
+            "; the case would be measuring the rejection path, not the work");
+        return;
+    }
+
+    auto totalSeconds = 0.0;
+    auto sumSquares = 0.0;
+    auto totalGas = 0.0;
+    auto rounds = std::int64_t{0};
+    for (auto _ : state)
+    {
+        auto hotHost = setUp();
+        auto const hot = timeRun(*hotHost, loaded);
+        auto coldHost = setUp();
+        auto const cold = timeRun(*coldHost, baseline);
+
+        // Clamped: on a noisy machine a pair can invert, and a negative iteration time would make
+        // Google Benchmark's statistics meaningless.
+        auto const perCall = std::max(0.0, hot.seconds - cold.seconds) / calls;
+        state.SetIterationTime(perCall);
+
+        totalSeconds += perCall;
+        sumSquares += perCall * perCall;
+        totalGas += static_cast(hot.gas - cold.gas) / calls;
+        ++rounds;
+    }
+
+    if (rounds > 0)
+    {
+        auto const meanSeconds = totalSeconds / rounds;
+        auto const variance = std::max(0.0, (sumSquares / rounds) - (meanSeconds * meanSeconds));
+        auto const spread = meanSeconds > 0.0 ? std::sqrt(variance) / meanSeconds : 0.0;
+
+        // Zero for the empty-name case — `guestInstruction`, which prices no host function. There
+        // is no host call to separate scaffolding *from*, and subtracting the full charge would
+        // leave `implied_gas = measured - charged`, ~0 by construction: it would turn the harness's
+        // one self-test into a tautology that cannot fail.
+        auto const chargedPerCall = totalGas / rounds;
+        auto const overhead = wasmName.empty() ? 0.0 : chargedPerCall - declaredGas(wasmName);
+
+        report(
+            state,
+            meanSeconds,
+            chargedPerCall,
+            std::max(0.0, overhead),
+            spread,
+            rounds,
+            wasmName,
+            true);
+    }
+}
+
+// Measure a host function's impl alone — no guest, no VM, no marshalling. Against the `ThroughVm`
+// case for the same function, the difference is what crossing the boundary costs.
+template 
+void
+benchmarkImpl(benchmark::State& state, std::string_view wasmName, SetUp&& setUp, Call&& call)
+{
+    auto host = setUp();
+
+    // A call that errors returns early and is far cheaper than one that works, so a subtly wrong
+    // argument yields a confident and always *too low* price. Probed either side of the loop rather
+    // than inside it, where the branch would land in the measurement: *before* catches wrong
+    // arguments, *after* catches a call that stopped working once the loop exhausted something.
+    //
+    // The `requires` skips host functions that answer nothing (`trace`) or answer a bare value.
+    auto const checkSucceeds = [&](char const* when) {
+        if constexpr (requires { call(*host).has_value(); })
+        {
+            if (auto const probe = call(*host); !probe.has_value())
+            {
+                state.SkipWithError(
+                    std::string{"the benchmarked host call returned error code "} +
+                    std::to_string(static_cast(probe.error())) + " " + when +
+                    " the timed loop; the case would be measuring the rejection path");
+                return false;
+            }
+        }
+        return true;
+    };
+
+    if (!checkSucceeds("before"))
+    {
+        return;
+    }
+
+    auto totalSeconds = 0.0;
+    auto sumSquares = 0.0;
+    auto rounds = std::int64_t{0};
+    for (auto _ : state)
+    {
+        auto const start = std::chrono::steady_clock::now();
+        for (int i = 0; i < kCallsPerRun; ++i)
+        {
+            // `trace` answers nothing, so `ClobberMemory` stands in for `DoNotOptimize`.
+            if constexpr (std::is_void_v)
+            {
+                call(*host);
+                benchmark::ClobberMemory();
+            }
+            else
+            {
+                auto result = call(*host);
+                benchmark::DoNotOptimize(result);
+            }
+        }
+        auto const elapsed = std::chrono::steady_clock::now() - start;
+
+        auto const perCall = std::chrono::duration(elapsed).count() / kCallsPerRun;
+        state.SetIterationTime(perCall);
+
+        totalSeconds += perCall;
+        sumSquares += perCall * perCall;
+        ++rounds;
+    }
+
+    if (!checkSucceeds("after"))
+    {
+        return;
+    }
+
+    if (rounds > 0)
+    {
+        auto const meanSeconds = totalSeconds / rounds;
+        auto const variance = std::max(0.0, (sumSquares / rounds) - (meanSeconds * meanSeconds));
+        auto const spread = meanSeconds > 0.0 ? std::sqrt(variance) / meanSeconds : 0.0;
+
+        // Nothing charged and no guest scaffolding; `report` adds the crossing back in.
+        report(state, meanSeconds, 0.0, 0.0, spread, rounds, wasmName, false);
+    }
+}
+
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/AccountKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/AccountKeylet.cpp
new file mode 100644
index 0000000000..5bd2c7c2e8
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/AccountKeylet.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+accountKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"accountroot_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.accountKeylet(Fixtures::instance().alice().id()); });
+}
+BENCHMARK(accountKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/AmmKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/AmmKeylet.cpp
new file mode 100644
index 0000000000..4cefc1c553
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/AmmKeylet.cpp
@@ -0,0 +1,31 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+ammKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"amm_id"};
+
+    auto const usd = Asset{Issue{toCurrency("USD"), Fixtures::instance().alice().id()}};
+    auto const xrp = Asset{xrpIssue()};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [&usd, &xrp](auto& host) { return host.ammKeylet(usd, xrp); });
+}
+BENCHMARK(ammKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/BaseFee.cpp b/src/benchmarks/libxrpl/wasm/host_functions/BaseFee.cpp
new file mode 100644
index 0000000000..b304beeb65
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/BaseFee.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+baseFeeImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"base_fee"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getBaseFee(); });
+}
+BENCHMARK(baseFeeImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/CacheLedgerObj.cpp b/src/benchmarks/libxrpl/wasm/host_functions/CacheLedgerObj.cpp
new file mode 100644
index 0000000000..b027396d1e
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/CacheLedgerObj.cpp
@@ -0,0 +1,28 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+cacheLedgerObjImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"cache_le"};
+
+    auto const key = keylet::account(Fixtures::instance().alice().id()).key;
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [&key](auto& host) { return host.cacheLedgerObj(key, 1); });
+}
+BENCHMARK(cacheLedgerObjImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/CheckKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/CheckKeylet.cpp
new file mode 100644
index 0000000000..24af99a6ac
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/CheckKeylet.cpp
@@ -0,0 +1,26 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+checkKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"check_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.checkKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
+        });
+}
+BENCHMARK(checkKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/CheckSignature.cpp b/src/benchmarks/libxrpl/wasm/host_functions/CheckSignature.cpp
new file mode 100644
index 0000000000..af7e333bc7
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/CheckSignature.cpp
@@ -0,0 +1,63 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+constexpr std::string_view kWasmName = "check_sig";
+
+constexpr std::string_view kImport =
+    R"(  (import "host_lib" "check_sig" (func $check_sig (param i32 i32 i32 i32 i32 i32) (result i32)))
+)";
+
+constexpr std::int32_t kMessageOffset = 0;
+constexpr std::int32_t kSignatureOffset = 256;
+constexpr std::int32_t kPubkeyOffset = 512;
+
+void
+checkSignatureThroughVm(benchmark::State& state)
+{
+    auto const& m = Fixtures::instance().signedMessage();
+    static auto const kData = dataSegment(kMessageOffset, m.message) +
+        dataSegment(kSignatureOffset, m.signature) + dataSegment(kPubkeyOffset, m.publicKey);
+    static auto const kBody = std::format(
+        "(call $check_sig (i32.const {}) (i32.const {}) (i32.const {}) (i32.const {}) "
+        "(i32.const {}) (i32.const {}))",
+        kMessageOffset,
+        m.message.size(),
+        kSignatureOffset,
+        m.signature.size(),
+        kPubkeyOffset,
+        m.publicKey.size());
+
+    benchmarkThroughVm(
+        state, kWasmName, kImport, kData, kBody, [] { return Fixtures::instance().host(); });
+}
+BENCHMARK(checkSignatureThroughVm)->UseManualTime()->Iterations(kBenchIterations);
+
+void
+checkSignatureImpl(benchmark::State& state)
+{
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            auto const& m = Fixtures::instance().signedMessage();
+            return host.checkSignature(
+                Slice{m.message.data(), m.message.size()},
+                Slice{m.signature.data(), m.signature.size()},
+                Slice{m.publicKey.data(), m.publicKey.size()});
+        });
+}
+BENCHMARK(checkSignatureImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/CredentialKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/CredentialKeylet.cpp
new file mode 100644
index 0000000000..219cc9051c
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/CredentialKeylet.cpp
@@ -0,0 +1,32 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+credentialKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"credential_id"};
+    static constexpr auto kType = std::string_view{"termsandconditions"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.credentialKeylet(
+                Fixtures::instance().alice().id(),
+                Fixtures::instance().bob().id(),
+                Slice{kType.data(), kType.size()});
+        });
+}
+BENCHMARK(credentialKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/CurrentLedgerObjArrayLen.cpp b/src/benchmarks/libxrpl/wasm/host_functions/CurrentLedgerObjArrayLen.cpp
new file mode 100644
index 0000000000..4678749e84
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/CurrentLedgerObjArrayLen.cpp
@@ -0,0 +1,26 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+currentLedgerObjArrayLenImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"home_le_arr_len"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().signerListHost(); },
+        [](auto& host) { return host.getCurrentLedgerObjArrayLen(sfSignerEntries); });
+}
+BENCHMARK(currentLedgerObjArrayLenImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/CurrentLedgerObjField.cpp b/src/benchmarks/libxrpl/wasm/host_functions/CurrentLedgerObjField.cpp
new file mode 100644
index 0000000000..5a441ffa42
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/CurrentLedgerObjField.cpp
@@ -0,0 +1,41 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+constexpr std::string_view kWasmName = "home_le_field";
+constexpr std::string_view kImport =
+    R"(  (import "host_lib" "home_le_field" (func $home_le_field (param i32 i32 i32) (result i32)))
+)";
+
+void
+currentLedgerObjFieldThroughVm(benchmark::State& state)
+{
+    static auto const kBody = std::format(
+        "(call $home_le_field (i32.const {}) (i32.const 0) (i32.const 32))", sfAccount.getCode());
+
+    benchmarkThroughVm(
+        state, kWasmName, kImport, "", kBody, [] { return Fixtures::instance().escrowHost(); });
+}
+BENCHMARK(currentLedgerObjFieldThroughVm)->UseManualTime()->Iterations(kBenchIterations);
+
+void
+currentLedgerObjFieldImpl(benchmark::State& state)
+{
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().escrowHost(); },
+        [](auto& host) { return host.getCurrentLedgerObjField(sfAccount); });
+}
+BENCHMARK(currentLedgerObjFieldImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/CurrentLedgerObjNestedArrayLen.cpp b/src/benchmarks/libxrpl/wasm/host_functions/CurrentLedgerObjNestedArrayLen.cpp
new file mode 100644
index 0000000000..e2c5d0ab73
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/CurrentLedgerObjNestedArrayLen.cpp
@@ -0,0 +1,30 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+currentLedgerObjNestedArrayLenImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"home_le_inner_arr_len"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().signerListHost(); },
+        [](auto& host) {
+            return host.getCurrentLedgerObjNestedArrayLen(
+                FieldLocator{{sfSignerEntries.getCode()}});
+        });
+}
+BENCHMARK(currentLedgerObjNestedArrayLenImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/CurrentLedgerObjNestedField.cpp b/src/benchmarks/libxrpl/wasm/host_functions/CurrentLedgerObjNestedField.cpp
new file mode 100644
index 0000000000..fd0a7f96ed
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/CurrentLedgerObjNestedField.cpp
@@ -0,0 +1,29 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+currentLedgerObjNestedFieldImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"home_le_inner"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.getCurrentLedgerObjNestedField(FieldLocator{{sfAccount.getCode()}});
+        });
+}
+BENCHMARK(currentLedgerObjNestedFieldImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/DelegateKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/DelegateKeylet.cpp
new file mode 100644
index 0000000000..6a66db420a
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/DelegateKeylet.cpp
@@ -0,0 +1,27 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+delegateKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"delegate_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.delegateKeylet(
+                Fixtures::instance().alice().id(), Fixtures::instance().bob().id());
+        });
+}
+BENCHMARK(delegateKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/DepositPreauthKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/DepositPreauthKeylet.cpp
new file mode 100644
index 0000000000..2931669c34
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/DepositPreauthKeylet.cpp
@@ -0,0 +1,27 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+depositPreauthKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"deposit_preauth_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.depositPreauthKeylet(
+                Fixtures::instance().alice().id(), Fixtures::instance().bob().id());
+        });
+}
+BENCHMARK(depositPreauthKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/DidKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/DidKeylet.cpp
new file mode 100644
index 0000000000..6402bf425a
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/DidKeylet.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+didKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"did_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.didKeylet(Fixtures::instance().alice().id()); });
+}
+BENCHMARK(didKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/EscrowKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/EscrowKeylet.cpp
new file mode 100644
index 0000000000..369388fb19
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/EscrowKeylet.cpp
@@ -0,0 +1,55 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+constexpr std::string_view kWasmName = "escrow_id";
+
+constexpr std::string_view kImport =
+    R"(  (import "host_lib" "escrow_id" (func $escrow_id (param i32 i32 i32 i32 i32 i32) (result i32)))
+)";
+constexpr std::string_view kBody =
+    "(call $escrow_id (i32.const 0) (i32.const 20) (i32.const 32) (i32.const 4) "
+    "(i32.const 64) (i32.const 32))";
+
+void
+escrowKeyletThroughVm(benchmark::State& state)
+{
+    static auto const kData = [] {
+        auto seq = Bytes(4);
+        for (auto i = 0U; i < 4; ++i)
+        {
+            seq[i] = static_cast((Fixtures::kSeq >> (8 * i)) & 0xFF);
+        }
+        return dataSegment(0, WasmLedger::toBytes(Fixtures::instance().alice().id())) +
+            dataSegment(32, seq);
+    }();
+
+    benchmarkThroughVm(
+        state, kWasmName, kImport, kData, kBody, [] { return Fixtures::instance().host(); });
+}
+BENCHMARK(escrowKeyletThroughVm)->UseManualTime()->Iterations(kBenchIterations);
+
+void
+escrowKeyletImpl(benchmark::State& state)
+{
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.escrowKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
+        });
+}
+BENCHMARK(escrowKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatAdd.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatAdd.cpp
new file mode 100644
index 0000000000..e5fb2360f3
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatAdd.cpp
@@ -0,0 +1,45 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+constexpr std::string_view kWasmName = "float_add";
+
+constexpr std::string_view kImport =
+    R"(  (import "host_lib" "float_add" (func $float_add (param i32 i32 i32 i32 i32 i32 i32) (result i32)))
+)";
+
+constexpr std::string_view kBody =
+    "(call $float_add (i32.const 0) (i32.const 12) (i32.const 16) (i32.const 12) "
+    "(i32.const 64) (i32.const 12) (i32.const 0))";
+
+void
+floatAddThroughVm(benchmark::State& state)
+{
+    static auto const kData =
+        dataSegment(0, FloatConstants::kPi) + dataSegment(16, FloatConstants::kTwo);
+    benchmarkThroughVm(
+        state, kWasmName, kImport, kData, kBody, [] { return Fixtures::instance().host(); });
+}
+BENCHMARK(floatAddThroughVm)->UseManualTime()->Iterations(kBenchIterations);
+
+void
+floatAddImpl(benchmark::State& state)
+{
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.floatAdd(Fixtures::floatX(), Fixtures::floatY(), Fixtures::kRoundingMode);
+        });
+}
+BENCHMARK(floatAddImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatCompare.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatCompare.cpp
new file mode 100644
index 0000000000..ea6c68c44f
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatCompare.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+floatCompareImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"float_cmp"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.floatCompare(Fixtures::floatX(), Fixtures::floatY()); });
+}
+BENCHMARK(floatCompareImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatDivide.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatDivide.cpp
new file mode 100644
index 0000000000..bdc802d4bc
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatDivide.cpp
@@ -0,0 +1,27 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+floatDivideImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"float_div"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.floatDivide(
+                Fixtures::floatX(), Fixtures::floatY(), Fixtures::kRoundingMode);
+        });
+}
+BENCHMARK(floatDivideImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatFromInt.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatFromInt.cpp
new file mode 100644
index 0000000000..5edc05d000
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatFromInt.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+floatFromIntImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"float_from_int"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.floatFromInt(3141592653589793, Fixtures::kRoundingMode); });
+}
+BENCHMARK(floatFromIntImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatFromMantExp.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatFromMantExp.cpp
new file mode 100644
index 0000000000..6f60b16136
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatFromMantExp.cpp
@@ -0,0 +1,26 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+floatFromMantExpImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"float_from_mant_exp"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.floatFromMantExp(3141592653589793, -15, Fixtures::kRoundingMode);
+        });
+}
+BENCHMARK(floatFromMantExpImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatFromStAmount.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatFromStAmount.cpp
new file mode 100644
index 0000000000..4cca4f48af
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatFromStAmount.cpp
@@ -0,0 +1,31 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+floatFromStAmountImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"float_from_stamount"};
+
+    auto const amount =
+        STAmount{Issue{toCurrency("USD"), Fixtures::instance().alice().id()}, 1234567, -3};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [&amount](auto& host) { return host.floatFromSTAmount(amount, Fixtures::kRoundingMode); });
+}
+BENCHMARK(floatFromStAmountImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatFromStNumber.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatFromStNumber.cpp
new file mode 100644
index 0000000000..b5081550c9
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatFromStNumber.cpp
@@ -0,0 +1,30 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+floatFromStNumberImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"float_from_stnumber"};
+
+    auto const number = STNumber{sfNumber, Number(3141592653589793, -15)};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [&number](auto& host) { return host.floatFromSTNumber(number, Fixtures::kRoundingMode); });
+}
+BENCHMARK(floatFromStNumberImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatFromUint.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatFromUint.cpp
new file mode 100644
index 0000000000..ffe04d409f
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatFromUint.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+floatFromUintImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"float_from_uint"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.floatFromUint(3141592653589793u, Fixtures::kRoundingMode); });
+}
+BENCHMARK(floatFromUintImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatMultiply.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatMultiply.cpp
new file mode 100644
index 0000000000..a2bcbbd06a
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatMultiply.cpp
@@ -0,0 +1,27 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+floatMultiplyImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"float_mult"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.floatMultiply(
+                Fixtures::floatX(), Fixtures::floatY(), Fixtures::kRoundingMode);
+        });
+}
+BENCHMARK(floatMultiplyImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatPower.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatPower.cpp
new file mode 100644
index 0000000000..81b5134d2e
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatPower.cpp
@@ -0,0 +1,42 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+constexpr std::string_view kWasmName = "float_pow";
+
+constexpr std::string_view kImport =
+    R"(  (import "host_lib" "float_pow" (func $float_pow (param i32 i32 i32 i32 i32 i32) (result i32)))
+)";
+
+constexpr std::string_view kBody =
+    "(call $float_pow (i32.const 0) (i32.const 12) (i32.const 7) "
+    "(i32.const 64) (i32.const 12) (i32.const 0))";
+
+void
+floatPowerThroughVm(benchmark::State& state)
+{
+    static auto const kData = dataSegment(0, FloatConstants::kPi);
+    benchmarkThroughVm(
+        state, kWasmName, kImport, kData, kBody, [] { return Fixtures::instance().host(); });
+}
+BENCHMARK(floatPowerThroughVm)->UseManualTime()->Iterations(kBenchIterations);
+
+void
+floatPowerImpl(benchmark::State& state)
+{
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.floatPower(Fixtures::floatX(), 7, Fixtures::kRoundingMode); });
+}
+BENCHMARK(floatPowerImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatSubtract.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatSubtract.cpp
new file mode 100644
index 0000000000..644aada775
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatSubtract.cpp
@@ -0,0 +1,27 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+floatSubtractImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"float_sub"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.floatSubtract(
+                Fixtures::floatX(), Fixtures::floatY(), Fixtures::kRoundingMode);
+        });
+}
+BENCHMARK(floatSubtractImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatToInt.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatToInt.cpp
new file mode 100644
index 0000000000..e8e3179b36
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatToInt.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+floatToIntImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"float_to_int"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.floatToInt(Fixtures::floatX(), Fixtures::kRoundingMode); });
+}
+BENCHMARK(floatToIntImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/FloatToMantExp.cpp b/src/benchmarks/libxrpl/wasm/host_functions/FloatToMantExp.cpp
new file mode 100644
index 0000000000..50522b5e5c
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/FloatToMantExp.cpp
@@ -0,0 +1,41 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+constexpr std::string_view kWasmName = "float_to_mant_exp";
+
+constexpr std::string_view kImport =
+    R"(  (import "host_lib" "float_to_mant_exp" (func $split (param i32 i32 i32 i32 i32 i32) (result i32)))
+)";
+constexpr std::string_view kBody =
+    "(call $split (i32.const 0) (i32.const 12) (i32.const 64) (i32.const 8) "
+    "(i32.const 128) (i32.const 4))";
+
+void
+floatToMantExpThroughVm(benchmark::State& state)
+{
+    static auto const kData = dataSegment(0, FloatConstants::kPi);
+    benchmarkThroughVm(
+        state, kWasmName, kImport, kData, kBody, [] { return Fixtures::instance().host(); });
+}
+BENCHMARK(floatToMantExpThroughVm)->UseManualTime()->Iterations(kBenchIterations);
+
+void
+floatToMantExpImpl(benchmark::State& state)
+{
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.floatToMantExp(Fixtures::floatX()); });
+}
+BENCHMARK(floatToMantExpImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/GetNFT.cpp b/src/benchmarks/libxrpl/wasm/host_functions/GetNFT.cpp
new file mode 100644
index 0000000000..e28addfed0
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/GetNFT.cpp
@@ -0,0 +1,34 @@
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+getNFTImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"nft_uri"};
+
+    // A really minted token, so the lookup walks a real page rather than failing fast — a
+    // not-found answer would measure the rejection instead of the work.
+    static constexpr auto kUri = std::string_view{"ipfs://benchmark"};
+    // Its own ledger rather than the shared `Fixtures`: minting mutates state, and the shared
+    // one is deliberately read-only after construction.
+    static auto nft = WasmLedger{};
+    static auto const kOwner = nft.fund("benchNftOwner");
+    static auto const kMinted = mintNft(nft, kOwner, kUri);
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return nft.makeHost(); },
+        [](auto& host) { return host.getNFT(kOwner.id(), kMinted); });
+}
+BENCHMARK(getNFTImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/IsAmendmentEnabled.cpp b/src/benchmarks/libxrpl/wasm/host_functions/IsAmendmentEnabled.cpp
new file mode 100644
index 0000000000..3fc49c706c
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/IsAmendmentEnabled.cpp
@@ -0,0 +1,54 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+constexpr std::string_view kWasmName = "amendment_enabled";
+
+std::string const&
+benchAmendment()
+{
+    static auto const kValue = std::string{"TokenEscrow"};
+    return kValue;
+}
+
+void
+isAmendmentEnabledByIdImpl(benchmark::State& state)
+{
+    auto const feature = getRegisteredFeature(benchAmendment());
+    if (!feature.has_value())
+    {
+        state.SkipWithError("the benchmarked amendment is not registered");
+        return;
+    }
+    auto const id = *feature;
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [&id](auto& host) { return host.isAmendmentEnabled(id); });
+}
+BENCHMARK(isAmendmentEnabledByIdImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+void
+isAmendmentEnabledByNameImpl(benchmark::State& state)
+{
+    // The gap over the id form is precisely what the shared price of 100 asserts does not exist.
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.isAmendmentEnabled(std::string_view{benchAmendment()}); });
+}
+BENCHMARK(isAmendmentEnabledByNameImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/LedgerObjArrayLen.cpp b/src/benchmarks/libxrpl/wasm/host_functions/LedgerObjArrayLen.cpp
new file mode 100644
index 0000000000..89b14997da
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/LedgerObjArrayLen.cpp
@@ -0,0 +1,26 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+ledgerObjArrayLenImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"le_arr_len"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().cachedSignerListHost(); },
+        [](auto& host) { return host.getLedgerObjArrayLen(1, sfSignerEntries); });
+}
+BENCHMARK(ledgerObjArrayLenImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/LedgerObjField.cpp b/src/benchmarks/libxrpl/wasm/host_functions/LedgerObjField.cpp
new file mode 100644
index 0000000000..2f707b6cb8
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/LedgerObjField.cpp
@@ -0,0 +1,26 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+ledgerObjFieldImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"le_field"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().cachedHost(); },
+        [](auto& host) { return host.getLedgerObjField(1, sfAccount); });
+}
+BENCHMARK(ledgerObjFieldImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/LedgerObjNestedArrayLen.cpp b/src/benchmarks/libxrpl/wasm/host_functions/LedgerObjNestedArrayLen.cpp
new file mode 100644
index 0000000000..b51460e664
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/LedgerObjNestedArrayLen.cpp
@@ -0,0 +1,29 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+ledgerObjNestedArrayLenImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"le_inner_arr_len"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().cachedSignerListHost(); },
+        [](auto& host) {
+            return host.getLedgerObjNestedArrayLen(1, FieldLocator{{sfSignerEntries.getCode()}});
+        });
+}
+BENCHMARK(ledgerObjNestedArrayLenImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/LedgerObjNestedField.cpp b/src/benchmarks/libxrpl/wasm/host_functions/LedgerObjNestedField.cpp
new file mode 100644
index 0000000000..757b3118c5
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/LedgerObjNestedField.cpp
@@ -0,0 +1,29 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+ledgerObjNestedFieldImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"le_inner"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().cachedHost(); },
+        [](auto& host) {
+            return host.getLedgerObjNestedField(1, FieldLocator{{sfAccount.getCode()}});
+        });
+}
+BENCHMARK(ledgerObjNestedFieldImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/LedgerSqn.cpp b/src/benchmarks/libxrpl/wasm/host_functions/LedgerSqn.cpp
new file mode 100644
index 0000000000..130cea3cd2
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/LedgerSqn.cpp
@@ -0,0 +1,38 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+constexpr std::string_view kWasmName = "ldgr_index";
+
+constexpr std::string_view kImport =
+    R"(  (import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
+)";
+
+void
+ledgerSqnThroughVm(benchmark::State& state)
+{
+    benchmarkThroughVm(
+        state, kWasmName, kImport, "", "(call $ldgr_index (i32.const 0) (i32.const 4))", [] {
+            return Fixtures::instance().host();
+        });
+}
+BENCHMARK(ledgerSqnThroughVm)->UseManualTime()->Iterations(kBenchIterations);
+
+void
+ledgerSqnImpl(benchmark::State& state)
+{
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getLedgerSqn(); });
+}
+BENCHMARK(ledgerSqnImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/MptokenIssuanceKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/MptokenIssuanceKeylet.cpp
new file mode 100644
index 0000000000..bda4c979be
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/MptokenIssuanceKeylet.cpp
@@ -0,0 +1,26 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+mptokenIssuanceKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"mpt_issuance_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.mptokenIssuanceKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
+        });
+}
+BENCHMARK(mptokenIssuanceKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/MptokenKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/MptokenKeylet.cpp
new file mode 100644
index 0000000000..b1e289181d
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/MptokenKeylet.cpp
@@ -0,0 +1,31 @@
+
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+mptokenKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"mptoken_id"};
+
+    auto const mptid = makeMptID(1, Fixtures::instance().alice().id());
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [&mptid](auto& host) {
+            return host.mptokenKeylet(mptid, Fixtures::instance().bob().id());
+        });
+}
+BENCHMARK(mptokenKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/NFTFlags.cpp b/src/benchmarks/libxrpl/wasm/host_functions/NFTFlags.cpp
new file mode 100644
index 0000000000..270f8337fa
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/NFTFlags.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+nftFlagsImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"nft_flags"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getNFTFlags(Fixtures::instance().nftId()); });
+}
+BENCHMARK(nftFlagsImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/NFTIssuer.cpp b/src/benchmarks/libxrpl/wasm/host_functions/NFTIssuer.cpp
new file mode 100644
index 0000000000..bcb960e6ee
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/NFTIssuer.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+nftIssuerImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"nft_issuer"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getNFTIssuer(Fixtures::instance().nftId()); });
+}
+BENCHMARK(nftIssuerImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/NFTSequence.cpp b/src/benchmarks/libxrpl/wasm/host_functions/NFTSequence.cpp
new file mode 100644
index 0000000000..f30a8d2348
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/NFTSequence.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+nftSequenceImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"nft_serial"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getNFTSequence(Fixtures::instance().nftId()); });
+}
+BENCHMARK(nftSequenceImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/NFTTaxon.cpp b/src/benchmarks/libxrpl/wasm/host_functions/NFTTaxon.cpp
new file mode 100644
index 0000000000..8a4bc91790
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/NFTTaxon.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+nftTaxonImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"nft_taxon"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getNFTTaxon(Fixtures::instance().nftId()); });
+}
+BENCHMARK(nftTaxonImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/NFTTransferFee.cpp b/src/benchmarks/libxrpl/wasm/host_functions/NFTTransferFee.cpp
new file mode 100644
index 0000000000..c574ccab9d
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/NFTTransferFee.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+nftTransferFeeImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"nft_xfer_fee"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getNFTTransferFee(Fixtures::instance().nftId()); });
+}
+BENCHMARK(nftTransferFeeImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/NftokenOfferKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/NftokenOfferKeylet.cpp
new file mode 100644
index 0000000000..46a3b074a8
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/NftokenOfferKeylet.cpp
@@ -0,0 +1,26 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+nftokenOfferKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"nft_offer_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.nftokenOfferKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
+        });
+}
+BENCHMARK(nftokenOfferKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/OfferKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/OfferKeylet.cpp
new file mode 100644
index 0000000000..7cd61c2b69
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/OfferKeylet.cpp
@@ -0,0 +1,26 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+offerKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"offer_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.offerKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
+        });
+}
+BENCHMARK(offerKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/OracleKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/OracleKeylet.cpp
new file mode 100644
index 0000000000..8a7679322c
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/OracleKeylet.cpp
@@ -0,0 +1,26 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+oracleKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"oracle_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.oracleKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
+        });
+}
+BENCHMARK(oracleKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/ParentLedgerHash.cpp b/src/benchmarks/libxrpl/wasm/host_functions/ParentLedgerHash.cpp
new file mode 100644
index 0000000000..8f3210d101
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/ParentLedgerHash.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+parentLedgerHashImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"parent_ldgr_hash"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getParentLedgerHash(); });
+}
+BENCHMARK(parentLedgerHashImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/ParentLedgerTime.cpp b/src/benchmarks/libxrpl/wasm/host_functions/ParentLedgerTime.cpp
new file mode 100644
index 0000000000..fa800c7520
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/ParentLedgerTime.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+parentLedgerTimeImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"parent_ldgr_time"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getParentLedgerTime(); });
+}
+BENCHMARK(parentLedgerTimeImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/PaychannelKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/PaychannelKeylet.cpp
new file mode 100644
index 0000000000..0892fdfbde
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/PaychannelKeylet.cpp
@@ -0,0 +1,27 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+paychannelKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"paychan_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.paychannelKeylet(
+                Fixtures::instance().alice().id(), Fixtures::instance().bob().id(), Fixtures::kSeq);
+        });
+}
+BENCHMARK(paychannelKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/PermissionedDomainedKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/PermissionedDomainedKeylet.cpp
new file mode 100644
index 0000000000..d97d35afba
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/PermissionedDomainedKeylet.cpp
@@ -0,0 +1,26 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+permissionedDomainKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"permissioned_domain_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.permissionedDomainKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
+        });
+}
+BENCHMARK(permissionedDomainKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/Sha512Half.cpp b/src/benchmarks/libxrpl/wasm/host_functions/Sha512Half.cpp
new file mode 100644
index 0000000000..0894956d8e
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/Sha512Half.cpp
@@ -0,0 +1,69 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+constexpr std::string_view kWasmName = "sha512_half";
+
+constexpr std::string_view kImport =
+    R"(  (import "host_lib" "sha512_half" (func $sha512_half (param i32 i32 i32 i32) (result i32)))
+)";
+
+void
+sha512HalfThroughVm(benchmark::State& state)
+{
+    // Zeroed guest memory is a perfectly good hash input: `sha512_half` validates nothing about
+    // its bytes, so there is no data segment to seed.
+    auto const body = std::format(
+        "(call $sha512_half (i32.const 0) (i32.const {}) (i32.const 8192) (i32.const 32))",
+        state.range(0));
+
+    // A hash is 32 bytes back to the guest whatever the input length, and the transfer budget
+    // counts only what the host writes — so the input sweep does not shrink the call count.
+    benchmarkThroughVm(
+        state,
+        kWasmName,
+        kImport,
+        "",
+        body,
+        [] { return Fixtures::instance().host(); },
+        callsWithinTransferBudget(32));
+    state.SetBytesProcessed(state.iterations() * state.range(0));
+}
+BENCHMARK(sha512HalfThroughVm)
+    ->RangeMultiplier(8)
+    ->Range(8, xrpl::kMaxWasmDataLength)
+    ->UseManualTime()
+    ->Iterations(kBenchIterations);
+
+void
+sha512HalfImpl(benchmark::State& state)
+{
+    auto const data = Bytes(static_cast(state.range(0)), 0x42);
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [&data](auto& host) {
+            return host.computeSha512HalfHash(Slice{data.data(), data.size()});
+        });
+    state.SetBytesProcessed(state.iterations() * state.range(0));
+}
+BENCHMARK(sha512HalfImpl)
+    ->RangeMultiplier(8)
+    ->Range(8, xrpl::kMaxWasmDataLength)
+    ->UseManualTime()
+    ->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/SignerListKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/SignerListKeylet.cpp
new file mode 100644
index 0000000000..0764bd140c
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/SignerListKeylet.cpp
@@ -0,0 +1,24 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+signerListKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"signers_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.signerListKeylet(Fixtures::instance().alice().id()); });
+}
+BENCHMARK(signerListKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/TicketKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/TicketKeylet.cpp
new file mode 100644
index 0000000000..78791a7d2c
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/TicketKeylet.cpp
@@ -0,0 +1,26 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+ticketKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"ticket_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.ticketKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
+        });
+}
+BENCHMARK(ticketKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/Trace.cpp b/src/benchmarks/libxrpl/wasm/host_functions/Trace.cpp
new file mode 100644
index 0000000000..c0eec7a2d6
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/Trace.cpp
@@ -0,0 +1,40 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+constexpr std::string_view kWasmName = "trace";
+constexpr std::string_view kMessage = "benchmark trace message";
+constexpr std::string_view kData = "0123456789abcdef";
+
+// The path a validator actually runs: journal pointed at a null sink.
+void
+traceDisabledImpl(benchmark::State& state)
+{
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.trace(kMessage, kData); });
+}
+BENCHMARK(traceDisabledImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+// The same call against a host whose sink records what it is given. The gap over the case above
+// is the cost the flat 30 does not cover.
+void
+traceEnabledImpl(benchmark::State& state)
+{
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().tracingHost(); },
+        [](auto& host) { return host.trace(kMessage, kData); });
+}
+BENCHMARK(traceEnabledImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/TrustLineKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/TrustLineKeylet.cpp
new file mode 100644
index 0000000000..ffb09dc65c
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/TrustLineKeylet.cpp
@@ -0,0 +1,31 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+trustLineKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"trustline_id"};
+
+    auto const currency = toCurrency("USD");
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [¤cy](auto& host) {
+            return host.trustLineKeylet(
+                Fixtures::instance().alice().id(), Fixtures::instance().bob().id(), currency);
+        });
+}
+BENCHMARK(trustLineKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/TxArrayLen.cpp b/src/benchmarks/libxrpl/wasm/host_functions/TxArrayLen.cpp
new file mode 100644
index 0000000000..a4b1b94811
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/TxArrayLen.cpp
@@ -0,0 +1,26 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+txArrayLenImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"tx_arr_len"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getTxArrayLen(sfMemos); });
+}
+BENCHMARK(txArrayLenImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/TxField.cpp b/src/benchmarks/libxrpl/wasm/host_functions/TxField.cpp
new file mode 100644
index 0000000000..afe0dcaf8d
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/TxField.cpp
@@ -0,0 +1,26 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+txFieldImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"tx_field"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getTxField(sfAccount); });
+}
+BENCHMARK(txFieldImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/TxNestedArrayLen.cpp b/src/benchmarks/libxrpl/wasm/host_functions/TxNestedArrayLen.cpp
new file mode 100644
index 0000000000..de7b4d4a4a
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/TxNestedArrayLen.cpp
@@ -0,0 +1,27 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+txNestedArrayLenImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"tx_inner_arr_len"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getTxNestedArrayLen(FieldLocator{{sfMemos.getCode()}}); });
+}
+BENCHMARK(txNestedArrayLenImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/TxNestedField.cpp b/src/benchmarks/libxrpl/wasm/host_functions/TxNestedField.cpp
new file mode 100644
index 0000000000..3707a55ee7
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/TxNestedField.cpp
@@ -0,0 +1,55 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+constexpr std::string_view kWasmName = "tx_inner";
+constexpr std::string_view kImport =
+    R"(  (import "host_lib" "tx_inner" (func $tx_inner (param i32 i32 i32 i32) (result i32)))
+)";
+constexpr std::string_view kBody =
+    "(call $tx_inner (i32.const 0) (i32.const 12) (i32.const 64) (i32.const 64))";
+
+void
+txNestedFieldThroughVm(benchmark::State& state)
+{
+    // The locator bytes are seeded rather than stored by the guest, so the loop measures the host
+    // call and not three `i32.store`s.
+    static auto const kData = dataSegment(0, [] {
+        auto bytes = Bytes{};
+        for (auto const step : {sfMemos.getCode(), 0, sfMemoData.getCode()})
+        {
+            for (auto i = 0U; i < 4; ++i)
+            {
+                bytes.push_back(static_cast((step >> (8 * i)) & 0xFF));
+            }
+        }
+        return bytes;
+    }());
+
+    benchmarkThroughVm(
+        state, kWasmName, kImport, kData, kBody, [] { return Fixtures::instance().host(); });
+}
+BENCHMARK(txNestedFieldThroughVm)->UseManualTime()->Iterations(kBenchIterations);
+
+void
+txNestedFieldImpl(benchmark::State& state)
+{
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) { return host.getTxNestedField(Fixtures::memoLocator()); });
+}
+BENCHMARK(txNestedFieldImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/UpdateData.cpp b/src/benchmarks/libxrpl/wasm/host_functions/UpdateData.cpp
new file mode 100644
index 0000000000..0cc8a19249
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/UpdateData.cpp
@@ -0,0 +1,62 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+constexpr std::string_view kWasmName = "set_data";
+constexpr std::string_view kImport =
+    R"(  (import "host_lib" "set_data" (func $set_data (param i32 i32) (result i32)))
+)";
+
+void
+updateDataThroughVm(benchmark::State& state)
+{
+    auto const body = std::format("(call $set_data (i32.const 0) (i32.const {}))", state.range(0));
+
+    benchmarkThroughVm(
+        state,
+        kWasmName,
+        kImport,
+        "",
+        body,
+        [] { return Fixtures::instance().host(); },
+        // `set_data` answers a scalar and writes nothing into guest memory, so the
+        // transfer budget does not constrain it however large the input gets.
+        callsWithinTransferBudget(0));
+    state.SetBytesProcessed(state.iterations() * state.range(0));
+}
+BENCHMARK(updateDataThroughVm)
+    ->RangeMultiplier(4)
+    ->Range(8, kMaxWasmDataLength)
+    ->UseManualTime()
+    ->Iterations(kBenchIterations);
+
+void
+updateDataImpl(benchmark::State& state)
+{
+    auto const data = Bytes(static_cast(state.range(0)), 0x42);
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [&data](auto& host) { return host.updateData(Slice{data.data(), data.size()}); });
+    state.SetBytesProcessed(state.iterations() * state.range(0));
+}
+BENCHMARK(updateDataImpl)
+    ->RangeMultiplier(4)
+    ->Range(8, kMaxWasmDataLength)
+    ->UseManualTime()
+    ->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/benchmarks/libxrpl/wasm/host_functions/VaultKeylet.cpp b/src/benchmarks/libxrpl/wasm/host_functions/VaultKeylet.cpp
new file mode 100644
index 0000000000..32b7feabac
--- /dev/null
+++ b/src/benchmarks/libxrpl/wasm/host_functions/VaultKeylet.cpp
@@ -0,0 +1,26 @@
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test::bench {
+namespace {
+
+void
+vaultKeyletImpl(benchmark::State& state)
+{
+    static constexpr auto kWasmName = std::string_view{"vault_id"};
+
+    benchmarkImpl(
+        state,
+        kWasmName,
+        [] { return Fixtures::instance().host(); },
+        [](auto& host) {
+            return host.vaultKeylet(Fixtures::instance().alice().id(), Fixtures::kSeq);
+        });
+}
+BENCHMARK(vaultKeyletImpl)->UseManualTime()->Iterations(kBenchIterations);
+
+}  // namespace
+}  // namespace xrpl::test::bench
diff --git a/src/libxrpl/basics/Archive.cpp b/src/libxrpl/basics/Archive.cpp
index bba144ed04..5ab0d88c1d 100644
--- a/src/libxrpl/basics/Archive.cpp
+++ b/src/libxrpl/basics/Archive.cpp
@@ -2,22 +2,20 @@
 
 #include 
 
-#include 
-#include 
-
 #include 
 #include 
 
 #include 
+#include 
 #include 
 #include 
 
 namespace xrpl {
 
 void
-extractTarLz4(boost::filesystem::path const& src, boost::filesystem::path const& dst)
+extractTarLz4(std::filesystem::path const& src, std::filesystem::path const& dst)
 {
-    if (!is_regular_file(src))
+    if (!std::filesystem::is_regular_file(src))
         Throw("Invalid source file");
 
     using archive_ptr = std::unique_ptr;
diff --git a/src/libxrpl/basics/FileUtilities.cpp b/src/libxrpl/basics/FileUtilities.cpp
index 1a6e604724..bed2b756ac 100644
--- a/src/libxrpl/basics/FileUtilities.cpp
+++ b/src/libxrpl/basics/FileUtilities.cpp
@@ -1,29 +1,31 @@
 #include 
 
-#include 
-#include 
-#include 
-#include 
-#include 
+#include 
 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
 
 namespace xrpl {
 
 std::string
 getFileContents(
-    boost::system::error_code& ec,
-    boost::filesystem::path const& sourcePath,
+    std::error_code& ec,
+    std::filesystem::path const& sourcePath,
     std::optional maxSize)
 {
-    using namespace boost::filesystem;
-    using namespace boost::system::errc;
+    using namespace std::filesystem;
 
     path const fullPath{canonical(sourcePath, ec)};
     if (ec)
@@ -32,15 +34,15 @@ getFileContents(
     if (maxSize && (file_size(fullPath, ec) > *maxSize || ec))
     {
         if (!ec)
-            ec = make_error_code(file_too_large);
+            ec = make_error_code(std::errc::file_too_large);
         return {};
     }
 
-    std::ifstream fileStream(fullPath.string(), std::ios::in);
+    std::ifstream fileStream(fullPath, std::ios::in);
 
     if (!fileStream)
     {
-        ec = make_error_code(static_cast(errno));
+        ec.assign(errno, std::generic_category());
         return {};
     }
 
@@ -49,7 +51,7 @@ getFileContents(
 
     if (fileStream.bad())
     {
-        ec = make_error_code(static_cast(errno));
+        ec.assign(errno, std::generic_category());
         return {};
     }
 
@@ -58,18 +60,15 @@ getFileContents(
 
 void
 writeFileContents(
-    boost::system::error_code& ec,
-    boost::filesystem::path const& destPath,
+    std::error_code& ec,
+    std::filesystem::path const& destPath,
     std::string const& contents)
 {
-    using namespace boost::filesystem;
-    using namespace boost::system::errc;
-
-    std::ofstream fileStream(destPath.string(), std::ios::out | std::ios::trunc);
+    std::ofstream fileStream(destPath, std::ios::out | std::ios::trunc);
 
     if (!fileStream)
     {
-        ec = make_error_code(static_cast(errno));
+        ec.assign(errno, std::generic_category());
         return;
     }
 
@@ -77,9 +76,64 @@ writeFileContents(
 
     if (fileStream.bad())
     {
-        ec = make_error_code(static_cast(errno));
+        ec.assign(errno, std::generic_category());
         return;
     }
 }
 
+std::filesystem::path
+uniqueRandomPath(
+    std::filesystem::path const& base,
+    std::string const& prefix,
+    std::size_t maxAttempts)
+{
+    std::random_device rd;
+    for (std::size_t attempt = 0; attempt < maxAttempts; ++attempt)
+    {
+        std::ostringstream oss;
+        oss << prefix << std::hex << std::setfill('0') << std::setw(8) << rd() << std::setw(8)
+            << rd();
+        auto candidate = base / oss.str();
+        std::error_code ec;
+        bool const exists = std::filesystem::exists(candidate, ec);
+        if (ec)
+        {
+            Throw(
+                "Unable to check path '" + candidate.string() + "': " + ec.message());
+        }
+        if (!exists)
+            return candidate;
+    }
+    Throw("Unable to generate a unique path under '" + base.string() + "'");
+}
+
+TempDir::TempDir() : path_(uniqueRandomPath(std::filesystem::temp_directory_path()))
+{
+    std::filesystem::create_directory(path_);
+}
+
+TempDir::~TempDir()
+{
+    // use non-throwing calls in the destructor
+    std::error_code ec;
+    std::filesystem::remove_all(path_, ec);
+    if (ec)
+    {
+        std::cerr << "Unable to remove temporary directory '" << path_.string()
+                  << "': " << ec.message() << '\n';
+    }
+}
+
+std::string
+TempDir::path() const
+{
+    return path_.string();
+}
+
+std::string
+TempDir::file(std::string const& name) const
+{
+    return (path_ / name).string();
+}
+
 }  // namespace xrpl
diff --git a/src/libxrpl/basics/Log.cpp b/src/libxrpl/basics/Log.cpp
index d1e54a515f..68525f5a65 100644
--- a/src/libxrpl/basics/Log.cpp
+++ b/src/libxrpl/basics/Log.cpp
@@ -5,10 +5,10 @@
 #include 
 
 #include 
-#include 
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -54,7 +54,7 @@ Logs::File::isOpen() const noexcept
 }
 
 bool
-Logs::File::open(boost::filesystem::path const& path)
+Logs::File::open(std::filesystem::path const& path)
 {
     close();
 
@@ -114,7 +114,7 @@ Logs::Logs(beast::Severity thresh) : thresh_(thresh)  // default severity
 }
 
 bool
-Logs::open(boost::filesystem::path const& pathToLogFile)
+Logs::open(std::filesystem::path const& pathToLogFile)
 {
     return file_.open(pathToLogFile);
 }
diff --git a/src/libxrpl/basics/Number.cpp b/src/libxrpl/basics/Number.cpp
index 1f2c41809a..0917627073 100644
--- a/src/libxrpl/basics/Number.cpp
+++ b/src/libxrpl/basics/Number.cpp
@@ -260,6 +260,11 @@ public:
     unsigned
     pop() noexcept;
 
+    // if true, there are no recoverable digits in the guard, though there may be dropped digits
+    // (xbit_)
+    [[nodiscard]] bool
+    unrecoverable() const noexcept;
+
     // if true, there are no digits in the guard, including dropped digits (xbit_)
     [[nodiscard]] bool
     empty() const noexcept;
@@ -277,6 +282,17 @@ public:
     void
     doDropDigit(T& mantissa, int& exponent) noexcept;
 
+    /**
+     * Drop a digit from the mantissa, and increment the exponent, storing the dropped digit in
+     * this Guard.
+     *
+     * If a drop will not do anything meaningful (there are no recoverable digits in the guard, and
+     * the mantissa is 0), and if targetExponent > exponent, simply set exponent to targetExponent.
+     */
+    template 
+    void
+    doDropDigitWithTarget(T& mantissa, int& exponent, int const targetExponent) noexcept;
+
     // Modify the result to the correctly rounded value
     template 
     void
@@ -374,10 +390,16 @@ Number::Guard::pop() noexcept
     return d;
 }
 
+inline bool
+Number::Guard::unrecoverable() const noexcept
+{
+    return digits_ == 0;
+}
+
 inline bool
 Number::Guard::empty() const noexcept
 {
-    return digits_ == 0 && !xbit_;
+    return unrecoverable() && !xbit_;
 }
 
 template 
@@ -401,6 +423,25 @@ Number::Guard::doDropDigit(uint128_t& mantissa, int& exponent) noexce
     ++exponent;
 }
 
+template 
+void
+Number::Guard::doDropDigitWithTarget(T& mantissa, int& exponent, int const targetExponent) noexcept
+{
+    XRPL_ASSERT(
+        exponent < targetExponent, "xrpl::Number::Guard::doDropDigitWithTarget : something to do");
+    while (exponent < targetExponent)
+    {
+        if (mantissa == 0 && unrecoverable())
+        {
+            // No number of dropped digits is going to change anything except the exponent at this
+            // point, so just jump to the result
+            exponent = targetExponent;
+            return;
+        }
+        doDropDigit(mantissa, exponent);
+    }
+}
+
 template 
 void
 Number::Guard::pushOverflow(T mantissa)
@@ -928,6 +969,7 @@ Number::operator+=(Number const& y)
     //  to match, if necessary.
     auto const adjust = [&g, &upperLimit](
                             uint128_t& expandM, int& expandE, uint128_t& shrinkM, int& shrinkE) {
+        XRPL_ASSERT(shrinkE < expandE, "xrpl::Number::operator+= : exponents ordered correctly");
         // Adjust up and down until the exponents match
         if (g.cuspRoundingFix == MantissaRange::CuspRoundingFix::Enabled330)
         {
@@ -935,6 +977,8 @@ Number::operator+=(Number const& y)
             // 1. First, shrink the mantissa of shrinkM/shrinkE while shrinkM ends in 0.
             while (shrinkE < expandE && shrinkM % 10 == 0)
             {
+                // Don't use doDropDigitWithTarget here, because the loop will stop before the
+                // mantissa gets to 0.
                 g.doDropDigit(shrinkM, shrinkE);
             }
 
@@ -950,10 +994,11 @@ Number::operator+=(Number const& y)
 
         // 3. Finally, shrink the mantissa of shrinkM/shrinkE until the exponents match. Any removed
         // digits will be put into the Guard. This is the only step for non-Enabled330 modes.
-        while (shrinkE < expandE)
+        if (shrinkE < expandE)
         {
-            g.doDropDigit(shrinkM, shrinkE);
+            g.doDropDigitWithTarget(shrinkM, shrinkE, expandE);
         }
+        XRPL_ASSERT(shrinkE == expandE, "xrpl::Number::operator+= : exponents are equal");
     };
 
     // Shrink the mantissa and raise the exponent of the value with the lower exponent. Store any
@@ -996,7 +1041,7 @@ Number::operator+=(Number const& y)
             // round.
             XRPL_ASSERT(
                 xm > maxMantissa || g.empty(),
-                "xrpl::Number::operator+ : rounding state expected after add");
+                "xrpl::Number::operator+= : rounding state expected after add");
         }
         else
         {
@@ -1038,7 +1083,7 @@ Number::operator+=(Number const& y)
             }
             XRPL_ASSERT(
                 xm > maxMantissa || g.empty(),
-                "xrpl::Number::operator+ : rounding state expected after subtract");
+                "xrpl::Number::operator+= : rounding state expected after subtract");
         }
         else
         {
@@ -1330,9 +1375,10 @@ operator rep() const
             g.setNegative();
             drops = -drops;
         }
-        while (offset < 0)
+        if (offset < 0)
         {
-            g.doDropDigit(drops, offset);
+            g.doDropDigitWithTarget(drops, offset, 0);
+            XRPL_ASSERT(offset == 0, "xrpl::Number::operator rep() : exponents are equal");
         }
         for (; offset > 0; --offset)
         {
diff --git a/src/libxrpl/basics/StringUtilities.cpp b/src/libxrpl/basics/StringUtilities.cpp
index 2b7deecb8e..9eb1bff995 100644
--- a/src/libxrpl/basics/StringUtilities.cpp
+++ b/src/libxrpl/basics/StringUtilities.cpp
@@ -5,15 +5,15 @@
 #include 
 
 #include 
-#include 
-#include 
 #include 
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -67,7 +67,7 @@ parseUrl(ParsedUrl& pUrl, std::string const& strUrl)
     }
 
     pUrl.scheme = smMatch[1];
-    boost::algorithm::to_lower(pUrl.scheme);
+    pUrl.scheme = toLower(pUrl.scheme);
     pUrl.username = smMatch[2];
     pUrl.password = smMatch[3];
     std::string const domain = smMatch[4];
@@ -93,10 +93,42 @@ parseUrl(ParsedUrl& pUrl, std::string const& strUrl)
     return true;
 }
 
+namespace {
+
+// Deliberately not std::isspace / std::tolower: those consult the current C
+// locale, so the same input could trim or fold differently depending on
+// process-wide state set by something else entirely. Everything these helpers
+// are used on (config keys and values, URL schemes, hex digests) is ASCII, and
+// the callers want a fixed answer, so spell the ASCII rules out.
+
+constexpr bool
+isAsciiSpace(char c)
+{
+    return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
+}
+
+constexpr char
+toAsciiLower(char c)
+{
+    return (c >= 'A' && c <= 'Z') ? static_cast(c - 'A' + 'a') : c;
+}
+
+}  // namespace
+
 std::string
 trimWhitespace(std::string str)
 {
-    boost::trim(str);
+    auto const end = std::ranges::find_if_not(str | std::views::reverse, isAsciiSpace).base();
+    str.erase(end, str.end());
+    str.erase(str.begin(), std::ranges::find_if_not(str, isAsciiSpace));
+
+    return str;
+}
+
+std::string
+toLower(std::string str)
+{
+    std::ranges::transform(str, str.begin(), toAsciiLower);
     return str;
 }
 
diff --git a/src/libxrpl/crypto/RFC1751.cpp b/src/libxrpl/crypto/RFC1751.cpp
index 4b17e1443c..f6342928ab 100644
--- a/src/libxrpl/crypto/RFC1751.cpp
+++ b/src/libxrpl/crypto/RFC1751.cpp
@@ -1,11 +1,11 @@
 #include 
 
+#include 
 #include 
 
 #include 
 #include 
 #include 
-#include 
 #include 
 
 #include 
@@ -397,7 +397,7 @@ RFC1751::getKeyFromEnglish(std::string& strKey, std::string const& strHuman)
 
     std::string strTrimmed(strHuman);
 
-    boost::algorithm::trim(strTrimmed);
+    strTrimmed = trimWhitespace(strTrimmed);
 
     boost::algorithm::split(
         vWords, strTrimmed, boost::algorithm::is_space(), boost::algorithm::token_compress_on);
diff --git a/src/libxrpl/json/Writer.cpp b/src/libxrpl/json/Writer.cpp
index 4c922a0e33..c5ce4666ef 100644
--- a/src/libxrpl/json/Writer.cpp
+++ b/src/libxrpl/json/Writer.cpp
@@ -9,6 +9,7 @@
 #include   // IWYU pragma: keep
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -87,14 +88,14 @@ public:
     }
 
     void
-    output(boost::beast::string_view const& bytes)
+    output(std::string_view bytes)
     {
         markStarted();
         output_(bytes);
     }
 
     void
-    stringOutput(boost::beast::string_view const& bytes)
+    stringOutput(std::string_view bytes)
     {
         markStarted();
         std::size_t position = 0, writtenUntil = 0;
diff --git a/src/libxrpl/ledger/View.cpp b/src/libxrpl/ledger/View.cpp
index 40ea69f427..67c7c8bdb7 100644
--- a/src/libxrpl/ledger/View.cpp
+++ b/src/libxrpl/ledger/View.cpp
@@ -35,6 +35,7 @@
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
@@ -45,20 +46,26 @@ namespace xrpl {
 //------------------------------------------------------------------------------
 
 bool
-hasExpired(ReadView const& view, std::optional const& exp)
+hasExpired(
+    ReadView const& view,
+    std::optional const& exp,
+    ExpiryComparison comparison)
 {
     using d = NetClock::duration;
     using tp = NetClock::time_point;
 
-    return exp && (view.parentCloseTime() >= tp{d{*exp}});
+    if (!exp)
+        return false;
+    auto const boundary = tp{d{*exp}};
+    return comparison == ExpiryComparison::Inclusive  //
+        ? view.parentCloseTime() >= boundary
+        : view.parentCloseTime() > boundary;
 }
 
-bool
-isVaultPseudoAccountFrozen(
-    ReadView const& view,
-    AccountID const& account,
-    MPTIssue const& mptShare,
-    std::uint8_t depth)
+namespace {
+
+std::optional
+checkVaultPseudoAccountFrozenPreconditions(ReadView const& view, std::uint8_t depth)
 {
     if (!view.rules().enabled(featureSingleAssetVault))
         return false;
@@ -66,26 +73,37 @@ isVaultPseudoAccountFrozen(
     if (depth >= kMaxAssetCheckDepth)
     {
         // LCOV_EXCL_START
-        UNREACHABLE("xrpl::View::isVaultPseudoAccountFrozen : reached asset check depth");
+        UNREACHABLE(
+            "xrpl::View::checkVaultPseudoAccountFrozenPreconditions : reached asset check depth");
         return true;
         // LCOV_EXCL_STOP
     }
 
-    auto const mptIssuance = view.read(keylet::mptokenIssuance(mptShare.getMptID()));
-    if (mptIssuance == nullptr)
-        return false;  // zero MPToken won't block deletion of MPTokenIssuance
+    return std::nullopt;
+}
 
-    auto const issuer = mptIssuance->getAccountID(sfIssuer);
+bool
+isVaultPseudoAccountFrozenForIssuance(
+    ReadView const& view,
+    AccountID const& account,
+    SLE const& issuanceSle,
+    std::uint8_t depth)
+{
+    XRPL_ASSERT(
+        issuanceSle.getType() == ltMPTOKEN_ISSUANCE,
+        "xrpl::isVaultPseudoAccountFrozenForIssuance : MPTokenIssuance SLE");
+
+    auto const issuer = issuanceSle.getAccountID(sfIssuer);
 
     // Post-fixCleanup3_2_0: vault shares carry sfReferenceHolding pointing
     // to the vault pseudo's MPToken or RippleState for the underlying.
     // Read it to derive the underlying asset and recurse, skipping the
     // issuer-account-then-vault chain. Pre-amendment shares (no field)
     // fall back to the chain lookup below.
-    if (mptIssuance->isFieldPresent(sfReferenceHolding))
+    if (issuanceSle.isFieldPresent(sfReferenceHolding))
     {
         auto const sleHolding =
-            view.read(keylet::unchecked(mptIssuance->getFieldH256(sfReferenceHolding)));
+            view.read(keylet::unchecked(issuanceSle.getFieldH256(sfReferenceHolding)));
         if (!sleHolding)
         {
             // LCOV_EXCL_START
@@ -94,7 +112,7 @@ isVaultPseudoAccountFrozen(
             // LCOV_EXCL_STOP
         }
         return isAnyFrozen(
-            view, {issuer, account}, assetOfHolding(*mptIssuance, *sleHolding), depth + 1);
+            view, {issuer, account}, assetOfHolding(issuanceSle, *sleHolding), depth + 1);
     }
 
     auto const mptIssuer = view.read(keylet::account(issuer));
@@ -120,6 +138,38 @@ isVaultPseudoAccountFrozen(
     return isAnyFrozen(view, {issuer, account}, vault->at(sfAsset), depth + 1);
 }
 
+}  // namespace
+
+bool
+isVaultPseudoAccountFrozen(
+    ReadView const& view,
+    AccountID const& account,
+    SLE const& issuanceSle,
+    std::uint8_t depth)
+{
+    if (auto const result = checkVaultPseudoAccountFrozenPreconditions(view, depth))
+        return *result;
+
+    return isVaultPseudoAccountFrozenForIssuance(view, account, issuanceSle, depth);
+}
+
+bool
+isVaultPseudoAccountFrozen(
+    ReadView const& view,
+    AccountID const& account,
+    MPTIssue const& mptShare,
+    std::uint8_t depth)
+{
+    if (auto const result = checkVaultPseudoAccountFrozenPreconditions(view, depth))
+        return *result;
+
+    auto const issuanceSle = view.read(keylet::mptokenIssuance(mptShare.getMptID()));
+    if (issuanceSle == nullptr)
+        return false;  // zero MPToken won't block deletion of MPTokenIssuance
+
+    return isVaultPseudoAccountFrozenForIssuance(view, account, *issuanceSle, depth);
+}
+
 bool
 isLPTokenFrozen(
     ReadView const& view,
@@ -130,6 +180,33 @@ isLPTokenFrozen(
     return isFrozen(view, account, asset) || isFrozen(view, account, asset2);
 }
 
+TER
+canTransferLPToken(
+    ReadView const& view,
+    AccountID const& from,
+    AccountID const& to,
+    AccountID const& lpTokenIssuer)
+{
+    // Only AMM-issued LPTokens are subject to this check. The LPToken's issuer
+    // is the AMM account; if it is not an AMM, this is not an LPToken.
+    auto const sleIssuer = view.read(keylet::account(lpTokenIssuer));
+    if (!sleIssuer || !sleIssuer->isFieldPresent(sfAMMID))
+        return tesSUCCESS;
+
+    auto const sleAmm = view.read(keylet::amm((*sleIssuer)[sfAMMID]));
+    if (!sleAmm)
+        return tecINTERNAL;  // LCOV_EXCL_LINE
+
+    auto const transferable = [&](Asset const& a) -> TER {
+        if (!a.holds())
+            return tesSUCCESS;
+        return canTransfer(view, a.get(), from, to);
+    };
+    if (auto const err = transferable((*sleAmm)[sfAsset]); !isTesSuccess(err))
+        return err;
+    return transferable((*sleAmm)[sfAsset2]);
+}
+
 bool
 areCompatible(
     ReadView const& validLedger,
@@ -391,7 +468,8 @@ canWithdraw(
     AccountID const& to,
     SLE::const_ref toSle,
     STAmount const& amount,
-    bool hasDestinationTag)
+    bool hasDestinationTag,
+    std::optional> const& credentialIDs)
 {
     if (auto const ret = checkDestinationAndTag(toSle, hasDestinationTag))
         return ret;
@@ -402,7 +480,28 @@ canWithdraw(
     if (toSle->isFlag(lsfDepositAuth))
     {
         if (!view.exists(keylet::depositPreauth(to, from)))
-            return tecNO_PERMISSION;
+        {
+            if (credentialIDs.has_value())
+            {
+                STVector256 const credIDs{*credentialIDs};
+
+                // Callers must have validated these in preclaim, so a missing
+                // credential here is an invariant violation.
+                for (auto const& h : credIDs)
+                {
+                    if (!view.exists(keylet::credential(h)))
+                        return tecINTERNAL;  // LCOV_EXCL_LINE
+                }
+
+                if (auto const ret = credentials::authorizedDepositPreauth(view, credIDs, to);
+                    !isTesSuccess(ret))
+                    return ret;
+            }
+            else
+            {
+                return tecNO_PERMISSION;
+            }
+        }
     }
 
     return withdrawToDestExceedsLimit(view, from, to, amount);
@@ -414,11 +513,12 @@ canWithdraw(
     AccountID const& from,
     AccountID const& to,
     STAmount const& amount,
-    bool hasDestinationTag)
+    bool hasDestinationTag,
+    std::optional> const& credentialIDs)
 {
     auto const toSle = view.read(keylet::account(to));
 
-    return canWithdraw(view, from, to, toSle, amount, hasDestinationTag);
+    return canWithdraw(view, from, to, toSle, amount, hasDestinationTag, credentialIDs);
 }
 
 [[nodiscard]] TER
@@ -427,7 +527,8 @@ canWithdraw(ReadView const& view, STTx const& tx)
     auto const from = tx[sfAccount];
     auto const to = tx[~sfDestination].value_or(from);
 
-    return canWithdraw(view, from, to, tx[sfAmount], tx.isFieldPresent(sfDestinationTag));
+    return canWithdraw(
+        view, from, to, tx[sfAmount], tx.isFieldPresent(sfDestinationTag), tx[~sfCredentialIDs]);
 }
 
 TER
@@ -442,12 +543,19 @@ doWithdraw(
 {
     auto const dstSle = ctx.view.read(keylet::account(dstAcct));
 
-    // Create trust line or MPToken for the receiving account
+    // Create a trust line or MPToken for a self-destination only when there
+    // is a payout to credit. Post-fixCleanup3_4_0, a zero-value withdraw
+    // (e.g. share redemption from a fully impaired vault) must not insert
+    // an empty holding: that records a one-sided zero delta and can also
+    // create+delete MPTokens in the same transaction.
     if (dstAcct == senderAcct)
     {
-        if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
-            !isTesSuccess(ter) && ter != tecDUPLICATE)
-            return ter;
+        if (amount > beast::kZero || !ctx.view.rules().enabled(fixCleanup3_4_0))
+        {
+            if (auto const ter = addEmptyHolding(ctx, senderAcct, priorBalance, amount.asset(), j);
+                !isTesSuccess(ter) && ter != tecDUPLICATE)
+                return ter;
+        }
     }
     else
     {
diff --git a/src/libxrpl/ledger/helpers/AMMHelpers.cpp b/src/libxrpl/ledger/helpers/AMMHelpers.cpp
index df6d335085..fcad22d2d5 100644
--- a/src/libxrpl/ledger/helpers/AMMHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/AMMHelpers.cpp
@@ -11,6 +11,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -633,7 +634,7 @@ ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const
     return asset.visit(
         [&](MPTIssue const& issue) {
             if (auto const sle = view.read(keylet::mptoken(issue, ammAccountID));
-                sle && !isFrozen(view, ammAccountID, issue))
+                sle && !isFrozen(view, ammAccountID, *sle))
                 return STAmount{issue, (*sle)[sfMPTAmount]};
             return STAmount{asset};
         },
diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp
index faca4ebfb6..819ebb04d1 100644
--- a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp
@@ -28,7 +28,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 
@@ -515,8 +514,8 @@ pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey)
 }
 
 // Pseudo-account designator fields MUST be maintained by including the
-// SField::sMD_PseudoAccount flag in the SField definition. (Don't forget to
-// "| SField::sMD_Default"!) The fields do NOT need to be amendment-gated,
+// SField::kSmdPseudoAccount flag in the SField definition. (Don't forget to
+// "| SField::kSmdDefault"!) The fields do NOT need to be amendment-gated,
 // since a non-active amendment will not set any field, by definition.
 // Specific properties of a pseudo-account are NOT checked here, that's what
 // InvariantCheck is for.
@@ -547,18 +546,14 @@ getPseudoAccountFields()
 }
 
 [[nodiscard]] bool
-isPseudoAccount(SLE::const_pointer sleAcct, std::set const& pseudoFieldFilter)
+isPseudoAccount(SLE::const_pointer sleAcct)
 {
-    auto const& fields = getPseudoAccountFields();
-
     // Intentionally use defensive coding here because it's cheap and makes the
     // semantics of true return value clean.
     return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT &&
-        std::count_if(
-            fields.begin(), fields.end(), [&sleAcct, &pseudoFieldFilter](SField const* sf) -> bool {
-                return sleAcct->isFieldPresent(*sf) &&
-                    (pseudoFieldFilter.empty() || pseudoFieldFilter.contains(sf));
-            }) > 0;
+        std::ranges::any_of(getPseudoAccountFields(), [&sleAcct](SField const* sf) {
+               return sleAcct->isFieldPresent(*sf);
+           });
 }
 
 std::expected
diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp
index 226ea100e9..5ba832957d 100644
--- a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp
@@ -22,6 +22,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -52,6 +53,9 @@ removeExpired(ApplyView& view, STVector256 const& arr, beast::Journal const j)
     for (auto const& h : arr)
     {
         // Credentials already checked in preclaim. Look only for expired here.
+        if (view.rules().enabled(fixCleanup3_4_0) && h.isZero())
+            return std::unexpected(tecINTERNAL);  // LCOV_EXCL_LINE
+
         auto const k = keylet::credential(h);
         auto const sleCred = view.peek(k);
 
@@ -124,7 +128,7 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j)
 }
 
 NotTEC
-checkFields(STTx const& tx, beast::Journal j)
+checkFields(STTx const& tx, Rules const& rules, beast::Journal j)
 {
     if (!tx.isFieldPresent(sfCredentialIDs))
         return tesSUCCESS;
@@ -137,6 +141,13 @@ checkFields(STTx const& tx, beast::Journal j)
         return temMALFORMED;
     }
 
+    if (rules.enabled(fixCleanup3_4_0) &&
+        std::ranges::any_of(credentials, [](uint256 const& id) { return id.isZero(); }))
+    {
+        JLOG(j.trace()) << "Malformed transaction: zero credential ID.";
+        return temMALFORMED;
+    }
+
     std::unordered_set duplicates;
     for (auto const& cred : credentials)
     {
@@ -160,6 +171,14 @@ valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal
     auto const& credIDs(tx.getFieldV256(sfCredentialIDs));
     for (auto const& h : credIDs)
     {
+        if (view.rules().enabled(fixCleanup3_4_0) && h.isZero())
+        {
+            // LCOV_EXCL_START
+            JLOG(j.trace()) << "Zero credential ID.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
+
         auto const sleCred = view.read(keylet::credential(h));
         if (!sleCred)
         {
@@ -234,6 +253,9 @@ authorizedDepositPreauth(ReadView const& view, STVector256 const& credIDs, Accou
     lifeExtender.reserve(credIDs.size());
     for (auto const& h : credIDs)
     {
+        if (view.rules().enabled(fixCleanup3_4_0) && h.isZero())
+            return tefINTERNAL;  // LCOV_EXCL_LINE
+
         auto sleCred = view.read(keylet::credential(h));
         if (!sleCred)            // already checked in preclaim
             return tefINTERNAL;  // LCOV_EXCL_LINE
diff --git a/src/libxrpl/ledger/helpers/LendingHelpers.cpp b/src/libxrpl/ledger/helpers/LendingHelpers.cpp
index 89b03a03a7..10c7e62c6c 100644
--- a/src/libxrpl/ledger/helpers/LendingHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/LendingHelpers.cpp
@@ -12,6 +12,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -20,12 +21,15 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -77,6 +81,40 @@ checkLendingProtocolDependencies(Rules const& rules, STTx const& tx)
     return true;
 }
 
+std::optional
+getLoanDefaultFreezeExemptAccounts(ReadView const& view, STTx const& tx)
+{
+    if (tx.getTxnType() != ttLOAN_MANAGE || !tx.isFlag(tfLoanDefault) ||
+        !view.rules().enabled(fixCleanup3_4_0))
+        return std::nullopt;
+
+    // Unlike the broker/vault lookups below, the submitter picks the LoanID,
+    // so a nonexistent Loan is an ordinary (if unusual) input, not a
+    // structural impossibility -- exercised directly in LendingHelpers_test.
+    auto const loanSle = view.read(keylet::loan(tx[sfLoanID]));
+    if (!loanSle)
+        return std::nullopt;
+
+    // A Loan can't outlive its LoanBroker (LoanBrokerDelete's preclaim
+    // rejects deletion while DebtTotal != 0), and a LoanBroker can't outlive
+    // its Vault (VaultDelete's preclaim has the equivalent guard) -- so these
+    // two lookups are structurally guaranteed to succeed here.
+    auto const brokerSle = view.read(keylet::loanBroker(loanSle->at(sfLoanBrokerID)));
+    if (!brokerSle)
+        return std::nullopt;  // LCOV_EXCL_LINE
+
+    auto const vaultSle = view.read(keylet::vault(brokerSle->at(sfVaultID)));
+    if (!vaultSle)
+        return std::nullopt;  // LCOV_EXCL_LINE
+
+    Asset const vaultAsset = vaultSle->at(sfAsset);
+    return LoanDefaultFreezeExemptAccounts{
+        .issuer = vaultAsset.getIssuer(),
+        .broker = brokerSle->at(sfAccount),
+        .vault = vaultSle->at(sfAccount),
+        .asset = vaultAsset};
+}
+
 LoanPaymentParts&
 LoanPaymentParts::operator+=(LoanPaymentParts const& other)
 {
@@ -131,6 +169,16 @@ isRounded(Asset const& asset, Number const& value, std::int32_t scale)
         roundToAsset(asset, value, scale, Number::RoundingMode::Upward);
 }
 
+[[nodiscard]] bool
+isPaymentLate(ReadView const& view, SLE::const_ref loanSle)
+{
+    return hasExpired(
+        view,
+        loanSle->at(sfNextPaymentDueDate),
+        view.rules().enabled(fixCleanup3_4_0) ? ExpiryComparison::Exclusive
+                                              : ExpiryComparison::Inclusive);
+}
+
 namespace accrual {
 
 AccountingDeltas
@@ -476,7 +524,7 @@ loanLatePaymentInterest(
     // If the payment is not late by any amount of time, then there's no late
     // interest
     if (now <= nextPaymentDueDate)
-        return 0;
+        return kNumZero;
 
     // Equation (3) from XLS-66 spec, Section A-2 Equation Glossary
     auto const secondsOverdue = now - nextPaymentDueDate;
@@ -997,7 +1045,7 @@ doOverpayment(
 std::expected
 computeLatePayment(
     Asset const& asset,
-    ApplyView const& view,
+    ReadView const& view,
     SLE::const_ref loan,
     ExtendedPaymentComponents const& periodic,
     STAmount const& amount,
@@ -1008,8 +1056,11 @@ computeLatePayment(
     std::int32_t const loanScale = loan->at(sfLoanScale);
 
     // Check if the due date has passed. If not, reject the payment as
-    // being too soon
-    if (!hasExpired(view, nextDueDate))
+    // being too soon. Uses isPaymentLate() so this agrees with the
+    // regular payment path on whether the loan is actually late at the
+    // exact due date boundary (amendment-gated: Exclusive once
+    // fixCleanup3_4_0 is enabled, Inclusive otherwise).
+    if (!isPaymentLate(view, loan))
         return std::unexpected(tecTOO_SOON);
 
     // Calculate the penalty interest based on how long the payment is overdue.
@@ -1090,7 +1141,7 @@ computeLatePayment(
 std::expected
 computeFullPayment(
     Asset const& asset,
-    ApplyView& view,
+    ReadView const& view,
     SLE::const_ref loan,
     Number const& periodicRate,
     STAmount const& amount,
@@ -2232,7 +2283,7 @@ loanMakePayment(
 
     // -------------------------------------------------------------
     // A late payment not flagged as late overrides all other options.
-    if (paymentType != LoanPaymentType::Late && hasExpired(view, nextDueDateProxy))
+    if (paymentType != LoanPaymentType::Late && isPaymentLate(view, loan))
     {
         // If the payment is late, and the late flag was not set, it's not
         // valid
diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
index 6fe7328fa7..27dcd84675 100644
--- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp
@@ -42,18 +42,35 @@ bool
 isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue)
 {
     if (auto const sle = view.read(keylet::mptokenIssuance(mptIssue.getMptID())))
-        return sle->isFlag(lsfMPTLocked);
+        return isGlobalFrozen(*sle);
     return false;
 }
 
+bool
+isGlobalFrozen(SLE const& issuanceSle)
+{
+    XRPL_ASSERT(
+        issuanceSle.getType() == ltMPTOKEN_ISSUANCE, "xrpl::isGlobalFrozen : MPTokenIssuance SLE");
+
+    return issuanceSle.isFlag(lsfMPTLocked);
+}
+
 bool
 isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue)
 {
     if (auto const sle = view.read(keylet::mptoken(mptIssue.getMptID(), account)))
-        return sle->isFlag(lsfMPTLocked);
+        return isIndividualFrozen(*sle);
     return false;
 }
 
+bool
+isIndividualFrozen(SLE const& mptSle)
+{
+    XRPL_ASSERT(mptSle.getType() == ltMPTOKEN, "xrpl::isIndividualFrozen : MPToken SLE");
+
+    return mptSle.isFlag(lsfMPTLocked);
+}
+
 bool
 isFrozen(
     ReadView const& view,
@@ -65,6 +82,34 @@ isFrozen(
         isVaultPseudoAccountFrozen(view, account, mptIssue, depth);
 }
 
+bool
+isFrozen(ReadView const& view, AccountID const& account, SLE const& sle, std::uint8_t depth)
+{
+    XRPL_ASSERT(
+        sle.getType() == ltMPTOKEN || sle.getType() == ltMPTOKEN_ISSUANCE,
+        "xrpl::isFrozen : MPToken or MPTokenIssuance SLE");
+
+    if (sle.getType() == ltMPTOKEN)
+    {
+        XRPL_ASSERT(sle[sfAccount] == account, "xrpl::isFrozen : valid MPToken holder");
+
+        MPTID const mptID = sle[sfMPTokenIssuanceID];
+        auto const issuanceSle = view.read(keylet::mptokenIssuance(mptID));
+
+        if ((issuanceSle && isGlobalFrozen(*issuanceSle)) || isIndividualFrozen(sle))
+            return true;
+
+        if (issuanceSle)
+            return isVaultPseudoAccountFrozen(view, account, *issuanceSle, depth);
+
+        return isVaultPseudoAccountFrozen(view, account, MPTIssue{mptID}, depth);
+    }
+
+    MPTIssue const mptIssue{sle[sfSequence], sle[sfIssuer]};
+    return isGlobalFrozen(sle) || isIndividualFrozen(view, account, mptIssue) ||
+        isVaultPseudoAccountFrozen(view, account, sle, depth);
+}
+
 [[nodiscard]] bool
 isAnyFrozen(
     ReadView const& view,
@@ -72,7 +117,8 @@ isAnyFrozen(
     MPTIssue const& mptIssue,
     std::uint8_t depth)
 {
-    if (isGlobalFrozen(view, mptIssue))
+    auto const issuanceSle = view.read(keylet::mptokenIssuance(mptIssue.getMptID()));
+    if (issuanceSle && isGlobalFrozen(*issuanceSle))
         return true;
 
     for (auto const& account : accounts)
@@ -81,9 +127,15 @@ isAnyFrozen(
             return true;
     }
 
-    return std::ranges::any_of(accounts, [&](auto const& account) {
-        return isVaultPseudoAccountFrozen(view, account, mptIssue, depth);
-    });
+    // Pass the issuance SLE when we have it to avoid re-reading it per account;
+    // otherwise defer to the MPTIssue overload, which handles a missing issuance.
+    auto const anyVaultFrozen = [&](auto const& shareOrIssuance) {
+        return std::ranges::any_of(accounts, [&](auto const& account) {
+            return isVaultPseudoAccountFrozen(view, account, shareOrIssuance, depth);
+        });
+    };
+
+    return issuanceSle ? anyVaultFrozen(*issuanceSle) : anyVaultFrozen(mptIssue);
 }
 
 Rate
@@ -132,6 +184,8 @@ addEmptyHolding(
     auto const mpt = ctx.view.peek(keylet::mptokenIssuance(mptID));
     if (!mpt)
         return tefINTERNAL;  // LCOV_EXCL_LINE
+    // Unlike IOU addEmptyHolding (post-fixCleanup3_4_0), a locked issuance is
+    // still rejected before the "MPToken already exists" short circuit.
     if (mpt->isFlag(lsfMPTLocked))
         return tefINTERNAL;  // LCOV_EXCL_LINE
     if (ctx.view.peek(keylet::mptoken(mptID, accountID)))
@@ -332,8 +386,7 @@ requireAuth(
     // They are implicitly authorized for any MPT they hold, including vault shares whose
     // underlying asset would otherwise require auth.
     auto const isPseudoAccountExempt = [&] {
-        return (featureSAVEnabled || featureMPTV2Enabled) &&
-            isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID, &sfAMMID});
+        return (featureSAVEnabled || featureMPTV2Enabled) && isPseudoAccount(view, account);
     };
 
     auto const mptID = keylet::mptokenIssuance(mptIssue.getMptID());
@@ -952,6 +1005,7 @@ checkCreateMPT(
     xrpl::MPTIssue const& mptIssue,
     xrpl::AccountID const& holder,
     SLE::ref sponsorSle,
+    std::uint32_t flags,
     beast::Journal j)
 {
     if (mptIssue.getIssuer() == holder)
@@ -961,7 +1015,7 @@ checkCreateMPT(
     auto const mptokenID = keylet::mptoken(mptIssuanceID.key, holder);
     if (!view.exists(mptokenID))
     {
-        if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, sponsorSle, 0);
+        if (auto const err = createMPToken(view, mptIssue.getMptID(), holder, sponsorSle, flags);
             !isTesSuccess(err))
         {
             return err;
@@ -977,6 +1031,16 @@ checkCreateMPT(
     return tesSUCCESS;
 }
 
+TER
+checkCreateMPT(
+    xrpl::ApplyView& view,
+    xrpl::MPTIssue const& mptIssue,
+    xrpl::AccountID const& holder,
+    beast::Journal j)
+{
+    return checkCreateMPT(view, mptIssue, holder, {}, 0, j);
+}
+
 std::int64_t
 maxMPTAmount(SLE const& sleIssuance)
 {
diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp
index d97b08981b..5d8526444c 100644
--- a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp
@@ -15,6 +15,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -776,6 +777,13 @@ tokenOfferCreatePreflight(
         return temBAD_AMOUNT;
     }
 
+    if (rules.enabled(fixCleanup3_4_0))
+    {
+        // We don't allow a non-native currency to use the currency code XRP.
+        if (badAsset() == amount.asset())
+            return temBAD_CURRENCY;
+    }
+
     if (!isXRP(amount))
     {
         if ((nftFlags & nft::kFlagOnlyXrp) != 0)
@@ -854,7 +862,13 @@ tokenOfferCreatePreclaim(
             return tefNFTOKEN_IS_NOT_TRANSFERABLE;
     }
 
-    if (isFrozen(view, acctID, amount.get().currency, amount.getIssuer()))
+    // The IOU issuer is not subject to their own global freeze when the offer
+    // is denominated in their own IOU (e.g. receiving their own transfer fees),
+    // and they cannot hold a trust line to themselves.
+    bool const acctIsIouIssuer =
+        view.rules().enabled(fixCleanup3_4_0) && acctID == amount.getIssuer();
+    if (!acctIsIouIssuer &&
+        isFrozen(view, acctID, amount.get().currency, amount.getIssuer()))
         return tecFROZEN;
 
     // If this is an offer to buy the token, the account must have the
diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp
index 868c9fb26d..cc02b56305 100644
--- a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp
@@ -584,9 +584,15 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account,
     {
         if (trustLine)
         {
-            return trustLine->isFlag((account > issue.account) ? lsfLowAuth : lsfHighAuth)
-                ? tesSUCCESS
-                : TER{tecNO_AUTH};
+            if (trustLine->isFlag((account > issue.account) ? lsfLowAuth : lsfHighAuth))
+                return tesSUCCESS;
+
+            // A pseudo-account cannot submit transactions and only stores assets for the object
+            // that owns it, so it is implicitly authorized.
+            if (view.rules().enabled(fixCleanup3_4_0) && isPseudoAccount(view, account))
+                return tesSUCCESS;
+
+            return TER{tecNO_AUTH};
         }
         return TER{tecNO_LINE};
     }
@@ -646,21 +652,32 @@ addEmptyHolding(
 
     auto const& issuerId = issue.getIssuer();
     auto const& currency = issue.currency;
-    if (isGlobalFrozen(ctx.view, issuerId))
-        return tecFROZEN;  // LCOV_EXCL_LINE
-
     auto const& srcId = issuerId;
     auto const& dstId = accountID;
     auto const high = srcId > dstId;
     auto const index = keylet::trustLine(srcId, dstId, currency);
+    // Post-fixCleanup3_4_0: an existing line is a no-op. Issuer freeze and
+    // DefaultRipple only matter when this function has to create a line.
+    bool const fix340Enabled = ctx.view.rules().enabled(fixCleanup3_4_0);
+    if (fix340Enabled && ctx.view.exists(index))
+        return tecDUPLICATE;
+
+    if (isGlobalFrozen(ctx.view, issuerId))
+        return tecFROZEN;  // LCOV_EXCL_LINE
+
     auto const sleSrc = ctx.view.peek(keylet::account(srcId));
     auto const sleDst = ctx.view.peek(keylet::account(dstId));
     if (!sleDst || !sleSrc)
         return tefINTERNAL;  // LCOV_EXCL_LINE
+    // Create path: DefaultRipple is still required. terNO_RIPPLE is
+    // intentional so VaultWithdraw / CoverWithdraw fail in preclaim via
+    // canAddHolding (retryable, no fee) rather than claiming a tec* fee
+    // in doApply. Transactor::operator() will not apply and will not
+    // convert it to tefINTERNAL.
     if (!sleSrc->isFlag(lsfDefaultRipple))
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+        return fix340Enabled ? TER{terNO_RIPPLE} : tecINTERNAL;
     // If the line already exists, don't create it again.
-    if (ctx.view.read(index))
+    if (!fix340Enabled && ctx.view.exists(index))
         return tecDUPLICATE;
 
     // A reserve sponsor only covers tx.Account's own objects.
diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp
index 79e10cdf79..2c2c943a9f 100644
--- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp
@@ -309,6 +309,15 @@ getLineIfUsable(
                 }
             }
         }
+
+        // An LPToken whose AMM pool contains an MPT that forbids transfers is not
+        // spendable. Issuer is the LPToken's AMM account; canTransferLPToken is
+        // a no-op for non-AMM issuers and non-MPT pool assets, so this is implicitly
+        // gated by featureMPTokensV2.
+        if (!isTesSuccess(canTransferLPToken(view, account, account, issuer)))
+        {
+            return nullptr;
+        }
     }
 
     return sle;
@@ -430,7 +439,7 @@ accountHolds(
     auto const sleMpt = view.read(keylet::mptoken(mptIssue.getMptID(), account));
 
     if (!sleMpt ||
-        (zeroIfFrozen == FreezeHandling::ZeroIfFrozen && isFrozen(view, account, mptIssue)))
+        (zeroIfFrozen == FreezeHandling::ZeroIfFrozen && isFrozen(view, account, *sleMpt)))
     {
         amount.clear(mptIssue);
     }
@@ -526,13 +535,19 @@ accountFunds(
 }
 
 Rate
-transferRate(ReadView const& view, STAmount const& amount)
+transferRate(ReadView const& view, Asset const& asset)
 {
-    return amount.asset().visit(
+    return asset.visit(
         [&](Issue const& issue) { return transferRate(view, issue.getIssuer()); },
         [&](MPTIssue const& issue) { return transferRate(view, issue.getMptID()); });
 }
 
+Rate
+transferRate(ReadView const& view, STAmount const& amount)
+{
+    return transferRate(view, amount.asset());
+}
+
 //------------------------------------------------------------------------------
 //
 // Holding operations
@@ -568,6 +583,32 @@ canAddHolding(ReadView const& view, Asset const& asset)
         asset.value());
 }
 
+[[nodiscard]] bool
+holdingExists(ReadView const& view, AccountID const& account, Issue const& issue)
+{
+    if (issue.native() || account == issue.getIssuer())
+        return true;
+    return view.exists(keylet::trustLine(account, issue));
+}
+
+[[nodiscard]] bool
+holdingExists(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue)
+{
+    if (account == mptIssue.getIssuer())
+        return true;
+    return view.exists(keylet::mptoken(mptIssue.getMptID(), account));
+}
+
+[[nodiscard]] bool
+holdingExists(ReadView const& view, AccountID const& account, Asset const& asset)
+{
+    return std::visit(
+        [&](TIss const& issue) -> bool {
+            return holdingExists(view, account, issue);
+        },
+        asset.value());
+}
+
 TER
 addEmptyHolding(
     ApplyViewContext ctx,
diff --git a/src/libxrpl/ledger/helpers/VaultHelpers.cpp b/src/libxrpl/ledger/helpers/VaultHelpers.cpp
index 78f64d2077..941b94143d 100644
--- a/src/libxrpl/ledger/helpers/VaultHelpers.cpp
+++ b/src/libxrpl/ledger/helpers/VaultHelpers.cpp
@@ -1,8 +1,11 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include   // IWYU pragma: keep
@@ -11,8 +14,11 @@
 #include 
 #include 
 #include   // IWYU pragma: keep
+#include 
+#include 
 
 #include 
+#include 
 #include 
 #include 
 
@@ -65,6 +71,82 @@ sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount co
     return assets;
 }
 
+[[nodiscard]] std::expected
+clampToAssetsTotalScale(SLE::const_ref vault, STAmount const& delta)
+{
+    XRPL_ASSERT(
+        delta.asset() == vault->at(sfAsset),
+        "xrpl::clampToAssetsTotalScale : delta and vault asset match");
+
+    Asset const asset = vault->at(sfAsset);
+
+    STAmount magnitude = delta.negative() ? -delta : delta;
+    if (asset.integral())
+    {
+        return magnitude;
+    }
+    Number const assetsTotal = vault->at(sfAssetsTotal);
+
+    // Calculate the scale after applying the delta using ToNearest rounding.
+    // This aligns the delta with scale checks used by vault invariants.
+    int const postScale = [&] {
+        NumberRoundModeGuard const rg(Number::RoundingMode::ToNearest);
+        return scale(assetsTotal + delta, asset);
+    }();
+
+    STAmount actualDelta;
+    if (delta.negative())
+    {
+        // For withdrawals (debits), floor the magnitude to the target scale
+        // to ensure exact grid alignment without paying out extra assets.
+        actualDelta = roundToScale(magnitude, postScale, Number::RoundingMode::Downward);
+    }
+    else
+    {
+        // For deposits (credits), derive actualDelta from the floored posterior total.
+        // This prevents grid alignment issues from crediting the vault more than deposited.
+        //
+        // Sum using Downward rounding so intermediate precision doesn't round up
+        // and exceed the original requested amount.
+        Number const posterior = [&] {
+            NumberRoundModeGuard const rg(Number::RoundingMode::Downward);
+            return assetsTotal + magnitude;
+        }();
+
+        Number const roundedPosterior =
+            roundToAsset(asset, posterior, postScale, Number::RoundingMode::Downward);
+        actualDelta = STAmount{asset, roundedPosterior - assetsTotal};
+    }
+
+    XRPL_ASSERT(
+        abs(actualDelta) <= abs(delta),
+        "xrpl::clampToAssetsTotalScale : actual delta smaller or equal to calculated delta");
+
+    // Reject changes below scale precision (1 ULP) to prevent share balance changes
+    // without corresponding asset movements.
+    if (actualDelta <= beast::kZero)
+        return std::unexpected(tecPRECISION_LOSS);
+
+    return actualDelta;
+}
+
+[[nodiscard]] Number
+assetsTotalForWithdrawal(SLE::const_ref vault, WaiveUnrealizedLoss waive)
+{
+    Number assetTotal = vault->at(sfAssetsTotal);
+    if (waive == WaiveUnrealizedLoss::No)
+        assetTotal -= vault->at(sfLossUnrealized);
+    return assetTotal;
+}
+
+[[nodiscard]] bool
+debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount)
+{
+    if (amount == 0)
+        return false;
+    return STAmount{asset, total - amount} == STAmount{asset, total};
+}
+
 [[nodiscard]] std::optional
 assetsToSharesWithdraw(
     SLE::const_ref vault,
@@ -80,9 +162,7 @@ assetsToSharesWithdraw(
     if (assets.negative() || assets.asset() != vault->at(sfAsset))
         return std::nullopt;  // LCOV_EXCL_LINE
 
-    Number assetTotal = vault->at(sfAssetsTotal);
-    if (waive == WaiveUnrealizedLoss::No)
-        assetTotal -= vault->at(sfLossUnrealized);
+    Number const assetTotal = assetsTotalForWithdrawal(vault, waive);
     STAmount shares{vault->at(sfShareMPTID)};
     if (assetTotal == 0)
         return shares;
@@ -108,9 +188,7 @@ sharesToAssetsWithdraw(
     if (shares.negative() || shares.asset() != vault->at(sfShareMPTID))
         return std::nullopt;  // LCOV_EXCL_LINE
 
-    Number assetTotal = vault->at(sfAssetsTotal);
-    if (waive == WaiveUnrealizedLoss::No)
-        assetTotal -= vault->at(sfLossUnrealized);
+    Number const assetTotal = assetsTotalForWithdrawal(vault, waive);
     STAmount assets{vault->at(sfAsset)};
     if (assetTotal == 0)
         return assets;
@@ -157,4 +235,96 @@ getVaultVersion(SLE::const_ref vault)
     return static_cast(version);
 }
 
+namespace {
+
+[[nodiscard]] VaultKind
+decodeVaultKind(std::optional vaultKind)
+{
+    if (vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded))
+        return VaultKind::ClosedEnded;
+    return VaultKind::OpenEnded;
+}
+
+}  // namespace
+
+[[nodiscard]] VaultKind
+getVaultKind(SLE::const_ref vault)
+{
+    XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultKind : valid Vault sle");
+    return decodeVaultKind(vault->at(~sfVaultKind));
+}
+
+[[nodiscard]] VaultKind
+getVaultKind(STTx const& tx)
+{
+    return decodeVaultKind(tx[~sfVaultKind]);
+}
+
+[[nodiscard]] bool
+isValidVaultKind(STTx const& tx)
+{
+    auto const kindField = tx[~sfVaultKind];
+    if (!kindField)
+        return true;
+    return *kindField == std::to_underlying(VaultKind::OpenEnded) ||
+        *kindField == std::to_underlying(VaultKind::ClosedEnded);
+}
+
+[[nodiscard]] bool
+isValidClosedEndedGap(std::uint32_t sub, std::uint32_t red)
+{
+    auto const s = static_cast(sub);
+    auto const r = static_cast(red);
+    return r >= s + kMinInvestmentPeriod && r < s + kMaxInvestmentPeriod;
+}
+
+[[nodiscard]] VaultPhase
+getVaultPhase(ReadView const& view, SLE::const_ref vault)
+{
+    XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::getVaultPhase : valid Vault sle");
+    return getVaultPhase(
+        view, (*vault)[~sfVaultKind], (*vault)[~sfSubscriptionDate], (*vault)[~sfRedemptionDate]);
+}
+
+[[nodiscard]] VaultPhase
+getVaultPhase(
+    ReadView const& view,
+    std::optional vaultKind,
+    std::optional subscriptionDate,
+    std::optional redemptionDate)
+{
+    if (!vaultKind || *vaultKind != std::to_underlying(VaultKind::ClosedEnded))
+        return VaultPhase::NoPhase;
+
+    // Subscription includes now == SubscriptionDate; Investment starts
+    // strictly after SubscriptionDate.
+    if (!hasExpired(view, subscriptionDate, ExpiryComparison::Exclusive))
+        return VaultPhase::Subscription;
+    if (!hasExpired(view, redemptionDate))
+        return VaultPhase::Investment;
+    return VaultPhase::Redemption;
+}
+
+[[nodiscard]] TER
+checkVaultDomain(
+    ReadView const& view,
+    SLE::const_ref issuance,
+    AccountID const& subject,
+    SuppressExpired suppressExpired)
+{
+    XRPL_ASSERT(
+        issuance && issuance->getType() == ltMPTOKEN_ISSUANCE,
+        "xrpl::checkVaultDomain : valid issuance SLE");
+
+    auto const maybeDomainID = issuance->at(~sfDomainID);
+    if (!maybeDomainID)
+        return tecNO_AUTH;
+
+    auto const err = credentials::validDomain(view, *maybeDomainID, subject);
+    if (err == tecEXPIRED && suppressExpired == SuppressExpired::Yes)
+        return tesSUCCESS;
+
+    return err;
+}
+
 }  // namespace xrpl
diff --git a/src/libxrpl/nodestore/backend/NuDBFactory.cpp b/src/libxrpl/nodestore/backend/NuDBFactory.cpp
index bbf37f3edf..98173858e8 100644
--- a/src/libxrpl/nodestore/backend/NuDBFactory.cpp
+++ b/src/libxrpl/nodestore/backend/NuDBFactory.cpp
@@ -16,8 +16,6 @@
 #include 
 #include 
 
-#include 
-#include 
 #include 
 
 #include 
@@ -36,12 +34,14 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl::node_store {
@@ -131,7 +131,7 @@ public:
     void
     open(bool createIfMissing, uint64_t appType, uint64_t uid, uint64_t salt) override
     {
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
         if (db.is_open())
         {
             // LCOV_EXCL_START
@@ -194,11 +194,12 @@ public:
 
             if (deletePath)
             {
-                boost::filesystem::remove_all(name, ec);
-                if (ec)
+                std::error_code fsec;
+                std::filesystem::remove_all(name, fsec);
+                if (fsec)
                 {
-                    JLOG(j.fatal())
-                        << "Filesystem remove_all of " << name << " failed with: " << ec.message();
+                    JLOG(j.fatal()) << "Filesystem remove_all of " << name
+                                    << " failed with: " << fsec.message();
                 }
             }
         }
@@ -352,7 +353,7 @@ private:
     static std::size_t
     parseBlockSize(std::string const& name, Section const& keyValues, beast::Journal journal)
     {
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
         auto const folder = path(name);
         auto const kp = (folder / "nudb.key").string();
 
diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp
index 4b7a1171fe..6f00b762b2 100644
--- a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp
+++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp
@@ -19,9 +19,6 @@
 #include 
 #include 
 
-#include 
-#include 
-
 #include 
 #include 
 #include 
@@ -37,6 +34,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -262,8 +260,8 @@ public:
             db.reset();
             if (deletePath_)
             {
-                boost::filesystem::path const dir = name;
-                boost::filesystem::remove_all(dir);
+                std::filesystem::path const dir = name;
+                std::filesystem::remove_all(dir);
             }
         }
     }
diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp
index ff4e5aa0ee..bf67defa3b 100644
--- a/src/libxrpl/protocol/BuildInfo.cpp
+++ b/src/libxrpl/protocol/BuildInfo.cpp
@@ -23,7 +23,7 @@ namespace {
 //------------------------------------------------------------------------------
 // clang-format off
 // NOLINTNEXTLINE(readability-identifier-naming)
-char const* const versionString = "3.4.0-b0"
+char const* const versionString = "3.4.0-rc1"
     // clang-format on
     ;
 
diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp
index fe8a08c2ef..ecd4832928 100644
--- a/src/libxrpl/protocol/ConfidentialTransfer.cpp
+++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp
@@ -4,6 +4,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -124,7 +125,12 @@ std::optional
 makeEcPair(Slice const& buffer)
 {
     if (buffer.length() != 2 * kEcCiphertextComponentLength)
-        return std::nullopt;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::makeEcPair : callers must pre-validate ciphertext length");
+        return std::nullopt;
+        // LCOV_EXCL_STOP
+    }
 
     auto parsePubKey = [](Slice const& slice, secp256k1_pubkey& out) {
         return secp256k1_ec_pubkey_parse(secp256k1Context(), &out, slice.data(), slice.length());
@@ -266,7 +272,13 @@ std::optional
 encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId)
 {
     if (pubKeySlice.size() != kEcPubKeyLength)
-        return std::nullopt;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::encryptCanonicalZeroAmount : callers must pre-validate public key length");
+        return std::nullopt;
+        // LCOV_EXCL_STOP
+    }
 
     EcPair pair{};
     secp256k1_pubkey pubKey;
@@ -274,14 +286,24 @@ encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, M
             secp256k1Context(), &pubKey, pubKeySlice.data(), kEcPubKeyLength);
         res != 1)
     {
-        return std::nullopt;  // LCOV_EXCL_LINE
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::encryptCanonicalZeroAmount : public key read from the ledger must already be "
+            "valid");
+        return std::nullopt;
+        // LCOV_EXCL_STOP
     }
 
     if (auto res = generate_canonical_encrypted_zero(
             secp256k1Context(), &pair.c1, &pair.c2, &pubKey, account.data(), mptId.data());
         res != 1)
     {
-        return std::nullopt;  // LCOV_EXCL_LINE
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::encryptCanonicalZeroAmount : canonical zero generation cannot fail for a "
+            "valid public key");
+        return std::nullopt;
+        // LCOV_EXCL_STOP
     }
 
     return serializeEcPair(pair);
@@ -301,7 +323,11 @@ verifyRevealedAmount(
         issuer.publicKey.size() != kEcPubKeyLength ||
         issuer.encryptedAmount.size() != kEcGamalEncryptedTotalLength)
     {
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::verifyRevealedAmount : callers must pre-validate holder/issuer field lengths");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
     }
 
     auto const holderP = toParticipant(holder);
@@ -313,7 +339,11 @@ verifyRevealedAmount(
         if (auditor->publicKey.size() != kEcPubKeyLength ||
             auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength)
         {
-            return tecINTERNAL;  // LCOV_EXCL_LINE
+            // LCOV_EXCL_START
+            UNREACHABLE(
+                "xrpl::verifyRevealedAmount : callers must pre-validate auditor field lengths");
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
         }
         auditorP = toParticipant(*auditor);
         auditorPtr = &auditorP;
@@ -337,7 +367,12 @@ checkEncryptedAmountFormat(STObject const& object)
     if (!object.isFieldPresent(sfHolderEncryptedAmount) ||
         !object.isFieldPresent(sfIssuerEncryptedAmount))
     {
-        return temMALFORMED;  // LCOV_EXCL_LINE
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::checkEncryptedAmountFormat : callers already enforce that these fields are "
+            "present");
+        return temMALFORMED;
+        // LCOV_EXCL_STOP
     }
 
     if (object[sfHolderEncryptedAmount].length() != kEcGamalEncryptedTotalLength ||
@@ -366,7 +401,12 @@ TER
 verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash)
 {
     if (proofSlice.size() != kEcSchnorrProofLength || pubKeySlice.size() != kEcPubKeyLength)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::verifySchnorrProof : callers must pre-validate proof/public key length");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     if (mpt_verify_convert_proof(proofSlice.data(), pubKeySlice.data(), contextHash.data()) != 0)
         return tecBAD_PROOF;
@@ -385,7 +425,12 @@ verifyClawbackProof(
     if (ciphertext.size() != kEcGamalEncryptedTotalLength ||
         pubKeySlice.size() != kEcPubKeyLength || proof.size() != kEcClawbackProofLength)
     {
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::verifyClawbackProof : callers must pre-validate ciphertext/public "
+            "key/proof length");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
     }
 
     if (mpt_verify_clawback_proof(
@@ -420,7 +465,12 @@ verifySendProof(
         amountCommitment.size() != kEcPedersenCommitmentLength ||
         balanceCommitment.size() != kEcPedersenCommitmentLength)
     {
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::verifySendProof : callers must pre-validate proof/participant/commitment "
+            "lengths");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
     }
 
     std::vector participants;
@@ -433,12 +483,22 @@ verifySendProof(
         if (auditor->publicKey.size() != kEcPubKeyLength ||
             auditor->encryptedAmount.size() != kEcGamalEncryptedTotalLength)
         {
-            return tecINTERNAL;  // LCOV_EXCL_LINE
+            // LCOV_EXCL_START
+            UNREACHABLE("xrpl::verifySendProof : callers must pre-validate auditor field lengths");
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
         }
         participants.push_back(toParticipant(*auditor));
     }
     if (participants.size() != recipientCount)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::verifySendProof : participant count must match the requested recipient "
+            "count");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     if (mpt_verify_send_proof(
             proof.data(),
@@ -468,7 +528,12 @@ verifyConvertBackProof(
         spendingBalance.size() != kEcGamalEncryptedTotalLength ||
         balanceCommitment.size() != kEcPedersenCommitmentLength)
     {
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::verifyConvertBackProof : callers must pre-validate proof/public "
+            "key/balance/commitment lengths");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
     }
 
     if (mpt_verify_convert_back_proof(
diff --git a/src/libxrpl/protocol/Emitable.cpp b/src/libxrpl/protocol/Emitable.cpp
index 06bf6feb7f..6cd189ab31 100644
--- a/src/libxrpl/protocol/Emitable.cpp
+++ b/src/libxrpl/protocol/Emitable.cpp
@@ -1,5 +1,6 @@
-#include 
 #include 
+
+#include 
 #include 
 #include 
 #include 
@@ -9,15 +10,20 @@ namespace xrpl {
 Emitable::Emitable()
 {
     emitableTx_ = {
+#pragma push_macro("UNWRAP")
+#undef UNWRAP
 #pragma push_macro("TRANSACTION")
 #undef TRANSACTION
 
-#define TRANSACTION(tag, value, name, delegatable, amendment, permissions, emitable, fields) \
-    {value, emitable},
+#define UNWRAP(...) __VA_ARGS__
+#define TRANSACTION(tag, value, name, settings, ...) \
+    {value, (TxSettings UNWRAP settings).emittance},
 #include 
 
 #undef TRANSACTION
 #pragma pop_macro("TRANSACTION")
+#undef UNWRAP
+#pragma pop_macro("UNWRAP")
     };
 
     granularEmitableMap_ = {
@@ -146,7 +152,7 @@ Emitable::isEmitable(std::uint32_t const& emitableValue) const
     //         return false;
     // }
 
-    if (it != emitableTx_.end() && it->second == Emittance::notEmitable)
+    if (it != emitableTx_.end() && it->second == Emittance::NotEmitable)
         return false;
 
     return true;
diff --git a/src/libxrpl/protocol/ErrorCodes.cpp b/src/libxrpl/protocol/ErrorCodes.cpp
index e81f975844..802bae100d 100644
--- a/src/libxrpl/protocol/ErrorCodes.cpp
+++ b/src/libxrpl/protocol/ErrorCodes.cpp
@@ -105,10 +105,9 @@ static constexpr ErrorInfo kUnorderedErrorInfos[]{
 };
 // clang-format on
 
-// Sort and validate unorderedErrorInfos at compile time.  Should be
-// converted to consteval when get to C++20.
+// Sort and validate unorderedErrorInfos at compile time.
 template 
-constexpr auto
+consteval auto
 sortErrorInfos(ErrorInfo const (&unordered)[N]) -> std::array
 {
     std::array ret = {};
diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp
index f8c8854111..81a1c58977 100644
--- a/src/libxrpl/protocol/Indexes.cpp
+++ b/src/libxrpl/protocol/Indexes.cpp
@@ -126,6 +126,10 @@ getBookBase(Book const& book)
 {
     XRPL_ASSERT(isConsistent(book), "xrpl::getBookBase : input is consistent");
 
+    constexpr std::uint8_t kIssueToMPTTag = 0x01;
+    constexpr std::uint8_t kMPTToIssueTag = 0x02;
+    constexpr std::uint8_t kMPTToMPTTag = 0x03;
+
     auto getIndexHash = [&book](Args... args) {
         if (book.domain)
             return indexHash(std::forward(args)..., *book.domain);
@@ -139,19 +143,36 @@ getBookBase(Book const& book)
                 return getIndexHash(
                     LedgerNameSpace::BookDir, in.currency, out.currency, in.account, out.account);
             }
+            // The three MPT-involving branches are new under MPTokensV2 and
+            // each gets a 1-byte discriminator to prevent preimage collisions
+            // between branches: the (Issue,MPT) and (MPT,Issue) preimages
+            // are both 64 bytes of raw concatenation, so without a
+            // per-branch tag chosen Currency / MPTID / AccountID values can
+            // align byte-for-byte and produce the same BookDir keylet for
+            // two distinct markets. (Issue,Issue) is left untagged to
+            // preserve existing mainnet order-book keylets.
             else if constexpr (std::is_same_v && std::is_same_v)
             {
                 return getIndexHash(
-                    LedgerNameSpace::BookDir, in.currency, out.getMptID(), in.account);
+                    LedgerNameSpace::BookDir,
+                    kIssueToMPTTag,
+                    in.currency,
+                    out.getMptID(),
+                    in.account);
             }
             else if constexpr (std::is_same_v && std::is_same_v)
             {
                 return getIndexHash(
-                    LedgerNameSpace::BookDir, in.getMptID(), out.currency, out.account);
+                    LedgerNameSpace::BookDir,
+                    kMPTToIssueTag,
+                    in.getMptID(),
+                    out.currency,
+                    out.account);
             }
             else
             {
-                return getIndexHash(LedgerNameSpace::BookDir, in.getMptID(), out.getMptID());
+                return getIndexHash(
+                    LedgerNameSpace::BookDir, kMPTToMPTTag, in.getMptID(), out.getMptID());
             }
         },
         book.in.value(),
diff --git a/src/libxrpl/protocol/NFTSyntheticSerializer.cpp b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp
deleted file mode 100644
index fd44ae1f33..0000000000
--- a/src/libxrpl/protocol/NFTSyntheticSerializer.cpp
+++ /dev/null
@@ -1,24 +0,0 @@
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-namespace xrpl::rpc {
-
-void
-insertNFTSyntheticInJson(
-    json::Value& response,
-    std::shared_ptr const& transaction,
-    TxMeta const& transactionMeta)
-{
-    insertNFTokenID(response[jss::meta], transaction, transactionMeta);
-    insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta);
-}
-
-}  // namespace xrpl::rpc
diff --git a/src/libxrpl/protocol/Permissions.cpp b/src/libxrpl/protocol/Permissions.cpp
index 2f3e25f823..a5adb294e9 100644
--- a/src/libxrpl/protocol/Permissions.cpp
+++ b/src/libxrpl/protocol/Permissions.cpp
@@ -10,6 +10,7 @@
 #include 
 #include   // IWYU pragma: keep
 #include 
+#include 
 
 #include 
 #include 
@@ -40,16 +41,24 @@ Permission::GranularPermissionEntry::GranularPermissionEntry(
 Permission::Permission()
 {
     {
+#pragma push_macro("UNWRAP")
+#undef UNWRAP
 #pragma push_macro("TRANSACTION")
 #undef TRANSACTION
 
-#define TRANSACTION(tag, value, name, delegable, amendment, ...) \
-    txDelegationMap_[static_cast(value)] = {amendment, delegable};
+#define UNWRAP(...) __VA_ARGS__
+#define TRANSACTION(tag, value, name, settings, ...)                               \
+    {                                                                              \
+        TxSettings const s = UNWRAP settings;                                      \
+        txDelegationMap_[static_cast(value)] = {s.amendment, s.delegable}; \
+    }
 
 #include 
 
 #undef TRANSACTION
 #pragma pop_macro("TRANSACTION")
+#undef UNWRAP
+#pragma pop_macro("UNWRAP")
     }
 
     granularPermissionsByName_ = {
@@ -242,7 +251,7 @@ Permission::isDelegable(std::uint32_t permissionValue, Rules const& rules) const
 
     // Tx-level permissions require the transaction type itself to be delegable, and
     // the corresponding amendment enabled.
-    return txIt != txDelegationMap_.end() && txIt->second.delegable != NotDelegable &&
+    return txIt != txDelegationMap_.end() && txIt->second.delegable != Delegation::NotDelegable &&
         amendmentEnabled(txIt->second);
 }
 
diff --git a/src/libxrpl/protocol/QualityFunction.cpp b/src/libxrpl/protocol/QualityFunction.cpp
index e862770406..ffe583b7e1 100644
--- a/src/libxrpl/protocol/QualityFunction.cpp
+++ b/src/libxrpl/protocol/QualityFunction.cpp
@@ -38,7 +38,22 @@ QualityFunction::outFromAvgQ(Quality const& quality)
             return std::nullopt;
         return out;
     }
-    return std::nullopt;
+    // The sole caller (StrandFlow::limitOut) only invokes this on a non-const
+    // quality function, so m_ != 0 here, and a real payment/offer never yields
+    // a zero-rate limit quality (it would divide by zero above). This fallback
+    // is therefore unreachable in practice.
+    return std::nullopt;  // LCOV_EXCL_LINE
+}
+
+bool
+QualityFunction::satisfiesAvgQ(Quality const& quality, Number const& out) const
+{
+    // satisfiesAvgQ is only reached from StrandFlow::limitOut *after*
+    // outFromAvgQ returned a value, which requires a non-zero rate. So a
+    // zero-rate quality never reaches here; this guard is defensive.
+    if (quality.rate() == beast::kZero)
+        return false;  // LCOV_EXCL_LINE
+    return m_ * out + b_ >= 1 / quality.rate();
 }
 
 }  // namespace xrpl
diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp
index 197139027a..cb71133d8f 100644
--- a/src/libxrpl/protocol/Rules.cpp
+++ b/src/libxrpl/protocol/Rules.cpp
@@ -193,12 +193,6 @@ Rules::operator==(Rules const& other) const
     return *impl_ == *other.impl_;
 }
 
-bool
-Rules::operator!=(Rules const& other) const
-{
-    return !(*this == other);
-}
-
 bool
 isFeatureEnabled(uint256 const& feature, bool resultIfNoRules)
 {
diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp
index 212c34322b..83b2983756 100644
--- a/src/libxrpl/protocol/STAmount.cpp
+++ b/src/libxrpl/protocol/STAmount.cpp
@@ -1445,6 +1445,59 @@ public:
     operator=(DontAffectNumberRoundMode const&) = delete;
 };
 
+Number::RoundingMode
+roundMode(bool const resultNegative, bool const roundUp)
+{
+    using enum Number::RoundingMode;
+    // STAmount roundUp means "away from zero". The legacy scaled-mantissa
+    // multiply and divide paths reach that result with slightly different
+    // mechanics, including a final TowardsZero materialization in multiply.
+    //
+    // The MPT/V2 Number path already performs the operation under the directed
+    // mode below. Use the same mode again when converting back to STAmount so a
+    // fractional integral result stays consistently rounded after Number
+    // arithmetic, independent of whether the operation was multiply or divide.
+    return roundUp ^ resultNegative ? Upward : Downward;
+}
+
+STAmount
+roundNumberResult(
+    Asset const& asset,
+    bool const resultNegative,
+    bool const roundUp,
+    Number const& number)
+{
+    // MPT/V2 Number arithmetic uses directed rounding both for the operation
+    // and for materializing the final integral amount.
+    NumberRoundModeGuard const finalRound(roundMode(resultNegative, roundUp));
+    auto result = STAmount{asset, number};
+    [[maybe_unused]] bool const nonzeroPositiveRoundUp =
+        roundUp && !resultNegative && number != beast::kZero;
+    ALWAYS(
+        !nonzeroPositiveRoundUp || result != beast::kZero,
+        "xrpl::roundNumberResult : positive rounded-up MPT result is representable");
+
+    if (roundUp && !resultNegative && !result)
+    {
+        // Intended to preserve existing mulRound/divRound behavior for a
+        // positive result too small to represent in the target asset.
+        //
+        // Unreachable in practice: when roundUp is set, roundMode() above
+        // selects Upward, and materializing a Number into an STAmount honors
+        // that mode (Number::operator rep()), so any positive value rounds up
+        // to at least the smallest representable unit. Hence, a positive result
+        // is never !result here; the only zero case is a zero operand, which
+        // the mulRound/divRound callers handle before reaching this function.
+        // LCOV_EXCL_START
+        if (asset.integral())
+            return STAmount{asset, 1};
+        return STAmount{asset, STAmount::kMinValue, STAmount::kMinOffset, false};
+        // LCOV_EXCL_STOP
+    }
+
+    return result;
+}
+
 }  // anonymous namespace
 
 // Pass the canonicalizeRound function pointer as a template parameter.
@@ -1486,6 +1539,22 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
         return STAmount(asset, minV * maxV);
     }
 
+    bool const resultNegative = v1.negative() != v2.negative();
+
+    if (asset.holds() && isFeatureEnabled(featureMPTokensV2, false))
+    {
+        // MPT DEX can combine 63-bit MPT amounts with IOU-shaped transfer
+        // rates. Use Number arithmetic under MPTokensV2 so the rounded
+        // operation is not limited by the legacy uint64_t scaled mantissa.
+        Number result;
+        {
+            NumberRoundModeGuard const operationRound(roundMode(resultNegative, roundUp));
+            result = Number{v1} * Number{v2};
+        }
+
+        return roundNumberResult(asset, resultNegative, roundUp, result);
+    }
+
     std::uint64_t value1 = v1.mantissa(), value2 = v2.mantissa();
     int offset1 = v1.exponent(), offset2 = v2.exponent();
 
@@ -1506,9 +1575,6 @@ mulRoundImpl(STAmount const& v1, STAmount const& v2, Asset const& asset, bool ro
             --offset2;
         }
     }
-
-    bool const resultNegative = v1.negative() != v2.negative();
-
     // We multiply the two mantissas (each is between 10^15
     // and 10^16), so their product is in the 10^30 to 10^32
     // range. Dividing their product by 10^14 maintains the
@@ -1575,6 +1641,22 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
     if (num == beast::kZero)
         return {asset};
 
+    bool const resultNegative = (num.negative() != den.negative());
+
+    if (asset.holds() && isFeatureEnabled(featureMPTokensV2, false))
+    {
+        // Match the multiply path above: Number performs the rounded
+        // operation, then STAmount materializes the final MPT amount using the
+        // same final rounding mode as the legacy path below.
+        Number result;
+        {
+            NumberRoundModeGuard const operationRound(roundMode(resultNegative, roundUp));
+            result = Number{num} / Number{den};
+        }
+
+        return roundNumberResult(asset, resultNegative, roundUp, result);
+    }
+
     std::uint64_t numVal = num.mantissa(), denVal = den.mantissa();
     int numOffset = num.exponent(), denOffset = den.exponent();
 
@@ -1596,8 +1678,6 @@ divRoundImpl(STAmount const& num, STAmount const& den, Asset const& asset, bool
         }
     }
 
-    bool const resultNegative = (num.negative() != den.negative());
-
     // We divide the two mantissas (each is between 10^15
     // and 10^16). To maintain precision, we multiply the
     // numerator by 10^17 (the product is in the range of
diff --git a/src/libxrpl/protocol/STBase.cpp b/src/libxrpl/protocol/STBase.cpp
index f029f10e75..1e56897e30 100644
--- a/src/libxrpl/protocol/STBase.cpp
+++ b/src/libxrpl/protocol/STBase.cpp
@@ -38,12 +38,6 @@ STBase::operator==(STBase const& t) const
     return (getSType() == t.getSType()) && isEquivalent(t);
 }
 
-bool
-STBase::operator!=(STBase const& t) const
-{
-    return (getSType() != t.getSType()) || !isEquivalent(t);
-}
-
 STBase*
 STBase::copy(std::size_t n, void* buf) const
 {
diff --git a/src/libxrpl/protocol/STLedgerEntry.cpp b/src/libxrpl/protocol/STLedgerEntry.cpp
index 8c5c5b5eae..9ee8d030ff 100644
--- a/src/libxrpl/protocol/STLedgerEntry.cpp
+++ b/src/libxrpl/protocol/STLedgerEntry.cpp
@@ -18,12 +18,11 @@
 #include 
 #include 
 
-#include 
-
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -111,7 +110,7 @@ STLedgerEntry::getSType() const
 std::string
 STLedgerEntry::getText() const
 {
-    return str(boost::format("{ %s, %s }") % to_string(key_) % STObject::getText());
+    return std::format("{{ {}, {} }}", to_string(key_), STObject::getText());
 }
 
 json::Value
diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp
index 7f1e19ea12..3db6a3dc6c 100644
--- a/src/libxrpl/protocol/STTx.cpp
+++ b/src/libxrpl/protocol/STTx.cpp
@@ -33,13 +33,13 @@
 #include 
 
 #include 
-#include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -168,10 +168,10 @@ STTx::getMentionedAccounts() const
 }
 
 static Blob
-getSigningData(STTx const& that)
+getSigningData(STTx const& that, HashPrefix prefix)
 {
     Serializer s;
-    s.add32(HashPrefix::TxSign);
+    s.add32(prefix);
     that.addWithoutSigningFields(s);
     return s.getData();
 }
@@ -212,30 +212,42 @@ STTx::getSeqProxy() const
     return SeqProxy::rawTicket(*ticketSeq);
 }
 
+void
+STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
+{
+    // The account's own signature always covers the plain transaction prefix;
+    // see signingPrefix for the role signatures that do not.
+    auto const data = getSigningData(*this, HashPrefix::TxSign);
+
+    setFieldVL(sfTxnSignature, xrpl::sign(publicKey, secretKey, makeSlice(data)));
+    tid_ = getHash(HashPrefix::TransactionId);
+}
+
 void
 STTx::sign(
     PublicKey const& publicKey,
     SecretKey const& secretKey,
-    std::optional> signatureTarget)
+    SignatureRole role,
+    Rules const& rules)
 {
-    auto const data = getSigningData(*this);
+    auto const data = getSigningData(*this, signingPrefix(role, false, rules));
 
     auto const sig = xrpl::sign(publicKey, secretKey, makeSlice(data));
 
-    if (signatureTarget)
+    if (auto const target = signatureField(role))
     {
-        auto& target = peekFieldObject(*signatureTarget);
-        target.setFieldVL(sfTxnSignature, sig);
+        peekFieldObject(*target).setFieldVL(sfTxnSignature, sig);
     }
     else
     {
         setFieldVL(sfTxnSignature, sig);
     }
+
     tid_ = getHash(HashPrefix::TransactionId);
 }
 
 std::expected
-STTx::checkSign(Rules const& rules, STObject const& sigObject) const
+STTx::checkSign(Rules const& rules, STObject const& sigObject, SignatureRole role) const
 {
     try
     {
@@ -244,8 +256,10 @@ STTx::checkSign(Rules const& rules, STObject const& sigObject) const
         // multi-signing.  Otherwise we're single-signing.
 
         Blob const& signingPubKey = sigObject.getFieldVL(sfSigningPubKey);
-        return signingPubKey.empty() ? checkMultiSign(rules, sigObject)
-                                     : checkSingleSign(sigObject);
+        bool const multiSigning = signingPubKey.empty();
+        auto const prefix = signingPrefix(role, multiSigning, rules);
+        return multiSigning ? checkMultiSign(sigObject, prefix)
+                            : checkSingleSign(sigObject, prefix);
     }
     catch (...)
     {
@@ -256,20 +270,20 @@ STTx::checkSign(Rules const& rules, STObject const& sigObject) const
 std::expected
 STTx::checkSign(Rules const& rules) const
 {
-    if (auto const ret = checkSign(rules, *this); !ret)
+    if (auto const ret = checkSign(rules, *this, SignatureRole::Transaction); !ret)
         return ret;
 
     if (isFieldPresent(sfCounterpartySignature))
     {
         auto const counterSig = getFieldObject(sfCounterpartySignature);
-        if (auto const ret = checkSign(rules, counterSig); !ret)
+        if (auto const ret = checkSign(rules, counterSig, SignatureRole::Counterparty); !ret)
             return std::unexpected("Counterparty: " + ret.error());
     }
 
     if (isFieldPresent(sfSponsorSignature))
     {
         auto const sponsorSignatureObj = getFieldObject(sfSponsorSignature);
-        if (auto const ret = checkSign(rules, sponsorSignatureObj); !ret)
+        if (auto const ret = checkSign(rules, sponsorSignatureObj, SignatureRole::Sponsor); !ret)
             return std::unexpected("Sponsor: " + ret.error());
     }
 
@@ -277,14 +291,14 @@ STTx::checkSign(Rules const& rules) const
     // of signature checking.
     if (isFieldPresent(sfBatchSigners))
     {
-        if (auto const ret = checkBatchSign(rules); !ret)
+        if (auto const ret = checkBatchSign(); !ret)
             return ret;
     }
     return {};
 }
 
 std::expected
-STTx::checkBatchSign(Rules const& rules) const
+STTx::checkBatchSign() const
 {
     try
     {
@@ -318,7 +332,7 @@ STTx::checkBatchSign(Rules const& rules) const
         for (auto const& signer : signers)
         {
             Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey);
-            auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, rules, txIds)
+            auto const result = signingPubKey.empty() ? checkBatchMultiSign(signer, txIds)
                                                       : checkBatchSingleSign(signer, txIds);
 
             if (!result)
@@ -399,16 +413,21 @@ STTx::getMetaSQL(
     TxnSql status,
     std::string const& escapedMetaData) const
 {
-    static boost::format const kBfTrans("('%s', '%s', '%s', '%d', '%d', '%c', %s, %s)");
     std::string rTxn = sqlBlobLiteral(rawTxn.peekData());
 
     auto format = TxFormats::getInstance().findByType(txType_);
     XRPL_ASSERT(format, "xrpl::STTx::getMetaSQL : non-null type format");
 
-    return str(
-        boost::format(kBfTrans) % to_string(getTransactionID()) % format->getName() %
-        toBase58(getAccountID(sfAccount)) % getFieldU32(sfSequence) % inLedger %
-        safeCast(status) % rTxn % escapedMetaData);
+    return std::format(
+        "('{}', '{}', '{}', '{}', '{}', '{}', {}, {})",
+        to_string(getTransactionID()),
+        format->getName(),
+        toBase58(getAccountID(sfAccount)),
+        getFieldU32(sfSequence),
+        inLedger,
+        safeCast(status),
+        rTxn,
+        escapedMetaData);
 }
 
 static std::expected
@@ -442,9 +461,9 @@ singleSignHelper(STObject const& sigObject, Slice const& data)
 }
 
 std::expected
-STTx::checkSingleSign(STObject const& sigObject) const
+STTx::checkSingleSign(STObject const& sigObject, HashPrefix prefix) const
 {
-    auto const data = getSigningData(*this);
+    auto const data = getSigningData(*this, prefix);
     return singleSignHelper(sigObject, makeSlice(data));
 }
 
@@ -462,8 +481,7 @@ std::expected
 multiSignHelper(
     STObject const& sigObject,
     std::optional txnAccountID,
-    std::function makeMsg,
-    Rules const& rules)
+    std::function makeMsg)
 {
     // Make sure the MultiSigners are present.  Otherwise they are not
     // attempting multi-signing and we just have a bad SigningPubKey.
@@ -536,10 +554,7 @@ multiSignHelper(
 }
 
 std::expected
-STTx::checkBatchMultiSign(
-    STObject const& batchSigner,
-    Rules const& rules,
-    std::vector const& txIds) const
+STTx::checkBatchMultiSign(STObject const& batchSigner, std::vector const& txIds) const
 {
     XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::checkBatchMultiSign : batch transaction");
     // We can ease the computational load inside the loop a bit by
@@ -550,18 +565,15 @@ STTx::checkBatchMultiSign(
     serializeBatch(dataStart, getAccountID(sfAccount), getSeqProxy().value(), getFlags(), txIds);
     dataStart.addBitString(batchSignerAccount);
     return multiSignHelper(
-        batchSigner,
-        batchSignerAccount,
-        [&dataStart](AccountID const& accountID) -> Serializer {
+        batchSigner, batchSignerAccount, [&dataStart](AccountID const& accountID) -> Serializer {
             Serializer s = dataStart;
             finishMultiSigningData(accountID, s);
             return s;
-        },
-        rules);
+        });
 }
 
 std::expected
-STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
+STTx::checkMultiSign(STObject const& sigObject, HashPrefix prefix) const
 {
     // Used inside the loop in multiSignHelper to enforce that
     // the account owner may not multisign for themselves.
@@ -573,16 +585,13 @@ STTx::checkMultiSign(Rules const& rules, STObject const& sigObject) const
     // We can ease the computational load inside the loop a bit by
     // pre-constructing part of the data that we hash.  Fill a Serializer
     // with the stuff that stays constant from signature to signature.
-    Serializer dataStart = startMultiSigningData(*this);
+    Serializer dataStart = startMultiSigningData(*this, prefix);
     return multiSignHelper(
-        sigObject,
-        txnAccountID,
-        [&dataStart](AccountID const& accountID) -> Serializer {
+        sigObject, txnAccountID, [&dataStart](AccountID const& accountID) -> Serializer {
             Serializer s = dataStart;
             finishMultiSigningData(accountID, s);
             return s;
-        },
-        rules);
+        });
 }
 
 void
diff --git a/src/libxrpl/protocol/STXChainBridge.cpp b/src/libxrpl/protocol/STXChainBridge.cpp
index 005c9ccbce..f9f1fd1dcc 100644
--- a/src/libxrpl/protocol/STXChainBridge.cpp
+++ b/src/libxrpl/protocol/STXChainBridge.cpp
@@ -11,9 +11,8 @@
 #include 
 #include 
 
-#include 
-
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -141,10 +140,15 @@ STXChainBridge::getJson(JsonOptions jo) const
 std::string
 STXChainBridge::getText() const
 {
-    return str(
-        boost::format("{ %s = %s, %s = %s, %s = %s, %s = %s }") % sfLockingChainDoor.getName() %
-        lockingChainDoor_.getText() % sfLockingChainIssue.getName() % lockingChainIssue_.getText() %
-        sfIssuingChainDoor.getName() % issuingChainDoor_.getText() % sfIssuingChainIssue.getName() %
+    return std::format(
+        "{{ {} = {}, {} = {}, {} = {}, {} = {} }}",
+        sfLockingChainDoor.getName(),
+        lockingChainDoor_.getText(),
+        sfLockingChainIssue.getName(),
+        lockingChainIssue_.getText(),
+        sfIssuingChainDoor.getName(),
+        issuingChainDoor_.getText(),
+        sfIssuingChainIssue.getName(),
         issuingChainIssue_.getText());
 }
 
diff --git a/src/libxrpl/protocol/Sign.cpp b/src/libxrpl/protocol/Sign.cpp
index 9e7ef7f999..ce3dabde33 100644
--- a/src/libxrpl/protocol/Sign.cpp
+++ b/src/libxrpl/protocol/Sign.cpp
@@ -1,17 +1,77 @@
 #include 
 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
+#include 
+
 namespace xrpl {
 
+SField const*
+signatureField(SignatureRole role)
+{
+    switch (role)
+    {
+        case SignatureRole::Transaction:
+            return nullptr;
+        case SignatureRole::Counterparty:
+            return &sfCounterpartySignature;
+        case SignatureRole::Sponsor:
+            return &sfSponsorSignature;
+    }
+    UNREACHABLE("xrpl::signatureField : unknown SignatureRole");
+    return nullptr;
+}
+
+std::optional
+signatureRole(SField const& sigField)
+{
+    if (sigField == sfCounterpartySignature)
+        return SignatureRole::Counterparty;
+    if (sigField == sfSponsorSignature)
+        return SignatureRole::Sponsor;
+    return std::nullopt;
+}
+
+// Signature validity depends on fixCleanup3_4_0: a role signature covers
+// different bytes before and after the amendment activates. checkValidity
+// caches its verdict per transaction ID, so it keeps two separate cache slots
+// for role-signature transactions (kSfSiggoodOldPrefix / kSfSigbadOldPrefix in
+// tx/apply.cpp) to keep a pre-fix verdict from being reused in the post-fix
+// era, and vice versa. See the block comment in tx/apply.cpp for the details
+// and the reason both directions matter.
+HashPrefix
+signingPrefix(SignatureRole role, bool multiSigning, Rules const& rules)
+{
+    // Before fixCleanup3_4_0 every signature on a transaction covered the same
+    // bytes, so a signature could be moved from one role to another.
+    if (!rules.enabled(fixCleanup3_4_0))
+        return multiSigning ? HashPrefix::TxMultiSign : HashPrefix::TxSign;
+
+    switch (role)
+    {
+        case SignatureRole::Transaction:
+            return multiSigning ? HashPrefix::TxMultiSign : HashPrefix::TxSign;
+        case SignatureRole::Counterparty:
+            return multiSigning ? HashPrefix::CounterpartyTxMultiSign
+                                : HashPrefix::CounterpartyTxSign;
+        case SignatureRole::Sponsor:
+            return multiSigning ? HashPrefix::SponsorTxMultiSign : HashPrefix::SponsorTxSign;
+    }
+    UNREACHABLE("xrpl::signingPrefix : unknown SignatureRole");
+    return multiSigning ? HashPrefix::TxMultiSign : HashPrefix::TxSign;
+}
+
 void
 sign(
     STObject& st,
@@ -70,18 +130,18 @@ verify(STObject const& st, HashPrefix const& prefix, PublicKey const& pk, SF_VL
 // So, if we support multiple levels of signing, then we'll need to
 // incorporate the "signing for" accounts into the signing data as well.
 Serializer
-buildMultiSigningData(STObject const& obj, AccountID const& signingID)
+buildMultiSigningData(STObject const& obj, AccountID const& signingID, HashPrefix prefix)
 {
-    Serializer s{startMultiSigningData(obj)};
+    Serializer s{startMultiSigningData(obj, prefix)};
     finishMultiSigningData(signingID, s);
     return s;
 }
 
 Serializer
-startMultiSigningData(STObject const& obj)
+startMultiSigningData(STObject const& obj, HashPrefix prefix)
 {
     Serializer s;
-    s.add32(HashPrefix::TxMultiSign);
+    s.add32(prefix);
     obj.addWithoutSigningFields(s);
     return s;
 }
diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp
index b635fd5dc1..04dd4d009a 100644
--- a/src/libxrpl/protocol/TER.cpp
+++ b/src/libxrpl/protocol/TER.cpp
@@ -137,7 +137,7 @@ transResults()
         MAKE_ERROR(tefNO_DST_PARTIAL,              "Partial payment to create account not allowed."),
         MAKE_ERROR(tefBAD_PATH_COUNT,              "Malformed: Too many paths."),
         MAKE_ERROR(tefNO_BYTECODE,                 "There is no WASM code to run, but a WASM-specific field was included."),
-        MAKE_ERROR(tefBYTECODE_NOT_INCLUDED,       "WASM code requires a field to be included that was not included."),
+        MAKE_ERROR(tefBYTECODE_NOT_INCLUDED,       "WASM code requires a field that was not included."),
 
         MAKE_ERROR(telLOCAL_ERROR,            "Local failure."),
         MAKE_ERROR(telBAD_DOMAIN,             "Domain too long."),
@@ -209,8 +209,7 @@ transResults()
         MAKE_ERROR(temBAD_TRANSFER_FEE,          "Malformed: Transfer fee is outside valid range."),
         MAKE_ERROR(temINVALID_INNER_BATCH,       "Malformed: Invalid inner batch transaction."),
         MAKE_ERROR(temBAD_CIPHERTEXT,            "Malformed: Invalid ciphertext."),
-        MAKE_ERROR(temBAD_WASM,                  "Malformed: Provided WASM code is invalid."),
-        MAKE_ERROR(temINVALID_BYTECODE,          "Malformed: Provided WASM code is invalid."),
+        MAKE_ERROR(temINVALID_BYTECODE,          "Malformed: Provided byte code is invalid."),
         MAKE_ERROR(temTEMP_DISABLED,             "The transaction requires logic that is currently temporarily disabled."),
 
         MAKE_ERROR(terRETRY,                  "Retry transaction."),
diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp
index 555aa38ed6..c393c606fe 100644
--- a/src/libxrpl/protocol/TxFormats.cpp
+++ b/src/libxrpl/protocol/TxFormats.cpp
@@ -45,7 +45,7 @@ TxFormats::TxFormats()
 #undef TRANSACTION
 
 #define UNWRAP(...) __VA_ARGS__
-#define TRANSACTION(tag, value, name, delegatable, amendment, privileges, emitable, fields) \
+#define TRANSACTION(tag, value, name, settings, fields) \
     add(jss::name, tag, UNWRAP fields, getCommonFields());
 
 #include 
diff --git a/src/libxrpl/rdb/SociDB.cpp b/src/libxrpl/rdb/SociDB.cpp
index 2c3fb1bde1..84006acbe7 100644
--- a/src/libxrpl/rdb/SociDB.cpp
+++ b/src/libxrpl/rdb/SociDB.cpp
@@ -5,13 +5,11 @@
 #include 
 #include 
 
-#include 
-#include 
-
 #include 
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -45,8 +43,8 @@ getSociSqliteInit(std::string const& name, std::string const& dir, std::string c
         Throw(
             "Sqlite databases must specify a dir and a name. Name: " + name + " Dir: " + dir);
     }
-    boost::filesystem::path file(dir);
-    if (is_directory(file))
+    std::filesystem::path file(dir);
+    if (std::filesystem::is_directory(file))
         file /= name + ext;
     return file.string();
 }
diff --git a/src/libxrpl/server/Manifest.cpp b/src/libxrpl/server/Manifest.cpp
index 0760196a3b..c85c8445f0 100644
--- a/src/libxrpl/server/Manifest.cpp
+++ b/src/libxrpl/server/Manifest.cpp
@@ -23,8 +23,6 @@
 #include 
 #include 
 
-#include 
-
 #include 
 #include 
 #include 
@@ -277,7 +275,7 @@ loadValidatorToken(std::vector const& blob, beast::Journal journal)
                 [](std::size_t init, std::string const& s) { return init + s.size(); }));
 
         for (auto const& line : blob)
-            tokenStr += boost::algorithm::trim_copy(line);
+            tokenStr += trimWhitespace(line);
 
         tokenStr = base64Decode(tokenStr);
 
@@ -653,7 +651,7 @@ ManifestCache::load(
                 [](std::size_t init, std::string const& s) { return init + s.size(); }));
 
         for (auto const& line : configRevocation)
-            revocationStr += boost::algorithm::trim_copy(line);
+            revocationStr += trimWhitespace(line);
 
         auto mo = deserializeManifest(base64Decode(revocationStr));
 
diff --git a/src/libxrpl/server/Port.cpp b/src/libxrpl/server/Port.cpp
index 694d4448d5..a7892bc0e8 100644
--- a/src/libxrpl/server/Port.cpp
+++ b/src/libxrpl/server/Port.cpp
@@ -1,5 +1,6 @@
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -9,7 +10,6 @@
 #include 
 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -98,7 +98,7 @@ populate(
 
     while (std::getline(ss, ip, ','))
     {
-        boost::algorithm::trim(ip);
+        ip = trimWhitespace(ip);
         bool v4 = false;
         boost::asio::ip::network_v4 v4Net;
         boost::asio::ip::network_v6 v6Net;
diff --git a/src/libxrpl/server/Vacuum.cpp b/src/libxrpl/server/Vacuum.cpp
index 63d40af156..c952e722b8 100644
--- a/src/libxrpl/server/Vacuum.cpp
+++ b/src/libxrpl/server/Vacuum.cpp
@@ -5,13 +5,10 @@
 #include 
 #include 
 
-#include 
-#include 
-#include   // IWYU pragma: keep
-
 #include 
 
 #include 
+#include 
 #include 
 #include 
 
@@ -20,12 +17,12 @@ namespace xrpl {
 bool
 doVacuumDB(DatabaseCon::Setup const& setup, beast::Journal j)
 {
-    boost::filesystem::path const dbPath = setup.dataDir / kTxDbName;
+    std::filesystem::path const dbPath = setup.dataDir / kTxDbName;
 
-    uintmax_t const dbSize = file_size(dbPath);
+    uintmax_t const dbSize = std::filesystem::file_size(dbPath);
     XRPL_ASSERT(dbSize != static_cast(-1), "xrpl::doVacuumDB : file_size succeeded");
 
-    if (auto available = space(dbPath.parent_path()).available; available < dbSize)
+    if (auto available = std::filesystem::space(dbPath.parent_path()).available; available < dbSize)
     {
         std::cerr << "The database filesystem must have at least as "
                      "much free space as the size of "
@@ -41,7 +38,7 @@ doVacuumDB(DatabaseCon::Setup const& setup, beast::Journal j)
     // Only the most trivial databases will fit in memory on typical
     // (recommended) hardware. Force temp files to be written to disk
     // regardless of the config settings.
-    session << boost::format(kCommonDbPragmaTemp) % "file";
+    session << commonDbPragmaTemp("file");
     session << "PRAGMA page_size;", soci::into(pageSize);
 
     std::cout << "VACUUM beginning. page_size: " << pageSize << std::endl;
diff --git a/src/libxrpl/server/Wallet.cpp b/src/libxrpl/server/Wallet.cpp
index 42ac80ef3f..56d0db67d4 100644
--- a/src/libxrpl/server/Wallet.cpp
+++ b/src/libxrpl/server/Wallet.cpp
@@ -16,7 +16,6 @@
 #include 
 #include 
 
-#include 
 #include   // IWYU pragma: keep
 
 #include   // IWYU pragma: keep
@@ -30,6 +29,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -172,11 +172,10 @@ getNodeIdentity(soci::session& session)
     // If a valid identity wasn't found, we randomly generate a new one:
     auto [newpublicKey, newsecretKey] = randomKeyPair(KeyType::Secp256k1);
 
-    session << str(
-        boost::format(
-            "INSERT INTO NodeIdentity (PublicKey,PrivateKey) "
-            "VALUES ('%s','%s');") %
-        toBase58(TokenType::NodePublic, newpublicKey) %
+    session << std::format(
+        "INSERT INTO NodeIdentity (PublicKey,PrivateKey) "
+        "VALUES ('{}','{}');",
+        toBase58(TokenType::NodePublic, newpublicKey),
         toBase58(TokenType::NodePrivate, newsecretKey));
 
     return {newpublicKey, newsecretKey};
diff --git a/src/libxrpl/shamap/SHAMap.cpp b/src/libxrpl/shamap/SHAMap.cpp
index 2483e6f6e1..3fa8d66be0 100644
--- a/src/libxrpl/shamap/SHAMap.cpp
+++ b/src/libxrpl/shamap/SHAMap.cpp
@@ -116,8 +116,7 @@ SHAMap::dirtyUp(SharedPtrNodeStack& stack, uint256 const& target, SHAMapTreeNode
         stack.pop();
         XRPL_ASSERT(node, "xrpl::SHAMap::dirtyUp : non-null node");
 
-        int const branch = selectBranch(nodeID, target);
-        XRPL_ASSERT(branch >= 0, "xrpl::SHAMap::dirtyUp : valid branch");
+        auto const branch = selectBranch(nodeID, target);
 
         node = unshareNode(std::move(node), nodeID);
         node->setChild(branch, std::move(child));
@@ -278,7 +277,7 @@ SHAMap::fetchNode(SHAMapHash const& hash) const
 }
 
 SHAMapTreeNode*
-SHAMap::descendThrow(SHAMapInnerNode* parent, int branch) const
+SHAMap::descendThrow(SHAMapInnerNode* parent, unsigned int branch) const
 {
     SHAMapTreeNode* ret = descend(parent, branch);  // NOLINT(misc-const-correctness)
 
@@ -289,7 +288,7 @@ SHAMap::descendThrow(SHAMapInnerNode* parent, int branch) const
 }
 
 SHAMapTreeNodePtr
-SHAMap::descendThrow(SHAMapInnerNode& parent, int branch) const
+SHAMap::descendThrow(SHAMapInnerNode& parent, unsigned int branch) const
 {
     SHAMapTreeNodePtr ret = descend(parent, branch);
 
@@ -300,7 +299,7 @@ SHAMap::descendThrow(SHAMapInnerNode& parent, int branch) const
 }
 
 SHAMapTreeNode*
-SHAMap::descend(SHAMapInnerNode* parent, int branch) const
+SHAMap::descend(SHAMapInnerNode* parent, unsigned int branch) const
 {
     SHAMapTreeNode* ret = parent->getChildPointer(branch);  // NOLINT(misc-const-correctness)
     if ((ret != nullptr) || !backed_)
@@ -315,7 +314,7 @@ SHAMap::descend(SHAMapInnerNode* parent, int branch) const
 }
 
 SHAMapTreeNodePtr
-SHAMap::descend(SHAMapInnerNode& parent, int branch) const
+SHAMap::descend(SHAMapInnerNode& parent, unsigned int branch) const
 {
     SHAMapTreeNodePtr node = parent.getChild(branch);
     if (node || !backed_)
@@ -332,7 +331,7 @@ SHAMap::descend(SHAMapInnerNode& parent, int branch) const
 // Gets the node that would be hooked to this branch,
 // but doesn't hook it up.
 SHAMapTreeNodePtr
-SHAMap::descendNoStore(SHAMapInnerNode& parent, int branch) const
+SHAMap::descendNoStore(SHAMapInnerNode& parent, unsigned int branch) const
 {
     SHAMapTreeNodePtr ret = parent.getChild(branch);
     if (!ret && backed_)
@@ -344,12 +343,11 @@ std::pair
 SHAMap::descend(
     SHAMapInnerNode* parent,
     SHAMapNodeID const& parentID,
-    int branch,
+    unsigned int branch,
     SHAMapSyncFilter const* filter) const
 {
     XRPL_ASSERT(parent->isInner(), "xrpl::SHAMap::descend : valid parent input");
-    XRPL_ASSERT(
-        (branch >= 0) && (branch < kBranchFactor), "xrpl::SHAMap::descend : valid branch input");
+    XRPL_ASSERT(branch < kBranchFactor, "xrpl::SHAMap::descend : valid branch input");
     XRPL_ASSERT(
         !parent->isEmptyBranch(branch), "xrpl::SHAMap::descend : parent branch is non-empty");
 
@@ -373,7 +371,7 @@ SHAMap::descend(
 SHAMapTreeNode*
 SHAMap::descendAsync(
     SHAMapInnerNode* parent,
-    int branch,
+    unsigned int branch,
     SHAMapSyncFilter const* filter,
     bool& pending,
     descendCallback&& callback) const
@@ -433,10 +431,9 @@ SHAMapLeafNode*
 SHAMap::belowHelper(
     SHAMapTreeNodePtr node,
     SharedPtrNodeStack& stack,
-    int branch,
-    std::tuple, std::function> const& loopParams) const
+    unsigned int branch,
+    BelowDirection direction) const
 {
-    auto& [init, cmp, incr] = loopParams;
     if (node->isLeaf())
     {
         auto n = intr_ptr::staticPointerCast(node);
@@ -452,11 +449,16 @@ SHAMap::belowHelper(
     {
         stack.emplace(inner, stack.top().second.getChildNodeID(branch));
     }
-    for (int i = init; cmp(i);)
+    // `scanned` counts how many branches of `inner` we have examined; the branch we look at is
+    // derived from it, so no index ever goes out of range.
+    for (auto scanned = 0u; scanned < kBranchFactor;)
     {
-        if (!inner->isEmptyBranch(i))
+        auto const childBranch =
+            (direction == BelowDirection::Last) ? (kBranchFactor - 1u - scanned) : scanned;
+
+        if (!inner->isEmptyBranch(childBranch))
         {
-            node.adopt(descendThrow(inner.get(), i));
+            node.adopt(descendThrow(inner.get(), childBranch));
             XRPL_ASSERT(!stack.empty(), "xrpl::SHAMap::belowHelper : non-empty stack");
             if (node->isLeaf())
             {
@@ -466,32 +468,24 @@ SHAMap::belowHelper(
             }
             inner = intr_ptr::staticPointerCast(node);
             stack.emplace(inner, stack.top().second.getChildNodeID(branch));
-            i = init;  // descend and reset loop
+            scanned = 0u;  // descend and restart the scan on the new node
         }
         else
         {
-            incr(i);  // scan next branch
+            ++scanned;  // scan next branch
         }
     }
     return nullptr;
 }
 SHAMapLeafNode*
-SHAMap::lastBelow(SHAMapTreeNodePtr node, SharedPtrNodeStack& stack, int branch) const
+SHAMap::lastBelow(SHAMapTreeNodePtr node, SharedPtrNodeStack& stack, unsigned int branch) const
 {
-    auto init = kBranchFactor - 1;
-    auto cmp = [](int i) { return i >= 0; };
-    auto incr = [](int& i) { --i; };
-
-    return belowHelper(node, stack, branch, {init, cmp, incr});
+    return belowHelper(node, stack, branch, BelowDirection::Last);
 }
 SHAMapLeafNode*
-SHAMap::firstBelow(SHAMapTreeNodePtr node, SharedPtrNodeStack& stack, int branch) const
+SHAMap::firstBelow(SHAMapTreeNodePtr node, SharedPtrNodeStack& stack, unsigned int branch) const
 {
-    auto init = 0;
-    auto cmp = [](int i) { return i <= kBranchFactor; };
-    auto incr = [](int& i) { ++i; };
-
-    return belowHelper(node, stack, branch, {init, cmp, incr});
+    return belowHelper(node, stack, branch, BelowDirection::First);
 }
 static boost::intrusive_ptr const kNoItem;
 
@@ -504,7 +498,7 @@ SHAMap::onlyBelow(SHAMapTreeNode* node) const
     {
         SHAMapTreeNode* nextNode = nullptr;
         auto inner = safeDowncast(node);
-        for (int i = 0; i < kBranchFactor; ++i)
+        for (auto i = 0u; i < kBranchFactor; ++i)
         {
             if (!inner->isEmptyBranch(i))
             {
@@ -650,8 +644,9 @@ SHAMap::lowerBound(uint256 const& id) const
         else
         {
             auto inner = intr_ptr::staticPointerCast(node);
-            for (int branch = selectBranch(nodeID, id) - 1; branch >= 0; --branch)
+            for (auto branch = selectBranch(nodeID, id); branch > 0u;)
             {
+                --branch;
                 if (!inner->isEmptyBranch(branch))
                 {
                     node = descendThrow(*inner, branch);
@@ -715,7 +710,7 @@ SHAMap::delItem(uint256 const& id)
         {
             // we may have made this a node with 1 or 0 children
             // And, if so, we need to remove this branch
-            int const bc = node->getBranchCount();
+            auto const bc = node->getBranchCount();
             if (bc == 0)
             {
                 // no children below this branch
@@ -730,7 +725,7 @@ SHAMap::delItem(uint256 const& id)
 
                 if (item)
                 {
-                    for (int i = 0; i < kBranchFactor; ++i)
+                    for (auto i = 0u; i < kBranchFactor; ++i)
                     {
                         if (!node->isEmptyBranch(i))
                         {
@@ -786,7 +781,7 @@ SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr
     {
         // easy case, we end on an inner node
         auto inner = intr_ptr::staticPointerCast(node);
-        int const branch = selectBranch(nodeID, tag);
+        auto const branch = selectBranch(nodeID, tag);
         XRPL_ASSERT(
             inner->isEmptyBranch(branch), "xrpl::SHAMap::addGiveItem : inner branch is empty");
         inner->setChild(branch, makeTypedLeaf(type, std::move(item), cowid_));
@@ -802,7 +797,7 @@ SHAMap::addGiveItem(SHAMapNodeType type, boost::intrusive_ptr
 
         node = intr_ptr::makeShared(node->cowid());
 
-        unsigned int b1 = 0, b2 = 0;
+        auto b1 = 0u, b2 = 0u;
 
         while ((b1 = selectBranch(nodeID, tag)) == (b2 = selectBranch(nodeID, otherItem->key())))
         {
@@ -1012,12 +1007,12 @@ SHAMap::walkSubTree(bool doWrite, NodeObjectType t)
 
     // Stack of {parent,index,child} pointers representing
     // inner nodes we are in the process of flushing
-    using StackEntry = std::pair, int>;
+    using StackEntry = std::pair, unsigned int>;
     std::stack> stack;
 
     node = preFlushNode(std::move(node));
 
-    int pos = 0;
+    auto pos = 0u;
 
     // We can't flush an inner node until we flush its children
     while (true)
@@ -1032,7 +1027,7 @@ SHAMap::walkSubTree(bool doWrite, NodeObjectType t)
             {
                 // No need to do I/O. If the node isn't linked,
                 // it can't need to be flushed
-                int const branch = pos;
+                auto const branch = pos;
                 auto child = node->getChild(pos++);
 
                 if (child && (child->cowid() != 0))
@@ -1126,7 +1121,7 @@ SHAMap::dump(bool hash) const
         if (node->isInner())
         {
             auto inner = safeDowncast(node);
-            for (int i = 0; i < kBranchFactor; ++i)
+            for (auto i = 0u; i < kBranchFactor; ++i)
             {
                 if (!inner->isEmptyBranch(i))
                 {
diff --git a/src/libxrpl/shamap/SHAMapDelta.cpp b/src/libxrpl/shamap/SHAMapDelta.cpp
index 8336ce5481..1306fe6990 100644
--- a/src/libxrpl/shamap/SHAMapDelta.cpp
+++ b/src/libxrpl/shamap/SHAMapDelta.cpp
@@ -54,7 +54,7 @@ SHAMap::walkBranch(
         {
             // This is an inner node, add all non-empty branches
             auto inner = safeDowncast(node);
-            for (int i = 0; i < 16; ++i)
+            for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i)
             {
                 if (!inner->isEmptyBranch(i))
                     nodeStack.push({descendThrow(inner, i)});
@@ -205,7 +205,7 @@ SHAMap::compare(SHAMap const& otherMap, Delta& differences, int maxCount) const
         {
             auto ours = safeDowncast(ourNode);
             auto other = safeDowncast(otherNode);
-            for (int i = 0; i < 16; ++i)
+            for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i)
             {
                 if (ours->getChildHash(i) != other->getChildHash(i))
                 {
@@ -257,7 +257,7 @@ SHAMap::walkMap(std::vector& missingNodes, int maxMissing) co
         intr_ptr::SharedPtr const node = std::move(nodeStack.top());
         nodeStack.pop();
 
-        for (int i = 0; i < 16; ++i)
+        for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i)
         {
             if (!node->isEmptyBranch(i))
             {
@@ -286,27 +286,29 @@ SHAMap::walkMapParallel(std::vector& missingNodes, int maxMis
         return false;
 
     using StackEntry = intr_ptr::SharedPtr;
-    std::array topChildren;
+    std::array topChildren;
     {
         auto const& innerRoot = intr_ptr::staticPointerCast(root_);
-        for (int i = 0; i < 16; ++i)
+        for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i)
         {
             if (!innerRoot->isEmptyBranch(i))
                 topChildren[i] = descendNoStore(*innerRoot, i);
         }
     }
     std::vector workers;
-    workers.reserve(16);
+    workers.reserve(SHAMapInnerNode::kBranchFactor);
     std::vector exceptions;
-    exceptions.reserve(16);
+    exceptions.reserve(SHAMapInnerNode::kBranchFactor);
 
-    std::array>, 16> nodeStacks;
+    std::array>, SHAMapInnerNode::kBranchFactor>
+        nodeStacks;
 
     // This mutex is used inside the worker threads to protect `missingNodes`
     // and `maxMissing` from race conditions
     std::mutex m;
 
-    for (int rootChildIndex = 0; rootChildIndex < 16; ++rootChildIndex)
+    for (auto rootChildIndex = 0u; rootChildIndex < SHAMapInnerNode::kBranchFactor;
+         ++rootChildIndex)
     {
         auto const& child = topChildren[rootChildIndex];
         if (!child || !child->isInner())
@@ -327,7 +329,7 @@ SHAMap::walkMapParallel(std::vector& missingNodes, int maxMis
                         XRPL_ASSERT(node, "xrpl::SHAMap::walkMapParallel : non-null node");
                         nodeStack.pop();
 
-                        for (int i = 0; i < 16; ++i)
+                        for (auto i = 0u; i < SHAMapInnerNode::kBranchFactor; ++i)
                         {
                             if (node->isEmptyBranch(i))
                                 continue;
diff --git a/src/libxrpl/shamap/SHAMapInnerNode.cpp b/src/libxrpl/shamap/SHAMapInnerNode.cpp
index 74a0e4515f..bdd89388b2 100644
--- a/src/libxrpl/shamap/SHAMapInnerNode.cpp
+++ b/src/libxrpl/shamap/SHAMapInnerNode.cpp
@@ -63,8 +63,8 @@ SHAMapInnerNode::resizeChildArrays(std::uint8_t toAllocate)
     hashesAndChildren_ = TaggedPointer(std::move(hashesAndChildren_), isBranch_, toAllocate);
 }
 
-std::optional
-SHAMapInnerNode::getChildIndex(int i) const
+std::optional
+SHAMapInnerNode::getChildIndex(unsigned int i) const
 {
     return hashesAndChildren_.getChildIndex(isBranch_, i);
 }
@@ -89,7 +89,7 @@ SHAMapInnerNode::clone(std::uint32_t cowid) const
 
     if (thisIsSparse)
     {
-        int cloneChildIndex = 0;
+        auto cloneChildIndex = 0u;
         iterNonEmptyChildIndexes([&](auto branchNum, auto indexNum) {
             cloneHashes[cloneChildIndex++] = thisHashes[indexNum];
         });
@@ -105,7 +105,7 @@ SHAMapInnerNode::clone(std::uint32_t cowid) const
 
     if (thisIsSparse)
     {
-        int cloneChildIndex = 0;
+        auto cloneChildIndex = 0u;
         iterNonEmptyChildIndexes([&](auto branchNum, auto indexNum) {
             cloneChildren[cloneChildIndex++] = thisChildren[indexNum];
         });
@@ -133,12 +133,12 @@ SHAMapInnerNode::makeFullInner(Slice data, SHAMapHash const& hash, bool hashVali
 
     auto hashes = ret->hashesAndChildren_.getHashes();
 
-    for (int i = 0; i < kBranchFactor; ++i)
+    for (auto i = 0u; i < kBranchFactor; ++i)
     {
         hashes[i].asUInt256() = si.getBitString<256>();
 
         if (hashes[i].isNonZero())
-            ret->isBranch_ |= (1 << i);
+            ret->isBranch_ |= (1u << i);
     }
 
     ret->resizeChildArrays(ret->getBranchCount());
@@ -182,7 +182,7 @@ SHAMapInnerNode::makeCompressedInner(Slice data)
         hashes[pos].asUInt256() = hash;
 
         if (hashes[pos].isNonZero())
-            ret->isBranch_ |= (1 << pos);
+            ret->isBranch_ |= (1u << pos);
     }
 
     ret->resizeChildArrays(ret->getBranchCount());
@@ -267,20 +267,19 @@ SHAMapInnerNode::getString(SHAMapNodeID const& id) const
 
 // We are modifying an inner node
 void
-SHAMapInnerNode::setChild(int m, SHAMapTreeNodePtr child)
+SHAMapInnerNode::setChild(unsigned int branch, SHAMapTreeNodePtr child)
 {
-    XRPL_ASSERT(
-        (m >= 0) && (m < kBranchFactor), "xrpl::SHAMapInnerNode::setChild : valid branch input");
+    XRPL_ASSERT(branch < kBranchFactor, "xrpl::SHAMapInnerNode::setChild : valid branch input");
     XRPL_ASSERT(cowid_, "xrpl::SHAMapInnerNode::setChild : nonzero cowid");
     XRPL_ASSERT(child.get() != this, "xrpl::SHAMapInnerNode::setChild : valid child input");
 
     auto const dstIsBranch = [&] {
         if (child)
         {
-            return isBranch_ | (1u << m);
+            return isBranch_ | (1u << branch);
         }
 
-        return isBranch_ & ~(1u << m);
+        return isBranch_ & ~(1u << branch);
     }();
 
     auto const dstToAllocate = popcnt16(dstIsBranch);
@@ -293,8 +292,8 @@ SHAMapInnerNode::setChild(int m, SHAMapTreeNodePtr child)
 
     if (child)
     {
-        auto const childIndex =
-            *getChildIndex(m);  // NOLINT(bugprone-unchecked-optional-access) isBranch_ set above
+        // NOLINTNEXTLINE(bugprone-unchecked-optional-access) isBranch_ set above
+        auto const childIndex = *getChildIndex(branch);
         auto [_, hashes, children] = hashesAndChildren_.getHashesAndChildren();
         hashes[childIndex].zero();
         children[childIndex] = std::move(child);
@@ -309,25 +308,24 @@ SHAMapInnerNode::setChild(int m, SHAMapTreeNodePtr child)
 
 // finished modifying, now make shareable
 void
-SHAMapInnerNode::shareChild(int m, SHAMapTreeNodePtr const& child)
+SHAMapInnerNode::shareChild(unsigned int branch, SHAMapTreeNodePtr const& child)
 {
-    XRPL_ASSERT(
-        (m >= 0) && (m < kBranchFactor), "xrpl::SHAMapInnerNode::shareChild : valid branch input");
+    XRPL_ASSERT(branch < kBranchFactor, "xrpl::SHAMapInnerNode::shareChild : valid branch input");
     XRPL_ASSERT(cowid_, "xrpl::SHAMapInnerNode::shareChild : nonzero cowid");
     XRPL_ASSERT(child, "xrpl::SHAMapInnerNode::shareChild : non-null child input");
     XRPL_ASSERT(child.get() != this, "xrpl::SHAMapInnerNode::shareChild : valid child input");
 
-    XRPL_ASSERT(!isEmptyBranch(m), "xrpl::SHAMapInnerNode::shareChild : non-empty branch input");
+    XRPL_ASSERT(
+        !isEmptyBranch(branch), "xrpl::SHAMapInnerNode::shareChild : non-empty branch input");
     // NOLINTNEXTLINE(bugprone-unchecked-optional-access) assert above
-    hashesAndChildren_.getChildren()[*getChildIndex(m)] = child;
+    hashesAndChildren_.getChildren()[*getChildIndex(branch)] = child;
 }
 
 SHAMapTreeNode*
-SHAMapInnerNode::getChildPointer(int branch)
+SHAMapInnerNode::getChildPointer(unsigned int branch)
 {
     XRPL_ASSERT(
-        branch >= 0 && branch < kBranchFactor,
-        "xrpl::SHAMapInnerNode::getChildPointer : valid branch input");
+        branch < kBranchFactor, "xrpl::SHAMapInnerNode::getChildPointer : valid branch input");
     XRPL_ASSERT(
         !isEmptyBranch(branch), "xrpl::SHAMapInnerNode::getChildPointer : non-empty branch input");
 
@@ -340,11 +338,9 @@ SHAMapInnerNode::getChildPointer(int branch)
 }
 
 SHAMapTreeNodePtr
-SHAMapInnerNode::getChild(int branch)
+SHAMapInnerNode::getChild(unsigned int branch)
 {
-    XRPL_ASSERT(
-        branch >= 0 && branch < kBranchFactor,
-        "xrpl::SHAMapInnerNode::getChild : valid branch input");
+    XRPL_ASSERT(branch < kBranchFactor, "xrpl::SHAMapInnerNode::getChild : valid branch input");
     XRPL_ASSERT(!isEmptyBranch(branch), "xrpl::SHAMapInnerNode::getChild : non-empty branch input");
 
     auto const index =
@@ -356,23 +352,20 @@ SHAMapInnerNode::getChild(int branch)
 }
 
 SHAMapHash const&
-SHAMapInnerNode::getChildHash(int m) const
+SHAMapInnerNode::getChildHash(unsigned int branch) const
 {
-    XRPL_ASSERT(
-        (m >= 0) && (m < kBranchFactor),
-        "xrpl::SHAMapInnerNode::getChildHash : valid branch input");
-    if (auto const i = getChildIndex(m))
+    XRPL_ASSERT(branch < kBranchFactor, "xrpl::SHAMapInnerNode::getChildHash : valid branch input");
+    if (auto const i = getChildIndex(branch))
         return hashesAndChildren_.getHashes()[*i];
 
     return kZeroShaMapHash;
 }
 
 SHAMapTreeNodePtr
-SHAMapInnerNode::canonicalizeChild(int branch, SHAMapTreeNodePtr node)
+SHAMapInnerNode::canonicalizeChild(unsigned int branch, SHAMapTreeNodePtr node)
 {
     XRPL_ASSERT(
-        branch >= 0 && branch < kBranchFactor,
-        "xrpl::SHAMapInnerNode::canonicalizeChild : valid branch input");
+        branch < kBranchFactor, "xrpl::SHAMapInnerNode::canonicalizeChild : valid branch input");
     XRPL_ASSERT(node != nullptr, "xrpl::SHAMapInnerNode::canonicalizeChild : valid node input");
     XRPL_ASSERT(
         !isEmptyBranch(branch),
@@ -410,7 +403,7 @@ SHAMapInnerNode::invariants(bool isRoot) const
     if (numAllocated != kBranchFactor)
     {
         auto const branchCount = getBranchCount();
-        for (int i = 0; i < branchCount; ++i)
+        for (auto i = 0u; i < branchCount; ++i)
         {
             XRPL_ASSERT(
                 hashes[i].isNonZero(),
@@ -422,12 +415,12 @@ SHAMapInnerNode::invariants(bool isRoot) const
     }
     else
     {
-        for (int i = 0; i < kBranchFactor; ++i)
+        for (auto i = 0u; i < kBranchFactor; ++i)
         {
             if (hashes[i].isNonZero())
             {
                 XRPL_ASSERT(
-                    (isBranch_ & (1 << i)),
+                    (isBranch_ & (1u << i)),
                     "xrpl::SHAMapInnerNode::invariants : valid branch when "
                     "nonzero hash");
                 if (children[i] != nullptr)
@@ -437,7 +430,7 @@ SHAMapInnerNode::invariants(bool isRoot) const
             else
             {
                 XRPL_ASSERT(
-                    (isBranch_ & (1 << i)) == 0,
+                    (isBranch_ & (1u << i)) == 0u,
                     "xrpl::SHAMapInnerNode::invariants : valid branch when "
                     "zero hash");
             }
diff --git a/src/libxrpl/shamap/SHAMapNodeID.cpp b/src/libxrpl/shamap/SHAMapNodeID.cpp
index a511fc038c..42b946b921 100644
--- a/src/libxrpl/shamap/SHAMapNodeID.cpp
+++ b/src/libxrpl/shamap/SHAMapNodeID.cpp
@@ -6,6 +6,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -16,7 +17,7 @@ namespace xrpl {
 static uint256 const&
 depthMask(unsigned int depth)
 {
-    static constexpr auto kMaskSize = 65;
+    static constexpr auto kMaskSize = SHAMap::kLeafDepth + 1;
 
     struct MasksT
     {
@@ -25,7 +26,7 @@ depthMask(unsigned int depth)
         MasksT()
         {
             uint256 selector;
-            for (int i = 0; i < kMaskSize - 1; i += 2)
+            for (auto i = 0u; i < kMaskSize - 1; i += 2)
             {
                 entry[i] = selector;
                 *(selector.begin() + (i / 2)) = 0xF0;
@@ -40,14 +41,43 @@ depthMask(unsigned int depth)
     return kMasks.entry[depth];
 }
 
+// The prefix of `key` at `depth`: the leading nibbles naming the subtree a node at that depth
+// identifies, with the remainder of the key masked off.
+static uint256
+maskedToDepth(uint256 const& key, unsigned int depth)
+{
+    return key & depthMask(depth);
+}
+
+// Whether `id` at `depth` is what `key` looks like once masked down to that depth, i.e.
+// whether an ID with this depth and id names a subtree that `key` falls under.
+static bool
+isPrefixOfAtDepth(uint256 const& id, unsigned int depth, uint256 const& key)
+{
+    return maskedToDepth(key, depth) == id;
+}
+
 // canonicalize the hash to a node ID for this depth
 SHAMapNodeID::SHAMapNodeID(unsigned int depth, uint256 const& hash) : id_(hash), depth_(depth)
 {
+    // Every SHAMapNodeID's depth is stored here, so this is the one place that can stop an
+    // out-of-range one from being kept: a depth past kLeafDepth would go on to index depthMask
+    // out of bounds, and getRawString would narrow it to a byte, silently renaming the node.
+    // Clamp rather than throw, since node IDs are built from peer-supplied depths on the ledger
+    // data path, where no caller catches an exception before it reaches a thread boundary.
+    if (depth_ > SHAMap::kLeafDepth)
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::SHAMapNodeID::SHAMapNodeID : depth within tree");
+        depth_ = SHAMap::kLeafDepth;
+        id_ = maskedToDepth(id_, depth_);
+        // LCOV_EXCL_STOP
+    }
+
+    // Reads the clamped member rather than the depth argument, so it cannot index depthMask past
+    // its last entry even once the clamp above has reported the bad input and carried on.
     XRPL_ASSERT(
-        depth <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::SHAMapNodeID : maximum depth input");
-    XRPL_ASSERT(
-        id_ == (id_ & depthMask(depth)),
-        "xrpl::SHAMapNodeID::SHAMapNodeID : hash and depth inputs do match");
+        isPrefixOf(id_), "xrpl::SHAMapNodeID::SHAMapNodeID : hash and depth inputs do match");
 }
 
 std::string
@@ -60,10 +90,10 @@ SHAMapNodeID::getRawString() const
 }
 
 SHAMapNodeID
-SHAMapNodeID::getChildNodeID(unsigned int m) const
+SHAMapNodeID::getChildNodeID(unsigned int branch) const
 {
     XRPL_ASSERT(
-        m < SHAMap::kBranchFactor, "xrpl::SHAMapNodeID::getChildNodeID : valid branch input");
+        branch < SHAMap::kBranchFactor, "xrpl::SHAMapNodeID::getChildNodeID : valid branch input");
 
     // A SHAMap has exactly 65 levels, so nodes must not exceed that
     // depth; if they do, this breaks the invariant of never allowing
@@ -79,14 +109,20 @@ SHAMapNodeID::getChildNodeID(unsigned int m) const
     if (depth_ >= SHAMap::kLeafDepth)
         Throw("Request for child node ID of " + to_string(*this));
 
-    if (id_ != (id_ & depthMask(depth_)))
+    if (!isPrefixOf(id_))
         Throw("Incorrect mask for " + to_string(*this));
 
     SHAMapNodeID node{depth_ + 1, id_};
-    node.id_.begin()[depth_ / 2] |= ((depth_ & 1) != 0u) ? m : (m << 4);
+    node.id_.begin()[depth_ / 2] |= ((depth_ & 1) != 0u) ? branch : (branch << 4);
     return node;
 }
 
+bool
+SHAMapNodeID::isPrefixOf(uint256 const& key) const
+{
+    return isPrefixOfAtDepth(id_, depth_, key);
+}
+
 [[nodiscard]] std::optional
 deserializeSHAMapNodeID(void const* data, std::size_t size)
 {
@@ -97,9 +133,9 @@ deserializeSHAMapNodeID(void const* data, std::size_t size)
         unsigned int const depth = *(static_cast(data) + 32);
         if (depth <= SHAMap::kLeafDepth)
         {
-            auto const id = uint256::fromVoid(data);
-
-            if (id == (id & depthMask(depth)))
+            // Reject a serialized ID carrying bits below its own depth. Checked before
+            // constructing, since the constructor asserts that same property.
+            if (auto const id = uint256::fromVoid(data); isPrefixOfAtDepth(id, depth, id))
                 ret.emplace(depth, id);
         }
     }
@@ -110,7 +146,11 @@ deserializeSHAMapNodeID(void const* data, std::size_t size)
 [[nodiscard]] unsigned int
 selectBranch(SHAMapNodeID const& id, uint256 const& hash)
 {
-    auto const depth = id.getDepth();
+    XRPL_ASSERT(id.getDepth() < SHAMap::kLeafDepth, "xrpl::selectBranch : depth below leaf depth");
+
+    // A depth-64 ID has no nibble left to select. Callers must not ask, but clamp anyway to keep
+    // the read below the end of the 32-byte key.
+    auto const depth = std::min(id.getDepth(), SHAMap::kLeafDepth - 1u);
     auto branch = static_cast(*(hash.begin() + (depth / 2)));
 
     if ((depth & 1) != 0u)
@@ -127,11 +167,20 @@ selectBranch(SHAMapNodeID const& id, uint256 const& hash)
 }
 
 SHAMapNodeID
-SHAMapNodeID::createID(int depth, uint256 const& key)
+SHAMapNodeID::createID(unsigned int depth, uint256 const& key)
 {
-    XRPL_ASSERT(
-        depth >= 0 && depth <= SHAMap::kLeafDepth, "xrpl::SHAMapNodeID::createID : valid depth");
-    return SHAMapNodeID(depth, key & depthMask(depth));
+    // The mask is chosen here, before the constructor runs, so the clamp there cannot cover this
+    // call: an out-of-range depth would index depthMask's table while still evaluating this
+    // argument. A public factory has to hold its own bound.
+    if (depth > SHAMap::kLeafDepth)
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::SHAMapNodeID::createID : depth within tree");
+        depth = SHAMap::kLeafDepth;
+        // LCOV_EXCL_STOP
+    }
+
+    return SHAMapNodeID(depth, maskedToDepth(key, depth));
 }
 
 }  // namespace xrpl
diff --git a/src/libxrpl/shamap/SHAMapSync.cpp b/src/libxrpl/shamap/SHAMapSync.cpp
index cbed6885c9..4319d0bcd4 100644
--- a/src/libxrpl/shamap/SHAMapSync.cpp
+++ b/src/libxrpl/shamap/SHAMapSync.cpp
@@ -54,15 +54,15 @@ SHAMap::visitNodes(std::function const& function) const
     if (!root_->isInner())
         return;
 
-    using StackEntry = std::pair>;
+    using StackEntry = std::pair>;
     std::stack> stack;
 
     auto node = intr_ptr::staticPointerCast(root_);
-    int pos = 0;
+    auto pos = 0u;
 
     while (true)
     {
-        while (pos < 16)
+        while (pos < kBranchFactor)
         {
             if (!node->isEmptyBranch(pos))
             {
@@ -77,10 +77,10 @@ SHAMap::visitNodes(std::function const& function) const
                 else
                 {
                     // If there are no more children, don't push this node
-                    while ((pos != 15) && (node->isEmptyBranch(pos + 1)))
+                    while ((pos != kBranchFactor - 1u) && (node->isEmptyBranch(pos + 1)))
                         ++pos;
 
-                    if (pos != 15)
+                    if (pos != kBranchFactor - 1u)
                     {
                         // save next position to resume at
                         stack.emplace(pos + 1, std::move(node));
@@ -143,8 +143,22 @@ SHAMap::visitDifferences(
         if (!function(*node))
             return;
 
+        // Nibbles run out at kLeafDepth, so only a leaf belongs there. A well-formed map never
+        // holds an inner node at that depth: addKnownNode marks the map invalid rather than hooking
+        // one in, and fetch-pack data is hash-verified against a validated root, so reaching this
+        // means a defect or a corrupt store, not something a peer can provoke. Report the node
+        // anyway - the wire form carries no depth, and the recipient hooks blobs in by hash - but
+        // skip the children rather than letting getChildNodeID throw on them.
+        if (nodeID.getDepth() >= kLeafDepth)
+        {
+            // LCOV_EXCL_START
+            UNREACHABLE("xrpl::SHAMap::visitDifferences : inner node at leaf depth");
+            continue;
+            // LCOV_EXCL_STOP
+        }
+
         // 2) push non-matching child inner nodes
-        for (int i = 0; i < 16; ++i)
+        for (auto i = 0u; i < kBranchFactor; ++i)
         {
             if (!node->isEmptyBranch(i))
             {
@@ -176,13 +190,13 @@ SHAMap::gmnProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
 {
     SHAMapInnerNode*& node = std::get<0>(se);
     SHAMapNodeID& nodeID = std::get<1>(se);
-    int& firstChild = std::get<2>(se);
-    int& currentChild = std::get<3>(se);
+    auto& firstChild = std::get<2>(se);
+    auto& currentChild = std::get<3>(se);
     bool& fullBelow = std::get<4>(se);
 
-    while (currentChild < 16)
+    while (currentChild < kBranchFactor)
     {
-        int const branch = (firstChild + currentChild++) % 16;
+        auto const branch = (firstChild + currentChild++) % kBranchFactor;
         if (node->isEmptyBranch(branch))
             continue;
 
@@ -262,7 +276,7 @@ SHAMap::gmnProcessDeferredReads(MissingNodes& mn)
     int complete = 0;
     while (complete != mn.deferred)
     {
-        std::tuple deferredNode;
+        MissingNodes::DeferredNode deferredNode;
         {
             std::unique_lock lock{mn.deferLock};
 
@@ -423,7 +437,7 @@ SHAMap::getNodeFat(
 
     while ((node != nullptr) && node->isInner() && (nodeID.getDepth() < wanted.getDepth()))
     {
-        int const branch = selectBranch(nodeID, wanted.getNodeID());
+        auto const branch = selectBranch(nodeID, wanted.getNodeID());
         auto inner = safeDowncast(node);
         if (inner->isEmptyBranch(branch))
             return false;
@@ -444,7 +458,7 @@ SHAMap::getNodeFat(
         return false;
     }
 
-    std::stack> stack;
+    std::stack> stack;
     stack.emplace(node, nodeID, depth);
 
     Serializer s(8192);
@@ -464,12 +478,12 @@ SHAMap::getNodeFat(
             // We descend inner nodes with only a single child
             // without decrementing the depth
             auto inner = safeDowncast(node);
-            int const bc = inner->getBranchCount();
+            auto const bc = inner->getBranchCount();
 
             if ((depth > 0) || (bc == 1))
             {
                 // We need to process this node's children
-                for (int i = 0; i < 16; ++i)
+                for (auto i = 0u; i < kBranchFactor; ++i)
                 {
                     if (!inner->isEmptyBranch(i))
                     {
@@ -555,10 +569,9 @@ SHAMap::addKnownNode(
 {
     XRPL_ASSERT(!nodeID.isRoot(), "xrpl::SHAMap::addKnownNode : valid node");
     XRPL_ASSERT(treeNode, "xrpl::SHAMap::addKnownNode : non-null tree node");
-    XRPL_ASSERT(
-        !treeNode->isLeaf() ||
-            SHAMapNodeID::createID(nodeID.getDepth(), leafKey(*treeNode)).getNodeID() ==
-                nodeID.getNodeID(),
+    XRPL_ASSERT_IF(
+        treeNode->isLeaf(),
+        nodeID.isPrefixOf(leafKey(*treeNode)),
         "xrpl::SHAMap::addKnownNode : leaf position consistent with node ID");
 
     if (!isSynching())
@@ -575,8 +588,7 @@ SHAMap::addKnownNode(
            !safeDowncast(currNode)->isFullBelow(generation) &&
            (currNodeID.getDepth() < nodeID.getDepth()))
     {
-        int const branch = selectBranch(currNodeID, nodeID.getNodeID());
-        XRPL_ASSERT(branch >= 0, "xrpl::SHAMap::addKnownNode : valid branch");
+        auto const branch = selectBranch(currNodeID, nodeID.getNodeID());
         auto inner = safeDowncast(currNode);
         if (inner->isEmptyBranch(branch))
         {
@@ -686,7 +698,7 @@ SHAMap::deepCompare(SHAMap& other) const
                 return false;
             auto nodeInner = safeDowncast(node);
             auto otherInner = safeDowncast(otherNode);
-            for (int i = 0; i < 16; ++i)
+            for (auto i = 0u; i < kBranchFactor; ++i)
             {
                 if (nodeInner->isEmptyBranch(i))
                 {
@@ -725,7 +737,7 @@ SHAMap::hasInnerNode(SHAMapNodeID const& targetNodeID, SHAMapHash const& targetN
 
     while (node->isInner() && (nodeID.getDepth() < targetNodeID.getDepth()))
     {
-        int const branch = selectBranch(nodeID, targetNodeID.getNodeID());
+        auto const branch = selectBranch(nodeID, targetNodeID.getNodeID());
         auto inner = safeDowncast(node);
         if (inner->isEmptyBranch(branch))
             return false;
@@ -751,7 +763,18 @@ SHAMap::hasLeafNode(uint256 const& tag, SHAMapHash const& targetNodeHash) const
 
     do
     {
-        int const branch = selectBranch(nodeID, tag);
+        // Same kLeafDepth hazard as in visitDifferences above. That guard bounds the caller's own
+        // traversal, not the map queried here, and the loop below descends from this map's root
+        // independently, so this check is what keeps a malformed map from reaching getChildNodeID.
+        if (nodeID.getDepth() >= kLeafDepth)
+        {
+            // LCOV_EXCL_START
+            UNREACHABLE("xrpl::SHAMap::hasLeafNode : inner node at leaf depth");
+            return false;
+            // LCOV_EXCL_STOP
+        }
+
+        auto const branch = selectBranch(nodeID, tag);
         auto inner = safeDowncast(node);
         if (inner->isEmptyBranch(branch))
             return false;  // Dead end, node must not be here
@@ -803,7 +826,7 @@ SHAMap::getProofPath(uint256 const& key) const
 bool
 SHAMap::verifyProofPath(uint256 const& rootHash, uint256 const& key, std::vector const& path)
 {
-    if (path.empty() || path.size() > 65)
+    if (path.empty() || path.size() > kLeafDepth + 1u)
         return false;
 
     SHAMapHash hash{rootHash};
@@ -819,15 +842,30 @@ SHAMap::verifyProofPath(uint256 const& rootHash, uint256 const& key, std::vector
             if (node->getHash() != hash)
                 return false;
 
-            auto depth = std::distance(path.rbegin(), rit);
+            auto const depth = static_cast(std::distance(path.rbegin(), rit));
             if (node->isInner())
             {
+                // Nibbles run out at kLeafDepth, so only the leaf terminating the path may sit
+                // there. These nodes come off the wire, so a peer can still claim an inner one;
+                // reject it rather than passing this depth to selectBranch.
+                SOMETIMES(
+                    depth >= kLeafDepth, "xrpl::SHAMap::verifyProofPath : inner at leaf depth");
+                if (depth >= kLeafDepth)
+                    return false;
+
                 auto nodeId = SHAMapNodeID::createID(depth, key);
                 hash = safeDowncast(node.get())
                            ->getChildHash(selectBranch(nodeId, key));
             }
             else
             {
+                // The hash chain up to rootHash only proves this leaf sits where the path claims,
+                // not that it is the leaf for `key`: a peer could substitute any other leaf whose
+                // subtree hashes to the same value at every level above it. Checking the terminal
+                // leaf's own key is what ties the proof to `key` specifically.
+                if (leafKey(*node) != key)
+                    return false;
+
                 // should exhaust all the blobs now
                 return depth + 1 == path.size();
             }
diff --git a/src/libxrpl/tx/AGENTS.md b/src/libxrpl/tx/AGENTS.md
new file mode 100644
index 0000000000..e2261fc1ad
--- /dev/null
+++ b/src/libxrpl/tx/AGENTS.md
@@ -0,0 +1,5 @@
+# AGENTS.md — tx
+
+See the repo-level [AGENTS.md](../../../AGENTS.md) for general build/test/style guidance.
+
+Any change to transaction-processing behavior must be gated behind an amendment. New amendments (and fixes, i.e. `fix*` amendments) are added to [`include/xrpl/protocol/detail/features.macro`](../../../include/xrpl/protocol/detail/features.macro), as an `XRPL_FEATURE(...)` or `XRPL_FIX(...)` entry added to the top of the list (the list is kept in reverse chronological order). Once the pre-amendment code path for a retired amendment is removed, move its entry to `XRPL_RETIRE_FEATURE(...)`/`XRPL_RETIRE_FIX(...)` instead of deleting it.
diff --git a/src/libxrpl/tx/ApplyContext.cpp b/src/libxrpl/tx/ApplyContext.cpp
index cb5815dcf8..d2b826bb53 100644
--- a/src/libxrpl/tx/ApplyContext.cpp
+++ b/src/libxrpl/tx/ApplyContext.cpp
@@ -1,27 +1,19 @@
 #include 
 
-#include 
 #include 
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
-#include 
 
-#include 
-#include 
 #include 
-#include 
 #include 
 #include 
-#include 
-#include 
 
 namespace xrpl {
 
@@ -95,75 +87,4 @@ ApplyContext::visit(
     view_->visit(base_.view(), func);  // NOLINT(bugprone-unchecked-optional-access)
 }
 
-TER
-ApplyContext::failInvariantCheck(TER const result)
-{
-    // If we already failed invariant checks before and we are now attempting to
-    // only charge a fee, and even that fails the invariant checks something is
-    // very wrong. We switch to tefINVARIANT_FAILED, which does NOT get included
-    // in a ledger.
-
-    return (result == tecINVARIANT_FAILED || result == tefINVARIANT_FAILED)
-        ? TER{tefINVARIANT_FAILED}
-        : TER{tecINVARIANT_FAILED};
-}
-
-template 
-TER
-ApplyContext::checkInvariantsHelper(
-    TER const result,
-    XRPAmount const fee,
-    std::index_sequence)
-{
-    try
-    {
-        auto checkers = getInvariantChecks();
-
-        // call each check's per-entry method
-        visit(
-            [&checkers](
-                uint256 const& index, bool isDelete, SLE::const_ref before, SLE::const_ref after) {
-                (..., std::get(checkers).visitEntry(isDelete, before, after));
-            });
-
-        // Note: do not replace this logic with a `...&&` fold expression.
-        // The fold expression will only run until the first check fails (it
-        // short-circuits). While the logic is still correct, the log
-        // message won't be. Every failed invariant should write to the log,
-        // not just the first one.
-        std::array const finalizers{{std::get(checkers).finalize(
-            tx, result, fee, *view_, journal)...}};  // NOLINT(bugprone-unchecked-optional-access)
-
-        // call each check's finalizer to see that it passes
-        if (!std::ranges::all_of(finalizers, [](auto const& b) { return b; }))
-        {
-            JLOG(journal.fatal()) << "Transaction has failed one or more global invariants: "
-                                  << to_string(tx.getJson(JsonOptions::Values::None));
-
-            return failInvariantCheck(result);
-        }
-    }
-    catch (std::exception const& ex)
-    {
-        JLOG(journal.fatal()) << "Transaction caused an exception in a global invariant"
-                              << ", ex: " << ex.what()
-                              << ", tx: " << to_string(tx.getJson(JsonOptions::Values::None));
-
-        return failInvariantCheck(result);
-    }
-
-    return result;
-}
-
-TER
-ApplyContext::checkInvariants(TER const result, XRPAmount const fee)
-{
-    XRPL_ASSERT(
-        isTesSuccess(result) || isTecClaim(result),
-        "xrpl::ApplyContext::checkInvariants : is tesSUCCESS or tecCLAIM");
-
-    return checkInvariantsHelper(
-        result, fee, std::make_index_sequence>{});
-}
-
 }  // namespace xrpl
diff --git a/src/libxrpl/tx/CLAUDE.md b/src/libxrpl/tx/CLAUDE.md
new file mode 120000
index 0000000000..47dc3e3d86
--- /dev/null
+++ b/src/libxrpl/tx/CLAUDE.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp
index b00a3a5db4..920a97f3e2 100644
--- a/src/libxrpl/tx/Transactor.cpp
+++ b/src/libxrpl/tx/Transactor.cpp
@@ -41,11 +41,12 @@
 #include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
 #include 
-#include 
+#include 
 #include 
 #include 
 #include 
@@ -1591,53 +1592,12 @@ Transactor::processPersistentChanges(TER result, XRPAmount fee)
 }
 
 [[nodiscard]] TER
-Transactor::checkTransactionInvariants(TER result, XRPAmount fee)
+Transactor::checkInvariants(TER result, XRPAmount fee, InvariantScope scope)
 {
-    try
-    {
-        // Phase 1: visit modified entries
-        ctx_.visit(
-            [this](uint256 const&, bool isDelete, SLE::const_ref before, SLE::const_ref after) {
-                this->visitInvariantEntry(isDelete, before, after);
-            });
+    if (scope == InvariantScope::Full)
+        return xrpl::checkInvariants(ctx_, result, fee, *this);
 
-        // Phase 2: finalize
-        if (!this->finalizeInvariants(ctx_.tx, result, fee, ctx_.view(), ctx_.journal))
-        {
-            JLOG(ctx_.journal.fatal()) <<                                             //
-                "Transaction has failed one or more transaction invariants, tx: " <<  //
-                to_string(ctx_.tx.getJson(JsonOptions::Values::None));
-            return tecINVARIANT_FAILED;
-        }
-    }
-    catch (std::exception const& ex)
-    {
-        JLOG(ctx_.journal.fatal()) <<                               //
-            "Exception while checking transaction invariants: " <<  //
-            ex.what() <<                                            //
-            ", tx: " <<                                             //
-            to_string(ctx_.tx.getJson(JsonOptions::Values::None));
-
-        return tecINVARIANT_FAILED;
-    }
-
-    return result;
-}
-
-[[nodiscard]] TER
-Transactor::checkInvariants(TER result, XRPAmount fee)
-{
-    /*
-     * DISABLED for 3.2.0 — Must be re-introduced for 3.3.0
-     *
-     * Transaction invariants are disabled due to a performance regression:
-     * the two-pass design (transaction-specific invariants + protocol invariants)
-     * iterates over modified ledger entries twice per transaction.
-     *
-     * Until resolved, only protocol invariants are checked (delegated to ctx_).
-     * This is safe because all transaction invariants in 3.2.0 are  no-ops.
-     */
-    return ctx_.checkInvariants(result, fee);
+    return xrpl::checkInvariants(ctx_, result, fee);
 }
 
 //------------------------------------------------------------------------------
@@ -1689,82 +1649,96 @@ Transactor::operator()()
     if (auto stream = j_.trace())
         stream << "preclaim result: " << transToken(result);
 
-    bool applied = isTesSuccess(result);
     auto fee = ctx_.tx.getFieldAmount(sfFee).xrp();
+    bool const canApply = std::invoke([&result, &fee, this] {
+        bool canApplyTmp = isTesSuccess(result);
 
-    if (ctx_.size() > kOversizeMetaDataCap)
-        result = tecOVERSIZE;
+        if (ctx_.size() > kOversizeMetaDataCap)
+            result = tecOVERSIZE;
 
-    if (isTecClaim(result) && ((view().flags() & TapFailHard) != 0u))
-    {
-        // If the TapFailHard flag is set, a tec result
-        // must not do anything
-        ctx_.discard();
-        applied = false;
-    }
-    else if (
-        (result == tecOVERSIZE) || (result == tecKILLED) || (result == tecINCOMPLETE) ||
-        (result == tecEXPIRED) || (result == tecBYTECODE_REJECTED) ||
-        (isTecClaimHardFail(result, view().flags())))
-    {
-        std::tie(result, fee, applied) = processPersistentChanges(result, fee);
-    }
-
-    if (applied)
-    {
-        // Check invariants: if `tecINVARIANT_FAILED` is not returned, we can
-        // proceed to apply the tx
-        result = checkInvariants(result, fee);
-        if (result == tecINVARIANT_FAILED)
+        if (isTecClaim(result) && ((view().flags() & TapFailHard) != 0u))
         {
-            // Reset to fee-claim only
-            auto const resetResult = reset(fee);
-            if (!isTesSuccess(resetResult.first))
-                result = resetResult.first;
-
-            fee = resetResult.second;
-
-            // Check invariants again to ensure the fee claiming doesn't violate
-            // invariants. After reset, only protocol invariants are re-checked.
-            // Transaction invariants are not meaningful here — the transaction's
-            // effects have been rolled back.
-            if (isTesSuccess(result) || isTecClaim(result))
-                result = ctx_.checkInvariants(result, fee);
+            // If the TapFailHard flag is set, a tec result
+            // must not do anything
+            ctx_.discard();
+            canApplyTmp = false;
         }
+        else if (
+            (result == tecOVERSIZE) || (result == tecKILLED) || (result == tecINCOMPLETE) ||
+            (result == tecEXPIRED) || (result == tecBYTECODE_REJECTED) ||
+            (isTecClaimHardFail(result, view().flags())))
+        {
+            // This is and must remain the only place where `canApplyTmp` can change from false to
+            // true. Changing from true to false is no problem.
+            std::tie(result, fee, canApplyTmp) = processPersistentChanges(result, fee);
+        }
+        return canApplyTmp;
+    });
 
-        // We ran through the invariant checker, which can, in some cases,
-        // return a tef error code. Don't apply the transaction in that case.
-        if (!isTecClaim(result) && !isTesSuccess(result))
-            applied = false;
+    auto const logger = [this](
+                            TER result,
+                            bool canApply,
+                            std::optional&& metadata = std::nullopt) -> ApplyResult {
+        JLOG(j_.trace()) << (canApply ? "applied " : "not applied ") << transToken(result);
+        return {result, canApply, std::move(metadata)};
+    };
+
+    if (!canApply)
+        return logger(result, canApply);
+
+    // First invariant pass: both protocol and transaction-specific
+    // checks run against the transaction's tentative outcome. If it
+    // does not return tecINVARIANT_FAILED, we can proceed to apply the
+    // tx.
+    result = checkInvariants(result, fee, InvariantScope::Full);
+    if (result == tecINVARIANT_FAILED)
+    {
+        // Fee-claim reset: roll the transaction's effects back so that
+        // only the fee deduction remains. This is the reset referenced
+        // by InvariantScope::ProtocolOnly.
+        auto const resetResult = reset(fee);
+        if (!isTesSuccess(resetResult.first))
+            result = resetResult.first;
+
+        fee = resetResult.second;
+
+        // Re-check invariants against the post-reset (fee-claim only)
+        // state. The transaction's effects are gone, so the
+        // transaction-specific invariants no longer apply and only the
+        // protocol invariants are re-run. A failure here escalates to
+        // tefINVARIANT_FAILED and excludes the tx from the ledger.
+        if (isTesSuccess(result) || isTecClaim(result))
+            result = checkInvariants(result, fee, InvariantScope::ProtocolOnly);
     }
 
+    // We ran through the invariant checker, which can, in some cases,
+    // return a tef error code. Don't apply the transaction in that case.
+    if (!isTecClaim(result) && !isTesSuccess(result))
+        return logger(result, false);
+
     std::optional metadata;
-    if (applied)
-    {
-        // Transaction succeeded fully or (retries are not allowed and the
-        // transaction could claim a fee)
 
-        // The transactor and invariant checkers guarantee that this will
-        // *never* trigger but if it, somehow, happens, don't allow a tx
-        // that charges a negative fee.
-        if (fee < beast::kZero)
-            Throw("fee charged is negative!");
+    // Transaction succeeded fully or (retries are not allowed and the
+    // transaction could claim a fee)
 
-        // Charge whatever fee they specified. The fee has already been
-        // deducted from the balance of the account that issued the
-        // transaction. We just need to account for it in the ledger
-        // header.
-        if (!view().open() && fee != beast::kZero)
-            ctx_.destroyXRP(fee);
+    // The transactor and invariant checkers guarantee that this will
+    // *never* trigger but if it, somehow, happens, don't allow a tx
+    // that charges a negative fee.
+    if (fee < beast::kZero)
+        Throw("fee charged is negative!");
 
-        // Once we call apply, we will no longer be able to look at view()
-        metadata = ctx_.apply(result);
-    }
+    // Charge whatever fee they specified. The fee has already been
+    // deducted from the balance of the account that issued the
+    // transaction. We just need to account for it in the ledger
+    // header.
+    if (!view().open() && fee != beast::kZero)
+        ctx_.destroyXRP(fee);
+
+    // Once we call apply, we will no longer be able to look at view()
+    metadata = ctx_.apply(result);
 
     if ((ctx_.flags() & TapDryRun) != 0u)
-    {
-        applied = false;
-    }
+        return logger(result, false, std::move(metadata));
 
     if (metadata && ctx_.getEmittedTxns().size() > 0)
     {
@@ -1820,7 +1794,7 @@ Transactor::operator()()
 
             // InvariantCheck. The context has just been reset, so only
             // protocol invariants are meaningful here.
-            result = ctx_.checkInvariants(result, fee);
+            result = checkInvariants(result, fee, InvariantScope::ProtocolOnly);
 
             // apply
             metadata = ctx_.apply(result);
@@ -1829,9 +1803,7 @@ Transactor::operator()()
 
     ctx_.finalize();
 
-    JLOG(j_.trace()) << (applied ? "applied " : "not applied ") << transToken(result);
-
-    return {result, applied, metadata};
+    return logger(result, canApply, std::move(metadata));
 }
 
 }  // namespace xrpl
diff --git a/src/libxrpl/tx/apply.cpp b/src/libxrpl/tx/apply.cpp
index f93b19a158..688c585e2a 100644
--- a/src/libxrpl/tx/apply.cpp
+++ b/src/libxrpl/tx/apply.cpp
@@ -8,6 +8,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -23,13 +24,38 @@
 
 namespace xrpl {
 
-// These are the same flags defined as HashRouterFlags::PRIVATE1-4 in
-// HashRouter.h
+// This file owns HashRouterFlags::PRIVATE1-4 and PRIVATE7-8 in HashRouter.h.
+// These are the first four; the other two are below.
 constexpr HashRouterFlags kSfSigbad = HashRouterFlags::PRIVATE1;     // Signature is bad
 constexpr HashRouterFlags kSfSiggood = HashRouterFlags::PRIVATE2;    // Signature is good
 constexpr HashRouterFlags kSfLocalbad = HashRouterFlags::PRIVATE3;   // Local checks failed
 constexpr HashRouterFlags kSfLocalgood = HashRouterFlags::PRIVATE4;  // Local checks passed
 
+// Before fixCleanup3_4_0, a signature in an alternate role field, such as
+// sfSponsorSignature, covered the same bytes as the top level signature. Which
+// bytes a role signature must cover therefore depends on whether the fix is
+// enabled, but the four flags above record only the verdict, not the rules that
+// produced it. A verdict reached under one prefix would otherwise be reused
+// under the other.
+//
+// The two flags below hold the verdict for the pre-fix prefixes, so the pre-fix
+// and post-fix verdicts occupy separate slots and neither is ever read in the
+// other's era. Nothing is cleared when the amendment activates: setFlags only
+// sets bits, so a stale pre-fix verdict simply stops being read and ages out
+// with the rest of the routing table.
+//
+// This is not one switchover at a single instant. The era is chosen per call
+// from the rules passed in, and callers do not agree on the rules: relay and
+// submit verify against the validated rules, which lag the open ledger rules
+// that preflight2 verifies against. At the amendment's flag ledger the same
+// transaction can therefore be checked under both prefixes, on the same node,
+// at the same time.
+//
+// Remove these two flags, and oldPrefixSig below, when Cleanup3_4_0 is retired
+// in features.macro.
+constexpr HashRouterFlags kSfSigbadOldPrefix = HashRouterFlags::PRIVATE7;
+constexpr HashRouterFlags kSfSiggoodOldPrefix = HashRouterFlags::PRIVATE8;
+
 //------------------------------------------------------------------------------
 
 std::pair
@@ -48,21 +74,41 @@ checkValidity(HashRouter& router, STTx const& tx, Rules const& rules)
         return {Validity::SigBad, "Batch inner transactions are never considered validly signed."};
     }
 
-    if (any(flags & kSfSigbad))
+    // Pick the cache slot for this call's era; see kSfSiggoodOldPrefix above.
+    // Only a transaction that carries a role signature, and only while the fix
+    // is disabled, uses the separate slot. Every other transaction, and every
+    // transaction once the fix is enabled, uses the ordinary flags and verifies
+    // exactly once, so there is no steady state cost.
+    //
+    // Both directions matter. A good verdict from before the fix must not let a
+    // signature moved between roles survive the amendment, and a bad verdict
+    // from before the fix must not condemn a transaction that the new prefixes
+    // accept.
+    //
+    // Whether a transaction carries a role signature is fixed for its ID: the
+    // fields are kNotSigning, so they are excluded from the signed bytes, but
+    // they are still covered by the transaction ID. Repeat calls for one ID
+    // therefore always agree on which slot pair to use.
+    bool const oldPrefixSig = !rules.enabled(fixCleanup3_4_0) &&
+        (tx.isFieldPresent(sfSponsorSignature) || tx.isFieldPresent(sfCounterpartySignature));
+    auto const sigbadFlag = oldPrefixSig ? kSfSigbadOldPrefix : kSfSigbad;
+    auto const siggoodFlag = oldPrefixSig ? kSfSiggoodOldPrefix : kSfSiggood;
+
+    if (any(flags & sigbadFlag))
     {
         // Signature is known bad
         return {Validity::SigBad, "Transaction has bad signature."};
     }
 
-    if (!any(flags & kSfSiggood))
+    if (!any(flags & siggoodFlag))
     {
         auto const sigVerify = tx.checkSign(rules);
         if (!sigVerify)
         {
-            router.setFlags(id, kSfSigbad);
+            router.setFlags(id, sigbadFlag);
             return {Validity::SigBad, sigVerify.error()};
         }
-        router.setFlags(id, kSfSiggood);
+        router.setFlags(id, siggoodFlag);
     }
 
     // Signature is now known good
@@ -94,6 +140,19 @@ checkValidity(HashRouter& router, STTx const& tx, Rules const& rules)
 void
 forceValidity(HashRouter& router, uint256 const& txid, Validity validity)
 {
+    // Callers reach here when they deliberately skip signature verification,
+    // such as a cluster peer that trusts its neighbor's checks, or a
+    // configuration that turns signature checks off. Nothing was verified, so
+    // there is no prefix era to record. Mark both of checkValidity's signature
+    // slots good: otherwise the forced verdict is ignored for a role-signature
+    // transaction until fixCleanup3_4_0 is enabled, and the signature the
+    // caller meant to skip gets verified after all. Marking both cannot leak a
+    // verdict across eras, because no verdict was reached, and this is the only
+    // place the distinction can be recorded: kSfSiggood alone does not say
+    // whether checkValidity verified a post-fix signature or a caller forced
+    // the result. An already cached bad verdict still wins, since checkValidity
+    // tests its bad flag first. Drop kSfSiggoodOldPrefix when Cleanup3_4_0 is
+    // retired.
     HashRouterFlags flags = HashRouterFlags::UNDEFINED;
     switch (validity)
     {
@@ -101,7 +160,7 @@ forceValidity(HashRouter& router, uint256 const& txid, Validity validity)
             flags |= kSfLocalgood;
             [[fallthrough]];
         case Validity::SigGoodOnly:
-            flags |= kSfSiggood;
+            flags |= kSfSiggood | kSfSiggoodOldPrefix;
             [[fallthrough]];
         case Validity::SigBad:
             // would be silly to call directly
diff --git a/src/libxrpl/tx/invariants/FreezeInvariant.cpp b/src/libxrpl/tx/invariants/FreezeInvariant.cpp
index 0a604d4c39..272e52f09a 100644
--- a/src/libxrpl/tx/invariants/FreezeInvariant.cpp
+++ b/src/libxrpl/tx/invariants/FreezeInvariant.cpp
@@ -4,7 +4,9 @@
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -17,6 +19,7 @@
 #include 
 
 #include 
+#include 
 #include 
 
 namespace xrpl {
@@ -73,6 +76,21 @@ TransfersNotFrozen::finalize(
      *           view.rules().enabled(fixFreezeExploit);
      */
     [[maybe_unused]] bool const enforce = view.rules().enabled(featureDeepFreeze);
+    bool const fixOverrideFreeze = view.rules().enabled(fixCleanup3_4_0);
+
+    /*
+     * XLS-0066: a broker must be able to default an already-late loan
+     * regardless of the vault asset's freeze state. LoanManage::defaultLoan
+     * moves First-Loss Capital from the broker to the vault pseudo-account via
+     * accountSend, which transits through the issuer in two hops (see
+     * getLoanDefaultFreezeExemptAccounts), so a frozen issuer would otherwise
+     * trip this invariant on either hop. Gated behind fixCleanup3_4_0, and
+     * scoped to exactly the issuer/broker and issuer/vault lines involved for
+     * the vault's own currency, so ledgers without the amendment (or an
+     * unrelated frozen currency/line touched by the same transaction) keep
+     * the current (blocking) behavior.
+     */
+    auto const loanDefaultAccounts = getLoanDefaultFreezeExemptAccounts(view, tx);
 
     return std::ranges::all_of(balanceChanges_, [&](auto const& entry) {
         auto const& [issue, changes] = entry;
@@ -90,7 +108,8 @@ TransfersNotFrozen::finalize(
             return !enforce;
         }
 
-        return validateIssuerChanges(issuerSle, changes, tx, j, enforce);
+        return validateIssuerChanges(
+            issuerSle, changes, tx, j, enforce, fixOverrideFreeze, loanDefaultAccounts);
     });
 }
 
@@ -199,7 +218,9 @@ TransfersNotFrozen::validateIssuerChanges(
     IssuerChanges const& changes,
     STTx const& tx,
     beast::Journal const& j,
-    bool enforce)
+    bool enforce,
+    bool fixOverrideFreeze,
+    std::optional const& loanDefaultAccounts)
 {
     if (!issuer)
     {
@@ -225,7 +246,15 @@ TransfersNotFrozen::validateIssuerChanges(
         {
             bool const high = change.line->at(sfLowLimit).getIssuer() == issuer->at(sfAccount);
 
-            if (!validateFrozenState(change, high, tx, j, enforce, globalFreeze))
+            if (!validateFrozenState(
+                    change,
+                    high,
+                    tx,
+                    j,
+                    enforce,
+                    globalFreeze,
+                    fixOverrideFreeze,
+                    loanDefaultAccounts))
             {
                 return false;
             }
@@ -241,29 +270,61 @@ TransfersNotFrozen::validateFrozenState(
     STTx const& tx,
     beast::Journal const& j,
     bool enforce,
-    bool globalFreeze)
+    bool globalFreeze,
+    bool fixOverrideFreeze,
+    std::optional const& loanDefaultAccounts)
 {
     bool const freeze =
         change.balanceChangeSign < 0 && change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze);
     bool const deepFreeze = change.line->isFlag(high ? lsfLowDeepFreeze : lsfHighDeepFreeze);
     bool const frozen = globalFreeze || deepFreeze || freeze;
 
-    bool const isAMMLine = change.line->isFlag(lsfAMMNode);
-
     if (!frozen)
     {
         return true;
     }
 
-    // AMMClawbacks are allowed to override some freeze rules
-    if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, OverrideFreeze))
+    // Pre-fixCleanup3_4_0: the isAMMLine check incorrectly blocked clawback on
+    // individually-frozen or deep-frozen AMM trust lines.
+    // Post-fixCleanup3_4_0: AMMClawbacks are allowed to override all freeze types.
+    bool const isAMMLine = change.line->isFlag(lsfAMMNode);
+    if ((fixOverrideFreeze || !isAMMLine || globalFreeze) &&
+        hasPrivilege(tx, Privilege::OverrideFreeze))
     {
         JLOG(j.debug()) << "Invariant check allowing funds to be moved "
                         << (change.balanceChangeSign > 0 ? "to" : "from")
-                        << " a frozen trustline for AMMClawback " << tx.getTransactionID();
+                        << " a frozen trustline for a freeze privileged transaction "
+                        << tx.getTransactionID();
         return true;
     }
 
+    // XLS-0066: LoanManage::defaultLoan's transfer is exempt from freeze (see
+    // finalize()). Since neither the broker nor vault pseudo-account is the
+    // asset's issuer, accountSend routes it as two hops through the issuer
+    // (broker -> issuer, issuer -> vault), so both the issuer/broker and
+    // issuer/vault lines are exempt -- but only for the vault's own currency,
+    // so an unrelated frozen line (a different currency, or one touched by
+    // the same transaction for some other reason) is still caught.
+    if (loanDefaultAccounts && loanDefaultAccounts->asset.holds() &&
+        loanDefaultAccounts->asset.get().currency ==
+            change.line->at(sfBalance).get().currency)
+    {
+        AccountID const lowAcct = change.line->at(sfLowLimit).getIssuer();
+        AccountID const highAcct = change.line->at(sfHighLimit).getIssuer();
+        auto const& accts = *loanDefaultAccounts;
+        auto const isPair = [&](AccountID const& a, AccountID const& b) {
+            return (lowAcct == a && highAcct == b) || (lowAcct == b && highAcct == a);
+        };
+        if (isPair(accts.issuer, accts.broker) || isPair(accts.issuer, accts.vault))
+        {
+            JLOG(j.debug()) << "Invariant check allowing funds to be moved "
+                            << (change.balanceChangeSign > 0 ? "to" : "from")
+                            << " a frozen trustline for LoanManage default "
+                            << tx.getTransactionID();
+            return true;
+        }
+    }
+
     JLOG(j.fatal()) << "Invariant failed: Attempting to move frozen funds for "
                     << tx.getTransactionID();
     // The comment above starting with "assert(enforce)" explains this assert.
diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp b/src/libxrpl/tx/invariants/InvariantCheck.cpp
index de3245fcd2..a3d4892289 100644
--- a/src/libxrpl/tx/invariants/InvariantCheck.cpp
+++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp
@@ -25,6 +25,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -40,12 +41,15 @@
 
 namespace xrpl {
 
+#pragma push_macro("UNWRAP")
+#undef UNWRAP
 #pragma push_macro("TRANSACTION")
 #undef TRANSACTION
 
-#define TRANSACTION(tag, value, name, delegable, amendment, privileges, ...) \
-    case tag: {                                                              \
-        return (privileges) & priv;                                          \
+#define UNWRAP(...) __VA_ARGS__
+#define TRANSACTION(tag, value, name, settings, ...)                                  \
+    case tag: {                                                                       \
+        return ((TxSettings UNWRAP settings).privileges & priv) != Privilege::NoPriv; \
     }
 
 bool
@@ -63,6 +67,8 @@ hasPrivilege(STTx const& tx, Privilege priv)
 
 #undef TRANSACTION
 #pragma pop_macro("TRANSACTION")
+#undef UNWRAP
+#pragma pop_macro("UNWRAP")
 
 // Returns the human-readable name of a ledger entry's type, falling back to
 // the numeric type if the format is somehow unknown.
@@ -436,7 +442,7 @@ AccountRootsNotDeleted::finalize(
     // transaction when the total AMM LP Tokens balance goes to 0.
     // A successful AccountDelete or AMMDelete MUST delete exactly
     // one account root.
-    if (hasPrivilege(tx, MustDeleteAcct) && isTesSuccess(result))
+    if (hasPrivilege(tx, Privilege::MustDeleteAcct) && isTesSuccess(result))
     {
         if (accountsDeleted_ == 1)
             return true;
@@ -457,7 +463,7 @@ AccountRootsNotDeleted::finalize(
     // A successful AMMWithdraw/AMMClawback MAY delete one account root
     // when the total AMM LP Tokens balance goes to 0. Not every AMM withdraw
     // deletes the AMM account, accountsDeleted_ is set if it is deleted.
-    if (hasPrivilege(tx, MayDeleteAcct) && isTesSuccess(result) && accountsDeleted_ == 1)
+    if (hasPrivilege(tx, Privilege::MayDeleteAcct) && isTesSuccess(result) && accountsDeleted_ == 1)
         return true;
 
     if (accountsDeleted_ == 0)
@@ -760,14 +766,15 @@ ValidNewAccountRoot::finalize(
     }
 
     // From this point on we know exactly one account was created.
-    if (hasPrivilege(tx, CreateAcct | CreatePseudoAcct) && isTesSuccess(result))
+    if (hasPrivilege(tx, Privilege::CreateAcct | Privilege::CreatePseudoAcct) &&
+        isTesSuccess(result))
     {
         bool const pseudoAccount =
             (pseudoAccount_ &&
              (view.rules().enabled(featureSingleAssetVault) ||
               view.rules().enabled(featureLendingProtocol)));
 
-        if (pseudoAccount && !hasPrivilege(tx, CreatePseudoAcct))
+        if (pseudoAccount && !hasPrivilege(tx, Privilege::CreatePseudoAcct))
         {
             JLOG(j.fatal()) << "Invariant failed: pseudo-account created by a "
                                "wrong transaction type";
@@ -1117,30 +1124,34 @@ NoModifiedUnmodifiableFields::finalize(
     ReadView const& view,
     beast::Journal const& j)
 {
-    static auto const kFieldChanged = [](auto const& before, auto const& after, auto const& field) {
+    auto const kFieldChanged = [&j, &tx](auto const& before, auto const& after, auto const& field) {
         bool const beforeField = before->isFieldPresent(field);
         bool const afterField = after->isFieldPresent(field);
-        return beforeField != afterField || (afterField && before->at(field) != after->at(field));
+        bool const changed =
+            beforeField != afterField || (afterField && before->at(field) != after->at(field));
+        if (changed)
+        {
+            JLOG(j.fatal()) << "Invariant failed: " << field.getName()
+                            << " changed on immutable ledger entry in " << tx.getTransactionID();
+        }
+        return changed;
     };
     for (auto const& slePair : changedEntries_)
     {
         auto const& before = slePair.first;
         auto const& after = slePair.second;
         auto const type = after->getType();
-        bool bad = false;
-        [[maybe_unused]] bool enforce = false;
+        // featureLendingProtocol gates enforcement, not detection: changes are
+        // always logged, but the transaction is only failed once the amendment
+        // is enabled. Type-specific field lists may add their own gates (see
+        // ltVAULT).
+        bool const enforce = view.rules().enabled(featureLendingProtocol);
+        bool bad = kFieldChanged(before, after, sfLedgerEntryType) ||
+            kFieldChanged(before, after, sfLedgerIndex);
         switch (type)
         {
             case ltLOAN_BROKER:
-                /*
-                 * We check this invariant regardless of lending protocol
-                 * amendment status, allowing for detection and logging of
-                 * potential issues even when the amendment is disabled.
-                 */
-                enforce = view.rules().enabled(featureLendingProtocol);
-                bad = kFieldChanged(before, after, sfLedgerEntryType) ||
-                    kFieldChanged(before, after, sfLedgerIndex) ||
-                    kFieldChanged(before, after, sfSequence) ||
+                bad = bad || kFieldChanged(before, after, sfSequence) ||
                     kFieldChanged(before, after, sfOwnerNode) ||
                     kFieldChanged(before, after, sfVaultNode) ||
                     kFieldChanged(before, after, sfVaultID) ||
@@ -1151,15 +1162,7 @@ NoModifiedUnmodifiableFields::finalize(
                     kFieldChanged(before, after, sfCoverRateLiquidation);
                 break;
             case ltLOAN:
-                /*
-                 * We check this invariant regardless of lending protocol
-                 * amendment status, allowing for detection and logging of
-                 * potential issues even when the amendment is disabled.
-                 */
-                enforce = view.rules().enabled(featureLendingProtocol);
-                bad = kFieldChanged(before, after, sfLedgerEntryType) ||
-                    kFieldChanged(before, after, sfLedgerIndex) ||
-                    kFieldChanged(before, after, sfSequence) ||
+                bad = bad || kFieldChanged(before, after, sfSequence) ||
                     kFieldChanged(before, after, sfOwnerNode) ||
                     kFieldChanged(before, after, sfLoanBrokerNode) ||
                     kFieldChanged(before, after, sfLoanBrokerID) ||
@@ -1177,20 +1180,59 @@ NoModifiedUnmodifiableFields::finalize(
                     kFieldChanged(before, after, sfPaymentInterval) ||
                     kFieldChanged(before, after, sfGracePeriod) ||
                     kFieldChanged(before, after, sfLoanScale);
+
+                // lsfLoanOverpayment must never toggle. lsfLoanDefault may only
+                // transition from unset to set, which combined with ValidLoan's rule that
+                // only LoanManage may change it makes the flag write-once.
+                if (view.rules().enabled(featureLendingProtocolV1_1))
+                {
+                    std::uint32_t const beforeFlags = before->getFlags();
+                    std::uint32_t const afterFlags = after->getFlags();
+                    bool const overpaymentChanged =
+                        (beforeFlags & lsfLoanOverpayment) != (afterFlags & lsfLoanOverpayment);
+                    if (overpaymentChanged)
+                    {
+                        JLOG(j.fatal()) << "Invariant failed: lsfLoanOverpayment flag "
+                                           "toggled on immutable ledger entry in "
+                                        << tx.getTransactionID();
+                    }
+                    bad = bad || overpaymentChanged;
+                    bool const defaultCleared =
+                        (beforeFlags & lsfLoanDefault) != 0 && (afterFlags & lsfLoanDefault) == 0;
+                    if (defaultCleared)
+                    {
+                        JLOG(j.fatal()) << "Invariant failed: lsfLoanDefault flag "
+                                           "cleared on immutable ledger entry in "
+                                        << tx.getTransactionID();
+                    }
+                    bad = bad || defaultCleared;
+                }
+                break;
+            case ltVAULT:
+                /*
+                 * All the fields below are only immutable from
+                 * featureLendingProtocolV1_1 onwards; some of them only exist on
+                 * V1_1 vaults. Before that amendment, sfAsset, sfAccount and
+                 * sfShareMPTID are checked by VaultInvariant instead.
+                 */
+                if (view.rules().enabled(featureLendingProtocolV1_1))
+                {
+                    bad = bad || kFieldChanged(before, after, sfVaultKind) ||
+                        kFieldChanged(before, after, sfSubscriptionDate) ||
+                        kFieldChanged(before, after, sfRedemptionDate) ||
+                        kFieldChanged(before, after, sfSequence) ||
+                        kFieldChanged(before, after, sfOwnerNode) ||
+                        kFieldChanged(before, after, sfOwner) ||
+                        kFieldChanged(before, after, sfWithdrawalPolicy) ||
+                        kFieldChanged(before, after, sfScale) ||
+                        kFieldChanged(before, after, sfLEVersion) ||
+                        kFieldChanged(before, after, sfAsset) ||
+                        kFieldChanged(before, after, sfAccount) ||
+                        kFieldChanged(before, after, sfShareMPTID);
+                }
                 break;
             default:
-                /*
-                 * We check this invariant regardless of lending protocol
-                 * amendment status, allowing for detection and logging of
-                 * potential issues even when the amendment is disabled.
-                 *
-                 * We use the lending protocol as a gate, even though
-                 * all transactions are affected because that's when it
-                 * was added.
-                 */
-                enforce = view.rules().enabled(featureLendingProtocol);
-                bad = kFieldChanged(before, after, sfLedgerEntryType) ||
-                    kFieldChanged(before, after, sfLedgerIndex);
+                break;
         }
         XRPL_ASSERT(
             !bad || enforce,
diff --git a/src/libxrpl/tx/invariants/InvariantRunner.cpp b/src/libxrpl/tx/invariants/InvariantRunner.cpp
new file mode 100644
index 0000000000..55bff2d693
--- /dev/null
+++ b/src/libxrpl/tx/invariants/InvariantRunner.cpp
@@ -0,0 +1,110 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include   // IWYU pragma: keep
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+namespace {
+
+TER
+failInvariantCheck(TER const result)
+{
+    return (result == tecINVARIANT_FAILED || result == tefINVARIANT_FAILED)
+        ? TER{tefINVARIANT_FAILED}
+        : TER{tecINVARIANT_FAILED};
+}
+
+template 
+TER
+checkInvariantsHelper(
+    ApplyContext& ctx,
+    TER const result,
+    XRPAmount const fee,
+    std::optional> txCheck,
+    std::index_sequence)
+{
+    bool allOk = true;
+
+    try
+    {
+        auto checkers = getInvariantChecks();
+
+        ctx.visit([&](uint256 const&, bool isDelete, SLE::const_ref before, SLE::const_ref after) {
+            if (txCheck)
+                txCheck->get().visitEntry(isDelete, before, after);
+            (..., std::get(checkers).visitEntry(isDelete, before, after));
+        });
+
+        if (txCheck)
+        {
+            if (!txCheck->get().finalize(ctx.tx, result, fee, ctx.view(), ctx.journal))
+            {
+                JLOG(ctx.journal.fatal())
+                    << "Transaction has failed one or more transaction invariants: "
+                    << to_string(ctx.tx.getJson(JsonOptions::Values::None));
+                allOk = false;
+            }
+        }
+
+        // Note: do not replace this logic with a `...&&` fold expression.
+        // The fold expression will only run until the first check fails (it
+        // short-circuits). While the logic is still correct, the log
+        // message won't be. Every failed invariant should write to the log,
+        // not just the first one.
+        std::array const finalizers{
+            {std::get(checkers).finalize(ctx.tx, result, fee, ctx.view(), ctx.journal)...}};
+
+        if (!std::all_of(finalizers.cbegin(), finalizers.cend(), [](auto const& b) { return b; }))
+        {
+            JLOG(ctx.journal.fatal()) << "Transaction has failed one or more global invariants: "
+                                      << to_string(ctx.tx.getJson(JsonOptions::Values::None));
+            allOk = false;
+        }
+    }
+    catch (std::exception const& ex)
+    {
+        JLOG(ctx.journal.fatal()) << "Transaction caused an exception during invariant checks"
+                                  << ", ex: " << ex.what() << ", tx: "
+                                  << to_string(ctx.tx.getJson(JsonOptions::Values::None));
+        return failInvariantCheck(result);
+    }
+
+    return allOk ? result : failInvariantCheck(result);
+}
+
+}  // namespace
+
+TER
+checkInvariants(
+    ApplyContext& ctx,
+    TER const result,
+    XRPAmount const fee,
+    std::optional> txCheck)
+{
+    XRPL_ASSERT(
+        isTesSuccess(result) || isTecClaim(result),
+        "xrpl::checkInvariants : is tesSUCCESS or tecCLAIM");
+
+    return checkInvariantsHelper(
+        ctx, result, fee, txCheck, std::make_index_sequence>{});
+}
+
+}  // namespace xrpl
diff --git a/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp b/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp
index b70c02947f..e15921b7b2 100644
--- a/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp
+++ b/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp
@@ -1,13 +1,18 @@
 #include 
 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include   // IWYU pragma: keep
 #include 
@@ -22,6 +27,24 @@ namespace xrpl {
 void
 ValidLoanBroker::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
 {
+    // Track LoanBroker deletions so finalize() can enforce:
+    //   (a) only ttLOAN_BROKER_DELETE removes a broker
+    //   (b) at most one broker is removed per transaction
+    //   (c) DebtTotal and OwnerCount were zero before deletion
+    // `before` is the pre-transaction state, which is what
+    // LoanBrokerDelete::preclaim reads. Erased trust lines and MPTokens need no
+    // special handling here: the `if (after)` branch below already records them.
+    if (isDelete && before && before->getType() == ltLOAN_BROKER)
+    {
+        if (deletedBroker_)
+        {
+            multipleBrokerDeletions_ = true;
+        }
+        else
+        {
+            deletedBroker_ = before;
+        }
+    }
     if (after)
     {
         if (after->getType() == ltLOAN_BROKER)
@@ -99,6 +122,64 @@ ValidLoanBroker::finalize(
     // Loan Brokers will not exist on ledger if the Lending Protocol amendment
     // is not enabled, so there's no need to check it.
 
+    // Deletion invariants (featureLendingProtocolV1_1). At most one
+    // LoanBroker may be removed per transaction, and only by
+    // ttLOAN_BROKER_DELETE, and only when its pre-state OwnerCount is zero and
+    // its pre-state DebtTotal is zero to the precision of the vault asset. The
+    // DebtTotal check complements ValidLoan's
+    // LoanBrokerDelete-must-not-touch-any-loan rule: even a broker that has
+    // finished paying off every loan may still hold non-zero exposure until
+    // its LoanBrokerCoverWithdraw settles, and neither state is safe to
+    // delete.
+    if (view.rules().enabled(featureLendingProtocolV1_1))
+    {
+        if (multipleBrokerDeletions_)
+        {
+            JLOG(j.fatal())
+                << "Invariant failed: more than one Loan Broker deleted in a single transaction";
+            return false;
+        }
+        if (deletedBroker_)
+        {
+            if (tx.getTxnType() != ttLOAN_BROKER_DELETE)
+            {
+                JLOG(j.fatal()) << "Invariant failed: " <<  //
+                    "Loan Broker deleted by a transaction other than LoanBrokerDelete";
+                return false;
+            }
+            // Mirror LoanBrokerDelete::preclaim, which accepts a DebtTotal
+            // that rounds to zero at the vault's AssetsTotal scale rather than
+            // requiring an exact zero. Requiring more here would turn a
+            // transaction the transactor deliberately permits into an
+            // invariant failure.
+            if (auto const debtTotal = deletedBroker_->at(sfDebtTotal); debtTotal != beast::kZero)
+            {
+                // The erased broker is also collected in brokers_, and that
+                // loop reports a missing vault, so no separate diagnostic is
+                // needed here. Without a vault there is no scale to round at,
+                // so the residue cannot be excused as dust.
+                auto const vault = view.read(keylet::vault(deletedBroker_->at(sfVaultID)));
+                if (!vault ||
+                    roundToAsset(
+                        Asset{vault->at(sfAsset)},
+                        debtTotal,
+                        getAssetsTotalScale(vault),
+                        Number::RoundingMode::TowardsZero) != beast::kZero)
+                {
+                    JLOG(j.fatal())
+                        << "Invariant failed: Loan Broker deleted with non-zero debt total";
+                    return false;
+                }
+            }
+            if (deletedBroker_->at(sfOwnerCount) != 0)
+            {
+                JLOG(j.fatal())
+                    << "Invariant failed: Loan Broker deleted with non-zero owner count";
+                return false;
+            }
+        }
+    }
+
     for (auto const& line : lines_)
     {
         for (auto const& field : {&sfLowLimit, &sfHighLimit})
@@ -142,7 +223,6 @@ ValidLoanBroker::finalize(
 
         auto const& before = broker.brokerBefore;
 
-        // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants
         // If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most
         // one node (the root), which will only hold entries for `RippleState`
         // or `MPToken` objects.
diff --git a/src/libxrpl/tx/invariants/LoanInvariant.cpp b/src/libxrpl/tx/invariants/LoanInvariant.cpp
index ce9a7c6e03..b34d7088be 100644
--- a/src/libxrpl/tx/invariants/LoanInvariant.cpp
+++ b/src/libxrpl/tx/invariants/LoanInvariant.cpp
@@ -1,23 +1,39 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
+#include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include   // IWYU pragma: keep
 #include 
 #include 
+#include 
 #include 
 
+#include 
+
 namespace xrpl {
 
 void
 ValidLoan::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
 {
-    if (after && after->getType() == ltLOAN)
+    // Classify here, but leave the decision about which checks apply to
+    // finalize(), which is the only place that can see the Rules.
+    if (isDelete)
+    {
+        if (before && before->getType() == ltLOAN)
+            deletedLoans_.emplace_back(before, after);
+    }
+    else if (after && after->getType() == ltLOAN)
     {
         loans_.emplace_back(before, after);
     }
@@ -26,7 +42,7 @@ ValidLoan::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after
 bool
 ValidLoan::finalize(
     STTx const& tx,
-    TER const,
+    TER const result,
     XRPAmount const,
     ReadView const& view,
     beast::Journal const& j)
@@ -34,8 +50,50 @@ ValidLoan::finalize(
     // Loans will not exist on ledger if the Lending Protocol amendment
     // is not enabled, so there's no need to check it.
 
+    auto const txType = tx.getTxnType();
+    bool const lpV11Enabled = view.rules().enabled(featureLendingProtocolV1_1);
+
+    // Without featureLendingProtocolV1_1 an erased Loan is subject to the same
+    // per-entry checks as any modified Loan. From V1_1 onward it is only subject
+    // to the ttLOAN_DELETE check below.
+    if (!lpV11Enabled)
+        loans_.insert(loans_.end(), deletedLoans_.begin(), deletedLoans_.end());
+
+    // Ledger entry validation checks.
     for (auto const& [before, after] : loans_)
     {
+        // A closed-ended vault must not accept a loan whose final scheduled payment falls fewer
+        // than kLoanRedemptionBuffer seconds before the vault's RedemptionDate. This mirrors the
+        // LoanSet::preclaim gate and only fires on loan creation; once the loan exists, its
+        // StartDate / PaymentInterval are immutable and PaymentRemaining only decreases, so the
+        // bound is preserved.
+        if (!before && isTesSuccess(result))
+        {
+            auto const broker = view.read(keylet::loanBroker(after->at(sfLoanBrokerID)));
+            if (broker)
+            {
+                auto const vault = view.read(keylet::vault(broker->at(sfVaultID)));
+                // We don't check for LendingProtocolV1_1 amendment because a ClosedEnded Vault will
+                // not exist without the amendment enabled
+                if (vault && getVaultKind(vault) == VaultKind::ClosedEnded)
+                {
+                    std::uint32_t const startDate = after->at(sfStartDate);
+                    std::uint32_t const interval = after->at(sfPaymentInterval);
+                    std::uint32_t const remaining = after->at(sfPaymentRemaining);
+                    std::uint32_t const redemption = vault->at(sfRedemptionDate);
+                    if (std::uint64_t{startDate} + (std::uint64_t{interval} * remaining) +
+                            kLoanRedemptionBuffer >
+                        redemption)
+                    {
+                        JLOG(j.fatal()) << "Invariant failed: closed-ended loan final payment "
+                                           "must precede RedemptionDate by at least "
+                                           "kLoanRedemptionBuffer";
+                        return false;
+                    }
+                }
+            }
+        }
+
         // https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants
         // If `Loan.PaymentRemaining = 0` then the loan MUST be fully paid off
         if (after->at(sfPaymentRemaining) == 0 &&
@@ -57,7 +115,11 @@ ValidLoan::finalize(
             JLOG(j.fatal()) << "Invariant failed: Fully paid off Loan still has payments remaining";
             return false;
         }
-        if (before && (before->isFlag(lsfLoanOverpayment) != after->isFlag(lsfLoanOverpayment)))
+
+        // From featureLendingProtocolV1_1 onwards this flag is immutable by way of
+        // NoModifiedUnmodifiableFields.
+        if (!lpV11Enabled && before &&
+            (before->isFlag(lsfLoanOverpayment) != after->isFlag(lsfLoanOverpayment)))
         {
             JLOG(j.fatal()) << "Invariant failed: Loan Overpayment flag changed";
             return false;
@@ -89,6 +151,125 @@ ValidLoan::finalize(
                 return false;
             }
         }
+        if (lpV11Enabled)
+        {
+            // Only LoanSet may create a loan.
+            if (!before && txType != ttLOAN_SET)
+            {
+                JLOG(j.fatal()) << "Invariant failed: Loan created by a transaction "
+                                   "other than LoanSet";
+                return false;
+            }
+
+            if (after->at(sfPaymentRemaining) == 0 &&
+                after->at(~sfNextPaymentDueDate).value_or(0) != 0)
+            {
+                JLOG(j.fatal()) << "Invariant failed: Loan with zero payments must have zero next "
+                                   "payment due date";
+                return false;
+            }
+
+            if (before)
+            {
+                bool const wasImpaired = before->isFlag(lsfLoanImpaired);
+                bool const isImpaired = after->isFlag(lsfLoanImpaired);
+                bool const wasDefaulted = before->isFlag(lsfLoanDefault);
+                bool const isDefaulted = after->isFlag(lsfLoanDefault);
+
+                if (wasImpaired != isImpaired && txType != ttLOAN_MANAGE && txType != ttLOAN_PAY)
+                {
+                    JLOG(j.fatal()) << "Invariant failed: lsfLoanImpaired changed "
+                                       "outside LoanManage or LoanPay";
+                    return false;
+                }
+                if (wasDefaulted != isDefaulted && txType != ttLOAN_MANAGE)
+                {
+                    JLOG(j.fatal()) << "Invariant failed: lsfLoanDefault changed "
+                                       "outside LoanManage";
+                    return false;
+                }
+            }
+
+            // A loan must reference a live loan broker, and that broker must
+            // reference a live vault; otherwise the loan is orphaned and its
+            // balances have no counterparty on the ledger.
+            auto const brokerSle = view.read(keylet::loanBroker(after->at(sfLoanBrokerID)));
+            if (!brokerSle)
+            {
+                JLOG(j.fatal()) << "Invariant failed: Loan broker does not exist";
+                return false;
+            }
+            auto const vaultSle = view.read(keylet::vault(brokerSle->at(sfVaultID)));
+            if (!vaultSle)
+            {
+                JLOG(j.fatal()) << "Invariant failed: Loan broker vault does not exist";
+                return false;
+            }
+
+            // Interest due (the total value owed less principal and management fee)
+            // must never be negative. TotalValueOutstanding, PrincipalOutstanding and
+            // ManagementFeeOutstanding are each independently rounded to sfLoanScale
+            // by the accounting code, so their difference can carry one unit of
+            // quantization noise even when the underlying flow is correct. Absorb
+            // one unit at that scale, matching the pattern used in ValidVault.
+            auto const interestDue = after->at(sfTotalValueOutstanding) -
+                after->at(sfPrincipalOutstanding) - after->at(sfManagementFeeOutstanding);
+
+            // Only IOU amounts can accumulate STAmount quantization noise. For integral-domain
+            // assets (XRP/MPT) enforce the boundary strictly.
+            bool const integral = Asset{vaultSle->at(sfAsset)}.integral();
+
+            Number const tolerance = integral ? Number{} : Number{-1, after->at(sfLoanScale)};
+            if (interestDue < tolerance)
+            {
+                JLOG(j.fatal()) << "Invariant failed: Loan interest due is negative";
+                return false;
+            }
+
+            // Transaction success post-conditions. A successful loan pay makes at least
+            // one scheduled payment, so a loan left with payments still outstanding
+            // must show that payment in its balance and schedule. A payment that clears
+            // the loan outright instead drives PaymentRemaining to zero, which the
+            // fully-paid-off and zero due-date checks above pin.
+            if (isTesSuccess(result) && txType == ttLOAN_PAY)
+            {
+                if (before && after->at(sfPaymentRemaining) != 0)
+                {
+                    if (!(after->at(sfPrincipalOutstanding) < before->at(sfPrincipalOutstanding)))
+                    {
+                        JLOG(j.fatal()) << "Invariant failed: loan pay must strictly decrease "
+                                           "PrincipalOutstanding on a non-full-repayment";
+                        return false;
+                    }
+                    if (!(after->at(sfPaymentRemaining) < before->at(sfPaymentRemaining)))
+                    {
+                        JLOG(j.fatal()) << "Invariant failed: loan pay must decrease "
+                                           "PaymentRemaining on a non-full-repayment";
+                        return false;
+                    }
+
+                    std::uint32_t const beforeDue = before->at(~sfNextPaymentDueDate).value_or(0);
+                    std::uint32_t const afterDue = after->at(~sfNextPaymentDueDate).value_or(0);
+                    std::uint32_t const interval = after->at(sfPaymentInterval);
+                    if (afterDue <= beforeDue || interval == 0 ||
+                        (afterDue - beforeDue) % interval != 0)
+                    {
+                        JLOG(j.fatal()) << "Invariant failed: loan pay must advance "
+                                           "NextPaymentDueDate by a positive multiple of "
+                                           "PaymentInterval on a non-full-repayment";
+                        return false;
+                    }
+                }
+            }
+        }
+    }
+
+    // Only LoanDelete may delete a loan.
+    if (lpV11Enabled && txType != ttLOAN_DELETE && !deletedLoans_.empty())
+    {
+        JLOG(j.fatal()) << "Invariant failed: Loan deleted by a transaction "
+                           "other than LoanDelete";
+        return false;
     }
     return true;
 }
diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp
index 77c5ad781e..e38e8f2b93 100644
--- a/src/libxrpl/tx/invariants/MPTInvariant.cpp
+++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp
@@ -7,6 +7,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -143,6 +144,8 @@ ValidMPTIssuance::finalize(
     //     must not dangle outside that controlled lifecycle.
     if (rules.enabled(fixCleanup3_2_0))
     {
+        // Not an amendment gate like the same-named flags below, just an
+        // accumulator, so that every violation gets logged before returning.
         bool invariantPasses = true;
         if (referenceHoldingMutated_)
         {
@@ -208,7 +211,7 @@ ValidMPTIssuance::finalize(
         }
 
         auto const txnType = tx.getTxnType();
-        if (hasPrivilege(tx, CreateMptIssuance))
+        if (hasPrivilege(tx, Privilege::CreateMptIssuance))
         {
             if (mptIssuancesCreated_ == 0)
             {
@@ -229,7 +232,7 @@ ValidMPTIssuance::finalize(
             return mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0;
         }
 
-        if (hasPrivilege(tx, DestroyMptIssuance))
+        if (hasPrivilege(tx, Privilege::DestroyMptIssuance))
         {
             if (mptIssuancesDeleted_ == 0)
             {
@@ -256,7 +259,8 @@ ValidMPTIssuance::finalize(
         // non-amendment-gated side effects.
         bool const enforceEscrowFinish = (txnType == ttESCROW_FINISH) &&
             (rules.enabled(featureSingleAssetVault) || lendingProtocolEnabled);
-        if (hasPrivilege(tx, MustAuthorizeMpt | MayAuthorizeMpt) || enforceEscrowFinish)
+        if (hasPrivilege(tx, Privilege::MustAuthorizeMpt | Privilege::MayAuthorizeMpt) ||
+            enforceEscrowFinish)
         {
             bool const submittedByIssuer = tx.isFieldPresent(sfHolder);
 
@@ -272,7 +276,7 @@ ValidMPTIssuance::finalize(
                                    "succeeded but deleted issuances";
                 return false;
             }
-            if (mptV2Enabled && hasPrivilege(tx, MayAuthorizeMpt) &&
+            if (mptV2Enabled && hasPrivilege(tx, Privilege::MayAuthorizeMpt) &&
                 (txnType == ttAMM_WITHDRAW || txnType == ttAMM_CLAWBACK))
             {
                 if (submittedByIssuer && txnType == ttAMM_WITHDRAW && mptokensCreated_ > 0)
@@ -282,12 +286,13 @@ ValidMPTIssuance::finalize(
                                        "but created bad number of mptokens";
                     return false;
                 }
-                //  At most one MPToken may be created on withdraw/clawback since:
+                //  At most two MPToken may be created on withdraw/clawback since:
                 //  - Liquidity Provider must have at least one token in order
-                //    participate in AMM pool liquidity.
+                //    participate in AMM pool liquidity or have LPTokens only.
                 //  - At most two MPTokens may be deleted if AMM pool, which has exactly
                 //    two tokens, is empty after withdraw/clawback.
-                if (mptokensCreated_ > 1 || mptokensDeleted_ > 2)
+                SOMETIMES(mptokensCreated_ == 2, "AMM withdraw/clawback recreated two MPTokens");
+                if (mptokensCreated_ > 2 || mptokensDeleted_ > 2)
                 {
                     JLOG(j.fatal()) << "Invariant failed: MPT authorize  succeeded "
                                        "but created/deleted bad number of mptokens";
@@ -307,7 +312,7 @@ ValidMPTIssuance::finalize(
                 return false;
             }
             else if (
-                !submittedByIssuer && hasPrivilege(tx, MustAuthorizeMpt) &&
+                !submittedByIssuer && hasPrivilege(tx, Privilege::MustAuthorizeMpt) &&
                 (mptokensCreated_ + mptokensDeleted_ != 1))
             {
                 // if the holder submitted this tx, then a mptoken must be
@@ -320,7 +325,7 @@ ValidMPTIssuance::finalize(
             return true;
         }
 
-        if (hasPrivilege(tx, MayCreateMpt))
+        if (hasPrivilege(tx, Privilege::MayCreateMpt))
         {
             bool const submittedByIssuer = tx.isFieldPresent(sfHolder);
 
@@ -375,7 +380,7 @@ ValidMPTIssuance::finalize(
             return true;
         }
 
-        if (hasPrivilege(tx, MayDeleteMpt) &&
+        if (hasPrivilege(tx, Privilege::MayDeleteMpt) &&
             ((txnType == ttAMM_DELETE && mptokensDeleted_ <= 2) || mptokensDeleted_ == 1) &&
             mptokensCreated_ == 0 && mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0)
             return true;
@@ -472,7 +477,9 @@ ValidMPTBalanceChanges::finalize(
     ReadView const& view,
     beast::Journal const& j)
 {
-    if (isTesSuccess(result))
+    auto const fix340Enabled = view.rules().enabled(fixCleanup3_4_0);
+
+    if (isTesSuccess(result) || fix340Enabled)
     {
         // Confidential transactions are validated by ValidConfidentialMPToken.
         // They modify encrypted fields and sfConfidentialOutstandingAmount
@@ -484,7 +491,9 @@ ValidMPTBalanceChanges::finalize(
             return true;
         }
 
-        bool const invariantPasses = !view.rules().enabled(featureMPTokensV2);
+        // Returned when a violation is found below, so this is the log-only
+        // condition. Either amendment makes the checks enforcing.
+        auto const invariantPasses = !(view.rules().enabled(featureMPTokensV2) || fix340Enabled);
         if (overflow_)
         {
             JLOG(j.fatal()) << "Invariant failed: OutstandingAmount overflow";
@@ -508,6 +517,18 @@ ValidMPTBalanceChanges::finalize(
                                 << " " << data.mptAmount;
                 return invariantPasses;
             }
+
+            // A failed transaction must not have moved MPT value; the check
+            // above ties mptAmount to the OutstandingAmount delta. No result
+            // code is exempt: on any tec the transactor discards the view and
+            // re-applies only offer, trust line, NFT offer and credential
+            // deletions (Transactor::typesForResult), none of which touch MPTs.
+            if (!isTesSuccess(result) && data.mptAmount != 0)
+            {
+                JLOG(j.fatal()) << "Invariant failed: OutstandingAmount balance changed on failure "
+                                << tx.getTxnType() << " " << result;
+                return invariantPasses;
+            }
         }
     }
 
@@ -803,6 +824,14 @@ ValidMPTTransfer::visitEntry(
 
     if (after)
         update(*after, false);
+
+    // Record whether every touched AccountRoot was a pseudo-account BEFORE
+    // the transaction applied (true and false). A transaction that erases a
+    // pseudo-account (and moves MPT out of it) in the same transaction leaves
+    // no trace of its pseudo-account status in the post-transaction view
+    // isAuthorized() sees at finalize() time.
+    if (before && before->getType() == ltACCOUNT_ROOT)
+        pseudoAccountsBefore_[before->at(sfAccount)] = isPseudoAccount(before);
 }
 
 bool
@@ -815,10 +844,19 @@ ValidMPTTransfer::isAuthorized(
     // Pseudo-accounts (Vault, LoanBroker, AMM) hold assets on behalf of their
     // participants and are implicitly authorized for any MPT they hold,
     // including vault shares whose underlying asset would otherwise require
-    // auth.  Exempt them here rather than relying on requireAuth: the recursive
+    // auth. Exempt them here rather than relying on requireAuth: the recursive
     // share -> underlying descent in requireAuth fails for a pseudo-account
     // that holds the share but not the underlying.
-    if (isPseudoAccount(view, holder, {&sfVaultID, &sfLoanBrokerID, &sfAMMID}))
+    //
+    // Use the pre-transaction classification for any account this
+    // transaction touched (pseudoAccountsBefore_): the post-transaction view
+    // is wrong for an account this same transaction erased. Untouched
+    // accounts aren't in the map, so fall back to the current view, which is
+    // still accurate for them since nothing changed.
+    auto const pseudoIt = pseudoAccountsBefore_.find(holder);
+    bool const isPseudo =
+        pseudoIt != pseudoAccountsBefore_.end() ? pseudoIt->second : isPseudoAccount(view, holder);
+    if (isPseudo)
         return true;
 
     auto const key = keylet::mptoken(mptid, holder);
@@ -831,14 +869,22 @@ ValidMPTTransfer::isAuthorized(
 bool
 ValidMPTTransfer::finalize(
     STTx const& tx,
-    TER const,
+    TER const result,
     XRPAmount const,
     ReadView const& view,
     beast::Journal const& j)
 {
-    if (hasPrivilege(tx, OverrideFreeze))
+    if (hasPrivilege(tx, Privilege::OverrideFreeze))
         return true;
 
+    // XLS-0066: a broker must be able to default an already-late loan
+    // regardless of the vault asset's lock state. Gated behind
+    // fixCleanup3_4_0, and scoped below to exactly the broker/vault
+    // pseudo-accounts and the vault's own MPT issuance -- see
+    // FreezeInvariant.cpp's TransfersNotFrozen::finalize for the IOU-side
+    // equivalent and rationale.
+    auto const loanDefaultAccounts = getLoanDefaultFreezeExemptAccounts(view, tx);
+
     // DEX transactions (AMM[Create,Deposit], cross-currency payments, offer creates) are
     // subject to the MPTCanTrade flag in addition to the standard transfer rules.
     // A payment is only DEX if it is a cross-currency payment.
@@ -854,9 +900,19 @@ ValidMPTTransfer::finalize(
         return txnType == ttAMM_CREATE || txnType == ttAMM_DEPOSIT || txnType == ttOFFER_CREATE;
     }();
 
-    // Only enforce once MPTokensV2 is enabled to preserve consensus with non-V2 nodes.
-    // Log invariant failure error even if MPTokensV2 is disabled.
-    auto const invariantPasses = !view.rules().enabled(featureMPTokensV2);
+    auto const fix340Enabled = view.rules().enabled(fixCleanup3_4_0);
+    // Returned when a violation is found below, so this is the log-only
+    // condition. Either amendment makes the checks enforcing.
+    auto const invariantPasses = !(view.rules().enabled(featureMPTokensV2) || fix340Enabled);
+
+    // A failed transaction must not persist an MPToken deletion. Pre-loop
+    // because deletedAuthorized_ is not issuance-scoped and orphans continue.
+    if (fix340Enabled && !isTesSuccess(result) && !deletedAuthorized_.empty())
+    {
+        JLOG(j.fatal()) << "Invariant failed: MPToken deleted on failure " << txnType << " "
+                        << result;
+        return invariantPasses;
+    }
 
     for (auto const& [mptID, values] : amount_)
     {
@@ -866,6 +922,20 @@ ValidMPTTransfer::finalize(
         auto const sleIssuance = view.read(keylet::mptokenIssuance(mptID));
         if (!sleIssuance)
         {
+            // MPTokenIssuanceDestroy only requires a zero OutstandingAmount, so
+            // an orphaned MPToken can outlive its issuance and be cleaned up
+            // later by a transaction of any type. There are no transfer rules
+            // left to check, but its balance is zero and nothing can raise it,
+            // so any change other than deletion is a bug.
+            for (auto const& [account, value] : values)
+            {
+                if (value.amtAfter.has_value() && value.amtBefore.value_or(0) != *value.amtAfter)
+                {
+                    JLOG(j.fatal()) << "Invariant failed: orphaned MPToken balance changed "
+                                    << txnType << " " << result;
+                    return invariantPasses;
+                }
+            }
             continue;
         }
 
@@ -880,6 +950,13 @@ ValidMPTTransfer::finalize(
         auto const canTrade = sleIssuance->isFlag(lsfMPTCanTrade);
         auto const reqAuth = sleIssuance->isFlag(lsfMPTRequireAuth);
 
+        // This issuance is the LoanManage default's own vault asset, so the
+        // broker/vault freeze exemption applies to it -- an unrelated MPT
+        // issuance the same accounts happen to hold is still caught.
+        bool const isLoanDefaultAsset = loanDefaultAccounts &&
+            loanDefaultAccounts->asset.holds() &&
+            loanDefaultAccounts->asset.get().getMptID() == mptID;
+
         for (auto const& [account, value] : values)
         {
             // Classify each account as a sender or receiver based on whether their MPTAmount
@@ -898,8 +975,15 @@ ValidMPTTransfer::finalize(
 
                 // Check once: if any involved account is frozen, the whole issuance transfer is
                 // considered frozen. Only need to check for frozen if there is a transfer of funds.
+                //
+                // The LoanManage default exemption only waives the frozen check, and only for
+                // the specific broker/vault pseudo-accounts identified above -- authorization is
+                // still enforced for them, and both checks still apply to every other account.
+                bool const exemptFromFreeze = isLoanDefaultAsset && loanDefaultAccounts &&
+                    (account == loanDefaultAccounts->broker ||
+                     account == loanDefaultAccounts->vault);
                 if (!invalidTransfer &&
-                    (isFrozen(view, account, MPTIssue{mptID}) ||
+                    ((!exemptFromFreeze && isFrozen(view, account, *sleIssuance)) ||
                      !isAuthorized(view, mptID, account, reqAuth)))
                 {
                     invalidTransfer = true;
@@ -915,6 +999,16 @@ ValidMPTTransfer::finalize(
             JLOG(j.fatal()) << "Invariant failed: invalid MPToken transfer between holders";
             return invariantPasses;
         }
+
+        // A failed transaction must not have changed a holder's balance. One
+        // side is enough, unlike the transfer check above, so this also catches
+        // a lock/unlock moving value between sfMPTAmount and sfLockedAmount.
+        if (fix340Enabled && !isTesSuccess(result) && (senders > 0 || receivers > 0))
+        {
+            JLOG(j.fatal()) << "Invariant failed: MPToken balance changed on failure " << txnType
+                            << " " << result;
+            return invariantPasses;
+        }
     }
 
     return true;
diff --git a/src/libxrpl/tx/invariants/NFTInvariant.cpp b/src/libxrpl/tx/invariants/NFTInvariant.cpp
index f935f893cc..92063fc183 100644
--- a/src/libxrpl/tx/invariants/NFTInvariant.cpp
+++ b/src/libxrpl/tx/invariants/NFTInvariant.cpp
@@ -208,7 +208,7 @@ NFTokenCountTracking::finalize(
     ReadView const& view,
     beast::Journal const& j) const
 {
-    if (!hasPrivilege(tx, ChangeNftCounts))
+    if (!hasPrivilege(tx, Privilege::ChangeNftCounts))
     {
         if (beforeMintedTotal_ != afterMintedTotal_)
         {
diff --git a/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp b/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp
index 44f623f284..5c53552a3f 100644
--- a/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp
+++ b/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp
@@ -7,6 +7,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -18,8 +19,13 @@
 namespace xrpl {
 
 void
-ValidPermissionedDEX::visitEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after)
+ValidPermissionedDEX::visitEntry(bool isDelete, SLE::const_ref, SLE::const_ref after)
 {
+    // Post-fixCleanup3_4_0: skip when after is null (defensive).
+    // Pre-amendment: original after-only path via the `if (after && ...)` checks below.
+    if (isFeatureEnabled(fixCleanup3_4_0) && !after)
+        return;
+
     auto trackDomain = [this, isDelete](uint256 const& domain) {
         domainsOld_.insert(domain);
         if (!isDelete)
diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp b/src/libxrpl/tx/invariants/VaultInvariant.cpp
index c577fdf356..69c3ce92e0 100644
--- a/src/libxrpl/tx/invariants/VaultInvariant.cpp
+++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp
@@ -3,9 +3,11 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -19,16 +21,33 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
 namespace xrpl {
 
+namespace {
+
+/*
+ * True iff the recorded sfVaultKind identifies a closed-ended vault.
+ * Centralizes the presence + enum-value check used by the phase-gate
+ * invariants below.
+ */
+[[nodiscard]] bool
+isClosedEnded(std::optional const& vaultKind)
+{
+    return vaultKind && *vaultKind == std::to_underlying(VaultKind::ClosedEnded);
+}
+
+}  // namespace
+
 ValidVault::Vault
 ValidVault::Vault::make(SLE const& from)
 {
@@ -44,6 +63,9 @@ ValidVault::Vault::make(SLE const& from)
     self.assetsAvailable = from.at(sfAssetsAvailable);
     self.assetsMaximum = from.at(sfAssetsMaximum);
     self.lossUnrealized = from.at(sfLossUnrealized);
+    self.vaultKind = from[~sfVaultKind];
+    self.subscriptionDate = from[~sfSubscriptionDate];
+    self.redemptionDate = from[~sfRedemptionDate];
     return self;
 }
 
@@ -214,21 +236,57 @@ ValidVault::deltaAssets(AccountID const& id) const
         vaultAsset.value());
 }
 
+std::optional
+ValidVault::feePayerAccountRoot(ReadView const& view, STTx const& tx)
+{
+    auto const feePayer = Transactor::getFeePayer(view, tx);
+    if (feePayer.type == FeePayerType::SponsorPreFunded)
+        return std::nullopt;
+    return feePayer.id;
+}
+
 std::optional
-ValidVault::deltaAssetsTxAccount(STTx const& tx, XRPAmount fee) const
+ValidVault::deltaAssetsForParty(
+    ReadView const& view,
+    AccountID const& id,
+    STTx const& tx,
+    XRPAmount fee,
+    bool fix340Enabled) const
 {
     auto const& vaultAsset = afterVault_[0].asset;
-    auto ret = deltaAssets(tx[sfAccount]);
+    auto ret = deltaAssets(id);
     if (!ret.has_value() || !vaultAsset.native())
         return ret;
 
-    // Only add the fee back if tx[sfAccount] actually paid it. When the fee is
-    // paid by someone else (a delegate or a fee sponsor), the
-    // account's XRP balance moved only by the vault amount.
-    if (tx.getFeePayerID() != tx[sfAccount])
-        return ret;
+    if (!fix340Enabled)
+    {
+        // Legacy behaviour: only tx[sfAccount] was ever considered for a fee
+        // correction, and only when STTx::getFeePayerID identified it as the
+        // fee payer (which is never true for a sponsor, since
+        // self-sponsorship is disallowed). After that sender-only correction
+        // a zero delta is collapsed to absence; if the correction does not
+        // apply, a present-zero is returned as-is.
+        if (id != tx[sfAccount] || tx.getFeePayerID() != id)
+            return ret;
 
-    ret->delta += fee.drops();
+        ret->delta += fee.drops();
+        if (ret->delta == kZero)
+            return std::nullopt;
+
+        return ret;
+    }
+
+    // Add the fee back only onto the AccountRoot that actually paid it: an
+    // ordinary sender, a delegate, or a co-signed fee sponsor -- but never a
+    // pre-funded sponsorship, whose fee is drawn from the ltSponsorship
+    // object rather than the sponsor's own XRP balance.
+    if (auto const payer = feePayerAccountRoot(view, tx); payer && *payer == id)
+        ret->delta += fee.drops();
+
+    // Normalize an economically zero delta to absence regardless of who (if
+    // anyone) paid the fee, so a touched-but-unchanged AccountRoot (e.g. the
+    // sender in a third-party withdrawal, touched only for sequence/ticket
+    // processing) is never misread as a second payout recipient.
     if (ret->delta == kZero)
         return std::nullopt;
 
@@ -254,6 +312,76 @@ ValidVault::isVaultEmpty(Vault const& vault)
     return vault.assetsAvailable == 0 && vault.assetsTotal == 0;
 }
 
+bool
+ValidVault::finalizeLoanSet(ReadView const& view, beast::Journal const& j) const
+{
+    if (afterVault_.empty())
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::ValidVault::finalizeLoanSet : vault exists");
+        return false;
+        // LCOV_EXCL_STOP
+    }
+
+    auto const& afterVault = afterVault_[0];
+
+    // Loan origination against a closed-ended vault is only permitted while the vault is in the
+    // Investment phase - strictly past SubscriptionDate and before RedemptionDate. Open-ended
+    // vaults have NoPhase and are unaffected.
+    auto const phase = getVaultPhase(
+        view, afterVault.vaultKind, afterVault.subscriptionDate, afterVault.redemptionDate);
+    if (phase == VaultPhase::NoPhase)
+        return true;
+
+    if (phase != VaultPhase::Investment)
+    {
+        JLOG(j.fatal()) <<  //
+            "Invariant failed: loan origination only allowed in Investment phase";
+        return false;
+    }
+
+    return true;
+}
+
+namespace {
+
+// sfAssetsTotal, sfAssetsAvailable and sfLossUnrealized are STNumber fields
+// with kSmdNeedsAsset, so IOU writes go through associateAsset -> roundToAsset
+// -> STAmount quantization. Since assetsTotal is the largest number, it lands
+// on the coarsest decimal grid, and strict equality on the deltas can fire on
+// a single unit of quantization noise even when the underlying flow is
+// correct. Absorb one unit at the coarsest scale.
+//
+// XRP and MPT are integer-domain assets (Asset::integral() is true) with no
+// sub-ULP quantization; treating a whole drop / MPT unit as "noise" would
+// hide real accounting bugs. Keep the strict comparison there. Note that
+// gating on the sign of `scale` would be wrong: IOU amounts >= 1e15 have a
+// non-negative STAmount exponent but still quantize.
+[[nodiscard]] bool
+agreesWithinOneUnit(Number const& lhs, Number const& rhs, Asset const& asset, std::int32_t scale)
+{
+    if (asset.integral())
+        return lhs == rhs;
+    auto const diff = lhs - rhs;
+    Number const tolerance{1, scale};
+    return (diff < beast::kZero ? -diff : diff) <= tolerance;
+}
+
+// L, T and A are each independently quantized; the strict L <= T - A check
+// can fire on residual noise even when the true relationship holds. Tolerate
+// one unit at scale(assetsTotal) - the coarsest of the three grids. As with
+// the delta check above, the tolerance is meaningful only for IOU
+// (Asset::integral() is false); XRP and MPT keep the strict comparison.
+[[nodiscard]] bool
+lessOrEqualPlusOneUnit(Number const& lhs, Number const& rhs, Asset const& asset, std::int32_t scale)
+{
+    if (asset.integral())
+        return lhs <= rhs;
+    return lhs <= rhs + Number{1, scale};
+}
+
+}  // namespace
+
 std::int32_t
 ValidVault::computeVaultMinScale(DeltaInfo const& vaultDelta, Rules const& rules) const
 {
@@ -289,13 +417,14 @@ ValidVault::finalize(
     beast::Journal const& j)
 {
     bool const enforce = view.rules().enabled(featureSingleAssetVault);
+    bool const fix340Enabled = view.rules().enabled(fixCleanup3_4_0);
 
     if (!isTesSuccess(ret))
         return true;  // Do not perform checks
 
     if (afterVault_.empty() && beforeVault_.empty())
     {
-        if (hasPrivilege(tx, MustModifyVault))
+        if (hasPrivilege(tx, Privilege::MustModifyVault))
         {
             JLOG(j.fatal()) <<  //
                 "Invariant failed: vault operation succeeded without modifying "
@@ -306,7 +435,8 @@ ValidVault::finalize(
 
         return true;  // Not a vault operation
     }
-    if (!(hasPrivilege(tx, MustModifyVault) || hasPrivilege(tx, MayModifyVault)))
+    if (!(hasPrivilege(tx, Privilege::MustModifyVault) ||
+          hasPrivilege(tx, Privilege::MayModifyVault)))
     {
         JLOG(j.fatal()) <<  //
             "Invariant failed: vault updated by a wrong transaction type";
@@ -422,7 +552,8 @@ ValidVault::finalize(
     bool result = true;
 
     // Universal transaction checks
-    if (!beforeVault_.empty())
+    // From LendingProtocolV1_1 onwards, vault immutability check is moved to InvariantCheck.cpp
+    if (!beforeVault_.empty() && !view.rules().enabled(featureLendingProtocolV1_1))
     {
         auto const& beforeVault = beforeVault_[0];
         if (afterVault.asset != beforeVault.asset || afterVault.pseudoId != beforeVault.pseudoId ||
@@ -475,15 +606,32 @@ ValidVault::finalize(
                            "not be greater than assets outstanding";
         result = false;
     }
-    else if (afterVault.lossUnrealized > afterVault.assetsTotal - afterVault.assetsAvailable)
+    else
     {
-        JLOG(j.fatal())  //
-            << "Invariant failed: loss unrealized must not exceed "
-               "the difference between assets outstanding and available";
-        result = false;
+        bool const gapExceeded = [&] {
+            if (!fix340Enabled)
+            {
+                return afterVault.lossUnrealized >
+                    afterVault.assetsTotal - afterVault.assetsAvailable;
+            }
+
+            auto const s = scale(afterVault.assetsTotal, afterVault.asset);
+            return !lessOrEqualPlusOneUnit(
+                afterVault.lossUnrealized,
+                afterVault.assetsTotal - afterVault.assetsAvailable,
+                afterVault.asset,
+                s);
+        }();
+        if (gapExceeded)
+        {
+            JLOG(j.fatal())  //
+                << "Invariant failed: loss unrealized must not exceed "
+                   "the difference between assets outstanding and available";
+            result = false;
+        }
     }
 
-    if (view.rules().enabled(fixCleanup3_4_0) && afterVault.lossUnrealized < kZero)
+    if (fix340Enabled && afterVault.lossUnrealized < kZero)
     {
         JLOG(j.fatal()) << "Invariant failed: loss unrealized must not be negative";
         result = false;
@@ -520,6 +668,9 @@ ValidVault::finalize(
         result = false;
     }
 
+    // Immutability of VaultKind, SubscriptionDate and RedemptionDate is enforced by
+    // NoModifiedUnmodifiableFields in InvariantCheck.cpp.
+
     auto const beforeShares = [&]() -> std::optional {
         if (beforeVault_.empty())
             return std::nullopt;
@@ -606,6 +757,26 @@ ValidVault::finalize(
                     result = false;
                 }
 
+                if (isClosedEnded(afterVault.vaultKind))
+                {
+                    if (!afterVault.subscriptionDate || !afterVault.redemptionDate)
+                    {
+                        JLOG(j.fatal())  //
+                            << "Invariant failed: closed-ended vault must have SubscriptionDate "
+                               "and RedemptionDate";
+                        result = false;
+                    }
+                    else if (!isValidClosedEndedGap(
+                                 *afterVault.subscriptionDate, *afterVault.redemptionDate))
+                    {
+                        JLOG(j.fatal())  //
+                            << "Invariant failed: closed-ended vault RedemptionDate - "
+                               "SubscriptionDate must be within [MIN_INVESTMENT_PERIOD, "
+                               "MAX_INVESTMENT_PERIOD)";
+                        result = false;
+                    }
+                }
+
                 return result;
             }
             case ttVAULT_SET: {
@@ -631,8 +802,13 @@ ValidVault::finalize(
                     result = false;
                 }
 
+                // AssetsTotal may exceed AssetsMaximum when the excess is interest. After
+                // fixCleanup3_4_0, only reject a VaultSet that supplies sfAssetsMaximum or
+                // otherwise changes the cap to a nonzero value still below AssetsTotal.
                 if (afterVault.assetsMaximum > kZero &&
-                    afterVault.assetsTotal > afterVault.assetsMaximum)
+                    afterVault.assetsTotal > afterVault.assetsMaximum &&
+                    (!fix340Enabled || tx.isFieldPresent(sfAssetsMaximum) ||
+                     beforeVault.assetsMaximum != afterVault.assetsMaximum))
                 {
                     JLOG(j.fatal()) <<  //
                         "Invariant failed: set assets outstanding must not "
@@ -666,6 +842,21 @@ ValidVault::finalize(
                     !beforeVault_.empty(), "xrpl::ValidVault::finalize : deposit updated a vault");
                 auto const& beforeVault = beforeVault_[0];
 
+                // Deposit is only allowed while the vault is in NoPhase or
+                // Subscription.
+                auto const depositPhase = getVaultPhase(
+                    view,
+                    afterVault.vaultKind,
+                    afterVault.subscriptionDate,
+                    afterVault.redemptionDate);
+                if (depositPhase != VaultPhase::NoPhase && depositPhase != VaultPhase::Subscription)
+                {
+                    JLOG(j.fatal()) <<  //
+                        "Invariant failed: deposit only allowed in "
+                        "Subscription or NoPhase";
+                    result = false;
+                }
+
                 auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
                 if (!maybeVaultDeltaAssets)
                 {
@@ -706,7 +897,8 @@ ValidVault::finalize(
 
                 if (!issuerDeposit)
                 {
-                    auto const maybeAccDeltaAssets = deltaAssetsTxAccount(tx, fee);
+                    auto const maybeAccDeltaAssets =
+                        deltaAssetsForParty(view, tx[sfAccount], tx, fee, fix340Enabled);
                     if (!maybeAccDeltaAssets)
                     {
                         JLOG(j.fatal())
@@ -731,7 +923,14 @@ ValidVault::finalize(
                         result = false;
                     }
 
-                    if (localVaultDeltaAssets * -1 != accountDeltaAssets)
+                    bool const acctVaultAddsUp = fix340Enabled
+                        ? agreesWithinOneUnit(
+                              localVaultDeltaAssets * -1,
+                              accountDeltaAssets,
+                              vaultAsset,
+                              localMinScale)
+                        : localVaultDeltaAssets * -1 == accountDeltaAssets;
+                    if (!acctVaultAddsUp)
                     {
                         JLOG(j.fatal()) << "Invariant failed: " <<  //
                             "deposit must change vault and depositor balance by equal amount";
@@ -779,7 +978,10 @@ ValidVault::finalize(
 
                 auto const assetTotalDelta = roundToAsset(
                     vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
-                if (assetTotalDelta != vaultDeltaAssets)
+                bool const totalAddsUp = fix340Enabled
+                    ? agreesWithinOneUnit(assetTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
+                    : assetTotalDelta == vaultDeltaAssets;
+                if (!totalAddsUp)
                 {
                     JLOG(j.fatal())
                         << "Invariant failed: deposit and assets outstanding must add up";
@@ -788,7 +990,11 @@ ValidVault::finalize(
 
                 auto const assetAvailableDelta = roundToAsset(
                     vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
-                if (assetAvailableDelta != vaultDeltaAssets)
+                bool const availableAddsUp = fix340Enabled
+                    ? agreesWithinOneUnit(
+                          assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
+                    : assetAvailableDelta == vaultDeltaAssets;
+                if (!availableAddsUp)
                 {
                     JLOG(j.fatal()) << "Invariant failed: deposit and assets available must add up";
                     result = false;
@@ -804,20 +1010,51 @@ ValidVault::finalize(
                     "xrpl::ValidVault::finalize : withdrawal updated a vault");
                 auto const& beforeVault = beforeVault_[0];
 
+                // Withdrawal from a closed-ended vault is not allowed during the Investment phase
+                // (strictly past SubscriptionDate, before RedemptionDate).
+                if (getVaultPhase(
+                        view,
+                        afterVault.vaultKind,
+                        afterVault.subscriptionDate,
+                        afterVault.redemptionDate) == VaultPhase::Investment)
+                {
+                    JLOG(j.fatal()) <<  //
+                        "Invariant failed: withdrawal not allowed during "
+                        "Investment phase";
+                    result = false;
+                }
+
                 auto const maybeVaultDeltaAssets = deltaAssets(afterVault.pseudoId);
-                if (!maybeVaultDeltaAssets)
+
+                // Post-fixCleanup3_4_0: a withdrawal that redeems shares from a
+                // pool with no effective value left to back them (e.g. fully
+                // impaired/insolvent) legitimately moves zero assets on both
+                // sides — VaultWithdraw::doApply does not touch either
+                // balance-holding entry for a zero-value transfer, so no delta
+                // is recorded. VaultWithdraw::doApply separately rejects
+                // (tecPRECISION_LOSS) the case where a *positive* per-share
+                // value merely rounds down to zero, so a missing delta while
+                // the pool still held positive effective value indicates a
+                // real accounting bug, not this exception.
+                bool const zeroDeltaIsLegitimate = fix340Enabled && !maybeVaultDeltaAssets &&
+                    beforeVault.assetsTotal == beforeVault.lossUnrealized;
+
+                if (!maybeVaultDeltaAssets && !zeroDeltaIsLegitimate)
                 {
                     JLOG(j.fatal()) << "Invariant failed: withdrawal must change vault balance";
                     return false;  // That's all we can do
                 }
 
+                DeltaInfo const vaultDeltaAssets = maybeVaultDeltaAssets.value_or(
+                    DeltaInfo{.delta = kNumZero, .scale = std::nullopt});
+
                 // Get the posterior scale to round calculations to
-                auto const minScale = computeVaultMinScale(*maybeVaultDeltaAssets, view.rules());
+                auto const minScale = computeVaultMinScale(vaultDeltaAssets, view.rules());
 
                 auto const vaultPseudoDeltaAssets =
-                    roundToAsset(vaultAsset, maybeVaultDeltaAssets->delta, minScale);
+                    roundToAsset(vaultAsset, vaultDeltaAssets.delta, minScale);
 
-                if (vaultPseudoDeltaAssets >= kZero)
+                if (!zeroDeltaIsLegitimate && vaultPseudoDeltaAssets >= kZero)
                 {
                     JLOG(j.fatal()) << "Invariant failed: withdrawal must decrease vault balance";
                     result = false;
@@ -834,73 +1071,107 @@ ValidVault::finalize(
 
                 if (!issuerWithdrawal)
                 {
-                    auto const maybeAccDelta = deltaAssetsTxAccount(tx, fee);
-                    auto const maybeOtherAccDelta = [&]() -> std::optional {
-                        if (auto const destination = tx[~sfDestination];
-                            destination && *destination != tx[sfAccount])
-                            return deltaAssets(*destination);
-                        return std::nullopt;
-                    }();
+                    // Identify the intended recipient explicitly from
+                    // sfDestination (falling back to sfAccount for a
+                    // self-withdrawal), rather than inferring it from which
+                    // side happens to show a delta. When a distinct
+                    // destination is named, the sending account must not
+                    // also show a real economic delta -- that would mean two
+                    // accounts were paid, which is always a bug, regardless
+                    // of what (if anything) the named destination received.
+                    auto const destinationField = tx[~sfDestination];
+                    AccountID const recipient = destinationField.value_or(tx[sfAccount]);
+                    bool const distinctDestination =
+                        destinationField.has_value() && *destinationField != tx[sfAccount];
 
-                    if (maybeAccDelta.has_value() == maybeOtherAccDelta.has_value())
+                    // Intentionally ungated: `fix340Enabled &&` here would let the
+                    // pre-amendment sponsored case succeed and change consensus.
+                    if (distinctDestination &&
+                        deltaAssetsForParty(view, tx[sfAccount], tx, fee, fix340Enabled)
+                            .has_value())
                     {
                         JLOG(j.fatal()) <<  //
                             "Invariant failed: withdrawal must change one destination balance";
                         return false;
                     }
 
-                    auto const destinationDelta =  //
-                        maybeAccDelta ? *maybeAccDelta : *maybeOtherAccDelta;
+                    auto const maybeRecipientDelta =
+                        deltaAssetsForParty(view, recipient, tx, fee, fix340Enabled);
 
-                    // the scale of destinationDelta can be coarser than
-                    // minScale, so we take that into account when rounding
-                    auto const destinationScale = computeCoarsestScale({destinationDelta});
-                    auto const localMinScale = std::max(minScale, destinationScale);
-
-                    auto const roundedDestinationDelta =
-                        roundToAsset(vaultAsset, destinationDelta.delta, localMinScale);
-
-                    // Post-fixCleanup3_2_0: Tolerate zero-rounded destination deltas for IOUs only.
-                    // If the receiver's trust line sits at a coarser scale, the inflow may
-                    // safely round down to zero.
-                    //
-                    // XRP and MPT remain strict. Because they are integer-exact, a zero
-                    // destination delta indicates a true accounting bug, not a rounding artifact.
-                    bool const tolerateZeroDelta =
-                        view.rules().enabled(fixCleanup3_2_0) && !vaultAsset.integral();
-                    auto const invalidBalanceChange = tolerateZeroDelta
-                        ? roundedDestinationDelta < kZero
-                        : roundedDestinationDelta <= kZero;
-                    if (invalidBalanceChange)
+                    if (!maybeRecipientDelta.has_value())
                     {
-                        JLOG(j.fatal()) <<  //
-                            "Invariant failed: withdrawal must increase destination balance";
-                        result = false;
+                        // A legitimate zero-value withdrawal moves nothing to
+                        // the recipient either; there is nothing left to
+                        // cross-check.
+                        if (!zeroDeltaIsLegitimate)
+                        {
+                            JLOG(j.fatal()) <<  //
+                                "Invariant failed: withdrawal must change one destination balance";
+                            return false;
+                        }
                     }
-
-                    auto const localPseudoDeltaAssets =
-                        roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
-                    // For IOU assets near a precision boundary the destination's STAmount
-                    // exponent can shift, making part of the sent value unrepresentable at the
-                    // receiver's new scale — that portion is irreversibly absorbed by the IOU
-                    // rail.  Tolerate the mismatch only when the destroyed amount (vault outflow
-                    // minus destination inflow, in Number space) is itself sub-ULP at the
-                    // destination's scale.  Floor rounding is used so that values exactly at the
-                    // step boundary are not mistakenly dismissed.  Any representable discrepancy
-                    // indicates a real accounting bug and must be caught.
-                    auto const destroyedIsSubUlp = tolerateZeroDelta &&
-                        roundToAsset(
-                            vaultAsset,
-                            maybeVaultDeltaAssets->delta * -1 - destinationDelta.delta,
-                            destinationScale,
-                            Number::RoundingMode::Downward) == kZero;
-                    if (!destroyedIsSubUlp &&
-                        localPseudoDeltaAssets * -1 != roundedDestinationDelta)
+                    else
                     {
-                        JLOG(j.fatal()) << "Invariant failed: " <<  //
-                            "withdrawal must change vault and destination balance by equal "
-                            "amount";
-                        result = false;
+                        // A one-sided change is cross-checked even for a
+                        // legitimate zero vault delta: the destination must
+                        // then have moved by (rounded) zero as well.
+                        auto const destinationDelta = *maybeRecipientDelta;
+
+                        // the scale of destinationDelta can be coarser than
+                        // minScale, so we take that into account when rounding
+                        auto const destinationScale = computeCoarsestScale({destinationDelta});
+                        auto const localMinScale = std::max(minScale, destinationScale);
+
+                        auto const roundedDestinationDelta =
+                            roundToAsset(vaultAsset, destinationDelta.delta, localMinScale);
+
+                        // Post-fixCleanup3_2_0: Tolerate zero-rounded destination deltas for IOUs
+                        // only. If the receiver's trust line sits at a coarser scale, the inflow
+                        // may safely round down to zero.
+                        //
+                        // XRP and MPT remain strict for rounding artifacts.
+                        bool const tolerateZeroDelta =
+                            view.rules().enabled(fixCleanup3_2_0) && !vaultAsset.integral();
+                        auto const invalidBalanceChange = tolerateZeroDelta
+                            ? roundedDestinationDelta < kZero
+                            : roundedDestinationDelta <= kZero;
+                        if (invalidBalanceChange)
+                        {
+                            JLOG(j.fatal()) <<  //
+                                "Invariant failed: withdrawal must increase destination balance";
+                            result = false;
+                        }
+
+                        auto const localPseudoDeltaAssets =
+                            roundToAsset(vaultAsset, vaultPseudoDeltaAssets, localMinScale);
+                        // For IOU assets near a precision boundary the destination's STAmount
+                        // exponent can shift, making part of the sent value unrepresentable at
+                        // the receiver's new scale — that portion is irreversibly absorbed by the
+                        // IOU rail.  Tolerate the mismatch only when the destroyed amount (vault
+                        // outflow minus destination inflow, in Number space) is itself sub-ULP at
+                        // the destination's scale.  Floor rounding is used so that values exactly
+                        // at the step boundary are not mistakenly dismissed.  Any representable
+                        // discrepancy indicates a real accounting bug and must be caught.
+                        auto const destroyedIsSubUlp = tolerateZeroDelta &&
+                            roundToAsset(
+                                vaultAsset,
+                                vaultDeltaAssets.delta * -1 - destinationDelta.delta,
+                                destinationScale,
+                                Number::RoundingMode::Downward) == kZero;
+                        bool const withdrawAddsUp = fix340Enabled
+                            ? agreesWithinOneUnit(
+                                  localPseudoDeltaAssets * -1,
+                                  roundedDestinationDelta,
+                                  vaultAsset,
+                                  localMinScale)
+                            : localPseudoDeltaAssets * -1 == roundedDestinationDelta;
+                        if (!destroyedIsSubUlp && !withdrawAddsUp)
+                        {
+                            JLOG(j.fatal()) << "Invariant failed: " <<  //
+                                "withdrawal must change vault and destination balance by equal "
+                                "amount";
+                            result = false;
+                        }
                     }
                 }
 
@@ -937,7 +1208,11 @@ ValidVault::finalize(
                 auto const assetTotalDelta = roundToAsset(
                     vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
                 // Note, vaultBalance is negative (see check above)
-                if (assetTotalDelta != vaultPseudoDeltaAssets)
+                bool const totalAddsUp = fix340Enabled
+                    ? agreesWithinOneUnit(
+                          assetTotalDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
+                    : assetTotalDelta == vaultPseudoDeltaAssets;
+                if (!totalAddsUp)
                 {
                     JLOG(j.fatal())
                         << "Invariant failed: withdrawal and assets outstanding must add up";
@@ -947,7 +1222,11 @@ ValidVault::finalize(
                 auto const assetAvailableDelta = roundToAsset(
                     vaultAsset, afterVault.assetsAvailable - beforeVault.assetsAvailable, minScale);
 
-                if (assetAvailableDelta != vaultPseudoDeltaAssets)
+                bool const availableAddsUp = fix340Enabled
+                    ? agreesWithinOneUnit(
+                          assetAvailableDelta, vaultPseudoDeltaAssets, vaultAsset, minScale)
+                    : assetAvailableDelta == vaultPseudoDeltaAssets;
+                if (!availableAddsUp)
                 {
                     JLOG(j.fatal())
                         << "Invariant failed: withdrawal and assets available must add up";
@@ -992,7 +1271,11 @@ ValidVault::finalize(
 
                     auto const assetsTotalDelta = roundToAsset(
                         vaultAsset, afterVault.assetsTotal - beforeVault.assetsTotal, minScale);
-                    if (assetsTotalDelta != vaultDeltaAssets)
+                    bool const totalAddsUp = fix340Enabled
+                        ? agreesWithinOneUnit(
+                              assetsTotalDelta, vaultDeltaAssets, vaultAsset, minScale)
+                        : assetsTotalDelta == vaultDeltaAssets;
+                    if (!totalAddsUp)
                     {
                         JLOG(j.fatal()) <<  //
                             "Invariant failed: clawback and assets outstanding must add up";
@@ -1003,7 +1286,11 @@ ValidVault::finalize(
                         vaultAsset,
                         afterVault.assetsAvailable - beforeVault.assetsAvailable,
                         minScale);
-                    if (assetAvailableDelta != vaultDeltaAssets)
+                    bool const availableAddsUp = fix340Enabled
+                        ? agreesWithinOneUnit(
+                              assetAvailableDelta, vaultDeltaAssets, vaultAsset, minScale)
+                        : assetAvailableDelta == vaultDeltaAssets;
+                    if (!availableAddsUp)
                     {
                         JLOG(j.fatal()) <<  //
                             "Invariant failed: clawback and assets available must add up";
@@ -1052,6 +1339,7 @@ ValidVault::finalize(
             }
 
             case ttLOAN_SET:
+                return finalizeLoanSet(view, j);
             case ttLOAN_MANAGE:
             case ttLOAN_PAY:
                 return true;
diff --git a/src/libxrpl/tx/paths/BookStep.cpp b/src/libxrpl/tx/paths/BookStep.cpp
index e7c2e9ee29..ae218a4cff 100644
--- a/src/libxrpl/tx/paths/BookStep.cpp
+++ b/src/libxrpl/tx/paths/BookStep.cpp
@@ -44,7 +44,9 @@
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -653,7 +655,15 @@ limitStepIn(
         // under an amendment.
         ofrAmt = offer.limitIn(ofrAmt, inLmt, /* roundUp */ false);
         stpAmt.out = ofrAmt.out;
-        ownerGives = mulRatio(ofrAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false);
+        // Round up for MPT output so the offer owner pays the full
+        // ceil(amount × rate) fee, matching direct Payment semantics.  IOU uses
+        // floating-point arithmetic so the floor/ceil distinction is sub-epsilon
+        // there; preserve the historical false to avoid changing IOU behavior.
+        ownerGives = mulRatio(
+            ofrAmt.out,
+            transferRateOut,
+            QUALITY_ONE,
+            /*roundUp*/ std::is_same_v);
     }
 }
 
@@ -672,7 +682,11 @@ limitStepOut(
     if (limit < stpAmt.out)
     {
         stpAmt.out = limit;
-        ownerGives = mulRatio(stpAmt.out, transferRateOut, QUALITY_ONE, /*roundUp*/ false);
+        ownerGives = mulRatio(
+            stpAmt.out,
+            transferRateOut,
+            QUALITY_ONE,
+            /*roundUp*/ std::is_same_v);
         ofrAmt = offer.limitOut(
             ofrAmt,
             stpAmt.out,
@@ -727,17 +741,20 @@ BookStep::forEachOffer(
         bool const isAssetInMPT = assetIn.holds();
         auto const& owner = offer.owner();
 
-        if (isAssetInMPT)
-        {
-            // Create MPToken for the offer's owner. No need to check
-            // for the reserve since the offer is removed if it is consumed.
-            // Therefore, the owner count remains the same.
-            if (auto const err = checkCreateMPT(sb, assetIn.get(), owner, {}, j_);
-                !isTesSuccess(err))
+        auto removeOffer = [&](std::string_view logMessage = {}) {
+            auto const key = offer.key();
+            if (!logMessage.empty())
             {
-                return true;
+                JLOG(j_.trace()) << logMessage << (key ? " " + to_string(*key) : "");
             }
-        }
+            if (key)
+                offers.permRmOffer(*key);
+            if (!offerAttempted)
+            {
+                // Change quality only if no previous offers were tried.
+                ofrQ = std::nullopt;
+            }
+        };
 
         // It shouldn't matter from auth point of view whether it's sb
         // or afView. Amendment guard this change just in case.
@@ -745,17 +762,15 @@ BookStep::forEachOffer(
         // Make sure offer owner has authorization to own Assets from issuer
         // and MPT assets can be traded/transferred.
         // An account can always own XRP or their own Assets.
-        if (!isTesSuccess(requireAuth(applyView, assetIn, owner)) || !checkMPTDEX(sb, owner))
+        // Missing MPTokens are allowed during offer discovery; they are
+        // created later if the offer is actually consumed.
+        auto const authType = isAssetInMPT ? AuthType::WeakAuth : AuthType::Legacy;
+        if (!isTesSuccess(requireAuth(applyView, assetIn, owner, authType)) ||
+            !checkMPTDEX(sb, owner))
         {
             // Offer owner not authorized to hold IOU/MPT from issuer.
             // Remove this offer even if no crossing occurs.
-            if (auto const key = offer.key())
-                offers.permRmOffer(*key);
-            if (!offerAttempted)
-            {
-                // Change quality only if no previous offers were tried.
-                ofrQ = std::nullopt;
-            }
+            removeOffer();
             // Returning true causes offers.step() to delete the offer.
             return true;
         }
@@ -768,52 +783,88 @@ BookStep::forEachOffer(
             static_cast(this)->getOfrOutRate(prevStep_, owner, strandDst_, trOut));
 
         auto ofrAmt = offer.amount();
-        TAmounts stpAmt{mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true), ofrAmt.out};
-
-        // owner pays the transfer fee.
-        auto ownerGives = mulRatio(ofrAmt.out, ofrOutRate, QUALITY_ONE, /*roundUp*/ false);
-
-        auto const funds = offer.isFunded()
-            ? ownerGives  // Offer owner is issuer; they have unlimited funds
-            : offers.ownerFunds();
-
-        // Only if CLOB offer
-        if (funds < ownerGives)
+        TAmounts stpAmt{ofrAmt.in, ofrAmt.out};
+        auto ownerGives = ofrAmt.out;
+        try
         {
-            // We already know offer.owner()!=offer.issueOut().account
-            ownerGives = funds;
-            stpAmt.out = mulRatio(ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false);
-
-            // It turns out we can prevent order book blocking by (strictly)
-            // rounding down the ceil_out() result.  This adjustment changes
-            // transaction outcomes, so it must be made under an amendment.
-            ofrAmt = offer.limitOut(ofrAmt, stpAmt.out, /*roundUp*/ false);
-
+            // All arithmetic in this block runs before the offer is consumed.
+            // A crafted MPTokensV2 offer can overflow while transfer rates or
+            // crossing limits are applied; remove that unusable offer instead
+            // of letting it persist as a tecINTERNAL source.
             stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
-        }
 
-        // Limit offer's input if MPT, BookStep is the first step (an issuer
-        // is making a cross-currency payment), and this offer is not owned
-        // by the issuer. Otherwise, OutstandingAmount may overflow.
-        auto const& issuer = assetIn.getIssuer();
-        if (isAssetInMPT && !prevStep_ && offer.owner() != issuer)
-        {
-            // Funds available to issue
-            auto const available = toAmount(accountFunds(
-                sb,
-                issuer,
-                assetIn,  // STAmount{0}, but the default is not used
-                FreezeHandling::IgnoreFreeze,
-                AuthHandling::IgnoreAuth,
-                j_));
-            if (stpAmt.in > available)
+            // owner pays the transfer fee.
+            ownerGives = mulRatio(
+                ofrAmt.out,
+                ofrOutRate,
+                QUALITY_ONE,
+                /*roundUp*/ std::is_same_v);
+
+            auto const funds = offer.isFunded()
+                ? ownerGives  // Offer owner is issuer; they have unlimited funds
+                : offers.ownerFunds();
+
+            // Only if CLOB offer
+            if (funds < ownerGives)
             {
-                limitStepIn(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available);
-            }
-        }
+                // We already know offer.owner()!=offer.issueOut().account
+                ownerGives = funds;
+                stpAmt.out = mulRatio(ownerGives, QUALITY_ONE, ofrOutRate, /*roundUp*/ false);
 
-        offerAttempted = true;
-        return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate);
+                // It turns out we can prevent order book blocking by (strictly)
+                // rounding down the ceil_out() result.  This adjustment changes
+                // transaction outcomes, so it must be made under an amendment.
+                ofrAmt = offer.limitOut(ofrAmt, stpAmt.out, /*roundUp*/ false);
+
+                stpAmt.in = mulRatio(ofrAmt.in, ofrInRate, QUALITY_ONE, /*roundUp*/ true);
+            }
+
+            // Limit offer's input if MPT, BookStep is the first step (an issuer
+            // is making a cross-currency payment), and this offer is not owned
+            // by the issuer. Otherwise, OutstandingAmount may overflow.
+            auto const& issuer = assetIn.getIssuer();
+            if (isAssetInMPT && !prevStep_ && offer.owner() != issuer)
+            {
+                // Funds available to issue
+                auto const available = toAmount(accountFunds(
+                    sb,
+                    issuer,
+                    assetIn,  // STAmount{0}, but the default is not used
+                    FreezeHandling::IgnoreFreeze,
+                    AuthHandling::IgnoreAuth,
+                    j_));
+                if (stpAmt.in > available)
+                {
+                    limitStepIn(
+                        offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available);
+                }
+            }
+
+            offerAttempted = true;
+            return callback(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate);
+        }
+        catch (std::overflow_error const&)
+        {
+            if (sb.rules().enabled(featureMPTokensV2))
+            {
+                SOMETIMES(
+                    true,
+                    "BookStep::forEachOffer removed MPT offer after "
+                    "overflow during crossing");
+                removeOffer("Removing offer with overflowing amount calculation");
+                return true;
+            }
+            // An overflow can only be produced by a crafted MPT offer, and MPT
+            // offers require featureMPTokensV2 (enforced at OfferCreate
+            // preflight). So the amendment is always enabled when we get here
+            // and this legacy re-throw is unreachable in practice.
+            // LCOV_EXCL_START
+            XRPL_ASSERT(
+                sb.rules().enabled(featureMPTokensV2),
+                "xrpl::BookStep::forEachOffer : overflow implies MPTokensV2");
+            throw;
+            // LCOV_EXCL_STOP
+        }
     };
 
     // At any payment engine iteration, AMM offer can only be consumed once.
@@ -873,6 +924,22 @@ BookStep::consumeOffer(
     // The offer owner gets the ofrAmt. The difference between ofrAmt and
     // stepAmt is a transfer fee that goes to book_.in.account
     {
+        if constexpr (std::is_same_v)
+        {
+            // If the offer's TakerPays asset is an MPT, the offer owner must
+            // hold an MPToken to receive it. Create one here if it doesn't
+            // already exist.
+            if (auto const err = checkCreateMPT(sb, book_.in.get(), offer.owner(), j_);
+                !isTesSuccess(err))
+            {
+                // checkCreateMPT only fails on tecDIR_FULL (its source line is
+                // itself LCOV-excluded) or a missing offer-owner account, which
+                // cannot happen since that account owns the offer being
+                // consumed. Defensive and unreachable in practice.
+                Throw(err);  // LCOV_EXCL_LINE
+            }
+        }
+
         auto const dr = offer.send(
             sb, book_.in.getIssuer(), offer.owner(), toSTAmount(ofrAmt.in, book_.in), j_);
         if (!isTesSuccess(dr))
@@ -1043,6 +1110,13 @@ BookStep::revImp(
         auto ofrAdjAmt = ofrAmt;
         auto stpAdjAmt = stpAmt;
         auto ownerGivesAdj = ownerGives;
+        // This reduction can overflow via the transfer-rate mulRatio() on a
+        // 63-bit MPT amount (IOU rescales instead of throwing, and XRP stays
+        // under the int64 limit, so only MPT reaches it today), but
+        // savedIns/savedOuts are not updated until after it succeeds. The outer
+        // execOffer() catch can therefore remove the offer under
+        // featureMPTokensV2 (legacy propagate-the-exception behavior otherwise)
+        // without rolling back local state.
         limitStepOut(
             offer,
             ofrAdjAmt,
@@ -1144,12 +1218,25 @@ BookStep::fwdImp(
         auto stpAdjAmt = stpAmt;
         auto ownerGivesAdj = ownerGives;
 
+        // limitStepIn()/limitStepOut() can throw std::overflow_error from the
+        // transfer-rate mulRatio() on a 63-bit MPT amount. (IOUAmount::mulRatio
+        // rescales rather than throwing, and XRP amounts/rates stay under the
+        // int64 limit, so in practice only MPT reaches this today.) execOffer()
+        // catches it: under featureMPTokensV2 the offending offer is removed;
+        // otherwise the legacy behavior (propagate the exception) is preserved.
+        // Keep candidate accumulator changes local until those calls succeed so
+        // the catch path does not observe partially updated state. Re-sum the
+        // staged sets to preserve historical flat_multiset summing behavior.
+        auto savedInsAdj = savedIns;
+        auto savedOutsAdj = savedOuts;
+        auto resultAdj = result;
         typename boost::container::flat_multiset::const_iterator lastOut;
+
         if (stpAmt.in <= remainingIn)
         {
-            savedIns.insert(stpAmt.in);
-            lastOut = savedOuts.insert(stpAmt.out);
-            result = TAmounts(sum(savedIns), sum(savedOuts));
+            savedInsAdj.insert(stpAmt.in);
+            lastOut = savedOutsAdj.insert(stpAmt.out);
+            resultAdj = TAmounts(sum(savedInsAdj), sum(savedOutsAdj));
             // consume the offer even if stepAmt.in == remainingIn
             processMore = true;
         }
@@ -1163,15 +1250,15 @@ BookStep::fwdImp(
                 transferRateIn,
                 transferRateOut,
                 remainingIn);
-            savedIns.insert(remainingIn);
-            lastOut = savedOuts.insert(stpAdjAmt.out);
-            result.out = sum(savedOuts);
-            result.in = in;
+            savedInsAdj.insert(remainingIn);
+            lastOut = savedOutsAdj.insert(stpAdjAmt.out);
+            resultAdj.out = sum(savedOutsAdj);
+            resultAdj.in = in;
 
             processMore = false;
         }
 
-        if (result.out > cache_->out && result.in <= cache_->in)
+        if (resultAdj.out > cache_->out && resultAdj.in <= cache_->in)
         {
             // The step produced more output in the forward pass than the
             // reverse pass while consuming the same input (or less). If we
@@ -1181,8 +1268,8 @@ BookStep::fwdImp(
             // input provided in the forward step and produce the output
             // requested from the reverse step.
             auto const lastOutAmt = *lastOut;
-            savedOuts.erase(lastOut);
-            auto const remainingOut = cache_->out - sum(savedOuts);
+            savedOutsAdj.erase(lastOut);
+            auto const remainingOut = cache_->out - sum(savedOutsAdj);
             auto ofrAdjAmtRev = ofrAmt;
             auto stpAdjAmtRev = stpAmt;
             auto ownerGivesAdjRev = ownerGives;
@@ -1197,13 +1284,13 @@ BookStep::fwdImp(
 
             if (stpAdjAmtRev.in == remainingIn)
             {
-                result.in = in;
-                result.out = cache_->out;
+                resultAdj.in = in;
+                resultAdj.out = cache_->out;
 
-                savedIns.clear();
-                savedIns.insert(result.in);
-                savedOuts.clear();
-                savedOuts.insert(result.out);
+                savedInsAdj.clear();
+                savedInsAdj.insert(resultAdj.in);
+                savedOutsAdj.clear();
+                savedOutsAdj.insert(resultAdj.out);
 
                 ofrAdjAmt = ofrAdjAmtRev;
                 stpAdjAmt.in = remainingIn;
@@ -1214,10 +1301,15 @@ BookStep::fwdImp(
             {
                 // This is (likely) a problem case, and will be caught
                 // with later checks
-                savedOuts.insert(lastOutAmt);
+                savedOutsAdj.insert(lastOutAmt);
             }
         }
 
+        // Commit the staged accounting only after limitStepIn()/limitStepOut()
+        // have succeeded.
+        savedIns = std::move(savedInsAdj);
+        savedOuts = std::move(savedOutsAdj);
+        result = resultAdj;
         remainingIn = in - result.in;
         this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
 
@@ -1408,6 +1500,13 @@ template 
 bool
 BookStep::checkMPTDEX(ReadView const& view, AccountID const& owner) const
 {
+    // Offer-owner locks on book_.in and book_.out are handled by the
+    // liquidity sources before an offer reaches this point. OfferStream
+    // filters CLOB offers through the assetIn deep-freeze check and the
+    // assetOut owner-funds check using FreezeHandling::ZeroIfFrozen, while
+    // AMMLiquidity gets pool balances through ammAccountHolds(), which zeroes
+    // locked holdings. This method only enforces MPT trade and transfer
+    // permissions.
     if (!isTesSuccess(canTrade(view, book_.in)) || !isTesSuccess(canTrade(view, book_.out)))
         return false;
 
@@ -1421,14 +1520,8 @@ BookStep::checkMPTDEX(ReadView const& view, AccountID const
             // Offer's owner is an issuer
             if (asset.getIssuer() == owner)
                 return true;
-            // The previous step could be MPTEndpointStep with non issuer account or
-            // BookStep. Fail both if in asset is locked. In the former case it is holder
-            // to locked holder transfer. In the latter case it is not possible to tell if
-            // it is issuer to holder or holder to holder transfer.
-            if (isFrozen(view, owner, book_.in.get()))
-                return false;
-            // Previous step is BookStep. BookStep only sends if CanTransfer is
-            // set and not locked or the offer is owned by an issuer
+            // Previous BookStep already enforced transferability for the asset
+            // it sends to this offer.
             if (prevStep_->bookStepBook())
                 return true;
             // Previous step is MPTEndpointStep and offer's owner is not an
diff --git a/src/libxrpl/tx/paths/DirectStep.cpp b/src/libxrpl/tx/paths/DirectStep.cpp
index f8f12bd421..1854bd3632 100644
--- a/src/libxrpl/tx/paths/DirectStep.cpp
+++ b/src/libxrpl/tx/paths/DirectStep.cpp
@@ -4,6 +4,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -845,8 +846,14 @@ DirectStepI::check(StrandContext const& ctx) const
     // pure issue/redeem can't be frozen
     if (!(ctx.isLast && ctx.isFirst))
     {
-        auto const ter = checkFreeze(ctx.view, src_, dst_, currency_);
-        if (!isTesSuccess(ter))
+        if (auto const ter = checkFreeze(ctx.view, src_, dst_, currency_); !isTesSuccess(ter))
+            return ter;
+
+        // An LPToken redeemed against its AMM (dst_ is the LPToken issuer on
+        // this hop) cannot move if a pool asset is an MPT that forbids
+        // transfers between these accounts. A no-op unless dst_ is an AMM whose
+        // pool holds such an MPT (so it is implicitly gated by featureMPTokensV2).
+        if (auto const ter = canTransferLPToken(ctx.view, src_, dst_, dst_); !isTesSuccess(ter))
             return ter;
     }
 
diff --git a/src/libxrpl/tx/paths/MPTEndpointStep.cpp b/src/libxrpl/tx/paths/MPTEndpointStep.cpp
index 0a0f6a9f27..8fd69d3106 100644
--- a/src/libxrpl/tx/paths/MPTEndpointStep.cpp
+++ b/src/libxrpl/tx/paths/MPTEndpointStep.cpp
@@ -13,6 +13,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -89,6 +90,13 @@ protected:
     void
     resetCache(DebtDirection dir);
 
+    [[nodiscard]] TER
+    sendWithMPTCreate(
+        ApplyView& view,
+        AccountID const& src,
+        AccountID const& dst,
+        MPTAmount const& amount);
+
 private:
     MPTEndpointStep(
         StrandContext const& ctx,
@@ -274,7 +282,7 @@ public:
 
     // Not applicable for payment
     static TER
-    checkCreateMPT(ApplyView&, DebtDirection)
+    checkCreateMPT(ApplyView&)
     {
         return tesSUCCESS;
     }
@@ -322,7 +330,7 @@ public:
 
     // Can be created in rev or fwd (if limiting step) direction.
     TER
-    checkCreateMPT(ApplyView& view, DebtDirection srcDebtDir);
+    checkCreateMPT(ApplyView& view);
 };
 
 //------------------------------------------------------------------------------
@@ -401,7 +409,7 @@ MPTEndpointOfferCrossingStep::check(StrandContext const& ctx, SLE::const_ref)
 }
 
 TER
-MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view, xrpl::DebtDirection srcDebtDir)
+MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view)
 {
     // TakerPays is the last step if offer crossing
     if (isLast_)
@@ -410,12 +418,16 @@ MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view, xrpl::DebtDirectio
         // for the reserve since the offer doesn't go on the books
         // if crossed. Insufficient reserve is allowed if the offer
         // crossed. See CreateOffer::applyGuts() for reserve check.
-        if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, {}, j_);
-            !isTesSuccess(err))
+        if (auto const err = xrpl::checkCreateMPT(view, mptIssue_, dst_, j_); !isTesSuccess(err))
         {
+            // Unreachable: offer-crossing checks reject an offer whose owner
+            // could fail to create the MPToken.
+            // LCOV_EXCL_START
+            UNREACHABLE(
+                "xrpl::MPTEndpointOfferCrossingStep::checkCreateMPT : create MPToken failed");
             JLOG(j_.trace()) << "MPTEndpointStep::checkCreateMPT: failed create MPT";
-            resetCache(srcDebtDir);
             return err;
+            // LCOV_EXCL_STOP
         }
     }
     return tesSUCCESS;
@@ -423,6 +435,30 @@ MPTEndpointOfferCrossingStep::checkCreateMPT(ApplyView& view, xrpl::DebtDirectio
 
 //------------------------------------------------------------------------------
 
+template 
+TER
+MPTEndpointStep::sendWithMPTCreate(
+    ApplyView& view,
+    AccountID const& src,
+    AccountID const& dst,
+    MPTAmount const& amount)
+{
+    // Only offer crossing can fail here (payment checkCreateMPT is a no-op),
+    // via the unreachable path excluded in checkCreateMPT() above.
+    if (auto const err = static_cast(this)->checkCreateMPT(view); !isTesSuccess(err))
+        return err;  // LCOV_EXCL_LINE
+
+    return directSendNoFee(
+        view,
+        src,
+        dst,
+        toSTAmount(amount, mptIssue_),
+        /*checkIssuer*/ false,
+        j_);
+}
+
+//------------------------------------------------------------------------------
+
 template 
 std::pair
 MPTEndpointStep::maxPaymentFlow(ReadView const& sb) const
@@ -479,8 +515,6 @@ MPTEndpointStep::revImp(
     auto const [srcQOut, dstQIn] = qualities(sb, srcDebtDir, StrandDirection::Reverse);
     (void)dstQIn;
 
-    MPTIssue const srcToDstIss(mptIssue_);
-
     JLOG(j_.trace()) << "MPTEndpointStep::rev"
                      << " srcRedeems: " << redeems(srcDebtDir) << " outReq: " << to_string(out)
                      << " maxSrcToDst: " << to_string(maxSrcToDst) << " srcQOut: " << srcQOut
@@ -493,59 +527,41 @@ MPTEndpointStep::revImp(
         return {beast::kZero, beast::kZero};
     }
 
-    if (auto const err = static_cast(this)->checkCreateMPT(sb, srcDebtDir);
-        !isTesSuccess(err))
-        return {beast::kZero, beast::kZero};
+    // When a previous step feeds this issuing step, srcQOut is the issuer's
+    // transfer rate and maxPaymentFlow() returns the issuance maximum rather
+    // than a real limit, so srcToDst * srcQOut need not be representable. Cap
+    // srcToDst at the largest amount whose input is; the previous step then
+    // limits the flow to what the source actually holds.
+    MPTAmount const maxRepresentable =
+        mulRatio(MPTAmount(kMaxMpTokenAmount), QUALITY_ONE, srcQOut, /*roundUp*/ false);
 
     // Don't have to factor in dstQIn since it is always QUALITY_ONE
-    MPTAmount const srcToDst = out;
+    MPTAmount const srcToDst = std::min({out, maxSrcToDst, maxRepresentable});
 
-    if (srcToDst <= maxSrcToDst)
-    {
-        MPTAmount const in = mulRatio(srcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
-        cache_.emplace(in, srcToDst, srcToDst, srcDebtDir);
-        auto const ter = directSendNoFee(
-            sb,
-            src_,
-            dst_,
-            toSTAmount(srcToDst, srcToDstIss),
-            /*checkIssuer*/ false,
-            j_);
-        if (!isTesSuccess(ter))
-        {
-            JLOG(j_.trace()) << "MPTEndpointStep::rev: error " << ter;
-            resetCache(srcDebtDir);
-            return {beast::kZero, beast::kZero};
-        }
-        JLOG(j_.trace()) << "MPTEndpointStep::rev: Non-limiting"
-                         << " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(in)
-                         << " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
-        return {in, out};
-    }
+    // Can't overflow: srcToDst <= kMaxMpTokenAmount * QUALITY_ONE / srcQOut,
+    // so the rounded up product is at most kMaxMpTokenAmount.
+    MPTAmount const in = mulRatio(srcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
 
-    // limiting node
-    MPTAmount const in = mulRatio(maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
-    // Don't have to factor in dsqQIn since it's always QUALITY_ONE
-    MPTAmount const actualOut = maxSrcToDst;
-    cache_.emplace(in, maxSrcToDst, actualOut, srcDebtDir);
+    cache_.emplace(in, srcToDst, srcToDst, srcDebtDir);
 
-    auto const ter = directSendNoFee(
-        sb,
-        src_,
-        dst_,
-        toSTAmount(maxSrcToDst, srcToDstIss),
-        /*checkIssuer*/ false,
-        j_);
+    auto const ter = sendWithMPTCreate(sb, src_, dst_, srcToDst);
     if (!isTesSuccess(ter))
     {
+        // Unreachable: send fails only on funds/auth/overflow, precluded by
+        // maxPaymentFlow, check() requireAuth, and 2*kMaxMpTokenAmount < 2^64.
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::MPTEndpointStep::revImp : send failed");
         JLOG(j_.trace()) << "MPTEndpointStep::rev: error " << ter;
         resetCache(srcDebtDir);
         return {beast::kZero, beast::kZero};
+        // LCOV_EXCL_STOP
     }
-    JLOG(j_.trace()) << "MPTEndpointStep::rev: Limiting"
+
+    JLOG(j_.trace()) << "MPTEndpointStep::rev: " << (srcToDst < out ? "Limiting" : "Non-limiting")
                      << " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(in)
-                     << " srcToDst: " << to_string(maxSrcToDst) << " out: " << to_string(out);
-    return {in, actualOut};
+                     << " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
+
+    return {in, srcToDst};
 }
 
 // The forward pass should never have more liquidity than the reverse
@@ -610,8 +626,6 @@ MPTEndpointStep::fwdImp(
     auto const [srcQOut, dstQIn] = qualities(sb, srcDebtDir, StrandDirection::Forward);
     (void)dstQIn;
 
-    MPTIssue const srcToDstIss(mptIssue_);
-
     JLOG(j_.trace()) << "MPTEndpointStep::fwd"
                      << " srcRedeems: " << redeems(srcDebtDir) << " inReq: " << to_string(in)
                      << " maxSrcToDst: " << to_string(maxSrcToDst) << " srcQOut: " << srcQOut
@@ -619,63 +633,81 @@ MPTEndpointStep::fwdImp(
 
     if (maxSrcToDst.signum() <= 0)
     {
+        // Unreachable: the reverse pass owns dry detection; every path that
+        // reaches fwdImp (see StrandFlow::flow) has a funded source.
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::MPTEndpointStep::fwdImp : dry source");
         JLOG(j_.trace()) << "MPTEndpointStep::fwd: dry";
         resetCache(srcDebtDir);
         return {beast::kZero, beast::kZero};
+        // LCOV_EXCL_STOP
     }
 
-    if (auto const err = static_cast(this)->checkCreateMPT(sb, srcDebtDir);
-        !isTesSuccess(err))
+    auto const maybeSrcToDst = tryMulRatio(in, QUALITY_ONE, srcQOut, /*roundUp*/ false);
+    if (!maybeSrcToDst)
+    {
+        // Unreachable: divides by srcQOut >= QUALITY_ONE, so result <= in <=
+        // maxMPTAmount and can never overflow int64.
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::MPTEndpointStep::fwdImp : source to destination overflow");
+        JLOG(j_.trace()) << "MPTEndpointStep::fwd: overflow";
+        resetCache(srcDebtDir);
         return {beast::kZero, beast::kZero};
+        // LCOV_EXCL_STOP
+    }
 
-    MPTAmount const srcToDst = mulRatio(in, QUALITY_ONE, srcQOut, /*roundUp*/ false);
+    MPTAmount const srcToDst = *maybeSrcToDst;
 
     if (srcToDst <= maxSrcToDst)
     {
         // Don't have to factor in dstQIn since it's always QUALITY_ONE
         MPTAmount const out = srcToDst;
         setCacheLimiting(in, srcToDst, out, srcDebtDir);
-        auto const ter = directSendNoFee(
-            sb,
-            src_,
-            dst_,
-            toSTAmount(cache_->srcToDst, srcToDstIss),
-            /*checkIssuer*/ false,
-            j_);
-        if (!isTesSuccess(ter))
-        {
-            JLOG(j_.trace()) << "MPTEndpointStep::fwd: error " << ter;
-            resetCache(srcDebtDir);
-            return {beast::kZero, beast::kZero};
-        }
+
         JLOG(j_.trace()) << "MPTEndpointStep::fwd: Non-limiting"
                          << " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(in)
                          << " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
     }
     else
     {
+        // Unreachable: the reverse pass owns all limiting; the forward driver
+        // (StrandFlow::flow) never re-finds a limit, so srcToDst <= maxSrcToDst.
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::MPTEndpointStep::fwdImp : forward pass limiting");
         // limiting node
-        MPTAmount const actualIn = mulRatio(maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
-        // Don't have to factor in dstQIn since it's always QUALITY_ONE
-        MPTAmount const out = maxSrcToDst;
-        setCacheLimiting(actualIn, maxSrcToDst, out, srcDebtDir);
-        auto const ter = directSendNoFee(
-            sb,
-            src_,
-            dst_,
-            toSTAmount(cache_->srcToDst, srcToDstIss),
-            /*checkIssuer*/ false,
-            j_);
-        if (!isTesSuccess(ter))
+        auto const maybeActualIn = tryMulRatio(maxSrcToDst, srcQOut, QUALITY_ONE, /*roundUp*/ true);
+        if (!maybeActualIn)
         {
-            JLOG(j_.trace()) << "MPTEndpointStep::fwd: error " << ter;
+            JLOG(j_.trace()) << "MPTEndpointStep::fwd: overflow";
             resetCache(srcDebtDir);
             return {beast::kZero, beast::kZero};
         }
+
+        MPTAmount const actualIn = *maybeActualIn;
+
+        // Don't have to factor in dstQIn since it's always QUALITY_ONE
+        MPTAmount const out = maxSrcToDst;
+        setCacheLimiting(actualIn, maxSrcToDst, out, srcDebtDir);
+
         JLOG(j_.trace()) << "MPTEndpointStep::fwd: Limiting"
                          << " srcRedeems: " << redeems(srcDebtDir) << " in: " << to_string(actualIn)
                          << " srcToDst: " << to_string(srcToDst) << " out: " << to_string(out);
+        // LCOV_EXCL_STOP
     }
+
+    auto const ter = sendWithMPTCreate(sb, src_, dst_, cache_->srcToDst);
+    if (!isTesSuccess(ter))
+    {
+        // Unreachable: send fails only on funds/auth/overflow, precluded by
+        // maxPaymentFlow, check() requireAuth, and 2*kMaxMpTokenAmount < 2^64.
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::MPTEndpointStep::fwdImp : send failed");
+        JLOG(j_.trace()) << "MPTEndpointStep::fwd: error " << ter;
+        resetCache(srcDebtDir);
+        return {beast::kZero, beast::kZero};
+        // LCOV_EXCL_STOP
+    }
+
     return {cache_->in, cache_->out};
     // NOLINTEND(bugprone-unchecked-optional-access)
 }
diff --git a/src/libxrpl/tx/paths/OfferStream.cpp b/src/libxrpl/tx/paths/OfferStream.cpp
index ecc8416a2b..6884a113bd 100644
--- a/src/libxrpl/tx/paths/OfferStream.cpp
+++ b/src/libxrpl/tx/paths/OfferStream.cpp
@@ -4,6 +4,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -25,10 +26,14 @@
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 
 #include 
 #include 
+#include 
+#include 
 
 namespace xrpl {
 
@@ -136,17 +141,17 @@ template 
 TOfferStreamBase::shouldRmSmallIncreasedQOffer() const
 {
     // Consider removing the offer if:
-    //  o `TakerPays` is XRP (because of XRP drops granularity) or
+    //  o `TakerPays` is integral (because XRP/MPT have indivisible units) or
     //  o `TakerPays` and `TakerGets` are both IOU and `TakerPays`<`TakerGets`
-    static constexpr bool kInIsXrp = std::is_same_v;
-    static constexpr bool kOutIsXrp = std::is_same_v;
+    constexpr bool const kInIsIntegral = !std::is_same_v;
+    constexpr bool const kOutIsIntegral = !std::is_same_v;
 
-    if constexpr (kOutIsXrp)
+    if constexpr (!kInIsIntegral && kOutIsIntegral)
     {
-        // If `TakerGets` is XRP, the worst this offer's quality can change is
-        // to about 10^-81 `TakerPays` and 1 drop `TakerGets`. This will be
-        // remarkably good quality for any realistic asset, so these offers
-        // don't need this extra check.
+        // If only `TakerGets` is integral, the worst this offer's quality can
+        // change is to about 10^-81 `TakerPays` and 1 unit `TakerGets`. This
+        // will be perfect quality for any realistic asset, so these
+        // offers don't need this extra check.
         return false;
     }
 
@@ -156,7 +161,7 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const
     TAmounts const ofrAmts{
         toAmount(offer_.amount().in), toAmount(offer_.amount().out)};
 
-    if constexpr (!kInIsXrp && !kOutIsXrp)
+    if constexpr (!kInIsIntegral && !kOutIsIntegral)
     {
         if (Number(ofrAmts.in) >= Number(ofrAmts.out))
             return false;
@@ -165,7 +170,12 @@ TOfferStreamBase::shouldRmSmallIncreasedQOffer() const
     TTakerGets const ownerFunds = toAmount(*ownerFunds_);
 
     auto const effectiveAmounts = [&] {
-        if (offer_.owner() != offer_.assetOut().getIssuer() && ownerFunds < ofrAmts.out)
+        // Issuer-owned IOU offers are self-funded without a limit. MPT issuer
+        // offers are bounded by remaining issuance capacity, so they still need
+        // to be clipped by ownerFunds.
+        bool const issuerHasUnlimitedFunds = offer_.owner() == offer_.assetOut().getIssuer() &&
+            offer_.assetOut().template holds();
+        if (!issuerHasUnlimitedFunds && ownerFunds < ofrAmts.out)
         {
             // adjust the amounts by owner funds.
             //
@@ -250,6 +260,23 @@ TOfferStreamBase::step()
             continue;
         }
 
+        // Post-fixCleanup3_4_0 defensive check: an offer indexed in a domain
+        // book must claim that same domain. This can only happen if the book
+        // directory is corrupt (i.e. a separate book indexing bug). An offer
+        // with no sfDomainID at all is just as wrong here: the domain
+        // membership check below is gated on that field being present, so
+        // such an offer would otherwise be consumed from a domain book
+        // without any credential check.
+        if (view_.rules().enabled(fixCleanup3_4_0) && book_.domain.has_value() &&
+            (!entry->isFieldPresent(sfDomainID) ||
+             entry->getFieldH256(sfDomainID) != *book_.domain))
+        {
+            JLOG(j_.error()) << "Offer " << entry->key()
+                             << " domain missing or does not match book domain";
+            Throw(
+                tecINTERNAL, "Offer domain missing or does not match book domain.");
+        }
+
         // Pre-fixCleanup3_3_0: validate domain membership for any book.
         // Post-fixCleanup3_3_0: only validate when walking a domain book.
         // Hybrid offers carry sfDomainID but also participate in the open
@@ -305,7 +332,41 @@ TOfferStreamBase::step()
             continue;
         }
 
-        if (shouldRmSmallIncreasedQOffer())
+        // Partially funded offers can be reduced before BookStep sees them.
+        // If that strict reduction overflows under MPTokensV2, remove the
+        // unusable offer instead of leaving it at the book tip.
+        bool shouldRemoveSmallIncreasedQOffer = false;
+        try
+        {
+            shouldRemoveSmallIncreasedQOffer = shouldRmSmallIncreasedQOffer();
+        }
+        catch (std::overflow_error const&)
+        {
+            if (view_.rules().enabled(featureMPTokensV2))
+            {
+                SOMETIMES(
+                    true,
+                    "OfferStream::step removed MPT offer with overflowing "
+                    "reduced quality");
+                permRmOffer(entry->key());
+                JLOG(j_.warn()) << "Removing offer with overflowing reduced quality "
+                                << entry->key();
+                offer_ = TOffer{};
+                continue;
+            }
+            // The strict reduction only overflows for a crafted MPT offer, and
+            // MPT offers require featureMPTokensV2 (enforced at OfferCreate
+            // preflight). So the amendment is always enabled here and this
+            // legacy re-throw is unreachable in practice.
+            // LCOV_EXCL_START
+            XRPL_ASSERT(
+                view_.rules().enabled(featureMPTokensV2),
+                "xrpl::TOfferStreamBase::step : overflow implies MPTokensV2");
+            throw;
+            // LCOV_EXCL_STOP
+        }
+
+        if (shouldRemoveSmallIncreasedQOffer)
         {
             auto const originalFunds = accountFundsHelper(
                 cancelView_,
diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp
index c6ece11aed..bf36ff39ad 100644
--- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp
+++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp
@@ -51,7 +51,7 @@ AccountDelete::preflight(PreflightContext const& ctx)
         return temDST_IS_SRC;
     }
 
-    if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
+    if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err))
         return err;
 
     return tesSUCCESS;
diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp b/src/libxrpl/tx/transactors/check/CheckCash.cpp
index e4d8f192c0..857f759752 100644
--- a/src/libxrpl/tx/transactors/check/CheckCash.cpp
+++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp
@@ -528,7 +528,7 @@ CheckCash::doApply()
                                 return tecINSUFFICIENT_RESERVE;
 
                             if (auto const err =
-                                    checkCreateMPT(psb, mptID, accountID_, *sponsorSle, j_);
+                                    checkCreateMPT(psb, mptID, accountID_, *sponsorSle, 0, j_);
                                 !isTesSuccess(err))
                             {
                                 return err;
diff --git a/src/libxrpl/tx/transactors/contract/ContractCall.cpp b/src/libxrpl/tx/transactors/contract/ContractCall.cpp
index d2f639be54..f2c2241ac8 100644
--- a/src/libxrpl/tx/transactors/contract/ContractCall.cpp
+++ b/src/libxrpl/tx/transactors/contract/ContractCall.cpp
@@ -268,63 +268,47 @@ ContractCall::doApply()
     }
 
     std::uint32_t const allowance = ctx_.tx[sfGas];
-    auto re = runEscrowWasm(wasm, ledgerDataProvider, allowance, funcName, {});
+    auto const re = runEscrowWasm(wasm, ledgerDataProvider, allowance, funcName);
 
-    // Wasm Result
-    if (re.has_value())
+    // Charge for what the contract burned whether or not it completed. A
+    // cost outside the allowance is an engine fault.
+    std::optional const cost = re.has_value() ? re->cost : re.error().cost;
+    if (cost.has_value())
     {
-        // TODO: better error handling for this conversion
-        // if (allowance > re.value().cost)
-        // {
-        //     allowance -= static_cast(re.value().cost);
-        //     // auto const returnAllowance = [&]() {
-        //     //     ctx_.view().update(
-        //     //         keylet::account(contractAccount),
-        //     //         [allowance](SLE& sle) {
-        //     //             sle.setFieldU32(
-        //     //                 sfBalance,
-        //     //                 sle.getFieldU32(sfBalance) + allowance);
-        //     //         });
-        //     // };
-        //     // returnAllowance();
-        // }
-
-        ctx_.setGasUsed(static_cast(re.value().cost));
-        auto ret = re.value().result;
-        if (ret < 0)
-        {
-            JLOG(j_.trace()) << "WASM Execution Failed: " << ret;
-            ctx_.setVMReturnCode(ret);
-            // ctx_.setWasmReturnStr(contractCtx.result.exitReason);
-            return tecBYTECODE_REJECTED;
-        }
-
-        if (auto res = contract::finalizeContractData(
-                ctx_.registry,
-                ctx_.view(),
-                contractAccount,
-                contractCtx.result.dataMap,
-                contractCtx.result.eventMap,
-                ctx_.tx.getTransactionID());
-            !isTesSuccess(res))
-        {
-            JLOG(j_.trace()) << "Contract data finalization failed: " << transHuman(res);
-            return res;
-        }
-
-        ctx_.setVMReturnCode(ret);
-        // ctx_.setWasmReturnStr(contractCtx.result.exitReason);
-        ctx_.setEmittedTxns(contractCtx.result.emittedTxns);
-        return tesSUCCESS;
+        if (*cost < 0 || *cost > allowance)
+            return tecINTERNAL;
+        ctx_.setGasUsed(static_cast(*cost));
     }
-    else
+
+    if (!re.has_value())
     {
-        JLOG(j_.trace()) << "WASM Failure: " + transHuman(re.error().ter);
-        auto const errorCode = TERtoInt(re.error().ter);
-        ctx_.setVMReturnCode(errorCode);
-        // ctx_.setWasmReturnStr(contractCtx.result.exitReason);
+        JLOG(j_.trace()) << "WASM Failure: " << transHuman(re.error().ter);
+        ctx_.setVMReturnCode(TERtoInt(re.error().ter));
         return re.error().ter;
     }
+
+    auto const ret = re->result;
+    ctx_.setVMReturnCode(ret);
+    if (ret < 0)
+    {
+        JLOG(j_.trace()) << "WASM Execution Failed: " << ret;
+        return tecBYTECODE_REJECTED;
+    }
+
+    if (auto const res = contract::finalizeContractData(
+            ctx_.registry,
+            ctx_.view(),
+            contractAccount,
+            contractCtx.result.dataMap,
+            contractCtx.result.eventMap,
+            ctx_.tx.getTransactionID());
+        !isTesSuccess(res))
+    {
+        JLOG(j_.trace()) << "Contract data finalization failed: " << transHuman(res);
+        return res;
+    }
+
+    ctx_.setEmittedTxns(contractCtx.result.emittedTxns);
     return tesSUCCESS;
 }
 
diff --git a/src/libxrpl/tx/transactors/contract/ContractCreate.cpp b/src/libxrpl/tx/transactors/contract/ContractCreate.cpp
index 71b0fab8a9..226f0a02af 100644
--- a/src/libxrpl/tx/transactors/contract/ContractCreate.cpp
+++ b/src/libxrpl/tx/transactors/contract/ContractCreate.cpp
@@ -205,7 +205,7 @@ ContractCreate::doApply()
         ctx_.view().insert(sourceSle);
     }
 
-    std::uint32_t const seq = ctx_.tx.getSeqValue();
+    std::uint32_t const seq = ctx_.tx.getSeqProxy().value();
     auto const contractKeylet = keylet::contract(*contractHash, accountID_, seq);
     auto contractSle = std::make_shared(contractKeylet);
 
diff --git a/src/libxrpl/tx/transactors/dex/AMMBid.cpp b/src/libxrpl/tx/transactors/dex/AMMBid.cpp
index 3454559e82..154e64ca8e 100644
--- a/src/libxrpl/tx/transactors/dex/AMMBid.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMBid.cpp
@@ -193,10 +193,10 @@ applyBid(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Journa
     auto const current =
         duration_cast(ctx.view().header().parentCloseTime.time_since_epoch()).count();
     // Auction slot discounted fee
-    auto const discountedFee = (*ammSle)[sfTradingFee] / kAuctionSlotDiscountedFeeFraction;
-    auto const tradingFee = getFee((*ammSle)[sfTradingFee]);
+    auto const ammTradingFee = (*ammSle)[sfTradingFee];
+    auto const discountedFee = ammTradingFee / kAuctionSlotDiscountedFeeFraction;
     // Min price
-    auto const minSlotPrice = lptAMMBalance * tradingFee / kAuctionSlotMinFeeFraction;
+    auto const minSlotPrice = ammAuctionMinSlotPrice(lptAMMBalance, ammTradingFee);
 
     static constexpr std::uint32_t kTailingSlot = kAuctionSlotTimeIntervals - 1;
 
@@ -260,31 +260,37 @@ applyBid(ApplyContext& ctx, Sandbox& sb, AccountID const& account, beast::Journa
     auto const bidMax = ctx.tx[~sfBidMax];
 
     auto getPayPrice = [&](Number const& computedPrice) -> std::expected {
+        auto effectivePrice = computedPrice;
+        if (ctx.view().rules().enabled(fixCleanup3_4_0) && ammTradingFee == 0)
+        {
+            // Prevent zero-fee pools from granting auction slots at zero or dust prices.
+            effectivePrice = std::max(effectivePrice, ammAuctionMinSlotPrice(lptAMMBalance, 1));
+        }
         auto const payPrice = [&]() -> std::optional {
             // Both min/max bid price are defined
             if (bidMin && bidMax)
             {
-                if (computedPrice <= *bidMax)
-                    return std::max(computedPrice, Number(*bidMin));
-                JLOG(ctx.journal.debug()) << "AMM Bid: not in range " << computedPrice << " "
+                if (effectivePrice <= *bidMax)
+                    return std::max(effectivePrice, Number(*bidMin));
+                JLOG(ctx.journal.debug()) << "AMM Bid: not in range " << effectivePrice << " "
                                           << *bidMin << " " << *bidMax;
                 return std::nullopt;
             }
-            // Bidder pays max(bidPrice, computedPrice)
+            // Bidder pays max(bidPrice, effectivePrice)
             if (bidMin)
             {
-                return std::max(computedPrice, Number(*bidMin));
+                return std::max(effectivePrice, Number(*bidMin));
             }
             if (bidMax)
             {
-                if (computedPrice <= *bidMax)
-                    return computedPrice;
+                if (effectivePrice <= *bidMax)
+                    return effectivePrice;
                 JLOG(ctx.journal.debug())
-                    << "AMM Bid: not in range " << computedPrice << " " << *bidMax;
+                    << "AMM Bid: not in range " << effectivePrice << " " << *bidMax;
                 return std::nullopt;
             }
 
-            return computedPrice;
+            return effectivePrice;
         }();
         if (!payPrice)
         {
diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp
index c1ef9f875e..f95f257ab6 100644
--- a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp
@@ -227,6 +227,7 @@ AMMClawback::applyGuts(Sandbox& sb)
                 sb,
                 *ammSle,
                 holder,
+                issuer,
                 ammAccount,
                 amountBalance,
                 amount2Balance,
@@ -236,6 +237,7 @@ AMMClawback::applyGuts(Sandbox& sb)
                 0,
                 FreezeHandling::IgnoreFreeze,
                 AuthHandling::IgnoreAuth,
+                ReserveHandling::IgnoreReserve,
                 WithdrawAll::Yes,
                 preFeeBalance_,
                 ctx_.journal);
@@ -256,7 +258,7 @@ AMMClawback::applyGuts(Sandbox& sb)
     }
 
     if (!isTesSuccess(result))
-        return result;  // LCOV_EXCL_LINE
+        return result;
 
     if (sb.rules().enabled(fixCleanup3_3_0) && sb.rules().enabled(fixAMMv1_3))
     {
@@ -311,19 +313,30 @@ AMMClawback::equalWithdrawMatchingOneAmount(
     STAmount const& holdLPtokens,
     STAmount const& amount)
 {
+    // The clawback issuer signs for its own asset only. Threaded into the
+    // withdrawal so a recreated MPToken is auto-authorized only for the
+    // clawback issuer's asset, never for a paired asset from another issuer.
+    // preflight guarantees sfAccount is the clawed asset's issuer (it rejects
+    // the tx as temMALFORMED when sfAsset's issuer != sfAccount), so this is
+    // the issuer, not just any signer.
+    AccountID const issuer = ctx_.tx[sfAccount];
+
     auto frac = Number{amount} / amountBalance;
     auto amount2Withdraw = amount2Balance * frac;
 
     auto const lpTokensWithdraw = toSTAmount(lptAMMBalance.asset(), lptAMMBalance * frac);
-    if (lpTokensWithdraw > holdLPtokens)
+    auto const& rules = sb.rules();
+    // Pre-fixCleanup3_4_0 only a strictly greater computed LP amount takes
+    // the withdraw-all path. Equality left the last holder unable to be
+    // fully clawed. The amendment treats equality as withdraw-all.
+    if (rules.enabled(fixCleanup3_4_0) ? lpTokensWithdraw >= holdLPtokens
+                                       : lpTokensWithdraw > holdLPtokens)
     {
-        // if lptoken balance less than what the issuer intended to clawback,
-        // clawback all the tokens. Because we are doing a two-asset withdrawal,
-        // tfee is actually not used, so pass tfee as 0.
         return AMMWithdraw::equalWithdrawTokens(
             sb,
             ammSle,
             holder,
+            issuer,
             ammAccount,
             amountBalance,
             amount2Balance,
@@ -333,12 +346,12 @@ AMMClawback::equalWithdrawMatchingOneAmount(
             0,
             FreezeHandling::IgnoreFreeze,
             AuthHandling::IgnoreAuth,
+            ReserveHandling::IgnoreReserve,
             WithdrawAll::Yes,
             preFeeBalance_,
             ctx_.journal);
     }
 
-    auto const& rules = sb.rules();
     if (rules.enabled(fixAMMClawbackRounding))
     {
         auto tokensAdj = getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit::No);
@@ -353,10 +366,18 @@ AMMClawback::equalWithdrawMatchingOneAmount(
 
         auto amountRounded = getRoundedAsset(rules, amountBalance, frac, IsDeposit::No);
 
+        // The requested clawback amount is likely too small and results in
+        // one-sided pool withdrawal due to round off. Fail so the issuer can
+        // clawback a larger amount.
+        if (rules.enabled(fixCleanup3_4_0) &&
+            (amountRounded == beast::kZero || amount2Rounded == beast::kZero))
+            return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}};
+
         return AMMWithdraw::withdraw(
             sb,
             ammSle,
             ammAccount,
+            issuer,
             holder,
             amountBalance,
             amountRounded,
@@ -366,6 +387,7 @@ AMMClawback::equalWithdrawMatchingOneAmount(
             0,
             FreezeHandling::IgnoreFreeze,
             AuthHandling::IgnoreAuth,
+            ReserveHandling::IgnoreReserve,
             WithdrawAll::No,
             preFeeBalance_,
             ctx_.journal);
@@ -377,6 +399,7 @@ AMMClawback::equalWithdrawMatchingOneAmount(
         sb,
         ammSle,
         ammAccount,
+        issuer,
         holder,
         amountBalance,
         amount,
@@ -386,6 +409,7 @@ AMMClawback::equalWithdrawMatchingOneAmount(
         0,
         FreezeHandling::IgnoreFreeze,
         AuthHandling::IgnoreAuth,
+        ReserveHandling::IgnoreReserve,
         WithdrawAll::No,
         preFeeBalance_,
         ctx_.journal);
diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
index 5294dd0c7f..77b9071cf8 100644
--- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
+++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp
@@ -19,6 +19,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -516,6 +517,7 @@ AMMWithdraw::withdraw(
         view,
         ammSle,
         ammAccount,
+        std::nullopt,
         accountID_,
         amountBalance,
         amountWithdraw,
@@ -525,6 +527,7 @@ AMMWithdraw::withdraw(
         tfee,
         issuerFreezeHandling(),
         AuthHandling::ZeroIfUnauthorized,
+        ReserveHandling::EnforceReserve,
         isWithdrawAll(ctx_.tx),
         preFeeBalance_,
         j_);
@@ -536,6 +539,7 @@ AMMWithdraw::withdraw(
     Sandbox& view,
     SLE const& ammSle,
     AccountID const& ammAccount,
+    std::optional const& clawbackIssuer,
     AccountID const& account,
     STAmount const& amountBalance,
     STAmount const& amountWithdraw,
@@ -545,6 +549,7 @@ AMMWithdraw::withdraw(
     std::uint16_t tfee,
     FreezeHandling freezeHandling,
     AuthHandling authHandling,
+    ReserveHandling reserveHandling,
     WithdrawAll withdrawAll,
     XRPAmount const& priorBalance,
     beast::Journal const& journal)
@@ -666,19 +671,26 @@ AMMWithdraw::withdraw(
         mptokenKey = std::nullopt;
         if (!enabledFixAmMv12 || isXRP(asset))
             return tesSUCCESS;
-        bool const isIssue = asset.holds();
-        bool const assetNotExists = [&] {
-            if (isIssue)
-                return !view.exists(keylet::trustLine(account, asset.get()));
-            auto const issuanceKey = keylet::mptokenIssuance(asset.get());
-            mptokenKey = keylet::mptoken(issuanceKey.key, account);
-            if (!view.exists(*mptokenKey))
-                return true;
-            mptokenKey = std::nullopt;
-            return false;
-        }();
+        bool const assetNotExists = asset.visit(
+            [&](Issue const& issue) { return !view.exists(keylet::trustLine(account, issue)); },
+            [&](MPTIssue const& issue) {
+                auto const issuanceKey = keylet::mptokenIssuance(issue);
+                mptokenKey = keylet::mptoken(issuanceKey.key, account);
+                if (!view.exists(*mptokenKey))
+                    return true;
+                mptokenKey = std::nullopt;
+                return false;
+            });
         if (assetNotExists)
         {
+            // Intentionally ignore the reserve check for AMMClawback, so the
+            // holder can not avoid clawback by deleting the trustline/MPToken
+            // and keeping a low spendable balance. AMMClawback has a higher
+            // priority than the reserve check.
+            if (view.rules().enabled(fixCleanup3_4_0) &&
+                reserveHandling == ReserveHandling::IgnoreReserve)
+                return tesSUCCESS;
+
             auto sleAccount = view.peek(keylet::account(account));
             if (!sleAccount)
                 return tecINTERNAL;  // LCOV_EXCL_LINE
@@ -690,7 +702,7 @@ AMMWithdraw::withdraw(
                     ? XRPAmount(beast::kZero)
                     : accountReserve(view, sleAccount, journal, {.ownerCountDelta = 1}));
 
-            auto const balanceAdj = isIssue ? std::max(priorBalance, balance) : priorBalance;
+            auto const balanceAdj = std::max(priorBalance, balance);
             if (balanceAdj < reserve)
                 return tecINSUFFICIENT_RESERVE;
         }
@@ -703,14 +715,48 @@ AMMWithdraw::withdraw(
         if (mptokenKey && account != asset.getIssuer())
         {
             auto const& mptIssue = asset.get();
+            std::uint32_t createFlags = 0;
             if (auto const err = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
                 !isTesSuccess(err))
-                return err;
+            {
+                if (authHandling != AuthHandling::IgnoreAuth || err != tecNO_AUTH)
+                {
+                    // Unreachable in practice. Normal withdraws (authHandling
+                    // != IgnoreAuth) are rejected for unauthorized holders in
+                    // preclaim, so they never get here. Under clawback
+                    // (IgnoreAuth) requireAuth returns a non-tecNO_AUTH error
+                    // (e.g. tecEXPIRED) only for a domain-authorized MPT, but no
+                    // such MPT can be in an AMM pool: a directly domain-gated
+                    // RequireAuth MPT fails AMMCreate/deposit with tecNO_AUTH,
+                    // and vault shares (whose recursive auth could yield
+                    // tecEXPIRED) are rejected by AMMCreate with tecWRONG_ASSET.
+                    return err;  // LCOV_EXCL_LINE
+                }
 
-            if (auto const err = checkCreateMPT(view, mptIssue, account, {}, journal);
+                // AMMClawback ignores authorization so the issuer can recover
+                // MPT locked in the pool even if the holder deleted their
+                // MPToken. Only auto-authorize the recreated MPToken for the
+                // clawback issuer's own asset: authorization is granted by an
+                // asset's issuer, and the clawback transaction is signed by
+                // that issuer only for its own asset. For a paired asset issued
+                // by a different account, recreate the MPToken *unauthorized* so
+                // the clawback does not grant authorization on behalf of that
+                // issuer (which would bypass its lsfMPTRequireAuth). The holder
+                // still receives the paired asset (accountSend only requires the
+                // MPToken to exist, not to be authorized); the balance remains
+                // gated by its issuer until that issuer authorizes it.
+                if (clawbackIssuer && asset.getIssuer() == *clawbackIssuer)
+                    createFlags = lsfMPTAuthorized;
+            }
+
+            if (auto const err = checkCreateMPT(view, mptIssue, account, {}, createFlags, journal);
                 !isTesSuccess(err))
             {
-                return err;
+                // checkCreateMPT only fails on tecDIR_FULL (its source line is
+                // itself LCOV-excluded) or a missing account, which cannot
+                // happen since `account` is the withdrawing LP. Defensive and
+                // unreachable in practice.
+                return err;  // LCOV_EXCL_LINE
             }
         }
         return tesSUCCESS;
@@ -804,6 +850,7 @@ AMMWithdraw::equalWithdrawTokens(
         view,
         ammSle,
         accountID_,
+        std::nullopt,
         ammAccount,
         amountBalance,
         amount2Balance,
@@ -813,6 +860,7 @@ AMMWithdraw::equalWithdrawTokens(
         tfee,
         issuerFreezeHandling(),
         AuthHandling::ZeroIfUnauthorized,
+        ReserveHandling::EnforceReserve,
         isWithdrawAll(ctx_.tx),
         preFeeBalance_,
         ctx_.journal);
@@ -856,6 +904,7 @@ AMMWithdraw::equalWithdrawTokens(
     Sandbox& view,
     SLE const& ammSle,
     AccountID const account,
+    std::optional const& clawbackIssuer,
     AccountID const& ammAccount,
     STAmount const& amountBalance,
     STAmount const& amount2Balance,
@@ -865,6 +914,7 @@ AMMWithdraw::equalWithdrawTokens(
     std::uint16_t tfee,
     FreezeHandling freezeHandling,
     AuthHandling authHandling,
+    ReserveHandling reserveHandling,
     WithdrawAll withdrawAll,
     XRPAmount const& priorBalance,
     beast::Journal const& journal)
@@ -878,6 +928,7 @@ AMMWithdraw::equalWithdrawTokens(
                 view,
                 ammSle,
                 ammAccount,
+                clawbackIssuer,
                 account,
                 amountBalance,
                 amountBalance,
@@ -887,6 +938,7 @@ AMMWithdraw::equalWithdrawTokens(
                 tfee,
                 freezeHandling,
                 authHandling,
+                reserveHandling,
                 WithdrawAll::Yes,
                 priorBalance,
                 journal);
@@ -913,6 +965,7 @@ AMMWithdraw::equalWithdrawTokens(
             view,
             ammSle,
             ammAccount,
+            clawbackIssuer,
             account,
             amountBalance,
             amountWithdraw,
@@ -922,6 +975,7 @@ AMMWithdraw::equalWithdrawTokens(
             tfee,
             freezeHandling,
             authHandling,
+            reserveHandling,
             withdrawAll,
             priorBalance,
             journal);
diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp
index 0492f9c062..57ba6eff0d 100644
--- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp
+++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp
@@ -11,6 +11,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -242,8 +243,31 @@ OfferCreate::preclaim(PreclaimContext const& ctx)
     // is part of the domain
     if (ctx.tx.isFieldPresent(sfDomainID))
     {
-        if (!permissioned_dex::accountInDomain(ctx.view, id, ctx.tx[sfDomainID]))
-            return tecNO_PERMISSION;
+        if (ctx.view.rules().enabled(fixCleanup3_4_0))
+        {
+            auto const domainID = ctx.tx[sfDomainID];
+            auto const sleDomain = ctx.view.read(keylet::permissionedDomain(domainID));
+            if (!sleDomain)
+                return tecNO_PERMISSION;
+
+            // Domain owner is always considered in the domain, no credential check
+            // needed. For all other accounts, use validDomain which detects expired
+            // credentials. Suppress tecEXPIRED here so doApply can run and delete
+            // the expired credential SLEs from the ledger.
+            if (sleDomain->getAccountID(sfOwner) != id)
+            {
+                // validDomain returns tecNO_AUTH when no matching credential is
+                // found. Map it to tecNO_PERMISSION to preserve existing behavior.
+                if (auto const err = credentials::validDomain(ctx.view, domainID, id);
+                    !isTesSuccess(err) && err != tecEXPIRED)
+                    return tecNO_PERMISSION;
+            }
+        }
+        else
+        {
+            if (!permissioned_dex::accountInDomain(ctx.view, id, ctx.tx[sfDomainID]))
+                return tecNO_PERMISSION;
+        }
     }
 
     if (auto const ter = canTrade(ctx.view, saTakerPays.asset()); !isTesSuccess(ter))
@@ -672,6 +696,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
     }
 
     bool crossed = false;
+    bool const mptV2 = ctx_.view().rules().enabled(featureMPTokensV2);
 
     if (isTesSuccess(result))
     {
@@ -694,7 +719,12 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
             if (sle && sle->isFieldPresent(sfTickSize))
                 uTickSize = std::min(uTickSize, (*sle)[sfTickSize]);
         }
-        if (uTickSize < Quality::kMaxTickSize)
+        // Quality's ctor is the same getRate() call that produced uRate, and
+        // round() maps zero to zero, so an unrepresentable quality would make
+        // divide() below throw (tefEXCEPTION). Skip the rounding instead: the
+        // offer still crosses, and any residual is stopped before placement.
+        bool const unrepresentableRate = mptV2 && uRate == 0;
+        if (uTickSize < Quality::kMaxTickSize && !unrepresentableRate)
         {
             auto const rate = Quality{saTakerGets, saTakerPays}.round(uTickSize).rate();
 
@@ -841,6 +871,20 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
         return {tesSUCCESS, true};
     }
 
+    // The remainder rests at uRate, the original pre-crossing rate. A zero
+    // rate (quality not representable) puts it in the directory whose index
+    // equals getBookBase(book), and BookTip scans keys strictly greater, so it
+    // could never be crossed while holding the owner's reserve. Don't place
+    // it; anything that crossed is kept, and a fully crossed offer has already
+    // returned above. Gated to preserve pre-amendment behavior.
+    if (mptV2 && uRate == 0)
+    {
+        JLOG(j_.debug()) << "Unrepresentable quality: remainder not placed";
+        if (!crossed)
+            return {tecKILLED, false};
+        return {tesSUCCESS, true};
+    }
+
     auto const sleCreator = sb.peek(keylet::account(accountID_));
     if (!sleCreator)
         return {tefINTERNAL, false};
@@ -980,6 +1024,27 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
 TER
 OfferCreate::doApply()
 {
+    // If a DomainID is present, verify the account is still in the domain and
+    // delete any expired credential SLEs. This must happen before the Sandboxes
+    // are created: if we return a tec error, the engine applies sbCancel (not
+    // sb) to rawView, so deletions made inside sb would be lost. Deletions made
+    // directly to ctx_.view() here are preserved regardless of which branch
+    // applyGuts takes.
+    if (ctx_.tx.isFieldPresent(sfDomainID) && ctx_.view().rules().enabled(fixCleanup3_4_0))
+    {
+        auto const domainID = ctx_.tx[sfDomainID];
+        auto const sleDomain = ctx_.view().read(keylet::permissionedDomain(domainID));
+        if (!sleDomain)
+            return tecINTERNAL;  // LCOV_EXCL_LINE
+
+        if (sleDomain->getAccountID(sfOwner) != accountID_)
+        {
+            if (auto const err = verifyValidDomain(ctx_.view(), accountID_, domainID, j_);
+                !isTesSuccess(err))
+                return err;
+        }
+    }
+
     // This is the ledger view that we work against. Transactions are applied
     // as we go on processing transactions.
     Sandbox sb(&ctx_.view());
diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp
index b1cfcb3df1..9128d0767f 100644
--- a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp
+++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp
@@ -169,6 +169,14 @@ EscrowCancel::doApply()
     auto const sle = ctx_.view().peek(keylet::account(account));
     STAmount const amount = slep->getFieldAmount(sfAmount);
 
+    auto const reserveToSubtract = calculateAdditionalReserve((*slep)[~sfBytecode]);
+
+    // The return can re-create a holding the owner deleted while the escrow
+    // was pending; the removed escrow must not be counted against its reserve.
+    bool const recycleReserve = ctx_.view().rules().enabled(fixCleanup3_4_0);
+    if (recycleReserve)
+        decreaseOwnerCountForObject(ctx_.view(), sle, slep, reserveToSubtract, ctx_.journal);
+
     // Transfer amount back to the owner
     if (isXRP(amount))
     {
@@ -212,8 +220,8 @@ EscrowCancel::doApply()
         }
     }
 
-    auto const reserveToSubtract = calculateAdditionalReserve((*slep)[~sfBytecode]);
-    decreaseOwnerCountForObject(ctx_.view(), sle, slep, reserveToSubtract, ctx_.journal);
+    if (!recycleReserve)
+        decreaseOwnerCountForObject(ctx_.view(), sle, slep, reserveToSubtract, ctx_.journal);
 
     // Remove escrow from ledger
     ctx_.view().erase(slep);
diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp
index a117f79b6b..91a42189aa 100644
--- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp
+++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp
@@ -34,7 +34,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 
 #include 
@@ -250,8 +249,7 @@ EscrowCreate::preflightSigValidated(PreflightContext const& ctx)
         auto const code = ctx.tx.getFieldVL(sfBytecode);
         // basic checks happen in `preflight`
 
-        HostFunctions mock(ctx.j);
-        auto const re = preflightEscrowWasm(code, mock, escrowFunctionName);
+        auto const re = preflightEscrowWasm(code, ctx.j, escrowFunctionName);
         if (!isTesSuccess(re))
         {
             JLOG(ctx.j.debug()) << "EscrowCreate.Bytecode bad WASM";
@@ -387,11 +385,11 @@ escrowCreatePreclaimHelper(
         return ter;
 
     // If the issuer has frozen the account, return tecLOCKED
-    if (isFrozen(ctx.view, account, mptIssue))
+    if (isFrozen(ctx.view, account, *sleIssuance))
         return tecLOCKED;
 
     // If the issuer has frozen the destination, return tecLOCKED
-    if (isFrozen(ctx.view, dest, mptIssue))
+    if (isFrozen(ctx.view, dest, *sleIssuance))
         return tecLOCKED;
 
     // If the mpt cannot be transferred, return tecNO_AUTH
diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp
index 8e8ff24e69..796241708b 100644
--- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp
+++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp
@@ -116,7 +116,7 @@ EscrowFinish::preflight(PreflightContext const& ctx)
         }
     }
 
-    if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
+    if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err))
         return err;
 
     return tesSUCCESS;
@@ -231,7 +231,7 @@ escrowFinishPreclaimHelper(
         return ter;
 
     // If the issuer has frozen the destination, return tecLOCKED
-    if (isFrozen(ctx.view, dest, mptIssue))
+    if (isFrozen(ctx.view, dest, *sleIssuance))
         return tecLOCKED;
 
     return tesSUCCESS;
@@ -492,14 +492,12 @@ EscrowFinish::doApply()
 
     auto const reserveToSubtract = calculateAdditionalReserve((*slep)[~sfBytecode]);
 
-    // With the Sponsor amendment, release the escrow reserve before delivery.
-    // Token delivery can auto-create a destination holding, and the same
-    // sponsor (or the same account, for a self-escrow) may cover both the
-    // escrow being removed and the holding being created. Without the
-    // amendment, keep the legacy order: releasing early changes the reserve
-    // arithmetic for self-escrows and would break consensus if not gated.
-    bool const sponsorEnabled = ctx_.view().rules().enabled(featureSponsor);
-    if (sponsorEnabled)
+    // Delivery can auto-create the destination's holding; the removed escrow
+    // must not be counted against its reserve. The two share a reserve payer
+    // for a self-escrow, or when one sponsor covers both.
+    bool const recycleReserve =
+        ctx_.view().rules().enabled(featureSponsor) || ctx_.view().rules().enabled(fixCleanup3_4_0);
+    if (recycleReserve)
         decreaseOwnerCountForObject(ctx_.view(), account, slep, reserveToSubtract, ctx_.journal);
 
     STAmount const amount = slep->getFieldAmount(sfAmount);
@@ -551,8 +549,7 @@ EscrowFinish::doApply()
 
     ctx_.view().update(sled);
 
-    // Adjust source owner count (legacy position, pre-Sponsor)
-    if (!sponsorEnabled)
+    if (!recycleReserve)
         decreaseOwnerCountForObject(ctx_.view(), account, slep, reserveToSubtract, ctx_.journal);
 
     // Remove escrow from ledger
diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp
index 498f3c99eb..88b6f8c38b 100644
--- a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp
@@ -5,6 +5,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -25,7 +26,11 @@ namespace xrpl {
 bool
 LoanBrokerCoverWithdraw::checkExtraFeatures(PreflightContext const& ctx)
 {
-    return checkLendingProtocolDependencies(ctx.rules, ctx.tx);
+    if (!checkLendingProtocolDependencies(ctx.rules, ctx.tx))
+        return false;
+
+    return !ctx.tx.isFieldPresent(sfCredentialIDs) ||
+        (ctx.rules.enabled(featureCredentials) && ctx.rules.enabled(fixCleanup3_4_0));
 }
 
 NotTEC
@@ -49,6 +54,9 @@ LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx)
         }
     }
 
+    if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err))
+        return err;
+
     return tesSUCCESS;
 }
 
@@ -57,6 +65,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
 {
     auto const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0);
     auto const fix330Enabled = ctx.view.rules().enabled(fixCleanup3_3_0);
+    auto const fix340Enabled = ctx.view.rules().enabled(fixCleanup3_4_0);
     auto const& tx = ctx.tx;
 
     auto const account = tx[sfAccount];
@@ -109,6 +118,12 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
     if (auto const ret = canTransfer(ctx.view, vaultAsset, pseudoAccountID, dstAcct, waive))
         return ret;
 
+    // Validate credentials (if any) before canWithdraw, since canWithdraw may
+    // call credentials::authorizedDepositPreauth which assumes credentials
+    // already exist.
+    if (auto const err = credentials::valid(ctx.tx, ctx.view, account, ctx.j); !isTesSuccess(err))
+        return err;
+
     // Withdrawal to a 3rd party destination account is essentially a transfer.
     // Enforce all the usual asset transfer checks.
     AuthType authType = AuthType::WeakAuth;
@@ -126,6 +141,12 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
     if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType))
         return ter;
 
+    if (fix340Enabled && account == dstAcct && !holdingExists(ctx.view, dstAcct, vaultAsset))
+    {
+        if (auto const ter = canAddHolding(ctx.view, vaultAsset); !isTesSuccess(ter))
+            return ter;
+    }
+
     if (fix330Enabled)
     {
         if (auto const ret =
diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp
index b36977d225..433d77806a 100644
--- a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp
@@ -12,7 +12,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -198,8 +197,6 @@ LoanBrokerDelete::doApply()
 
     view().erase(broker);
 
-    associateAsset(*broker, vaultAsset);
-
     return tesSUCCESS;
 }
 
diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp
index d6cda9c326..1ab4eb2ce0 100644
--- a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp
@@ -8,7 +8,9 @@
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -144,6 +146,20 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
     }
     else
     {
+        // LP V1.1: only closed-ended vaults may host a loan broker. The
+        // lending protocol relies on the closed-ended Subscription /
+        // Investment / Redemption phase structure; attaching a broker to
+        // an open-ended vault has no well-defined lifecycle. VaultCreate
+        // stays unrestricted so existing open-ended flows keep working;
+        // the constraint is enforced here, at the point where the vault
+        // is first bound to the lending protocol.
+        if (ctx.view.rules().enabled(featureLendingProtocolV1_1) &&
+            getVaultKind(sleVault) != VaultKind::ClosedEnded)
+        {
+            JLOG(ctx.j.warn()) << "LoanBroker requires a closed-ended Vault.";
+            return tecNO_PERMISSION;
+        }
+
         if (auto const ter = canAddHolding(ctx.view, asset))
             return ter;
 
diff --git a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp
index 1a77489b4b..bc8e974d10 100644
--- a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp
@@ -130,9 +130,6 @@ LoanDelete::doApply()
     // Decrement the borrower's owner count
     decreaseOwnerCountForObject(view, borrowerSle, loanSle, 1, j_);
 
-    // These associations shouldn't do anything, but do them just to be safe
-    associateAsset(*loanSle, vaultAsset);
-    associateAsset(*brokerSle, vaultAsset);
     associateAsset(*vaultSle, vaultAsset);
 
     return tesSUCCESS;
diff --git a/src/libxrpl/tx/transactors/lending/LoanManage.cpp b/src/libxrpl/tx/transactors/lending/LoanManage.cpp
index a312dba3b3..2d710ceebe 100644
--- a/src/libxrpl/tx/transactors/lending/LoanManage.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp
@@ -104,7 +104,11 @@ LoanManage::preclaim(PreclaimContext const& ctx)
         return tecNO_PERMISSION;
     }
     if (tx.isFlag(tfLoanDefault) &&
-        !hasExpired(ctx.view, loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod)))
+        !hasExpired(
+            ctx.view,
+            loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod),
+            ctx.view.rules().enabled(fixCleanup3_4_0) ? ExpiryComparison::Exclusive
+                                                      : ExpiryComparison::Inclusive))
     {
         JLOG(ctx.j.warn()) << "A loan can not be defaulted before the next payment due date.";
         return tecTOO_SOON;
@@ -287,6 +291,14 @@ LoanManage::impairLoan(
     Asset const& vaultAsset,
     beast::Journal j)
 {
+    bool const fixEnabled340 = view.rules().enabled(fixCleanup3_4_0);
+
+    if (fixEnabled340 && !isPaymentLate(view, loanSle))
+    {
+        JLOG(j.warn()) << "Cannot impair a loan that is not late";
+        return tecTOO_SOON;
+    }
+
     Number const lossUnrealized = loanVaultExposure(vaultSle, loanSle);
 
     // The vault may be at a different scale than the loan. Reduce rounding
@@ -301,20 +313,22 @@ LoanManage::impairLoan(
     {
         // Having a loss greater than the vault's unavailable assets
         // will leave the vault in an invalid / inconsistent state.
-        JLOG(j.warn()) << "Vault unrealized loss is too large, and will "
-                          "corrupt the vault.";
+        JLOG(j.warn()) << "Vault unrealized loss is too large, and will corrupt the vault.";
         return tecLIMIT_EXCEEDED;
     }
     view.update(vaultSle);
 
     // Update the Loan object
     loanSle->setFlag(lsfLoanImpaired);
-    auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate);
-    if (!hasExpired(view, loanNextDueProxy))
+
+    if (!fixEnabled340)
     {
-        // loan payment is not yet late -
-        // move the next payment due date to now
-        loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
+        auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate);
+        if (!isPaymentLate(view, loanSle))
+        {
+            // loan payment is not yet late move the next payment due date to now
+            loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
+        }
     }
     view.update(loanSle);
 
@@ -351,19 +365,24 @@ LoanManage::unimpairLoan(
 
     // Update the Loan object
     loanSle->clearFlag(lsfLoanImpaired);
-    auto const paymentInterval = loanSle->at(sfPaymentInterval);
-    auto const normalPaymentDueDate =
-        std::max(loanSle->at(sfPreviousPaymentDueDate), loanSle->at(sfStartDate)) + paymentInterval;
-    if (!hasExpired(view, normalPaymentDueDate))
+    if (!view.rules().enabled(fixCleanup3_4_0))
     {
-        // loan was unimpaired within the payment interval
-        loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate;
-    }
-    else
-    {
-        // loan was unimpaired after the original payment due date
-        loanSle->at(sfNextPaymentDueDate) =
-            view.parentCloseTime().time_since_epoch().count() + paymentInterval;
+        auto const paymentInterval = loanSle->at(sfPaymentInterval);
+        auto const normalPaymentDueDate =
+            std::max(loanSle->at(sfPreviousPaymentDueDate), loanSle->at(sfStartDate)) +
+            paymentInterval;
+
+        if (!hasExpired(view, normalPaymentDueDate))
+        {
+            // loan was unimpaired within the payment interval
+            loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate;
+        }
+        else
+        {
+            // loan was unimpaired after the original payment due date
+            loanSle->at(sfNextPaymentDueDate) =
+                view.parentCloseTime().time_since_epoch().count() + paymentInterval;
+        }
     }
     view.update(loanSle);
 
diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp b/src/libxrpl/tx/transactors/lending/LoanPay.cpp
index 4619540295..18886b2682 100644
--- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp
@@ -2,13 +2,15 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -33,6 +35,34 @@
 
 namespace xrpl {
 
+namespace {
+// Returns the account's true, unclamped balance in `asset`, for use only in
+// fund-conservation checks. accountHolds(..., SpendableHandling::FullBalance)
+// cannot be used for this: for XRP it always defers to xrpLiquid, which
+// subtracts the account's reserve, so a payee sitting below its own reserve
+// would appear to receive nothing even though its raw ledger balance grew.
+// That mismatch is exactly what a conservation check must not see.
+STAmount
+conservationBalance(ReadView const& view, AccountID const& id, Asset const& asset, beast::Journal j)
+{
+    if (isXRP(asset))
+    {
+        auto const sle = view.read(keylet::account(id));
+        if (!sle)
+            return STAmount{asset};  // LCOV_EXCL_LINE
+        return view.balanceHookIOU(id, xrpAccount(), sle->getFieldAmount(sfBalance));
+    }
+    return accountHolds(
+        view,
+        id,
+        asset,
+        FreezeHandling::IgnoreFreeze,
+        AuthHandling::IgnoreAuth,
+        j,
+        SpendableHandling::FullBalance);
+}
+}  // namespace
+
 bool
 LoanPay::checkExtraFeatures(PreflightContext const& ctx)
 {
@@ -103,10 +133,13 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
         return normalCost;
     }
 
-    if (hasExpired(view, loanSle->at(sfNextPaymentDueDate)))
+    if (isPaymentLate(view, loanSle))
     {
         // If the payment is late, and the late payment flag is not set, it'll
-        // fail
+        // fail. Uses isPaymentLate() so the fee matches apply at the exact
+        // NextPaymentDueDate boundary (Exclusive once fixCleanup3_4_0 is
+        // enabled): a catch-up at that instant can still process up to
+        // kLoanMaximumPaymentsPerTransaction payments.
         return normalCost;
     }
 
@@ -581,36 +614,20 @@ LoanPay::doApply()
     }
 
     // These three values are used to check that funds are conserved after the transfers
-    auto const accountBalanceBefore = accountHolds(
-        view,
-        accountID_,
-        asset,
-        FreezeHandling::IgnoreFreeze,
-        AuthHandling::IgnoreAuth,
-        j_,
-        SpendableHandling::FullBalance);
+    auto const accountBalanceBefore = conservationBalance(view, accountID_, asset, j_);
     auto const vaultBalanceBefore = accountID_ == vaultPseudoAccount
         ? STAmount{asset, 0}
-        : accountHolds(
-              view,
-              vaultPseudoAccount,
-              asset,
-              FreezeHandling::IgnoreFreeze,
-              AuthHandling::IgnoreAuth,
-              j_,
-              SpendableHandling::FullBalance);
+        : conservationBalance(view, vaultPseudoAccount, asset, j_);
     auto const brokerBalanceBefore = accountID_ == brokerPayee
         ? STAmount{asset, 0}
-        : accountHolds(
-              view,
-              brokerPayee,
-              asset,
-              FreezeHandling::IgnoreFreeze,
-              AuthHandling::IgnoreAuth,
-              j_,
-              SpendableHandling::FullBalance);
+        : conservationBalance(view, brokerPayee, asset, j_);
 
-    if (totalPaidToVaultRounded != beast::kZero)
+    // Only ledgers without the rule below reach these payee checks. Once it is in force
+    // requireAuth can no longer reject a pseudo-account, so the whole block goes away with the
+    // gate.
+    bool const skipPayeeAuth = view.rules().enabled(fixCleanup3_4_0);
+
+    if (!skipPayeeAuth && totalPaidToVaultRounded != beast::kZero)
     {
         if (auto const ter = requireAuth(view, asset, vaultPseudoAccount, AuthType::StrongAuth))
             return ter;
@@ -634,8 +651,11 @@ LoanPay::doApply()
                 return ter;
             }
         }
-        if (auto const ter = requireAuth(view, asset, brokerPayee, AuthType::StrongAuth))
-            return ter;
+        if (!skipPayeeAuth)
+        {
+            if (auto const ter = requireAuth(view, asset, brokerPayee, AuthType::StrongAuth))
+                return ter;
+        }
     }
 
     if (auto const ter = accountSendMulti(
@@ -664,33 +684,13 @@ LoanPay::doApply()
 #endif
 
     // Check that funds are conserved
-    auto const accountBalanceAfter = accountHolds(
-        view,
-        accountID_,
-        asset,
-        FreezeHandling::IgnoreFreeze,
-        AuthHandling::IgnoreAuth,
-        j_,
-        SpendableHandling::FullBalance);
+    auto const accountBalanceAfter = conservationBalance(view, accountID_, asset, j_);
     auto const vaultBalanceAfter = accountID_ == vaultPseudoAccount
         ? STAmount{asset, 0}
-        : accountHolds(
-              view,
-              vaultPseudoAccount,
-              asset,
-              FreezeHandling::IgnoreFreeze,
-              AuthHandling::IgnoreAuth,
-              j_,
-              SpendableHandling::FullBalance);
-    auto const brokerBalanceAfter = accountID_ == brokerPayee ? STAmount{asset, 0}
-                                                              : accountHolds(
-                                                                    view,
-                                                                    brokerPayee,
-                                                                    asset,
-                                                                    FreezeHandling::IgnoreFreeze,
-                                                                    AuthHandling::IgnoreAuth,
-                                                                    j_,
-                                                                    SpendableHandling::FullBalance);
+        : conservationBalance(view, vaultPseudoAccount, asset, j_);
+    auto const brokerBalanceAfter = accountID_ == brokerPayee
+        ? STAmount{asset, 0}
+        : conservationBalance(view, brokerPayee, asset, j_);
     auto const balanceScale = [&]() {
         // Find a reasonable scale to use for the balance comparisons.
         //
diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp b/src/libxrpl/tx/transactors/lending/LoanSet.cpp
index 6533a47916..f7b97dfedf 100644
--- a/src/libxrpl/tx/transactors/lending/LoanSet.cpp
+++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp
@@ -10,6 +10,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -39,6 +40,12 @@
 
 namespace xrpl {
 
+// StartDate is strictly after SubscriptionDate. A min-gap vault must still
+// fit a minimum-interval loan plus kLoanRedemptionBuffer. The interval and
+// buffer constants are independent; only their sum (plus the +1 for a
+// strictly-later StartDate) is required to fit in kMinInvestmentPeriod.
+static_assert(kMinInvestmentPeriod >= LoanSet::kMinPaymentInterval + kLoanRedemptionBuffer + 1);
+
 bool
 LoanSet::checkExtraFeatures(PreflightContext const& ctx)
 {
@@ -225,6 +232,8 @@ TER
 LoanSet::preclaim(PreclaimContext const& ctx)
 {
     auto const& tx = ctx.tx;
+    auto const interval = ctx.tx.at(~sfPaymentInterval).value_or(kDefaultPaymentInterval);
+    auto const total = ctx.tx.at(~sfPaymentTotal).value_or(kDefaultPaymentTotal);
 
     {
         // Check for numeric overflow of the schedule before we load any
@@ -238,9 +247,6 @@ LoanSet::preclaim(PreclaimContext const& ctx)
         static_assert(kMaxTime == 4'294'967'295);
 
         auto const timeAvailable = kMaxTime - getStartDate(ctx.view);
-
-        auto const interval = ctx.tx.at(~sfPaymentInterval).value_or(kDefaultPaymentInterval);
-        auto const total = ctx.tx.at(~sfPaymentTotal).value_or(kDefaultPaymentTotal);
         auto const grace = ctx.tx.at(~sfGracePeriod).value_or(kDefaultGracePeriod);
 
         // The grace period can't be larger than the interval. Check it first,
@@ -310,7 +316,39 @@ LoanSet::preclaim(PreclaimContext const& ctx)
         return tefBAD_LEDGER;  // LCOV_EXCL_LINE
     }
 
-    if (vault->at(sfAssetsMaximum) != 0 && vault->at(sfAssetsTotal) >= vault->at(sfAssetsMaximum))
+    if (ctx.view.rules().enabled(featureLendingProtocolV1_1))
+    {
+        auto const phase = getVaultPhase(ctx.view, vault);
+        if (phase == VaultPhase::Subscription)
+        {
+            JLOG(ctx.j.warn()) << "Vault is still in the subscription phase.";
+            return tecTOO_SOON;
+        }
+        if (phase == VaultPhase::Redemption)
+        {
+            JLOG(ctx.j.warn()) << "Vault has entered the redemption phase.";
+            return tecEXPIRED;
+        }
+        if (phase == VaultPhase::Investment)
+        {
+            auto const finalPayment =
+                std::uint64_t{getStartDate(ctx.view)} + (std::uint64_t{interval} * total);
+            if (finalPayment + kLoanRedemptionBuffer > vault->at(sfRedemptionDate))
+            {
+                JLOG(ctx.j.warn())
+                    << "Final loan payment date is fewer than " << kLoanRedemptionBuffer
+                    << " seconds before the vault's redemption date.";
+                return tecNO_PERMISSION;
+            }
+        }
+    }
+
+    // Accrual origination credits interestDue into AssetsTotal, so a vault
+    // already at AssetsMaximum cannot take another loan. Cash-basis origination
+    // does not change AssetsTotal (see cash_basis::loanOriginationDeltas), so
+    // this leftover accrual gate must not apply there.
+    if (getVaultVersion(vault) != VaultVersion::CashBasis && vault->at(sfAssetsMaximum) != 0 &&
+        vault->at(sfAssetsTotal) >= vault->at(sfAssetsMaximum))
     {
         JLOG(ctx.j.warn()) << "Vault at maximum assets limit. Can't add another loan.";
         return tecLIMIT_EXCEEDED;
@@ -334,8 +372,24 @@ LoanSet::preclaim(PreclaimContext const& ctx)
         }
     }
 
-    if (auto const ter = canAddHolding(ctx.view, asset))
-        return ter;
+    // canAddHolding is an issuer-level check (DefaultRipple for IOU,
+    // lsfMPTCanTransfer for MPT); neither overload looks at the
+    // destination, so the holdingExists() clauses only decide whether a
+    // create path is reachable at all. It always runs before
+    // fixCleanup3_4_0: IOU addEmptyHolding checks DefaultRipple ahead of
+    // the existing-line case, so only preclaim can turn an existing line
+    // under a cleared DefaultRipple into terNO_RIPPLE rather than
+    // tecINTERNAL. After the amendment an existing line short-circuits to
+    // tecDUPLICATE, which doApply ignores, so run the check only when the
+    // borrower lacks a holding, or the origination fee is nonzero and the
+    // broker owner lacks one.
+    auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{});
+    if (!ctx.view.rules().enabled(fixCleanup3_4_0) || !holdingExists(ctx.view, borrower, asset) ||
+        (originationFee != beast::kZero && !holdingExists(ctx.view, brokerOwner, asset)))
+    {
+        if (auto const ter = canAddHolding(ctx.view, asset))
+            return ter;
+    }
 
     // vaultPseudo is going to send funds, so it can't be frozen.
     if (auto const ret = checkFrozen(ctx.view, vaultPseudo, asset))
@@ -441,9 +495,11 @@ LoanSet::doApply()
         properties.loanState.managementFeeDue);
 
     XRPL_ASSERT_PARTS(
-        *vaultSle->at(sfAssetsMaximum) == 0 || *vaultSle->at(sfAssetsMaximum) > *vaultTotalProxy,
+        *vaultSle->at(sfAssetsMaximum) == 0 ||
+            getVaultVersion(vaultSle) == VaultVersion::CashBasis ||
+            *vaultSle->at(sfAssetsMaximum) > *vaultTotalProxy,
         "xrpl::LoanSet::doApply",
-        "Vault is below maximum limit");
+        "accrual vault is below maximum limit");
 
     if (loanOriginationExceedsVaultMaximum(vaultSle, vaultTotalProxy, state.interestDue))
     {
diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp
index 797d3dcbb3..dd8df0eedb 100644
--- a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp
+++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp
@@ -8,12 +8,14 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -48,6 +50,13 @@ NFTokenAcceptOffer::preflight(PreflightContext const& ctx)
 
         if (*bf <= beast::kZero)
             return temMALFORMED;
+
+        if (ctx.rules.enabled(fixCleanup3_4_0))
+        {
+            // We don't allow a non-native currency to use the currency code XRP.
+            if (badAsset() == bf->asset())
+                return temBAD_CURRENCY;
+        }
     }
 
     return tesSUCCESS;
diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp
index 17c96a1919..c4c2f9227b 100644
--- a/src/libxrpl/tx/transactors/payment/Payment.cpp
+++ b/src/libxrpl/tx/transactors/payment/Payment.cpp
@@ -281,7 +281,7 @@ Payment::preflight(PreflightContext const& ctx)
         }
     }
 
-    if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
+    if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err))
         return err;
 
     return tesSUCCESS;
@@ -458,11 +458,41 @@ Payment::preclaim(PreclaimContext const& ctx)
 
     if (ctx.tx.isFieldPresent(sfDomainID))
     {
-        if (!permissioned_dex::accountInDomain(ctx.view, ctx.tx[sfAccount], ctx.tx[sfDomainID]))
-            return tecNO_PERMISSION;
+        if (ctx.view.rules().enabled(fixCleanup3_4_0))
+        {
+            auto const domainID = ctx.tx[sfDomainID];
+            auto const sleDomain = ctx.view.read(keylet::permissionedDomain(domainID));
+            if (!sleDomain)
+                return tecNO_PERMISSION;
 
-        if (!permissioned_dex::accountInDomain(ctx.view, ctx.tx[sfDestination], ctx.tx[sfDomainID]))
-            return tecNO_PERMISSION;
+            // Domain owner is always considered in the domain. For other accounts,
+            // suppress tecEXPIRED so doApply can run and delete expired credential
+            // SLEs from the ledger.
+            auto const checkAccount = [&](AccountID const& acct) -> TER {
+                if (sleDomain->getAccountID(sfOwner) == acct)
+                    return tesSUCCESS;
+                // validDomain returns tecNO_AUTH when no matching credential is
+                // found. Map it to tecNO_PERMISSION to preserve existing behavior.
+                if (auto const err = credentials::validDomain(ctx.view, domainID, acct);
+                    !isTesSuccess(err) && err != tecEXPIRED)
+                    return tecNO_PERMISSION;
+                return tesSUCCESS;
+            };
+
+            if (auto const err = checkAccount(ctx.tx[sfAccount]); !isTesSuccess(err))
+                return err;
+            if (auto const err = checkAccount(ctx.tx[sfDestination]); !isTesSuccess(err))
+                return err;
+        }
+        else
+        {
+            if (!permissioned_dex::accountInDomain(ctx.view, ctx.tx[sfAccount], ctx.tx[sfDomainID]))
+                return tecNO_PERMISSION;
+
+            if (!permissioned_dex::accountInDomain(
+                    ctx.view, ctx.tx[sfDestination], ctx.tx[sfDomainID]))
+                return tecNO_PERMISSION;
+        }
     }
 
     return tesSUCCESS;
@@ -471,6 +501,31 @@ Payment::preclaim(PreclaimContext const& ctx)
 TER
 Payment::doApply()
 {
+    // If a DomainID is present, verify both sender and destination are still in
+    // the domain and delete any expired credential SLEs from the ledger.
+    if (ctx_.tx.isFieldPresent(sfDomainID) && ctx_.view().rules().enabled(fixCleanup3_4_0))
+    {
+        auto const domainID = ctx_.tx[sfDomainID];
+        auto const sleDomain = ctx_.view().read(keylet::permissionedDomain(domainID));
+        if (!sleDomain)
+            return tecINTERNAL;  // LCOV_EXCL_LINE
+
+        auto const cleanupFor = [&](AccountID const& acct) -> TER {
+            if (sleDomain->getAccountID(sfOwner) == acct)
+                return tesSUCCESS;
+            return verifyValidDomain(ctx_.view(), acct, domainID, j_);
+        };
+
+        auto const destination = ctx_.tx[sfDestination];
+        auto const senderErr = cleanupFor(accountID_);
+        auto const destinationErr = accountID_ == destination ? senderErr : cleanupFor(destination);
+
+        if (!isTesSuccess(senderErr))
+            return senderErr;
+        if (!isTesSuccess(destinationErr))
+            return destinationErr;
+    }
+
     auto const deliverMin = ctx_.tx[~sfDeliverMin];
 
     // Ripple if source or destination is non-native or if there are paths.
diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp
index b8118bc49f..9143a675f6 100644
--- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp
+++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp
@@ -87,7 +87,7 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx)
             return temBAD_SIGNATURE;
     }
 
-    if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
+    if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err))
         return err;
 
     return tesSUCCESS;
diff --git a/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp
index 0e036649fd..c3131714f8 100644
--- a/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp
+++ b/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp
@@ -8,6 +8,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -412,9 +413,24 @@ SponsorshipTransfer::doApply()
             if (!oldSponsorSle)
                 return tefINTERNAL;  // LCOV_EXCL_LINE
 
-            // The owner reclaims the reserve burden when the object is no longer sponsored.
-            // We do not check the sponsee's reserve here (via `checkReserve`) so that a sponsor can
-            // always end a sponsorship, even if the sponsee lacks sufficient reserve.
+            // The owner reclaims the reserve burden when the object is no longer
+            // sponsored, so it must be able to hold that reserve on its own once the
+            // sponsorship is removed. This mirrors the account-level End check below,
+            // keeping the behavior consistent across accounts and objects: a
+            // sponsorship can only be ended if the sponsee self-funds, another sponsor
+            // steps in (Reassign), or the object/account is deleted.
+            if (view().rules().enabled(fixCleanup3_4_0))
+            {
+                if (auto const ter = checkReserve(
+                        ctx_.getApplyViewContext(),
+                        sponseeSle,
+                        balanceBeforeFee(sponseeSle),
+                        SLE::pointer(),
+                        {.ownerCountDelta = ownerCountDelta},
+                        ctx_.journal);
+                    !isTesSuccess(ter))
+                    return ter;
+            }
 
             // Decrement sponsored count
             if (auto const ter = decrementSponsorCount(
diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp
index 6366e99105..19ec99702a 100644
--- a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp
+++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp
@@ -1,6 +1,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -70,7 +71,14 @@ ConfidentialMPTClawback::preclaim(PreclaimContext const& ctx)
 
     // Sanity check: account must be the same as issuer
     if (sleIssuance->getAccountID(sfIssuer) != account)
-        return tefINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTClawback::preclaim : preflight already validated the "
+            "submitter is the issuer");
+        return tefINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     // Check if issuance has issuer ElGamal public key
     if (!sleIssuance->isFieldPresent(sfIssuerEncryptionKey))
@@ -127,7 +135,14 @@ ConfidentialMPTClawback::doApply()
     auto sleHolderMPToken = view().peek(keylet::mptoken(mptIssuanceID, holder));
 
     if (!sleIssuance || !sleHolderMPToken)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTClawback::doApply : preclaim already validated these "
+            "objects exist");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     auto const clawAmount = ctx_.tx[sfMPTAmount];
 
@@ -137,11 +152,25 @@ ConfidentialMPTClawback::doApply()
     // After clawback, the balance should be encrypted zero.
     auto const encZeroForHolder = encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID);
     if (!encZeroForHolder)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTClawback::doApply : canonical zero encryption cannot fail "
+            "for an already-valid holder public key");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     auto encZeroForIssuer = encryptCanonicalZeroAmount(issuerPubKey, holder, mptIssuanceID);
     if (!encZeroForIssuer)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTClawback::doApply : canonical zero encryption cannot fail "
+            "for an already-valid issuer public key");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     // Set holder's confidential balances to encrypted zero
     (*sleHolderMPToken)[sfConfidentialBalanceInbox] = *encZeroForHolder;
@@ -154,14 +183,28 @@ ConfidentialMPTClawback::doApply()
         // Sanity check: the issuance must have an auditor public key if
         // auditing is enabled.
         if (!sleIssuance->isFieldPresent(sfAuditorEncryptionKey))
-            return tecINTERNAL;  // LCOV_EXCL_LINE
+        {
+            // LCOV_EXCL_START
+            UNREACHABLE(
+                "xrpl::ConfidentialMPTClawback::doApply : the holder's auditor balance implies "
+                "the issuance has an auditor public key");
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
 
         auto const auditorPubKey = (*sleIssuance)[sfAuditorEncryptionKey];
 
         auto encZeroForAuditor = encryptCanonicalZeroAmount(auditorPubKey, holder, mptIssuanceID);
 
         if (!encZeroForAuditor)
-            return tecINTERNAL;  // LCOV_EXCL_LINE
+        {
+            // LCOV_EXCL_START
+            UNREACHABLE(
+                "xrpl::ConfidentialMPTClawback::doApply : canonical zero encryption cannot "
+                "fail for an already-valid auditor public key");
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
 
         (*sleHolderMPToken)[sfAuditorEncryptedBalance] = std::move(*encZeroForAuditor);
     }
diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp
index 454eb39ead..5be3892151 100644
--- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp
+++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp
@@ -3,6 +3,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -89,7 +90,14 @@ ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx)
     // already checked in preflight, but should also check that issuer on the
     // issuance isn't the account either
     if (sleIssuance->getAccountID(sfIssuer) == account)
-        return tefINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTConvert::preclaim : issuer derived from the MPT ID must "
+            "match the ledger's stored issuer");
+        return tefINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
     bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorEncryptionKey);
@@ -207,11 +215,25 @@ ConfidentialMPTConvert::doApply()
 
     auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
     if (!sleMptoken)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTConvert::doApply : preclaim already validated the MPToken "
+            "exists");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
     if (!sleIssuance)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTConvert::doApply : preclaim already validated the issuance "
+            "exists");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     auto const amtToConvert = ctx_.tx[sfMPTAmount];
     auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0);
@@ -273,7 +295,14 @@ ConfidentialMPTConvert::doApply()
         if (auditorEc)
         {
             if (!sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
-                return tecINTERNAL;  // LCOV_EXCL_LINE
+            {
+                // LCOV_EXCL_START
+                UNREACHABLE(
+                    "xrpl::ConfidentialMPTConvert::doApply : issuance-level auditing implies "
+                    "the MPToken already carries an auditor balance");
+                return tecINTERNAL;
+                // LCOV_EXCL_STOP
+            }
 
             auto sum = homomorphicAdd(*auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance]);
             if (!sum)
@@ -308,7 +337,14 @@ ConfidentialMPTConvert::doApply()
             (*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID);
 
         if (!zeroBalance)
-            return tecINTERNAL;  // LCOV_EXCL_LINE
+        {
+            // LCOV_EXCL_START
+            UNREACHABLE(
+                "xrpl::ConfidentialMPTConvert::doApply : canonical zero encryption cannot fail "
+                "for an already-valid holder public key");
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
 
         (*sleMptoken)[sfConfidentialBalanceSpending] = std::move(*zeroBalance);
     }
@@ -316,7 +352,12 @@ ConfidentialMPTConvert::doApply()
     {
         // both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
         // exist together
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTConvert::doApply : confidential balance fields must be all "
+            "present or all absent");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
     }
 
     view().update(sleIssuance);
diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp
index 87f9e476d6..1e3617ffbd 100644
--- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp
+++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -72,7 +73,14 @@ verifyProofs(
     std::shared_ptr const& mptoken)
 {
     if (!mptoken->isFieldPresent(sfHolderEncryptionKey))
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::verifyProofs : preclaim already validated the holder encryption key is "
+            "present");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     auto const mptIssuanceID = tx[sfMPTokenIssuanceID];
     auto const account = tx[sfAccount];
@@ -169,7 +177,14 @@ ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx)
     // already checked in preflight, but should also check that issuer on
     // the issuance isn't the account either
     if (sleIssuance->getAccountID(sfIssuer) == account)
-        return tefINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTConvertBack::preclaim : issuer derived from the MPT ID must "
+            "match the ledger's stored issuer");
+        return tefINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
     if (!sleMptoken)
@@ -185,7 +200,14 @@ ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx)
     // Sanity check: holder's MPToken must have auditor balance field if auditing
     // is enabled
     if (requiresAuditor && !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))
-        return tefINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTConvertBack::preclaim : issuance-level auditing implies the "
+            "MPToken already carries an auditor balance");
+        return tefINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     // if the total circulating confidential balance is smaller than what the
     // holder is trying to convert back, we know for sure this txn should
@@ -215,11 +237,25 @@ ConfidentialMPTConvertBack::doApply()
 
     auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
     if (!sleMptoken)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTConvertBack::doApply : preclaim already validated the "
+            "MPToken exists");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     auto sleIssuance = view().peek(keylet::mptokenIssuance(mptIssuanceID));
     if (!sleIssuance)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTConvertBack::doApply : preclaim already validated the "
+            "issuance exists");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     auto const amtToConvertBack = ctx_.tx[sfMPTAmount];
     auto const amt = (*sleMptoken)[~sfMPTAmount].valueOr(0);
diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp
index 0b98382a61..6485578cb4 100644
--- a/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp
+++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTMergeInbox.cpp
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -49,7 +50,14 @@ ConfidentialMPTMergeInbox::preclaim(PreclaimContext const& ctx)
     // already checked in preflight, but should also check that issuer on the
     // issuance isn't the account either
     if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
-        return tefINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTMergeInbox::preclaim : issuer derived from the MPT ID must "
+            "match the ledger's stored issuer");
+        return tefINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     auto const sleMptoken =
         ctx.view.read(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
@@ -82,14 +90,26 @@ ConfidentialMPTMergeInbox::doApply()
     auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
     auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, accountID_));
     if (!sleMptoken)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTMergeInbox::doApply : preclaim already validated the "
+            "MPToken exists");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     // sanity check
     if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
         !sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
         !sleMptoken->isFieldPresent(sfHolderEncryptionKey))
     {
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTMergeInbox::doApply : preclaim already validated these "
+            "fields are present");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
     }
 
     // Merge inbox into spending: spending = spending + inbox
@@ -114,7 +134,14 @@ ConfidentialMPTMergeInbox::doApply()
         encryptCanonicalZeroAmount((*sleMptoken)[sfHolderEncryptionKey], accountID_, mptIssuanceID);
 
     if (!zeroEncryption)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTMergeInbox::doApply : canonical zero encryption cannot fail "
+            "for an already-valid holder public key");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     (*sleMptoken)[sfConfidentialBalanceInbox] = std::move(*zeroEncryption);
 
diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp
index d121ec2634..e713ae5029 100644
--- a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp
+++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp
@@ -2,6 +2,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -82,7 +83,7 @@ ConfidentialMPTSend::preflight(PreflightContext const& ctx)
     if (hasAuditor && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount]))
         return temBAD_CIPHERTEXT;
 
-    if (auto const err = credentials::checkFields(ctx.tx, ctx.j); !isTesSuccess(err))
+    if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err))
         return err;
 
     return tesSUCCESS;
@@ -105,7 +106,14 @@ verifySendProofs(
 {
     // Sanity check
     if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::detail::verifySendProofs : caller must pre-validate sender/destination/"
+            "issuance existence");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
 
@@ -204,7 +212,14 @@ ConfidentialMPTSend::preclaim(PreclaimContext const& ctx)
 
     // Sanity check: issuer isn't the sender
     if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
-        return tefINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTSend::preclaim : issuer derived from the MPT ID must match "
+            "the ledger's stored issuer");
+        return tefINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     // Check sender's MPToken existence
     auto const sleSenderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
@@ -238,7 +253,12 @@ ConfidentialMPTSend::preclaim(PreclaimContext const& ctx)
         (!sleSenderMPToken->isFieldPresent(sfAuditorEncryptedBalance) ||
          !sleDestinationMPToken->isFieldPresent(sfAuditorEncryptedBalance)))
     {
-        return tefINTERNAL;  // LCOV_EXCL_LINE
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTSend::preclaim : issuance-level auditing implies both "
+            "MPTokens already carry an auditor balance");
+        return tefINTERNAL;
+        // LCOV_EXCL_STOP
     }
 
     // Check lock
@@ -283,7 +303,14 @@ ConfidentialMPTSend::doApply()
     auto const sleDestAcct = view().read(keylet::account(destination));
 
     if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance || !sleDestAcct)
-        return tecINTERNAL;  // LCOV_EXCL_LINE
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE(
+            "xrpl::ConfidentialMPTSend::doApply : preclaim already validated these objects "
+            "exist");
+        return tecINTERNAL;
+        // LCOV_EXCL_STOP
+    }
 
     // Deposit preauth authorization was already verified in preclaim.
     // Remove any expired credentials.
@@ -353,7 +380,13 @@ ConfidentialMPTSend::doApply()
         auto rerandomizedDestEc = rerandomizeCiphertext(
             destEc, (*sleDestinationMPToken)[sfHolderEncryptionKey], sendChallenge);
         if (!rerandomizedDestEc)
-            return tecINTERNAL;  // LCOV_EXCL_LINE
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx_.journal.error())
+                << "ConfidentialMPTSend failed to rerandomize destination inbox ciphertext.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
 
         auto const curInbox = (*sleDestinationMPToken)[sfConfidentialBalanceInbox];
         auto newInbox = homomorphicAdd(curInbox, *rerandomizedDestEc);
@@ -374,7 +407,13 @@ ConfidentialMPTSend::doApply()
         auto rerandomizedIssuerEc =
             rerandomizeCiphertext(issuerEc, (*sleIssuance)[sfIssuerEncryptionKey], sendChallenge);
         if (!rerandomizedIssuerEc)
-            return tecINTERNAL;  // LCOV_EXCL_LINE
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx_.journal.error())
+                << "ConfidentialMPTSend failed to rerandomize destination issuer ciphertext.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
 
         auto const curIssuerEnc = (*sleDestinationMPToken)[sfIssuerEncryptedBalance];
         auto newIssuerEnc = homomorphicAdd(curIssuerEnc, *rerandomizedIssuerEc);
@@ -396,7 +435,13 @@ ConfidentialMPTSend::doApply()
         auto rerandomizedAuditorEc = rerandomizeCiphertext(
             *auditorEc, (*sleIssuance)[sfAuditorEncryptionKey], sendChallenge);
         if (!rerandomizedAuditorEc)
-            return tecINTERNAL;  // LCOV_EXCL_LINE
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx_.journal.error())
+                << "ConfidentialMPTSend failed to rerandomize destination auditor ciphertext.";
+            return tecINTERNAL;
+            // LCOV_EXCL_STOP
+        }
 
         auto const curAuditorEnc = (*sleDestinationMPToken)[sfAuditorEncryptedBalance];
         auto newAuditorEnc = homomorphicAdd(curAuditorEnc, *rerandomizedAuditorEc);
diff --git a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp
index 0aeb6f33d1..60b5c6d3af 100644
--- a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp
+++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp
@@ -37,6 +37,7 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
 {
     auto const accountID = ctx.tx[sfAccount];
     auto const holderID = ctx.tx[~sfHolder];
+    auto const sleMptIssuance = ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
 
     // if non-issuer account submits this tx, then they are trying either:
     // 1. Unauthorize/delete MPToken
@@ -51,9 +52,8 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
 
         // There is an edge case where all holders have zero balance, issuance
         // is legally destroyed, then outstanding MPT(s) are deleted afterwards.
-        // Thus, there is no need to check for the existence of the issuance if
-        // the MPT is being deleted with a zero balance. Check for unauthorize
-        // before fetching the MPTIssuance object.
+        // Thus, the unauthorize/delete path below does not require the issuance
+        // to exist when the MPT is being deleted with a zero balance.
 
         // if holder wants to delete/unauthorize a mpt
         if (ctx.tx.isFlag(tfMPTUnauthorize))
@@ -63,8 +63,6 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
 
             if ((*sleMpt)[sfMPTAmount] != 0)
             {
-                auto const sleMptIssuance =
-                    ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
                 if (!sleMptIssuance)
                     return tefINTERNAL;  // LCOV_EXCL_LINE
 
@@ -73,21 +71,24 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
 
             if ((*sleMpt)[~sfLockedAmount].value_or(0) != 0)
             {
-                auto const sleMptIssuance =
-                    ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
                 if (!sleMptIssuance)
                     return tefINTERNAL;  // LCOV_EXCL_LINE
 
                 return tecHAS_OBLIGATIONS;
             }
-            if (ctx.view.rules().enabled(featureSingleAssetVault) && sleMpt->isFlag(lsfMPTLocked))
+            if (ctx.view.rules().enabled(fixCleanup3_4_0))
+            {
+                if (sleMptIssuance && sleMpt->isFlag(lsfMPTLocked))
+                    return tecNO_PERMISSION;
+            }
+            else if (
+                ctx.view.rules().enabled(featureSingleAssetVault) && sleMpt->isFlag(lsfMPTLocked))
+            {
                 return tecNO_PERMISSION;
+            }
 
             if (ctx.view.rules().enabled(featureConfidentialTransfer))
             {
-                auto const sleMptIssuance =
-                    ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
-
                 // if there still existing encrypted balances of MPT in
                 // circulation
                 if (sleMptIssuance &&
@@ -106,9 +107,6 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
         }
 
         // Now test when the holder wants to hold/create/authorize a new MPT
-        auto const sleMptIssuance =
-            ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
-
         if (!sleMptIssuance)
             return tecOBJECT_NOT_FOUND;
 
@@ -126,7 +124,6 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
     if (!sleHolder)
         return tecNO_DST;
 
-    auto const sleMptIssuance = ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID]));
     if (!sleMptIssuance)
         return tecOBJECT_NOT_FOUND;
 
@@ -153,7 +150,7 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
     // always authorized. No need to amendment gate since Vault and LoanBroker
     // can only be created if the Vault amendment is enabled; AMM with MPToken asset
     // can only be created if MPTokensV2 is enabled.
-    if (isPseudoAccount(ctx.view, *holderID, {&sfVaultID, &sfLoanBrokerID, &sfAMMID}))
+    if (isPseudoAccount(ctx.view, *holderID))
         return tecNO_PERMISSION;
 
     return tesSUCCESS;
diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp
index e8fd2e22b6..6bd7140631 100644
--- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp
+++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp
@@ -119,7 +119,15 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
     if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey))
         return temMALFORMED;
 
-    if (hasAuditorElGamalKey && !hasIssuerElGamalKey)
+    // Pre-ConfidentialMPTKeyRotation amendment, the auditor key could not be
+    // registered independently of the issuer key. The issuer could either:
+    // - Register only the issuer key (in which case an auditor key could not be added later), or
+    // - Register both the issuer and auditor keys simultaneously.
+    //
+    // Post-ConfidentialMPTKeyRotation amendment, the auditor key can be
+    // registered after the issuer key has already been registered.
+    if (hasAuditorElGamalKey && !hasIssuerElGamalKey &&
+        !ctx.rules.enabled(featureConfidentialMPTKeyRotation))
         return temMALFORMED;
 
     if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerEncryptionKey]))
@@ -219,18 +227,57 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
             return tecNO_PERMISSION;
     }
 
-    // cannot update issuer public key
-    if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
-        sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey))
-    {
-        return tecNO_PERMISSION;
-    }
+    // Updating an existing encryption key requires the
+    // ConfidentialMPTKeyRotation amendment.
+    bool const canRotateKey = ctx.view.rules().enabled(featureConfidentialMPTKeyRotation);
 
-    // cannot update auditor public key
-    if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
-        sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey))
+    bool const txHasIssuerKey = ctx.tx.isFieldPresent(sfIssuerEncryptionKey);
+    bool const txHasAuditorKey = ctx.tx.isFieldPresent(sfAuditorEncryptionKey);
+    bool const sleHasIssuerKey = sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey);
+    bool const sleHasAuditorKey = sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey);
+
+    if (canRotateKey)
     {
-        return tecNO_PERMISSION;  // LCOV_EXCL_LINE
+        // Post-ConfidentialMPTKeyRotation amendment, the encryption keys can be updated.
+        // A first-time auditor key registration requires an issuer key,
+        // either already on the issuance or set by the same transaction.
+        bool const registersAuditorKey = txHasAuditorKey && !sleHasAuditorKey;
+        bool const issuerKeyExists = sleHasIssuerKey || txHasIssuerKey;
+        if (registersAuditorKey && !issuerKeyExists)
+            return tecNO_PERMISSION;
+
+        // Rotating a key to its current value is not permitted: a key epoch
+        // increment must always correspond to an actual key change.
+        if (txHasIssuerKey && sleHasIssuerKey &&
+            ctx.tx[sfIssuerEncryptionKey] == (*sleMptIssuance)[sfIssuerEncryptionKey])
+            return tecDUPLICATE;
+
+        if (txHasAuditorKey && sleHasAuditorKey &&
+            ctx.tx[sfAuditorEncryptionKey] == (*sleMptIssuance)[sfAuditorEncryptionKey])
+            return tecDUPLICATE;
+
+        // Key epochs must never wrap. Epoch 0 serves as the sentinel for "never
+        // rotated." Holders' mirror epochs are checked against it for equality,
+        // so a wrap would cause stale mirror ciphertexts to appear valid instead
+        // of failing loudly.
+        if (txHasIssuerKey && sleHasIssuerKey &&
+            (*sleMptIssuance)[~sfIssuerKeyEpoch].value_or(0) == kMaxKeyEpoch)
+            return tecNO_PERMISSION;
+
+        if (txHasAuditorKey && sleHasAuditorKey &&
+            (*sleMptIssuance)[~sfAuditorKeyEpoch].value_or(0) == kMaxKeyEpoch)
+            return tecNO_PERMISSION;
+    }
+    else
+    {
+        // Pre-ConfidentialMPTKeyRotation amendment, the encryption keys can not be updated.
+        // cannot update issuer public key
+        if (txHasIssuerKey && sleHasIssuerKey)
+            return tecNO_PERMISSION;
+
+        // cannot update auditor public key
+        if (txHasAuditorKey && sleHasAuditorKey)
+            return tecNO_PERMISSION;  // LCOV_EXCL_LINE
     }
 
     auto const enablesConfidentialBalance =
@@ -241,25 +288,30 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
 
     // Encryption keys can only be set if confidential amounts are already
     // enabled on the issuance OR if the transaction is enabling it
-    if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
-        !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialBalance)
+    if (txHasIssuerKey && !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) &&
+        !enablesConfidentialBalance)
     {
         return tecNO_PERMISSION;
     }
 
-    if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
-        !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialBalance)
+    if (txHasAuditorKey && !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) &&
+        !enablesConfidentialBalance)
     {
         return tecNO_PERMISSION;
     }
 
-    // cannot upload key if there's circulating supply of COA
-    if ((ctx.tx.isFieldPresent(sfIssuerEncryptionKey) ||
-         ctx.tx.isFieldPresent(sfAuditorEncryptionKey) || enablesConfidentialBalance) &&
-        (*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
-    {
+    bool const hasConfidentialOA =
+        (*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0;
+
+    // Pre-ConfidentialMPTKeyRotation amendment, keys cannot be uploaded while
+    // COA > 0. Post-amendment they can be uploaded even if COA > 0.
+    if (!canRotateKey && (txHasIssuerKey || txHasAuditorKey) && hasConfidentialOA)
         return tecNO_PERMISSION;  // LCOV_EXCL_LINE
-    }
+
+    // Enabling confidential balances when COA > 0 is not permitted, regardless of
+    // ConfidentialMPTKeyRotation.
+    if (enablesConfidentialBalance && hasConfidentialOA)
+        return tecNO_PERMISSION;
 
     return tesSUCCESS;
 }
@@ -377,25 +429,69 @@ MPTokenIssuanceSet::doApply()
         }
     }
 
-    if (auto const pubKey = ctx_.tx[~sfIssuerEncryptionKey])
-    {
-        // This is enforced in preflight.
+    // Sets an encryption key on the issuance. Overwriting an existing key
+    // (a rotation) increments the corresponding key epoch; a first-time
+    // registration leaves the epoch absent (epoch 0), matching issuances
+    // whose keys were registered before the ConfidentialMPTKeyRotation
+    // amendment.
+    bool const canRotateKey = view().rules().enabled(featureConfidentialMPTKeyRotation);
+    auto const setEncryptionKey = [&](SF_VL const& keyField, SF_UINT32 const& epochField) -> TER {
+        auto const pubKey = ctx_.tx[~keyField];
+        if (!pubKey)
+            return tesSUCCESS;
+
+        // This is enforced in preflight, which rejects a transaction carrying
+        // both sfHolder and an encryption key.
         XRPL_ASSERT(
             sle->getType() == ltMPTOKEN_ISSUANCE,
             "MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
 
-        sle->setFieldVL(sfIssuerEncryptionKey, *pubKey);
-    }
+        // Add sanity check under the amendment ConfidentialMPTKeyRotation.
+        // Pre-confidentialMPTKeyRotation did not return tecINTERNAL so
+        // this should be under the amendment guard.
+        if (canRotateKey && sle->getType() != ltMPTOKEN_ISSUANCE)
+            return tecINTERNAL;  // LCOV_EXCL_LINE
 
-    if (auto const pubKey = ctx_.tx[~sfAuditorEncryptionKey])
-    {
-        // This is enforced in preflight.
-        XRPL_ASSERT(
-            sle->getType() == ltMPTOKEN_ISSUANCE,
-            "MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
+        // NOTE: presence must be checked before the key is overwritten below.
+        bool const isRotation = sle->isFieldPresent(keyField);
+        sle->setFieldVL(keyField, *pubKey);
 
-        sle->setFieldVL(sfAuditorEncryptionKey, *pubKey);
-    }
+        if (isRotation)
+        {
+            // Preclaim rejects overwriting an existing key unless the amendment is
+            // enabled.
+            if (!canRotateKey)
+            {
+                // LCOV_EXCL_START
+                UNREACHABLE("xrpl::MPTokenIssuanceSet::doApply : rotation without amendment");
+                return tecINTERNAL;
+                // LCOV_EXCL_STOP
+            }
+
+            auto const epoch = (*sle)[~epochField].valueOr(0);
+
+            // Preclaim rejects a rotation that would wrap the epoch. So this should never happen.
+            if (epoch >= kMaxKeyEpoch)
+            {
+                // LCOV_EXCL_START
+                UNREACHABLE("xrpl::MPTokenIssuanceSet::doApply : key epoch overflow");
+                return tecINTERNAL;
+                // LCOV_EXCL_STOP
+            }
+
+            (*sle)[epochField] = epoch + 1;
+        }
+
+        return tesSUCCESS;
+    };
+
+    if (auto const ter = setEncryptionKey(sfIssuerEncryptionKey, sfIssuerKeyEpoch);
+        !isTesSuccess(ter))
+        return ter;  // LCOV_EXCL_LINE
+
+    if (auto const ter = setEncryptionKey(sfAuditorEncryptionKey, sfAuditorKeyEpoch);
+        !isTesSuccess(ter))
+        return ter;  // LCOV_EXCL_LINE
 
     view().update(sle);
 
diff --git a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp
index d77286b667..059da7cc0f 100644
--- a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp
@@ -6,6 +6,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -95,6 +96,17 @@ VaultClawback::preclaim(PreclaimContext const& ctx)
         // LCOV_EXCL_STOP
     }
 
+    // A pseudo-account holds no vault shares, so a clawback naming one is a no-op: the vault's own
+    // pseudo-account issues the shares, and no flow hands them to another one.
+    // Pre-fixCleanup3_4_0: an implicit amount ends in tecPRECISION_LOSS, an explicit one debits the
+    // vault and trips the "shares must move" invariant.
+    // Post-fixCleanup3_4_0: refused here.
+    if (ctx.view.rules().enabled(fixCleanup3_4_0) && isPseudoAccount(ctx.view, holder))
+    {
+        JLOG(ctx.j.debug()) << "VaultClawback: holder is a pseudo-account.";
+        return tecPSEUDO_ACCOUNT;
+    }
+
     Asset const share = MPTIssue{mptIssuanceID};
 
     // Ambiguous case: If Issuer is Owner they must specify the asset
@@ -225,6 +237,7 @@ VaultClawback::assetsToClawback(
     AccountID const& holder,
     STAmount const& clawbackAmount)
 {
+    bool const fix340Enabled = ctx_.view().rules().enabled(fixCleanup3_4_0);
     if (clawbackAmount.asset() != vault->at(sfAsset))
     {
         // preclaim should have blocked this , now it's an internal error
@@ -256,14 +269,35 @@ VaultClawback::assetsToClawback(
     STAmount sharesDestroyed;
     STAmount assetsRecovered;
 
+    // Number arithmetic can throw overflow_error when Scale and totals are large. Caught below.
     try
     {
+        // Do not discount a sole holder's shares: clawing back AssetsAvailable
+        // at the discounted rate can burn every share while loan assets remain.
+        auto const waiveUnrealizedLoss =
+            fix340Enabled && isSoleShareholder(view(), holder, sleShareIssuance)
+            ? WaiveUnrealizedLoss::Yes
+            : WaiveUnrealizedLoss::No;
+
         if (clawbackAmount == beast::kZero)
         {
-            sharesDestroyed = accountHolds(
-                view(), holder, share, FreezeHandling::IgnoreFreeze, AuthHandling::IgnoreAuth, j_);
-            auto const maybeAssets =
-                sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed);
+            // Zero amount means clawback all shares the holder has; derive the corresponding asset
+            // amount from the share balance.
+            // isSoleShareholder already established that the holder owns the
+            // entire outstanding share supply whenever the waiver applies, so
+            // sfOutstandingAmount gives sharesDestroyed directly, avoiding a
+            // redundant MPToken read via accountHolds.
+            sharesDestroyed = waiveUnrealizedLoss == WaiveUnrealizedLoss::Yes
+                ? STAmount{share, sleShareIssuance->at(sfOutstandingAmount)}
+                : accountHolds(
+                      view(),
+                      holder,
+                      share,
+                      FreezeHandling::IgnoreFreeze,
+                      AuthHandling::IgnoreAuth,
+                      j_);
+            auto const maybeAssets = sharesToAssetsWithdraw(
+                vault, sleShareIssuance, sharesDestroyed, waiveUnrealizedLoss);
             if (!maybeAssets)
                 return std::unexpected(tecINTERNAL);  // LCOV_EXCL_LINE
 
@@ -271,38 +305,48 @@ VaultClawback::assetsToClawback(
         }
         else
         {
-            auto const maybeShares =
-                assetsToSharesWithdraw(vault, sleShareIssuance, clawbackAmount);
+            // Pre-fixCleanup3_4_0: shares were rounded to nearest, so the
+            // round-trip back to assets could exceed clawbackAmount.
+            // Post-amendment: truncate shares so assetsRecovered <=
+            // clawbackAmount by construction (matches the clamp branch
+            // below).
+            auto const truncate = fix340Enabled ? TruncateShares::Yes : TruncateShares::No;
+            auto const maybeShares = assetsToSharesWithdraw(
+                vault, sleShareIssuance, clawbackAmount, truncate, waiveUnrealizedLoss);
             if (!maybeShares)
                 return std::unexpected(tecINTERNAL);  // LCOV_EXCL_LINE
             sharesDestroyed = *maybeShares;
 
-            auto const maybeAssets =
-                sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed);
+            auto const maybeAssets = sharesToAssetsWithdraw(
+                vault, sleShareIssuance, sharesDestroyed, waiveUnrealizedLoss);
             if (!maybeAssets)
                 return std::unexpected(tecINTERNAL);  // LCOV_EXCL_LINE
             assetsRecovered = *maybeAssets;
         }
-        // Clamp to maximum.
+        // Clamp assetsRecovered to sfAssetsAvailable, then re-derive shares and assets so the pair
+        // stays consistent.
         if (assetsRecovered > *assetsAvailable)
         {
             assetsRecovered = *assetsAvailable;
-            // Note, it is important to truncate the number of shares,
-            // otherwise the corresponding assets might breach the
-            // AssetsAvailable
             {
                 auto const maybeShares = assetsToSharesWithdraw(
-                    vault, sleShareIssuance, assetsRecovered, TruncateShares::Yes);
+                    vault,
+                    sleShareIssuance,
+                    assetsRecovered,
+                    TruncateShares::Yes,
+                    waiveUnrealizedLoss);
                 if (!maybeShares)
                     return std::unexpected(tecINTERNAL);  // LCOV_EXCL_LINE
                 sharesDestroyed = *maybeShares;
             }
 
-            auto const maybeAssets =
-                sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed);
+            auto const maybeAssets = sharesToAssetsWithdraw(
+                vault, sleShareIssuance, sharesDestroyed, waiveUnrealizedLoss);
             if (!maybeAssets)
                 return std::unexpected(tecINTERNAL);  // LCOV_EXCL_LINE
             assetsRecovered = *maybeAssets;
+            // Truncation should guarantee the invariant holds. If it does not, a conversion
+            // helper is broken; refuse rather than over-recover.
             if (assetsRecovered > *assetsAvailable)
             {
                 // LCOV_EXCL_START
@@ -311,6 +355,18 @@ VaultClawback::assetsToClawback(
                 // LCOV_EXCL_STOP
             }
         }
+
+        // Post-fixCleanup3_4_0: round the recovery down at the posterior sfAssetsTotal scale so all
+        // rails change by the same representable delta. sharesDestroyed is intentionally NOT
+        // re-derived here: the holder's shares are burned for their pre-clamp value, so any
+        // sub-ULP trimmed off stays in the vault for the remaining shareholders.
+        if (ctx_.view().rules().enabled(fixCleanup3_4_0) && assetsRecovered > beast::kZero)
+        {
+            auto const maybeClamped = clampToAssetsTotalScale(vault, -assetsRecovered);
+            if (!maybeClamped)
+                return std::unexpected(maybeClamped.error());
+            assetsRecovered = *maybeClamped;
+        }
     }
     catch (std::overflow_error const&)
     {
@@ -322,6 +378,8 @@ VaultClawback::assetsToClawback(
             << ", assetsTotal=" << vault->at(sfAssetsTotal).value()
             << ", sharesTotal=" << sleShareIssuance->at(sfOutstandingAmount)
             << ", amount=" << clawbackAmount.value();
+        // Overflow means this transaction cannot apply, but ledger state is still consistent.
+        // Return tecPATH_DRY rather than a hard internal error.
         return std::unexpected(tecPATH_DRY);
     }
 
@@ -353,11 +411,6 @@ VaultClawback::doApply()
     auto assetsAvailable = vault->at(sfAssetsAvailable);
     auto assetsTotal = vault->at(sfAssetsTotal);
 
-    [[maybe_unused]] auto const lossUnrealized = vault->at(sfLossUnrealized);
-    XRPL_ASSERT(
-        lossUnrealized <= (assetsTotal - assetsAvailable),
-        "xrpl::VaultClawback::doApply : loss and assets do balance");
-
     AccountID const holder = tx[sfHolder];
     STAmount sharesDestroyed = {share};
     STAmount assetsRecovered = {vault->at(sfAsset)};
@@ -380,9 +433,46 @@ VaultClawback::doApply()
         sharesDestroyed = clawbackParts->second;
     }
 
+    // The holder has no shares (or the recovery clamped to zero). Nothing to burn; refuse rather
+    // than modifying vault state.
     if (sharesDestroyed == beast::kZero)
         return tecPRECISION_LOSS;
 
+    // Number arithmetic can throw overflow_error when Scale and totals are large.
+    if (view().rules().enabled(fixCleanup3_4_0))
+    {
+        try
+        {
+            // A non-zero recovery can be too small to change the stored sfAssetsTotal at
+            // STAmount's precision. Shares would still be burned, reject it instead.
+            if (debitIsNonZeroDust(vaultAsset, assetsTotal, assetsRecovered))
+            {
+                // LCOV_EXCL_START
+                JLOG(j_.debug())
+                    << "VaultClawback: clawback amount too small to change stored vault"
+                       " balance";
+                return tecPRECISION_LOSS;
+                // LCOV_EXCL_STOP
+            }
+        }
+        // LCOV_EXCL_START
+        catch (std::overflow_error const&)
+        {
+            // It's easy to hit this exception from Number with large enough Scale
+            // so we avoid spamming the log and only use debug here.
+            JLOG(j_.debug())  //
+                << "VaultClawback: overflow error with"
+                << " scale=" << (int)vault->at(sfScale).value()  //
+                << ", assetsTotal=" << vault->at(sfAssetsTotal).value()
+                << ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
+                << ", amount=" << amount.value();
+            // Overflow means this transaction cannot apply, but ledger state is still
+            // consistent. Return tecPATH_DRY rather than a hard internal error.
+            return tecPATH_DRY;
+        }
+        // LCOV_EXCL_STOP
+    }
+
     assetsTotal -= assetsRecovered;
     assetsAvailable -= assetsRecovered;
     view().update(vault);
diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp
index f74a27c39b..7ade4ed5ab 100644
--- a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp
@@ -8,6 +8,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -43,6 +44,11 @@ VaultCreate::checkExtraFeatures(PreflightContext const& ctx)
     if (ctx.tx.isFieldPresent(sfDomainID) && !ctx.rules.enabled(featurePermissionedDomains))
         return false;
 
+    if (!ctx.rules.enabled(featureLendingProtocolV1_1) &&
+        (ctx.tx.isFieldPresent(sfVaultKind) || ctx.tx.isFieldPresent(sfSubscriptionDate) ||
+         ctx.tx.isFieldPresent(sfRedemptionDate)))
+        return false;
+
     return true;
 }
 
@@ -99,6 +105,22 @@ VaultCreate::preflight(PreflightContext const& ctx)
             return temMALFORMED;
     }
 
+    if (!isValidVaultKind(ctx.tx))
+        return temMALFORMED;
+    auto const kind = getVaultKind(ctx.tx);
+    auto const hasSubscription = ctx.tx.isFieldPresent(sfSubscriptionDate);
+    auto const hasRedemption = ctx.tx.isFieldPresent(sfRedemptionDate);
+    auto const isClosedEnded = kind == VaultKind::ClosedEnded;
+    if (!isClosedEnded && (hasSubscription || hasRedemption))
+        return temMALFORMED;
+    if (isClosedEnded)
+    {
+        if (!hasSubscription || !hasRedemption)
+            return temMALFORMED;
+        if (!isValidClosedEndedGap(ctx.tx[sfSubscriptionDate], ctx.tx[sfRedemptionDate]))
+            return temMALFORMED;
+    }
+
     return tesSUCCESS;
 }
 
@@ -136,6 +158,16 @@ VaultCreate::preclaim(PreclaimContext const& ctx)
         accountId == beast::kZero)
         return terADDRESS_COLLISION;
 
+    // preflight enforces red >= sub + kMinInvestmentPeriod for closed-ended
+    // vaults, so a past RedemptionDate always implies a strictly-earlier,
+    // equally-past SubscriptionDate. The RedemptionDate arm below is therefore
+    // defensive: it cannot be the sole cause of tecEXPIRED. It is kept to
+    // preserve the invariant locally in case the preflight gap check is ever
+    // weakened.
+    if (hasExpired(ctx.view, ctx.tx[~sfSubscriptionDate]) ||
+        hasExpired(ctx.view, ctx.tx[~sfRedemptionDate]))
+        return tecEXPIRED;
+
     return tesSUCCESS;
 }
 
@@ -242,7 +274,17 @@ VaultCreate::doApply()
     if (scale != 0u)
         vault->at(sfScale) = scale;
     if (view().rules().enabled(featureLendingProtocolV1_1))
+    {
         vault->at(sfLEVersion) = std::to_underlying(VaultVersion::CashBasis);
+
+        auto const kind = getVaultKind(tx);
+        vault->at(sfVaultKind) = std::to_underlying(kind);
+        if (kind == VaultKind::ClosedEnded)
+        {
+            vault->at(sfSubscriptionDate) = tx[sfSubscriptionDate];
+            vault->at(sfRedemptionDate) = tx[sfRedemptionDate];
+        }
+    }
     view().insert(vault);
 
     // Explicitly create MPToken for the vault owner
diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp
index 497a2f2465..9c6c41654b 100644
--- a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp
@@ -33,6 +33,7 @@ VaultDelete::preflight(PreflightContext const& ctx)
     if (ctx.tx.isFieldPresent(sfMemoData) && !ctx.rules.enabled(featureLendingProtocolV1_1))
         return temDISABLED;
 
+    // The sfMemoData field is an optional field used to record the deletion reason.
     if (!validDataLength(ctx.tx[~sfMemoData], kMaxDataPayloadLength))
         return temMALFORMED;
 
diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
index aa9cfc8537..fc72159444 100644
--- a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
@@ -2,17 +2,20 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
-#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -46,6 +49,39 @@ roundToVaultScale(STAmount const& amount, SLE::const_ref vault)
     return roundToScale(amount, postScale, Number::RoundingMode::Downward);
 }
 
+// True if debiting `assets` would leave the depositor's balance where it started, so the deposit
+// would mint shares against a transfer that never happened. Asking the balance directly whether it
+// notices the debit avoids having to infer the rounding step: it has to be the stored balance that
+// answers, because that magnitude is what governs the rounding, and it is not the same as the
+// spendable amount, which also counts what the counterparty's limit allows.
+[[nodiscard]]
+static bool
+roundsToZeroForDepositor(
+    ReadView const& view,
+    AccountID const& account,
+    STAmount const& assets,
+    beast::Journal j)
+{
+    if (assets.integral())
+        return false;
+
+    auto const balance = accountHolds(
+        view,
+        account,
+        assets.asset(),
+        FreezeHandling::ZeroIfFrozen,
+        AuthHandling::ZeroIfUnauthorized,
+        j,
+        SpendableHandling::SimpleBalance);
+
+    if (balance - assets != balance)
+        return false;
+
+    JLOG(j.warn()) << "VaultDeposit: amount " << assets.getFullText()
+                   << " leaves the depositor's balance " << balance.getFullText() << " unchanged";
+    return true;
+}
+
 NotTEC
 VaultDeposit::preflight(PreflightContext const& ctx)
 {
@@ -71,6 +107,17 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
     if (!vault)
         return tecNO_ENTRY;
 
+    if (ctx.view.rules().enabled(featureLendingProtocolV1_1))
+    {
+        auto const phase = getVaultPhase(ctx.view, vault);
+        if (phase == VaultPhase::Investment || phase == VaultPhase::Redemption)
+        {
+            JLOG(ctx.j.debug()) << "VaultDeposit: vault deposit is not allowed in the investment "
+                                   "or redemption phase.";
+            return tecEXPIRED;
+        }
+    }
+
     auto const& account = ctx.tx[sfAccount];
     auto const amount = ctx.tx[sfAmount];
     auto const vaultAsset = vault->at(sfAsset);
@@ -127,26 +174,13 @@ VaultDeposit::preclaim(PreclaimContext const& ctx)
             return tecLOCKED;
     }
 
+    // The vault owner is authorized to deposit unconditionally. An expired
+    // credential is tolerated here because doApply deletes it.
     if (vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner))
     {
-        auto const maybeDomainID = sleIssuance->at(~sfDomainID);
-        // Since this is a private vault and the account is not its owner, we
-        // perform authorization check based on DomainID read from sleIssuance.
-        // Had the vault shares been a regular MPToken, we would allow
-        // authorization granted by the Issuer explicitly, but Vault uses Issuer
-        // pseudo-account, which cannot grant an authorization.
-        if (maybeDomainID)
-        {
-            // As per validDomain documentation, we suppress tecEXPIRED error
-            // here, so we can delete any expired credentials inside doApply.
-            if (auto const err = credentials::validDomain(ctx.view, *maybeDomainID, account);
-                !isTesSuccess(err) && err != tecEXPIRED)
-                return err;
-        }
-        else
-        {
-            return tecNO_AUTH;
-        }
+        if (auto const err = checkVaultDomain(ctx.view, sleIssuance, account, SuppressExpired::Yes);
+            !isTesSuccess(err))
+            return err;
     }
 
     // Source MPToken must exist (if asset is an MPT)
@@ -196,6 +230,7 @@ TER
 VaultDeposit::doApply()
 {
     bool const fix320Enabled = view().rules().enabled(fixCleanup3_2_0);
+    bool const fix340Enabled = view().rules().enabled(fixCleanup3_4_0);
     auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID]));
     auto applyViewContext = ctx_.getApplyViewContext();
     if (!vault)
@@ -272,6 +307,8 @@ VaultDeposit::doApply()
     }
 
     STAmount sharesCreated = {vault->at(sfShareMPTID)}, assetsDeposited;
+
+    // Number arithmetic can throw overflow_error when Scale and totals are large. Caught below.
     try
     {
         // Compute exchange before transferring any amounts.
@@ -281,14 +318,20 @@ VaultDeposit::doApply()
                 return tecINTERNAL;  // LCOV_EXCL_LINE
             sharesCreated = *maybeShares;
         }
+
         if (sharesCreated == beast::kZero)
             return tecPRECISION_LOSS;
 
+        // Convert shares back to assets so the depositor is debited for the amount actually minted.
+        // The truncated share count is worth <= amount; without this the difference would be
+        // credited to the vault for free.
         auto const maybeAssets = sharesToAssetsDeposit(vault, sleIssuance, sharesCreated);
         if (!maybeAssets)
         {
             return tecINTERNAL;  // LCOV_EXCL_LINE
         }
+        // The round-trip must never return more than the original amount. If it does, a conversion
+        // helper is broken. Reject rather than overcharge the depositor.
         if (*maybeAssets > amount)
         {
             // LCOV_EXCL_START
@@ -297,6 +340,27 @@ VaultDeposit::doApply()
             // LCOV_EXCL_STOP
         }
         assetsDeposited = *maybeAssets;
+
+        // Post-fixCleanup3_4_0: round the deposit to the sfAssetsTotal scale so all accounting
+        // fields (trust line / MPT, sfAssetsAvailable, sfAssetsTotal) change by the same
+        // representable delta.
+        if (fix340Enabled)
+        {
+            // Round down at the posterior sfAssetsTotal scale so the vault is credited by no more
+            // than the depositor paid. Keep the share count from the first round trip: the clamp
+            // only drops a last digit of the new total. Converting the clamped amount back to
+            // shares would mint fewer shares while still charging the N-share debit.
+            auto const maybeClamped = clampToAssetsTotalScale(vault, assetsDeposited);
+            if (!maybeClamped)
+                return maybeClamped.error();
+            assetsDeposited = *maybeClamped;
+
+            // The actual deposit amount is truncated to whole shares, converted back to assets,
+            // and clamped to the sfAssetsTotal scale (post-fixCleanup3_4_0). Check the depositor's
+            // balance here—after clamping—before making any state changes.
+            if (roundsToZeroForDepositor(view(), accountID_, assetsDeposited, j_))
+                return tecPRECISION_LOSS;
+        }
     }
     catch (std::overflow_error const&)
     {
diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
index 353b72c30d..4dc5b95c89 100644
--- a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
+++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp
@@ -1,11 +1,14 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -27,6 +30,13 @@
 
 namespace xrpl {
 
+bool
+VaultWithdraw::checkExtraFeatures(PreflightContext const& ctx)
+{
+    return !ctx.tx.isFieldPresent(sfCredentialIDs) ||
+        (ctx.rules.enabled(featureCredentials) && ctx.rules.enabled(fixCleanup3_4_0));
+}
+
 static WaiveUnrealizedLoss
 shouldWaiveWithdrawal(ReadView const& view, AccountID const& account, SLE::const_ref issuance)
 {
@@ -59,6 +69,9 @@ VaultWithdraw::preflight(PreflightContext const& ctx)
         }
     }
 
+    if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err))
+        return err;
+
     return tesSUCCESS;
 }
 
@@ -68,11 +81,22 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
     auto const fix313Enabled = ctx.view.rules().enabled(fixCleanup3_1_3);
     auto const fix320Enabled = ctx.view.rules().enabled(fixCleanup3_2_0);
     auto const fix330Enabled = ctx.view.rules().enabled(fixCleanup3_3_0);
+    auto const fix340Enabled = ctx.view.rules().enabled(fixCleanup3_4_0);
 
     auto const vault = ctx.view.read(keylet::vault(ctx.tx[sfVaultID]));
     if (!vault)
         return tecNO_ENTRY;
 
+    if (ctx.view.rules().enabled(featureLendingProtocolV1_1))
+    {
+        if (getVaultPhase(ctx.view, vault) == VaultPhase::Investment)
+        {
+            JLOG(ctx.j.debug())
+                << "VaultWithdraw: vault withdrawal is not allowed in the investment phase.";
+            return tecTOO_SOON;
+        }
+    }
+
     auto const amount = ctx.tx[sfAmount];
     auto const vaultAsset = vault->at(sfAsset);
     auto const vaultShare = vault->at(sfShareMPTID);
@@ -103,6 +127,23 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
         // LCOV_EXCL_STOP
     }
 
+    // Validate credentials (if any) before canWithdraw, since canWithdraw may
+    // call credentials::authorizedDepositPreauth which assumes credentials
+    // already exist.
+    if (auto const err = credentials::valid(ctx.tx, ctx.view, account, ctx.j); !isTesSuccess(err))
+        return err;
+
+    // A pseudo-account belongs to a ledger object rather than to a person and
+    // must never receive funds from a user-initiated transaction. Deposit
+    // authorization, which every pseudo-account carries, already refuses the
+    // payout, but it reports only that the destination declines deposits and
+    // leaves the real reason unsaid.
+    if (fix340Enabled && isPseudoAccount(ctx.view, dstAcct))
+    {
+        JLOG(ctx.j.debug()) << "VaultWithdraw: cannot withdraw into a pseudo-account.";
+        return tecPSEUDO_ACCOUNT;
+    }
+
     if (fix313Enabled && amount.asset() == vaultShare)
     {
         // Post-fixCleanup3_1_3: if the user specified shares, convert
@@ -134,7 +175,8 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
                     account,
                     dstAcct,
                     *maybeAssets,
-                    ctx.tx.isFieldPresent(sfDestinationTag)))
+                    ctx.tx.isFieldPresent(sfDestinationTag),
+                    ctx.tx[~sfCredentialIDs]))
                 return ret;
         }
         catch (std::overflow_error const&)
@@ -163,6 +205,48 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
     if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType); !isTesSuccess(ter))
         return ter;
 
+    // Fail early when self-destination would have to create a holding.
+    // Skip when a holding already exists: canAddHolding does not look at that,
+    // and would block a no-op create (the DefaultRipple-cleared self-withdraw).
+    if (fix340Enabled && account == dstAcct && !holdingExists(ctx.view, dstAcct, vaultAsset))
+    {
+        if (auto const ter = canAddHolding(ctx.view, vaultAsset); !isTesSuccess(ter))
+            return ter;
+    }
+
+    // The checks above only establish that an account may hold the asset. A
+    // private vault additionally restricts who may take part in it, so paying
+    // its asset out to a third party requires both ends of that payout to be
+    // inside the vault's permissioned domain. VaultDeposit applies the same
+    // domain check on the way in.
+    //
+    // Two cases deliberately skip the check. Withdrawing to self is never
+    // restricted: losing vault access must not strand funds already deposited.
+    // The asset issuer is always allowed to receive, which keeps the return
+    // path for frozen assets open even for a submitter who lost access.
+    if (fix340Enabled && vault->isFlag(lsfVaultPrivate) && dstAcct != account &&
+        dstAcct != vaultAsset.getIssuer())
+    {
+        auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(vaultShare));
+        if (!sleIssuance)
+        {
+            // LCOV_EXCL_START
+            JLOG(ctx.j.error()) << "VaultWithdraw: missing issuance of vault shares.";
+            return tefINTERNAL;
+            // LCOV_EXCL_STOP
+        }
+
+        // Unlike VaultDeposit we do not suppress tecEXPIRED: there is no
+        // doApply step here that would clean up the expired credential.
+        if (auto const ter = checkVaultDomain(ctx.view, sleIssuance, account, SuppressExpired::No);
+            !isTesSuccess(ter))
+            return ter;
+
+        if (auto const ter = checkVaultDomain(ctx.view, sleIssuance, dstAcct, SuppressExpired::No);
+            !isTesSuccess(ter))
+            return ter;
+    }
+
     if (fix330Enabled)
     {
         // checkWithdrawFreeze checks the underlying asset on the source
@@ -193,6 +277,7 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
 TER
 VaultWithdraw::doApply()
 {
+    bool const fix340Enabled = view().rules().enabled(fixCleanup3_4_0);
     auto const vault = view().peek(keylet::vault(ctx_.tx[sfVaultID]));
     auto applyViewContext = ctx_.getApplyViewContext();
     if (!vault)
@@ -211,7 +296,9 @@ VaultWithdraw::doApply()
     // Note, we intentionally do not check lsfVaultPrivate flag on the Vault. If
     // you have a share in the vault, it means you were at some point authorized
     // to deposit into it, and this means you are also indefinitely authorized
-    // to withdraw from it.
+    // to withdraw it to yourself. Sending the proceeds to somebody else is a
+    // different matter, and preclaim checks such a withdrawal against the
+    // vault's permissioned domain.
 
     auto const amount = ctx_.tx[sfAmount];
     Asset const vaultAsset = vault->at(sfAsset);
@@ -224,21 +311,37 @@ VaultWithdraw::doApply()
     // We waive the unrealized-loss subtraction in this case to avoid user withdrawing all of their
     // shares but keeping future value in the vault.
     auto const waiveUnrealizedLoss = shouldWaiveWithdrawal(view(), accountID_, sleIssuance);
+    // Number arithmetic can throw overflow_error when Scale and totals are large. Caught below.
     try
     {
         if (amount.asset() == vaultAsset)
         {
             // Fixed assets, variable shares.
+            //
+            // Pre-fixCleanup3_4_0: shares were rounded to nearest, so the
+            // round-trip back to assets could exceed the requested amount.
+            // That over-delivers to the depositor and can bypass the
+            // preclaim canWithdraw check on the destination, which was
+            // validated against the requested amount only.
+            // Post-amendment: truncate shares so assetsWithdrawn <=
+            // requested amount by construction. If truncation yields zero
+            // shares, the tecPRECISION_LOSS guard below fires.
+            auto const truncate =
+                view().rules().enabled(fixCleanup3_4_0) ? TruncateShares::Yes : TruncateShares::No;
             {
                 auto const maybeShares = assetsToSharesWithdraw(
-                    vault, sleIssuance, amount, TruncateShares::No, waiveUnrealizedLoss);
+                    vault, sleIssuance, amount, truncate, waiveUnrealizedLoss);
                 if (!maybeShares)
                     return tecINTERNAL;  // LCOV_EXCL_LINE
                 sharesRedeemed = *maybeShares;
             }
 
+            // Shares are MPT (integer). Small requested amounts truncate to zero; refuse rather
+            // than burn nothing while paying out assets.
             if (sharesRedeemed == beast::kZero)
                 return tecPRECISION_LOSS;
+            // Convert shares back to assets so the payout matches the shares actually burned, not
+            // the requested amount. The extra would otherwise be paid from the vault for free.
             auto const maybeAssets =
                 sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed, waiveUnrealizedLoss);
             if (!maybeAssets)
@@ -247,7 +350,8 @@ VaultWithdraw::doApply()
         }
         else if (amount.asset() == share)
         {
-            // Fixed shares, variable assets.
+            // Fixed shares, variable assets. No round-trip: the share count is exactly what the
+            // caller specified; only the payout amount is derived.
             sharesRedeemed = amount;
             auto const maybeAssets =
                 sharesToAssetsWithdraw(vault, sleIssuance, sharesRedeemed, waiveUnrealizedLoss);
@@ -270,9 +374,62 @@ VaultWithdraw::doApply()
             << ", assetsTotal=" << vault->at(sfAssetsTotal).value()
             << ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
             << ", amount=" << amount.value();
+        // Overflow means this transaction cannot apply, but ledger state is still consistent.
+        // Return tecPATH_DRY rather than a hard internal error.
         return tecPATH_DRY;
     }
 
+    // The "final withdrawal" rule below handles its own zero-value case using
+    // sfAssetsAvailable directly, so it is exempt from the checks below.
+    bool const isFinalWithdrawal =
+        sharesRedeemed == STAmount{share, sleIssuance->at(sfOutstandingAmount)};
+
+    auto assetsAvailable = vault->at(sfAssetsAvailable);
+    auto assetsTotal = vault->at(sfAssetsTotal);
+    auto const lossUnrealized = vault->at(sfLossUnrealized);
+
+    if (fix340Enabled && !isFinalWithdrawal)
+    {
+        // Fixed-shares path: a small share count can round to zero assets even though the vault has
+        // backing value. Reject rather than burn shares for a zero payout. The fixed-assets branch
+        // above has already rejected zero via the sharesRedeemed check.
+        if (amount.asset() == share && assetsWithdrawn == beast::kZero &&
+            assetsTotalForWithdrawal(vault, waiveUnrealizedLoss) != beast::kZero)
+        {
+            JLOG(j_.debug()) << "VaultWithdraw: fixed-share withdrawal rounds to zero assets";
+            return tecPRECISION_LOSS;
+        }
+
+        // Number arithmetic can throw overflow_error when Scale and totals are large.
+        try
+        {
+            // A non-zero payout can be too small to change the stored sfAssetsTotal at
+            // STAmount's precision. Shares would still be burned, reject it instead.
+            if (debitIsNonZeroDust(vaultAsset, assetsTotal, assetsWithdrawn))
+            {
+                JLOG(j_.debug()) << "VaultWithdraw: withdrawal amount too small to change stored"
+                                    " vault balance";
+                return tecPRECISION_LOSS;
+            }
+        }
+        // LCOV_EXCL_START
+        catch (std::overflow_error const&)
+        {
+            // It's easy to hit this exception from Number with large enough Scale
+            // so we avoid spamming the log and only use debug here.
+            JLOG(j_.debug())  //
+                << "VaultWithdraw: overflow error with"
+                << " scale=" << (int)vault->at(sfScale).value()  //
+                << ", assetsTotal=" << vault->at(sfAssetsTotal).value()
+                << ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
+                << ", amount=" << amount.value();
+            // Overflow means this transaction cannot apply, but ledger state is still consistent.
+            // Return tecPATH_DRY rather than a hard internal error.
+            return tecPATH_DRY;
+        }
+        // LCOV_EXCL_STOP
+    }
+
     // Post-fixCleanup3_3_0: preclaim already validated all freeze conditions
     // (checkWithdrawFreeze), so IgnoreFreeze avoids a redundant check that
     // would incorrectly return zero for vault pseudo-accounts whose shares
@@ -287,12 +444,53 @@ VaultWithdraw::doApply()
         return tecINSUFFICIENT_FUNDS;
     }
 
-    auto assetsAvailable = vault->at(sfAssetsAvailable);
-    auto assetsTotal = vault->at(sfAssetsTotal);
-    auto const lossUnrealized = vault->at(sfLossUnrealized);
-    XRPL_ASSERT(
-        lossUnrealized <= (assetsTotal - assetsAvailable),
-        "xrpl::VaultWithdraw::doApply : loss and assets do balance");
+    // Post-fixCleanup3_4_0: round the payout to the sfAssetsTotal scale so all three rails
+    // (trust line / MPT, sfAssetsAvailable, sfAssetsTotal) change by the same representable delta.
+    // Skip when assetsWithdrawn is already zero: the earlier fix340 guard above deliberately
+    // permits fixed-share zero-asset withdrawals in a fully-impaired vault (where
+    // assetsTotalForWithdrawal == 0), and clamping-then-rejecting would undo that. Also skip on
+    // the final-withdrawal path, which overwrites assetsWithdrawn with sfAssetsAvailable below.
+    if (fix340Enabled && !isFinalWithdrawal && assetsWithdrawn > beast::kZero)
+    {
+        // Check availability against the unclamped amount first, so a withdrawal that is both
+        // over the vault's available balance and sub-ULP at the posterior sfAssetsTotal scale
+        // reports tecINSUFFICIENT_FUNDS rather than tecPRECISION_LOSS. The clamp below only ever
+        // shrinks assetsWithdrawn, so this check stays valid; the post-clamp check further down
+        // remains in place to catch the (now smaller) clamped value too.
+        if (*assetsAvailable < assetsWithdrawn)
+        {
+            JLOG(j_.debug()) << "VaultWithdraw: vault doesn't hold enough assets";
+            return tecINSUFFICIENT_FUNDS;
+        }
+
+        // Number arithmetic can throw overflow_error when Scale and totals are large.
+        try
+        {
+            // Round down at the posterior sfAssetsTotal scale so the payout never exceeds the
+            // value represented by the redeemed shares. sharesRedeemed is intentionally not
+            // re-derived: any trimmed residue stays with remaining shareholders.
+            auto const maybeClamped = clampToAssetsTotalScale(vault, -assetsWithdrawn);
+            if (!maybeClamped)
+                return maybeClamped.error();  // LCOV_EXCL_LINE
+            assetsWithdrawn = *maybeClamped;
+        }
+        // LCOV_EXCL_START
+        catch (std::overflow_error const&)
+        {
+            // It's easy to hit this exception from Number with large enough Scale
+            // so we avoid spamming the log and only use debug here.
+            JLOG(j_.debug())  //
+                << "VaultWithdraw: overflow error with"
+                << " scale=" << (int)vault->at(sfScale).value()  //
+                << ", assetsTotal=" << vault->at(sfAssetsTotal).value()
+                << ", sharesTotal=" << sleIssuance->at(sfOutstandingAmount)
+                << ", amount=" << amount.value();
+            // Overflow means this transaction cannot apply, but ledger state is still consistent.
+            // Return tecPATH_DRY rather than a hard internal error.
+            return tecPATH_DRY;
+        }
+        // LCOV_EXCL_STOP
+    }
 
     // The vault must have enough assets on hand.
     if (*assetsAvailable < assetsWithdrawn)
@@ -301,16 +499,12 @@ VaultWithdraw::doApply()
         return tecINSUFFICIENT_FUNDS;
     }
 
-    // Post-fixCleanup3_2_0 "final withdrawal" rule:
-    // a transaction that would burn every outstanding share is only permitted when the vault is in
-    // a clean state — no outstanding receivables and no unrealized loss. Otherwise the resulting
-    // (shares == 0, assetsTotal > 0) state would violate the zero-sized-vault invariant.
+    // Post-fixCleanup3_2_0: burning every outstanding share is only allowed when the vault has no
+    // unrealized loss. Otherwise the resulting (shares == 0, assetsTotal > 0) state would violate
+    // the zero-sized-vault invariant.
     //
-    // When the rule applies, the payout is the remaining sfAssetsAvailable; in a clean vault
-    // the helper result should already equal that value, and any mismatch is a rounding artifact
-    // worth logging.
-    bool const isFinalWithdrawal =
-        sharesRedeemed == STAmount{share, sleIssuance->at(sfOutstandingAmount)};
+    // The payout is set to the remaining sfAssetsAvailable. The helper result should already
+    // equal that value in a clean vault; any mismatch is a rounding artifact and is logged.
     if (view().rules().enabled(fixCleanup3_2_0) && isFinalWithdrawal)
     {
         // Unreachable: a final withdrawal with lossUnrealized > 0 has
@@ -344,6 +538,8 @@ VaultWithdraw::doApply()
     }
     else
     {
+        // Debit both rails by the same delta so sfAssetsTotal and sfAssetsAvailable stay in step,
+        // as required by the ValidVault invariant.
         assetsTotal -= assetsWithdrawn;
         assetsAvailable -= assetsWithdrawn;
     }
diff --git a/src/libxrpl/tx/wasm/ContractContext.cpp b/src/libxrpl/tx/wasm/ContractContext.cpp
index 46dc773298..ecf9872904 100644
--- a/src/libxrpl/tx/wasm/ContractContext.cpp
+++ b/src/libxrpl/tx/wasm/ContractContext.cpp
@@ -1,7 +1,8 @@
+#include 
+
 #include 
 #include 
 #include 
-#include 
 
 namespace xrpl {
 
diff --git a/src/libxrpl/tx/wasm/ContractHostFuncImpl.cpp b/src/libxrpl/tx/wasm/ContractHostFuncImpl.cpp
index fe8d1ebf6e..aac7f91616 100644
--- a/src/libxrpl/tx/wasm/ContractHostFuncImpl.cpp
+++ b/src/libxrpl/tx/wasm/ContractHostFuncImpl.cpp
@@ -217,7 +217,7 @@ getDataOrCache(ContractContext& contractCtx, AccountID const& account)
     return {cacheEntry.second.isObject(), cacheEntry.second};
 }
 
-inline HostFunctionError
+inline std::expected
 setDataCache(
     ContractContext& contractCtx,
     AccountID const& account,
@@ -233,7 +233,7 @@ setDataCache(
     if (!sleAccount)
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " << "setDataCache: Account not found";
-        return HostFunctionError::InvalidAccount;
+        return std::unexpected(HostFunctionError::InvalidAccount);
     }
 
     uint32_t const maxDataModifications = 1000u;
@@ -242,7 +242,7 @@ setDataCache(
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                         << "setDataCache: Exceeded max data modifications";
-        return HostFunctionError::InvalidState;
+        return std::unexpected(HostFunctionError::InvalidState);
     }
 
     if (dataMap.find(account) == dataMap.end())
@@ -262,7 +262,7 @@ setDataCache(
         {
             JLOG(j.trace()) << "WasmTrace[" << contractId
                             << "]: " << "setDataCache: Insufficient reserve";
-            return HostFunctionError::InvalidState;
+            return std::unexpected(HostFunctionError::InvalidState);
         }
 
         dataMap.modifiedCount++;
@@ -276,7 +276,7 @@ setDataCache(
         //         << entry.second.getJson(JsonOptions::Values::None).toStyledString();
         // }
 
-        return HostFunctionError::Success;
+        return {};
     }
 
     // auto& availableForReserves = std::get<0>(dataMap[account]);
@@ -284,7 +284,7 @@ setDataCache(
     if (modified)
     {
         // if (!canReserveNew)
-        //     return HostFunctionError::InsufficientReserve;
+        //     return std::unexpected(HostFunctionError::InsufficientReserve);
 
         // availableForReserves--;
         dataMap.modifiedCount++;
@@ -299,7 +299,7 @@ setDataCache(
     //         << ", Data: "
     //         << entry.second.getJson(JsonOptions::Values::None).toStyledString();
     // }
-    return HostFunctionError::Success;
+    return {};
 }
 
 std::expected
@@ -349,11 +349,11 @@ ContractHostFunctionsImpl::getDataObjectField(AccountID const& account, std::str
         STJson const data = dataSle->getFieldJson(sfContractJson);
         // it exists add it to cache and return it
         if (auto const cacheResult = setDataCache(contractCtx, account, data, j, false);
-            cacheResult != HostFunctionError::Success)
+            !cacheResult)
         {
             JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                             << "getDataObjectField: Failed to set data cache";
-            return std::unexpected(cacheResult);
+            return std::unexpected(cacheResult.error());
         }
 
         auto const keyValue = data.getObjectField(std::string(key));
@@ -372,7 +372,7 @@ ContractHostFunctionsImpl::getDataObjectField(AccountID const& account, std::str
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                         << "getDataObjectField: Exception: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
@@ -427,11 +427,11 @@ ContractHostFunctionsImpl::getDataNestedObjectField(
         STJson const data = dataSle->getFieldJson(sfContractJson);
         // it exists add it to cache and return it
         if (auto const cacheResult = setDataCache(contractCtx, account, data, j, false);
-            cacheResult != HostFunctionError::Success)
+            !cacheResult)
         {
             JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                             << "getDataNestedObjectField: Failed to set data cache";
-            return std::unexpected(cacheResult);
+            return std::unexpected(cacheResult.error());
         }
 
         auto const keyValue = data.getNestedObjectField(std::string(key), std::string(nestedKey));
@@ -450,7 +450,7 @@ ContractHostFunctionsImpl::getDataNestedObjectField(
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                         << "getDataNestedObjectField: Exception: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
@@ -472,21 +472,20 @@ ContractHostFunctionsImpl::setDataObjectField(
         }
 
         data.setObjectField(std::string(key), value);
-        if (HostFunctionError const ret = setDataCache(contractCtx, account, data, j, true);
-            ret != HostFunctionError::Success)
+        if (auto const ret = setDataCache(contractCtx, account, data, j, true); !ret)
         {
             JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                             << "setDataObjectField: Failed to set object field";
-            return std::unexpected(ret);
+            return std::unexpected(ret.error());
         }
 
-        return static_cast(HostFunctionError::Success);
+        return 0;
     }
     catch (std::exception const& e)
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                         << "setDataObjectField: Exception: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
@@ -509,22 +508,21 @@ ContractHostFunctionsImpl::setDataNestedObjectField(
         }
 
         data.setNestedObjectField(std::string(key), std::string(nestedKey), value);
-        if (HostFunctionError const ret = setDataCache(contractCtx, account, data, j, true);
-            ret != HostFunctionError::Success)
+        if (auto const ret = setDataCache(contractCtx, account, data, j, true); !ret)
         {
             JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                             << "setDataNestedObjectField: Failed to set nested "
                                "object field";
-            return std::unexpected(ret);
+            return std::unexpected(ret.error());
         }
 
-        return static_cast(HostFunctionError::Success);
+        return 0;
     }
     catch (std::exception const& e)
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                         << "setDataNestedObjectField: Exception: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
@@ -595,12 +593,12 @@ ContractHostFunctionsImpl::getDataArrayElementField(
 
         // it exists add it to cache and return it
         if (auto const cacheResult = setDataCache(contractCtx, account, data, j, false);
-            cacheResult != HostFunctionError::Success)
+            !cacheResult)
         {
             JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                             << "setDataArrayElementField: Failed to set array "
                                "element field";
-            return std::unexpected(cacheResult);
+            return std::unexpected(cacheResult.error());
         }
 
         auto const fieldValue = data.getArrayElementField(index, std::string(key));
@@ -620,7 +618,7 @@ ContractHostFunctionsImpl::getDataArrayElementField(
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                         << "getDataArrayElementField: Exception: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
@@ -704,12 +702,12 @@ ContractHostFunctionsImpl::getDataNestedArrayElementField(
 
         // it exists add it to cache and return it
         if (auto const cacheResult = setDataCache(contractCtx, account, data, j, false);
-            cacheResult != HostFunctionError::Success)
+            !cacheResult)
         {
             JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                             << "setDataNestedArrayElementField: Failed to set "
                                "nested array element field";
-            return std::unexpected(cacheResult);
+            return std::unexpected(cacheResult.error());
         }
 
         auto const fieldValue =
@@ -730,7 +728,7 @@ ContractHostFunctionsImpl::getDataNestedArrayElementField(
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                         << "getDataNestedArrayElementField: Exception: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
@@ -770,22 +768,21 @@ ContractHostFunctionsImpl::setDataArrayElementField(
         }
 
         data.setArrayElementField(index, std::string(key), value);
-        if (HostFunctionError const ret = setDataCache(contractCtx, account, data, j, true);
-            ret != HostFunctionError::Success)
+        if (auto const ret = setDataCache(contractCtx, account, data, j, true); !ret)
         {
             JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                             << "setDataArrayElementField: Failed to set array "
                                "element field";
-            return std::unexpected(ret);
+            return std::unexpected(ret.error());
         }
 
-        return static_cast(HostFunctionError::Success);
+        return 0;
     }
     catch (std::exception const& e)
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                         << "setDataArrayElementField: Exception: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
@@ -810,22 +807,21 @@ ContractHostFunctionsImpl::setDataNestedArrayElementField(
         }
 
         data.setNestedArrayElementField(std::string(key), index, std::string(nestedKey), value);
-        if (HostFunctionError const ret = setDataCache(contractCtx, account, data, j, true);
-            ret != HostFunctionError::Success)
+        if (auto const ret = setDataCache(contractCtx, account, data, j, true); !ret)
         {
             JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                             << "setDataNestedArrayElementField: Failed to set "
                                "nested array element field";
-            return std::unexpected(ret);
+            return std::unexpected(ret.error());
         }
 
-        return static_cast(HostFunctionError::Success);
+        return 0;
     }
     catch (std::exception const& e)
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: "
                         << "setDataNestedArrayElementField: Exception: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
@@ -862,7 +858,7 @@ ContractHostFunctionsImpl::buildTxn(std::uint16_t const& txType)
     catch (std::exception const& e)
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: Exception in buildTxn: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
@@ -925,13 +921,13 @@ ContractHostFunctionsImpl::addTxnField(
         obj.addFieldFromSlice(field, data);
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: " << "addTxnField: TXN: "
                         << obj.getJson(JsonOptions::Values::None).toStyledString();
-        return static_cast(HostFunctionError::Success);
+        return 0;
     }
     catch (std::exception const& e)
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId
                         << "]: Exception in addTxnField: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
@@ -994,7 +990,7 @@ ContractHostFunctionsImpl::emitBuiltTxn(std::uint32_t const& index)
     {
         JLOG(j.trace()) << "WasmTrace[" << parentBatchId
                         << "]: Exception in emitBuiltTxn: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
@@ -1050,7 +1046,7 @@ ContractHostFunctionsImpl::emitTxn(std::shared_ptr const& stxPtr)
     {
         JLOG(j.trace()) << "WasmTrace[" << parentTx.getTransactionID()
                         << "]: Exception in emitTxn: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
@@ -1064,12 +1060,12 @@ ContractHostFunctionsImpl::emitEvent(std::string_view const& eventName, STJson c
         // TODO: Validation
         auto& eventMap = contractCtx.result.eventMap;
         eventMap[std::string(eventName)] = eventData;
-        return static_cast(HostFunctionError::Success);
+        return 0;
     }
     catch (std::exception const& e)
     {
         JLOG(j.trace()) << "WasmTrace[" << contractId << "]: Exception in emitEvent: " << e.what();
-        Throw(std::string(hfErrInternal));
+        return std::unexpected(HostFunctionError::InternalFatal);
     }
 }
 
diff --git a/src/libxrpl/tx/wasm/HostContext.cpp b/src/libxrpl/tx/wasm/HostContext.cpp
new file mode 100644
index 0000000000..6e0212c8f3
--- /dev/null
+++ b/src/libxrpl/tx/wasm/HostContext.cpp
@@ -0,0 +1,1278 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+// For `TraceDataType`: declared in the cxx bridge, defined in the header it generates.
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+namespace {
+
+// What a host call answers when it could not be served at all: every method below hands it
+// to `guarded` as the answer for a body that throws.
+constexpr std::int32_t kHostInternal = hfErrorToInt(HostFunctionError::InternalFatal);
+
+// Copy `value` into `out` only if the whole of it fits, and answer its true length either
+// way. A value too large for the guest's buffer must reach it in no part: a prefix would
+// be a wrong answer where a length is a usable one.
+std::int32_t
+answer(rust::Slice out, std::uint8_t const* value, std::size_t size)
+{
+    XRPL_ASSERT(
+        value != nullptr || size == 0, "xrpl::answer : nullptr value should have zero size");
+    if (value != nullptr && size <= out.size())
+    {
+        std::memcpy(out.data(), value, size);
+    }
+    size = std::min(size, static_cast(std::numeric_limits::max()));
+    return static_cast(size);
+}
+
+// A scalar the ABI carries as bytes, in the wire's byte order.
+//
+// `adjustWasmEndianess` is the one place that order is decided for the whole wasm boundary,
+// and it is `constexpr` with the swap under `if constexpr (std::endian::native ==
+// std::endian::big)` - so this costs nothing on a little-endian host and is correct on a
+// big-endian one, which a hand-written shift sequence per call site would have to get right
+// each time.
+template 
+std::int32_t
+answerScalar(rust::Slice out, T value)
+{
+    auto const wire = adjustWasmEndianess(value);
+    return answer(out, reinterpret_cast(&wire), sizeof(wire));
+}
+
+// Decode an asset from its wire bytes, whose length selects the kind: an MPT id, a
+// bare currency (which must be XRP), or a currency followed by an issuer (which must
+// not be XRP). Any other length is malformed.
+std::expected
+parseAsset(rust::Slice bytes)
+{
+    if (bytes.size() == MPTID::size())
+    {
+        return Asset{MPTID::fromVoid(bytes.data())};
+    }
+
+    if (bytes.size() == Currency::size())
+    {
+        auto const issue = Issue{Currency::fromVoid(bytes.data()), xrpAccount()};
+        if (!issue.native())
+        {
+            return std::unexpected(HostFunctionError::InvalidParams);
+        }
+        return Asset{issue};
+    }
+
+    if (bytes.size() == Currency::size() + AccountID::size())
+    {
+        auto const issue = Issue{
+            Currency::fromVoid(bytes.data()), AccountID::fromVoid(bytes.data() + Currency::size())};
+        if (issue.native())
+        {
+            return std::unexpected(HostFunctionError::InvalidParams);
+        }
+        return Asset{issue};
+    }
+
+    return std::unexpected(HostFunctionError::InvalidParams);
+}
+
+// Decode a `uint64` from its eight wire bytes, in the wire's byte order. The region
+// must be exactly eight bytes, else `InvalidParams`.
+std::expected
+parseUint64(rust::Slice bytes)
+{
+    if (bytes.size() != sizeof(std::uint64_t))
+    {
+        return std::unexpected(HostFunctionError::InvalidParams);
+    }
+
+    auto x = std::uint64_t{};
+    std::memcpy(&x, bytes.data(), sizeof(x));
+    return adjustWasmEndianess(x);
+}
+
+// Deserialize an `ST` object from its wire bytes; `InvalidParams` if the bytes are not
+// a well-formed one, which `SerialIter` reports by throwing.
+template 
+std::expected
+parseST(rust::Slice bytes)
+{
+    try
+    {
+        auto sit = SerialIter{Slice{bytes.data(), bytes.size()}};
+        return T{sit, sfGeneric};
+    }
+    catch (std::exception const&)
+    {
+        return std::unexpected(HostFunctionError::InvalidParams);
+    }
+}
+
+template 
+std::int32_t
+invokeWithLocator(
+    rust::Slice locator,
+    rust::Slice out,
+    Functor&& functor)
+{
+    if (locator.empty() || (locator.size() & 3) != 0)
+    {
+        return hfErrorToInt(HostFunctionError::LocatorMalformed);
+    }
+
+    std::uint32_t const steps = locator.size() / sizeof(std::int32_t);
+    auto locBuf = std::vector(steps);
+    std::memcpy(locBuf.data(), locator.data(), locator.size());
+    auto const fl = FieldLocator{std::move(locBuf)};
+
+    auto const value = functor(fl);
+    if (!value)
+    {
+        return hfErrorToInt(value.error());
+    }
+
+    return answer(out, value->data(), value->size());
+}
+
+template 
+std::int32_t
+invokeWithLocator(rust::Slice locator, Functor&& functor)
+{
+    if (locator.empty() || (locator.size() & 3) != 0)
+    {
+        return hfErrorToInt(HostFunctionError::LocatorMalformed);
+    }
+
+    std::uint32_t const steps = locator.size() / sizeof(std::int32_t);
+    auto locBuf = std::vector(steps);
+    std::memcpy(locBuf.data(), locator.data(), locator.size());
+    auto const fl = FieldLocator{std::move(locBuf)};
+
+    auto const value = functor(fl);
+    if (!value)
+    {
+        return hfErrorToInt(value.error());
+    }
+
+    return *value;
+}
+
+template 
+std::int32_t
+invokeWithField(std::int32_t field, rust::Slice out, Functor&& functor)
+{
+    auto const& knownSFields = SField::getKnownCodeToField();
+    auto const it = knownSFields.find(field);
+    if (it == std::end(knownSFields))
+    {
+        return hfErrorToInt(HostFunctionError::InvalidField);
+    }
+
+    auto const value = functor(*it->second);
+    if (!value)
+    {
+        return hfErrorToInt(value.error());
+    }
+
+    return answer(out, value->data(), value->size());
+}
+
+template 
+std::int32_t
+invokeWithField(std::int32_t field, Functor&& functor)
+{
+    auto const& knownSFields = SField::getKnownCodeToField();
+    auto const it = knownSFields.find(field);
+    if (it == std::end(knownSFields))
+    {
+        return hfErrorToInt(HostFunctionError::InvalidField);
+    }
+
+    auto const len = functor(*it->second);
+    if (!len)
+    {
+        return hfErrorToInt(len.error());
+    }
+
+    return *len;
+}
+
+template 
+std::int32_t
+invokeWithAccount(
+    rust::Slice account,
+    rust::Slice out,
+    Functor&& functor)
+{
+    if (account.size() != AccountID::size())
+    {
+        return hfErrorToInt(HostFunctionError::InvalidParams);
+    }
+
+    auto const value = functor(AccountID::fromVoid(account.data()));
+    if (!value)
+    {
+        return hfErrorToInt(value.error());
+    }
+
+    return answer(out, value->data(), value->size());
+}
+
+template 
+std::int32_t
+invokeWithAccounts(
+    rust::Slice account1,
+    rust::Slice account2,
+    rust::Slice out,
+    Functor&& functor)
+{
+    if (account1.size() != AccountID::size() || account2.size() != AccountID::size())
+    {
+        return hfErrorToInt(HostFunctionError::InvalidParams);
+    }
+
+    auto const value =
+        functor(AccountID::fromVoid(account1.data()), AccountID::fromVoid(account2.data()));
+    if (!value)
+    {
+        return hfErrorToInt(value.error());
+    }
+
+    return answer(out, value->data(), value->size());
+}
+
+template 
+std::int32_t
+invokeNFT(rust::Slice nftId, rust::Slice out, Functor&& functor)
+{
+    if (nftId.size() != uint256::size())
+    {
+        return hfErrorToInt(HostFunctionError::InvalidParams);
+    }
+
+    auto const value = functor(uint256::fromVoid(nftId.data()));
+    if (!value)
+    {
+        return hfErrorToInt(value.error());
+    }
+
+    if constexpr (Scalar)
+    {
+        return answerScalar(out, *value);
+    }
+    else
+    {
+        return answer(out, value->data(), value->size());
+    }
+}
+
+template 
+std::int32_t
+invokeNFT(rust::Slice nftId, Functor&& functor)
+{
+    if (nftId.size() != uint256::size())
+    {
+        return hfErrorToInt(HostFunctionError::InvalidParams);
+    }
+
+    auto const value = functor(uint256::fromVoid(nftId.data()));
+    if (!value)
+    {
+        return hfErrorToInt(value.error());
+    }
+
+    return *value;
+}
+
+template 
+std::int32_t
+invoke(rust::Slice out, Functor&& functor)
+{
+    auto const value = functor();
+    if (!value)
+    {
+        return hfErrorToInt(value.error());
+    }
+
+    if constexpr (Scalar)
+    {
+        return answerScalar(out, *value);
+    }
+    else
+    {
+        return answer(out, value->data(), value->size());
+    }
+}
+
+template 
+std::int32_t
+invoke(Functor&& functor)
+{
+    auto const value = functor();
+    if (!value)
+    {
+        return hfErrorToInt(value.error());
+    }
+
+    return *value;
+}
+
+// A traced integer, which the guest sends as bytes rather than as a wasm scalar so that one
+// import serves every type. `std::nullopt` if the buffer is not the width the type needs.
+//
+// `memcpy` regardless of alignment, and no `reinterpret_cast` fast path: a trace must cost
+// the same whatever address the guest chose for its buffer.
+template 
+std::optional
+traceInt(Slice const& data)
+{
+    static_assert(std::is_integral_v);
+    if (data.size() != sizeof(T))
+        return std::nullopt;
+
+    T x;
+    std::memcpy(&x, data.data(), sizeof(T));
+    return adjustWasmEndianess(x);
+}
+
+// The guest's bytes as the text a log line carries, or `std::nullopt` when they do not hold
+// the type they claim.
+//
+// The engine refuses a code that names no type before it crosses, so `type` is always one of
+// the variants; the trailing `return` is what the `switch` owes a scoped enum, not a case
+// this can meet.
+//
+// May throw: `STAmount`'s deserializer rejects malformed input that way.
+std::optional
+traceFormat(TraceDataType type, Slice const& data)
+{
+    switch (type)
+    {
+        case TraceDataType::Int64:
+            if (auto const x = traceInt(data))
+                return std::to_string(*x);
+            return std::nullopt;
+
+        case TraceDataType::Uint64:
+            if (auto const x = traceInt(data))
+                return std::to_string(*x);
+            return std::nullopt;
+
+        case TraceDataType::Xfloat:
+            return wasm_float::floatToString(data);
+
+        case TraceDataType::Account:
+            if (data.size() != AccountID::size())
+                return std::nullopt;
+            return toBase58(AccountID::fromVoid(data.data()));
+
+        case TraceDataType::Amount: {
+            SerialIter iter(data);
+            STAmount const amount(iter, sfGeneric);
+            return amount.getFullText();
+        }
+
+        case TraceDataType::AsHex:
+            return strHex(data);
+
+        case TraceDataType::AsText:
+            // An empty Slice has a null data(), which std::string may not be handed.
+            if (data.empty())
+                return std::string();
+            return std::string(reinterpret_cast(data.data()), data.size());
+    }
+
+    return std::nullopt;
+}
+
+}  // namespace
+
+HostContext::HostContext(HostFunctions& hostFunctions) : hostFunctions_{hostFunctions}
+{
+}
+
+std::int32_t
+HostContext::getLedgerSqn(rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(out, [&] { return hostFunctions_.getLedgerSqn(); });
+    });
+}
+
+std::int32_t
+HostContext::getParentLedgerTime(rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(out, [&] { return hostFunctions_.getParentLedgerTime(); });
+    });
+}
+
+std::int32_t
+HostContext::getParentLedgerHash(rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(out, [&] { return hostFunctions_.getParentLedgerHash(); });
+    });
+}
+
+std::int32_t
+HostContext::getBaseFee(rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(out, [&] { return hostFunctions_.getBaseFee(); });
+    });
+}
+
+std::int32_t
+HostContext::isAmendmentEnabled(rust::Slice amendment) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        // A 32-byte input may be an amendment id; try that first and fall through to
+        // a name lookup if it is not an enabled amendment - the 32 bytes could spell
+        // a name instead.
+        if (amendment.size() == uint256::size())
+        {
+            auto const enabled =
+                hostFunctions_.isAmendmentEnabled(uint256::fromVoid(amendment.data()));
+            if (enabled && *enabled == 1)
+            {
+                return *enabled;
+            }
+        }
+
+        static constexpr auto kMaxAmendmentSize = 64UZ;
+        if (amendment.size() > kMaxAmendmentSize)
+        {
+            return hfErrorToInt(HostFunctionError::DataFieldTooLarge);
+        }
+
+        auto const name =
+            std::string_view{reinterpret_cast(amendment.data()), amendment.size()};
+        return invoke([&] { return hostFunctions_.isAmendmentEnabled(name); });
+    });
+}
+
+std::int32_t
+HostContext::cacheLedgerObj(rust::Slice objId, std::int32_t cacheIdx)
+    const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        if (objId.size() != uint256::size())
+        {
+            return hfErrorToInt(HostFunctionError::InvalidParams);
+        }
+        return invoke([&] {
+            return hostFunctions_.cacheLedgerObj(uint256::fromVoid(objId.data()), cacheIdx);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getTxField(std::int32_t field, rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithField(field, out, [&](auto const& innerField) {
+            return hostFunctions_.getTxField(innerField);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getCurrentLedgerObjField(std::int32_t field, rust::Slice out)
+    const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithField(field, out, [&](auto const& innerField) {
+            return hostFunctions_.getCurrentLedgerObjField(innerField);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getLedgerObjField(
+    std::int32_t cacheIdx,
+    std::int32_t field,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithField(field, out, [&](auto const& innerField) {
+            return hostFunctions_.getLedgerObjField(cacheIdx, innerField);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getTxNestedField(
+    rust::Slice locator,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithLocator(locator, out, [&](FieldLocator const& fl) {
+            return hostFunctions_.getTxNestedField(fl);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getCurrentLedgerObjNestedField(
+    rust::Slice locator,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithLocator(locator, out, [&](FieldLocator const& fl) {
+            return hostFunctions_.getCurrentLedgerObjNestedField(fl);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getLedgerObjNestedField(
+    std::int32_t cacheIdx,
+    rust::Slice locator,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithLocator(locator, out, [&](FieldLocator const& fl) {
+            return hostFunctions_.getLedgerObjNestedField(cacheIdx, fl);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getTxArrayLen(std::int32_t field) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithField(field, [&](auto const& innerField) {
+            return hostFunctions_.getTxArrayLen(innerField);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getCurrentLedgerObjArrayLen(std::int32_t field) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithField(field, [&](auto const& innerField) {
+            return hostFunctions_.getCurrentLedgerObjArrayLen(innerField);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getLedgerObjArrayLen(std::int32_t cacheIdx, std::int32_t field) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithField(field, [&](auto const& innerField) {
+            return hostFunctions_.getLedgerObjArrayLen(cacheIdx, innerField);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getTxNestedArrayLen(rust::Slice locator) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithLocator(locator, [&](FieldLocator const& fl) {
+            return hostFunctions_.getTxNestedArrayLen(fl);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getCurrentLedgerObjNestedArrayLen(
+    rust::Slice locator) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithLocator(locator, [&](FieldLocator const& fl) {
+            return hostFunctions_.getCurrentLedgerObjNestedArrayLen(fl);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getLedgerObjNestedArrayLen(
+    std::int32_t cacheIdx,
+    rust::Slice locator) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithLocator(locator, [&](FieldLocator const& fl) {
+            return hostFunctions_.getLedgerObjNestedArrayLen(cacheIdx, fl);
+        });
+    });
+}
+
+std::int32_t
+HostContext::checkSignature(
+    rust::Slice message,
+    rust::Slice signature,
+    rust::Slice pubkey) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke([&] {
+            return hostFunctions_.checkSignature(
+                Slice{message.data(), message.size()},
+                Slice{signature.data(), signature.size()},
+                Slice{pubkey.data(), pubkey.size()});
+        });
+    });
+}
+
+std::int32_t
+HostContext::accountKeylet(rust::Slice account, rust::Slice out)
+    const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(account, out, [&](auto const& accountId) {
+            return hostFunctions_.accountKeylet(accountId);
+        });
+    });
+}
+
+std::int32_t
+HostContext::ammKeylet(
+    rust::Slice asset1,
+    rust::Slice asset2,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        auto const a1 = parseAsset(asset1);
+        if (!a1)
+        {
+            return hfErrorToInt(a1.error());
+        }
+
+        auto const a2 = parseAsset(asset2);
+        if (!a2)
+        {
+            return hfErrorToInt(a2.error());
+        }
+        return invoke(out, [&] { return hostFunctions_.ammKeylet(*a1, *a2); });
+    });
+}
+
+std::int32_t
+HostContext::checkKeylet(
+    rust::Slice account,
+    std::uint32_t seq,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(account, out, [&](auto const& accountId) {
+            return hostFunctions_.checkKeylet(accountId, seq);
+        });
+    });
+}
+
+std::int32_t
+HostContext::credentialKeylet(
+    rust::Slice subject,
+    rust::Slice issuer,
+    rust::Slice credentialType,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccounts(
+            subject, issuer, out, [&](auto const& account1, auto const& account2) {
+                return hostFunctions_.credentialKeylet(
+                    account1, account2, Slice{credentialType.data(), credentialType.size()});
+            });
+    });
+}
+
+std::int32_t
+HostContext::delegateKeylet(
+    rust::Slice account,
+    rust::Slice authorize,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccounts(
+            account, authorize, out, [&](auto const& account1, auto const& account2) {
+                return hostFunctions_.delegateKeylet(account1, account2);
+            });
+    });
+}
+
+std::int32_t
+HostContext::depositPreauthKeylet(
+    rust::Slice account,
+    rust::Slice authorize,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccounts(
+            account, authorize, out, [&](auto const& account1, auto const& account2) {
+                return hostFunctions_.depositPreauthKeylet(account1, account2);
+            });
+    });
+}
+
+std::int32_t
+HostContext::didKeylet(rust::Slice account, rust::Slice out)
+    const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(account, out, [&](auto const& accountId) {
+            return hostFunctions_.didKeylet(accountId);
+        });
+    });
+}
+
+std::int32_t
+HostContext::escrowKeylet(
+    rust::Slice account,
+    std::uint32_t seq,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(account, out, [&](auto const& accountId) {
+            return hostFunctions_.escrowKeylet(accountId, seq);
+        });
+    });
+}
+
+std::int32_t
+HostContext::trustLineKeylet(
+    rust::Slice account1,
+    rust::Slice account2,
+    rust::Slice currency,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        if (currency.size() != Currency::size())
+        {
+            return hfErrorToInt(HostFunctionError::InvalidParams);
+        }
+
+        return invokeWithAccounts(
+            account1, account2, out, [&](auto const& innerAccount1, auto const& innerAccount2) {
+                return hostFunctions_.trustLineKeylet(
+                    innerAccount1, innerAccount2, Currency::fromVoid(currency.data()));
+            });
+    });
+}
+
+std::int32_t
+HostContext::mptokenIssuanceKeylet(
+    rust::Slice issuer,
+    std::uint32_t seq,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(issuer, out, [&](auto const& accountId) {
+            return hostFunctions_.mptokenIssuanceKeylet(accountId, seq);
+        });
+    });
+}
+
+std::int32_t
+HostContext::mptokenKeylet(
+    rust::Slice mptid,
+    rust::Slice holder,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        if (mptid.size() != MPTID::size() || holder.size() != AccountID::size())
+        {
+            return hfErrorToInt(HostFunctionError::InvalidParams);
+        }
+        return invoke(out, [&] {
+            return hostFunctions_.mptokenKeylet(
+                MPTID::fromVoid(mptid.data()), AccountID::fromVoid(holder.data()));
+        });
+    });
+}
+
+std::int32_t
+HostContext::nftokenOfferKeylet(
+    rust::Slice account,
+    std::uint32_t seq,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(account, out, [&](auto const& accountId) {
+            return hostFunctions_.nftokenOfferKeylet(accountId, seq);
+        });
+    });
+}
+
+std::int32_t
+HostContext::offerKeylet(
+    rust::Slice account,
+    std::uint32_t seq,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(account, out, [&](auto const& accountId) {
+            return hostFunctions_.offerKeylet(accountId, seq);
+        });
+    });
+}
+
+std::int32_t
+HostContext::oracleKeylet(
+    rust::Slice account,
+    std::uint32_t docId,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(account, out, [&](auto const& accountId) {
+            return hostFunctions_.oracleKeylet(accountId, docId);
+        });
+    });
+}
+
+std::int32_t
+HostContext::paychannelKeylet(
+    rust::Slice account,
+    rust::Slice destination,
+    std::uint32_t seq,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccounts(
+            account, destination, out, [&](auto const& account1, auto const& account2) {
+                return hostFunctions_.paychannelKeylet(account1, account2, seq);
+            });
+    });
+}
+
+std::int32_t
+HostContext::permissionedDomainKeylet(
+    rust::Slice account,
+    std::uint32_t seq,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(account, out, [&](auto const& accountId) {
+            return hostFunctions_.permissionedDomainKeylet(accountId, seq);
+        });
+    });
+}
+
+std::int32_t
+HostContext::signerListKeylet(
+    rust::Slice account,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(account, out, [&](auto const& accountId) {
+            return hostFunctions_.signerListKeylet(accountId);
+        });
+    });
+}
+
+std::int32_t
+HostContext::ticketKeylet(
+    rust::Slice account,
+    std::uint32_t seq,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(account, out, [&](auto const& accountId) {
+            return hostFunctions_.ticketKeylet(accountId, seq);
+        });
+    });
+}
+
+std::int32_t
+HostContext::vaultKeylet(
+    rust::Slice account,
+    std::uint32_t seq,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(account, out, [&](auto const& accountId) {
+            return hostFunctions_.vaultKeylet(accountId, seq);
+        });
+    });
+}
+
+std::int32_t
+HostContext::sponsorshipKeylet(
+    rust::Slice sponsor,
+    rust::Slice sponsee,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccounts(
+            sponsor, sponsee, out, [&](auto const& sponsorId, auto const& sponseeId) {
+                return hostFunctions_.sponsorshipKeylet(sponsorId, sponseeId);
+            });
+    });
+}
+
+std::int32_t
+HostContext::loanBrokerKeylet(
+    rust::Slice owner,
+    std::uint32_t seq,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeWithAccount(owner, out, [&](auto const& ownerId) {
+            return hostFunctions_.loanBrokerKeylet(ownerId, seq);
+        });
+    });
+}
+
+std::int32_t
+HostContext::loanKeylet(
+    rust::Slice loanBrokerID,
+    std::uint32_t loanSeq,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        if (loanBrokerID.size() != uint256::size())
+        {
+            return hfErrorToInt(HostFunctionError::InvalidParams);
+        }
+        return invoke(out, [&] {
+            return hostFunctions_.loanKeylet(uint256::fromVoid(loanBrokerID.data()), loanSeq);
+        });
+    });
+}
+
+std::int32_t
+HostContext::sha512Half(rust::Slice data, rust::Slice out)
+    const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(out, [&] {
+            return hostFunctions_.computeSha512HalfHash(Slice{data.data(), data.size()});
+        });
+    });
+}
+
+void
+HostContext::trace(rust::Str msg, rust::Slice data, TraceDataType dataType)
+    const noexcept
+{
+    auto const journal = hostFunctions_.getJournal();
+
+    // Not `guarded`: a buffer that does not hold what it claims is an ordinary contract
+    // mistake, so it belongs in the log the contract is writing to rather than in the error
+    // log as an internal failure - and it must not become one, since there is nothing to
+    // report it to.
+    try
+    {
+        if (msg.size() + data.size() > kMaxWasmDataLength)
+        {
+            JLOG(journal.trace()) << "WasmTrace: message and data too long";
+            return;
+        }
+
+        // Rendered whatever the log level: the level decides what is written, never whether
+        // the host is called, so a run costs the same on every node.
+        auto const text = traceFormat(dataType, Slice{data.data(), data.size()});
+        if (!text)
+        {
+            JLOG(journal.trace()) << "WasmTrace: data does not hold the type it names";
+            return;
+        }
+
+        hostFunctions_.trace(std::string_view{msg.data(), msg.size()}, *text);
+    }
+    catch (std::exception const& e)
+    {
+        JLOG(journal.trace()) << "WasmTrace: threw: " << e.what();
+    }
+    catch (...)
+    {
+        JLOG(journal.trace()) << "WasmTrace: threw";
+    }
+}
+
+std::int32_t
+HostContext::updateData(rust::Slice data) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke([&] { return hostFunctions_.updateData(Slice{data.data(), data.size()}); });
+    });
+}
+
+std::int32_t
+HostContext::getNFT(
+    rust::Slice account,
+    rust::Slice nftId,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        if (account.size() != AccountID::size())
+        {
+            return hfErrorToInt(HostFunctionError::InvalidParams);
+        }
+        return invokeNFT(nftId, out, [&](auto const& nft) {
+            return hostFunctions_.getNFT(AccountID::fromVoid(account.data()), nft);
+        });
+    });
+}
+
+std::int32_t
+HostContext::getNFTIssuer(rust::Slice nftId, rust::Slice out)
+    const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeNFT(
+            nftId, out, [&](auto const& nft) { return hostFunctions_.getNFTIssuer(nft); });
+    });
+}
+
+std::int32_t
+HostContext::getNFTTaxon(rust::Slice nftId, rust::Slice out)
+    const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeNFT(
+            nftId, out, [&](auto const& nft) { return hostFunctions_.getNFTTaxon(nft); });
+    });
+}
+
+std::int32_t
+HostContext::getNFTFlags(rust::Slice nftId) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeNFT(nftId, [&](auto const& nft) { return hostFunctions_.getNFTFlags(nft); });
+    });
+}
+
+std::int32_t
+HostContext::getNFTTransferFee(rust::Slice nftId) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeNFT(
+            nftId, [&](auto const& nft) { return hostFunctions_.getNFTTransferFee(nft); });
+    });
+}
+
+std::int32_t
+HostContext::getNFTSequence(rust::Slice nftId, rust::Slice out)
+    const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invokeNFT(
+            nftId, out, [&](auto const& nft) { return hostFunctions_.getNFTSequence(nft); });
+    });
+}
+
+std::int32_t
+HostContext::floatFromInt(std::int64_t x, std::int32_t mode, rust::Slice out)
+    const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(out, [&] { return hostFunctions_.floatFromInt(x, mode); });
+    });
+}
+
+std::int32_t
+HostContext::floatFromUint(
+    rust::Slice x,
+    std::int32_t mode,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        auto const parsed = parseUint64(x);
+        if (!parsed)
+        {
+            return hfErrorToInt(parsed.error());
+        }
+        return invoke(out, [&] { return hostFunctions_.floatFromUint(*parsed, mode); });
+    });
+}
+
+std::int32_t
+HostContext::floatFromSTAmount(
+    rust::Slice amount,
+    std::int32_t mode,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        auto const parsed = parseST(amount);
+        if (!parsed)
+        {
+            return hfErrorToInt(parsed.error());
+        }
+        return invoke(out, [&] { return hostFunctions_.floatFromSTAmount(*parsed, mode); });
+    });
+}
+
+std::int32_t
+HostContext::floatFromSTNumber(
+    rust::Slice number,
+    std::int32_t mode,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        auto const parsed = parseST(number);
+        if (!parsed)
+        {
+            return hfErrorToInt(parsed.error());
+        }
+        return invoke(out, [&] { return hostFunctions_.floatFromSTNumber(*parsed, mode); });
+    });
+}
+
+std::int32_t
+HostContext::floatToInt(
+    rust::Slice x,
+    std::int32_t mode,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(
+            out, [&] { return hostFunctions_.floatToInt(Slice{x.data(), x.size()}, mode); });
+    });
+}
+
+std::int32_t
+HostContext::floatToMantExp(
+    rust::Slice x,
+    rust::Slice mantissaOut,
+    rust::Slice exponentOut) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        auto const value = hostFunctions_.floatToMantExp(Slice{x.data(), x.size()});
+        if (!value)
+        {
+            return hfErrorToInt(value.error());
+        }
+
+        // The engine copies each region only if the whole value fits, so writing the
+        // true lengths here and summing them matches its accounting.
+        auto const r1 = answerScalar(mantissaOut, value->first);
+        auto const r2 = answerScalar(exponentOut, value->second);
+        return r1 + r2;
+    });
+}
+
+std::int32_t
+HostContext::floatFromMantExp(
+    std::int64_t mantissa,
+    std::int32_t exponent,
+    std::int32_t mode,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(
+            out, [&] { return hostFunctions_.floatFromMantExp(mantissa, exponent, mode); });
+    });
+}
+
+std::int32_t
+HostContext::floatCompare(rust::Slice x, rust::Slice y)
+    const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke([&] {
+            return hostFunctions_.floatCompare(
+                Slice{x.data(), x.size()}, Slice{y.data(), y.size()});
+        });
+    });
+}
+
+std::int32_t
+HostContext::floatAdd(
+    rust::Slice x,
+    rust::Slice y,
+    std::int32_t mode,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(out, [&] {
+            return hostFunctions_.floatAdd(
+                Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode);
+        });
+    });
+}
+
+std::int32_t
+HostContext::floatSubtract(
+    rust::Slice x,
+    rust::Slice y,
+    std::int32_t mode,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(out, [&] {
+            return hostFunctions_.floatSubtract(
+                Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode);
+        });
+    });
+}
+
+std::int32_t
+HostContext::floatMultiply(
+    rust::Slice x,
+    rust::Slice y,
+    std::int32_t mode,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(out, [&] {
+            return hostFunctions_.floatMultiply(
+                Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode);
+        });
+    });
+}
+
+std::int32_t
+HostContext::floatDivide(
+    rust::Slice x,
+    rust::Slice y,
+    std::int32_t mode,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(out, [&] {
+            return hostFunctions_.floatDivide(
+                Slice{x.data(), x.size()}, Slice{y.data(), y.size()}, mode);
+        });
+    });
+}
+
+std::int32_t
+HostContext::floatPower(
+    rust::Slice x,
+    std::int32_t n,
+    std::int32_t mode,
+    rust::Slice out) const noexcept
+{
+    return guarded(hostFunctions_.getJournal(), kHostInternal, [&] {
+        return invoke(
+            out, [&] { return hostFunctions_.floatPower(Slice{x.data(), x.size()}, n, mode); });
+    });
+}
+
+}  // namespace xrpl
diff --git a/src/libxrpl/tx/wasm/HostFuncImplFloat.cpp b/src/libxrpl/tx/wasm/HostFuncImplFloat.cpp
index 2abd73d82c..4ec93eb2d9 100644
--- a/src/libxrpl/tx/wasm/HostFuncImplFloat.cpp
+++ b/src/libxrpl/tx/wasm/HostFuncImplFloat.cpp
@@ -383,32 +383,6 @@ floatDivideImpl(Slice const& x, Slice const& y, int32_t mode)
     }
 }
 
-std::expected
-floatRootImpl(Slice const& x, int32_t n, int32_t mode)
-{
-    try
-    {
-        if (n < 1)
-            return std::unexpected(HostFunctionError::FloatInputMalformed);
-
-        detail::FloatState const rm(mode);
-        if (!rm)
-            return std::unexpected(HostFunctionError::FloatInputMalformed);
-
-        auto const xx = detail::floatDecode(x);
-        if (!xx)
-            return std::unexpected(HostFunctionError::FloatInputMalformed);
-
-        return detail::floatEncode(root(*xx, n));
-    }
-    // LCOV_EXCL_START
-    catch (...)
-    {
-        return std::unexpected(HostFunctionError::FloatComputationError);
-    }
-    // LCOV_EXCL_STOP
-}
-
 std::expected
 floatPowerImpl(Slice const& x, int32_t n, int32_t mode)
 {
@@ -515,12 +489,6 @@ WasmHostFunctionsImpl::floatDivide(Slice const& x, Slice const& y, int32_t mode)
     return wasm_float::floatDivideImpl(x, y, mode);
 }
 
-std::expected
-WasmHostFunctionsImpl::floatRoot(Slice const& x, int32_t n, int32_t mode) const
-{
-    return wasm_float::floatRootImpl(x, n, mode);
-}
-
 std::expected
 WasmHostFunctionsImpl::floatPower(Slice const& x, int32_t n, int32_t mode) const
 {
diff --git a/src/libxrpl/tx/wasm/HostFuncImplGetter.cpp b/src/libxrpl/tx/wasm/HostFuncImplGetter.cpp
index 4ae0c72426..27b0370171 100644
--- a/src/libxrpl/tx/wasm/HostFuncImplGetter.cpp
+++ b/src/libxrpl/tx/wasm/HostFuncImplGetter.cpp
@@ -15,7 +15,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 
@@ -124,9 +123,10 @@ getAnyFieldData(FieldValue const& variantObj)
     if (uint256 const* const* u = std::get_if(&variantObj))
         return Bytes((*u)->begin(), (*u)->end());
 
-    // Unreachable: the variant only holds the two alternatives above. If not,
-    // it's an xrpld bug -> tecINTERNAL (thrown, caught by HostFuncMain_wrap).
-    Throw(std::string(hfErrInternal));  // LCOV_EXCL_LINE
+    // Unreachable: the variant only holds the two alternatives above. If not, it is an
+    // xrpld bug, and `guarded` turns the throw into `InternalFatal`, which stops the run ->
+    // tecINTERNAL.
+    Throw("field value variant holds neither alternative");  // LCOV_EXCL_LINE
 }
 
 static inline bool
diff --git a/src/libxrpl/tx/wasm/HostFuncImplKeylet.cpp b/src/libxrpl/tx/wasm/HostFuncImplKeylet.cpp
index 3ffb9ef364..85963b1278 100644
--- a/src/libxrpl/tx/wasm/HostFuncImplKeylet.cpp
+++ b/src/libxrpl/tx/wasm/HostFuncImplKeylet.cpp
@@ -220,4 +220,33 @@ WasmHostFunctionsImpl::vaultKeylet(AccountID const& account, std::uint32_t seq)
     return Bytes{keylet.key.begin(), keylet.key.end()};
 }
 
+std::expected
+WasmHostFunctionsImpl::sponsorshipKeylet(AccountID const& sponsor, AccountID const& sponsee) const
+{
+    if (!sponsor || !sponsee)
+        return std::unexpected(HostFunctionError::InvalidAccount);
+    if (sponsor == sponsee)
+        return std::unexpected(HostFunctionError::InvalidParams);
+    auto const keylet = keylet::sponsorship(sponsor, sponsee);
+    return Bytes{keylet.key.begin(), keylet.key.end()};
+}
+
+std::expected
+WasmHostFunctionsImpl::loanBrokerKeylet(AccountID const& owner, std::uint32_t seq) const
+{
+    if (!owner)
+        return std::unexpected(HostFunctionError::InvalidAccount);
+    auto const keylet = keylet::loanBroker(owner, SeqProxy::rawSequence(seq));
+    return Bytes{keylet.key.begin(), keylet.key.end()};
+}
+
+std::expected
+WasmHostFunctionsImpl::loanKeylet(uint256 const& loanBrokerID, std::uint32_t loanSeq) const
+{
+    if (!loanBrokerID)
+        return std::unexpected(HostFunctionError::InvalidParams);
+    auto const keylet = keylet::loan(loanBrokerID, SeqProxy::rawSequence(loanSeq));
+    return Bytes{keylet.key.begin(), keylet.key.end()};
+}
+
 }  // namespace xrpl
diff --git a/src/libxrpl/tx/wasm/HostFuncWrapper.cpp b/src/libxrpl/tx/wasm/HostFuncWrapper.cpp
deleted file mode 100644
index e5d3eb79db..0000000000
--- a/src/libxrpl/tx/wasm/HostFuncWrapper.cpp
+++ /dev/null
@@ -1,2480 +0,0 @@
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-namespace xrpl {
-
-using SFieldCRef = std::reference_wrapper;
-
-constexpr int64_t unalignedGas = 50;
-
-// Charge `delta` gas; returns the remaining gas. Out-of-gas throws hfErrOutOfGas
-// (-> tecOUT_OF_GAS); a failed setGas is an xrpld bug, throws hfErrInternal
-// (-> tecINTERNAL). HostFuncMain_wrap turns both into traps.
-static inline std::int64_t
-checkGas(WasmRuntimeWrapper& rt, int64_t delta)
-{
-    int64_t const gas = rt.getGas();
-    if (delta == 0)
-        return gas;
-
-    int64_t const x = gas >= delta ? gas - delta : 0;
-
-    if (rt.setGas(x) < 0)
-        Throw(std::string(hfErrInternal));  // LCOV_EXCL_LINE
-
-    if (gas < delta)
-        Throw(std::string(hfErrOutOfGas));
-
-    return x;
-}
-
-// Transfer limit is a separate soft budget: exceeding it is a normal guest-facing
-// return code, not a trap. Only a failed setTransferLimit (an xrpld bug) throws.
-static inline std::expected
-checkTransfer(WasmRuntimeWrapper& rt, int64_t delta)
-{
-    auto const transLimit = rt.getTransferLimit();
-    int64_t const x = transLimit >= delta ? transLimit - delta : 0;
-
-    if (rt.setTransferLimit(x) < 0)
-        Throw(std::string(hfErrInternal));  // LCOV_EXCL_LINE
-
-    if (transLimit < delta)
-        return std::unexpected(HostFunctionError::OutOfTransferLimit);
-
-    return x;
-}
-
-// On any failure here a C++ exception is thrown; HostFuncMain_wrap's catch-all
-// turns it into tecINTERNAL. These conditions are all xrpld-side invariants.
-static std::tuple
-mainCheck(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results)
-{
-    if (env == nullptr)
-        Throw(std::string(hfErrInternal));  // LCOV_EXCL_LINE
-
-    if (params == nullptr)
-        Throw(std::string(hfErrInternal));  // LCOV_EXCL_LINE
-
-    if (results == nullptr)
-        Throw(std::string(hfErrInternal));  // LCOV_EXCL_LINE
-
-    WasmUserData const* udata = reinterpret_cast(env);
-    HostFunctions& hf = udata->first;
-    WasmRuntimeWrapper& rt = hf.getRT();
-    WasmImportFunc const& impFunc = udata->second;
-
-    // Charge the per-call gas. Throws (and terminates) if out of gas.
-    checkGas(rt, impFunc.gas);
-
-    return std::tie(hf, impFunc);
-}
-
-//----------------------------------------------------------------------------------------------------------------------
-
-static int32_t
-setData(
-    WasmRuntimeWrapper& runtime,
-    int32_t dst,
-    int32_t dstSize,
-    uint8_t const* src,
-    int32_t srcSize)
-{
-    if (srcSize == 0)
-        return 0;  // LCOV_EXCL_LINE
-
-    if (dst < 0 || dstSize < 0 || (src == nullptr) || srcSize < 0)
-        return hfErrorToInt(HostFunctionError::InvalidParams);
-
-    if (srcSize > kMaxWasmDataLength)
-        return hfErrorToInt(HostFunctionError::DataFieldTooLarge);
-
-    auto const memory = runtime.getMem();
-
-    // LCOV_EXCL_START
-    if (memory.s == 0u)
-        return hfErrorToInt(HostFunctionError::NoMemExported);
-    // LCOV_EXCL_STOP
-    if (std::cmp_greater((int64_t)dst + dstSize, memory.s))
-        return hfErrorToInt(HostFunctionError::PointerOutOfBounds);
-    if (srcSize > dstSize)
-        return hfErrorToInt(HostFunctionError::BufferTooSmall);
-
-    if (auto t = checkTransfer(runtime, srcSize); !t)
-        return hfErrorToInt(t.error());
-
-    memcpy(memory.p + dst, src, srcSize);
-
-    return srcSize;
-}
-
-static std::expected
-getDataSlice(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t& i)
-{
-    int64_t const ptr = params->data[i].of.i32;
-    int64_t const size = params->data[i + 1].of.i32;
-    i += 2;
-    if (ptr < 0 || size < 0)
-        return std::unexpected(HostFunctionError::InvalidParams);
-
-    if (size == 0)
-        return Slice();
-
-    if (size > kMaxWasmDataLength)
-        return std::unexpected(HostFunctionError::DataFieldTooLarge);
-
-    auto const memory = runtime.getMem();
-    // LCOV_EXCL_START
-    if (memory.s == 0u)
-        return std::unexpected(HostFunctionError::NoMemExported);
-    // LCOV_EXCL_STOP
-
-    if (std::cmp_greater(ptr + size, memory.s))
-        return std::unexpected(HostFunctionError::PointerOutOfBounds);
-
-    Slice const data(memory.p + ptr, size);
-    return data;
-}
-
-static std::expected
-getDataInt32(WasmRuntimeWrapper const&, wasm_val_vec_t const* params, int32_t& i)
-{
-    auto const result = params->data[i].of.i32;
-    i++;
-    return result;
-}
-
-static std::expected
-getDataInt64(WasmRuntimeWrapper const&, wasm_val_vec_t const* params, int32_t& i)
-{
-    auto const result = params->data[i].of.i64;
-    i++;
-    return result;
-}
-
-template 
-static std::expected
-getDataUnsigned(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t& i)
-{
-    static_assert(std::is_unsigned_v);
-    auto const r = getDataSlice(runtime, params, i);
-    if (!r)
-        return std::unexpected(r.error());
-    if (r->size() != sizeof(T))
-        return std::unexpected(HostFunctionError::InvalidParams);
-
-    T x;
-    auto const p = reinterpret_cast(r->data());
-    if (p & (alignof(T) - 1))  // unaligned
-    {
-        memcpy(&x, r->data(), sizeof(T));
-    }
-    else
-    {
-        x = *reinterpret_cast(r->data());
-    }
-    x = adjustWasmEndianess(x);
-
-    return x;
-}
-
-static std::expected
-getDataUInt32(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t& i)
-{
-    return getDataUnsigned(runtime, params, i);
-}
-
-static std::expected
-getDataUInt64(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t& i)
-{
-    return getDataUnsigned(runtime, params, i);
-}
-
-static std::expected
-getDataSField(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t& i)
-{
-    auto const& m = SField::getKnownCodeToField();
-    auto const it = m.find(params->data[i].of.i32);
-    i++;
-    if (it == m.end())
-        return std::unexpected(HostFunctionError::InvalidField);
-
-    return *it->second;
-}
-
-static std::expected
-getDataUInt256(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t& i)
-{
-    auto const slice = getDataSlice(runtime, params, i);
-    if (!slice)
-        return std::unexpected(slice.error());
-
-    if (slice->size() != uint256::size())
-        return std::unexpected(HostFunctionError::InvalidParams);
-
-    if (auto t = checkTransfer(runtime, uint256::size()); !t)
-        return std::unexpected(t.error());
-
-    return uint256::fromVoid(slice->data());
-}
-
-static std::expected
-getDataAccountID(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t& i)
-{
-    auto const slice = getDataSlice(runtime, params, i);
-    if (!slice)
-        return std::unexpected(slice.error());
-
-    if (slice->size() != AccountID::size())
-        return std::unexpected(HostFunctionError::InvalidParams);
-
-    if (auto t = checkTransfer(runtime, AccountID::size()); !t)
-        return std::unexpected(t.error());
-
-    return AccountID::fromVoid(slice->data());
-}
-
-static std::expected
-getDataCurrency(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t& i)
-{
-    auto const slice = getDataSlice(runtime, params, i);
-    if (!slice)
-        return std::unexpected(slice.error());
-
-    if (slice->size() != Currency::size())
-        return std::unexpected(HostFunctionError::InvalidParams);
-
-    if (auto t = checkTransfer(runtime, Currency::size()); !t)
-        return std::unexpected(t.error());
-
-    return Currency::fromVoid(slice->data());
-}
-
-static std::expected
-getDataAsset(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t& i)
-{
-    auto const slice = getDataSlice(runtime, params, i);
-    if (!slice)
-        return std::unexpected(slice.error());
-
-    if (slice->size() == MPTID::size())
-    {
-        if (auto t = checkTransfer(runtime, slice->size()); !t)
-            return std::unexpected(t.error());
-
-        auto const mptid = MPTID::fromVoid(slice->data());
-        return Asset{mptid};
-    }
-
-    if (slice->size() == Currency::size())
-    {
-        if (auto t = checkTransfer(runtime, slice->size()); !t)
-            return std::unexpected(t.error());
-
-        auto const currency = Currency::fromVoid(slice->data());
-        auto const issue = Issue{currency, xrpAccount()};
-        if (!issue.native())
-            return std::unexpected(HostFunctionError::InvalidParams);
-
-        return Asset{issue};
-    }
-
-    if (slice->size() == (Currency::size() + AccountID::size()))
-    {
-        if (auto t = checkTransfer(runtime, slice->size()); !t)
-            return std::unexpected(t.error());
-
-        auto const issue = Issue(
-            Currency::fromVoid(slice->data()),
-            AccountID::fromVoid(slice->data() + Currency::size()));
-
-        if (issue.native())
-            return std::unexpected(HostFunctionError::InvalidParams);
-
-        return Asset{issue};
-    }
-
-    return std::unexpected(HostFunctionError::InvalidParams);
-}
-
-static std::expected
-getDataString(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t& i)
-{
-    auto const slice = getDataSlice(runtime, params, i);
-    if (!slice)
-        return std::unexpected(slice.error());
-
-    return std::string_view(reinterpret_cast(slice->data()), slice->size());
-}
-
-static std::expected
-getDataLocator(WasmRuntimeWrapper& runtime, wasm_val_vec_t const* params, int32_t& i)
-{
-    static_assert(kMaxWasmDataLength % sizeof(int32_t) == 0);
-
-    auto const slice = getDataSlice(runtime, params, i);
-    if (!slice)
-        return std::unexpected(slice.error());
-    if (slice->empty() || ((slice->size() & 3) != 0u))  // must be multiple of 4
-        return std::unexpected(HostFunctionError::LocatorMalformed);
-
-    uint32_t const locSize = slice->size() / sizeof(int32_t);
-    auto const p = reinterpret_cast(slice->data());
-
-    if ((p & (alignof(int32_t) - 1)) != 0u)
-    {  // unaligned
-
-        // Use gas and transfer limit for copying. checkGas throws (and
-        // terminates execution) if out of gas; checkTransfer keeps returning a
-        // guest-facing code when the transfer limit is exceeded.
-        checkGas(runtime, unalignedGas);
-        if (auto t = checkTransfer(runtime, slice->size()); !t)
-            return std::unexpected(t.error());
-
-        std::vector locBuf(locSize);
-        memcpy(&locBuf[0], slice->data(), slice->size());
-        FieldLocator locator(std::move(locBuf));
-
-        return locator;
-    }
-
-    auto const* locPtr = reinterpret_cast(slice->data());
-    return FieldLocator(locPtr, locSize);
-}
-
-static inline std::nullptr_t
-hfResult(wasm_val_vec_t* results, int32_t value)
-{
-    results->data[0] = WASM_I32_VAL(value);
-    // results->size = 1;
-    return nullptr;
-}
-
-static inline std::nullptr_t
-hfResult(wasm_val_vec_t* results, HostFunctionError value)
-{
-    results->data[0] = WASM_I32_VAL(hfErrorToInt(value));
-    // results->size = 1;
-    return nullptr;
-}
-
-template 
-static std::nullptr_t
-returnResult(
-    WasmRuntimeWrapper& runtime,
-    wasm_val_vec_t const* params,
-    wasm_val_vec_t* results,
-    std::expected const& res,
-    int32_t index)
-{
-    if (!res)
-        return hfResult(results, res.error());
-
-    if constexpr (std::is_same_v)
-    {
-        if (index < 0 || index + 1 >= params->size)
-            Throw(std::string(hfErrInternal));  // LCOV_EXCL_LINE
-
-        auto const dataResult = setData(
-            runtime,
-            params->data[index].of.i32,
-            params->data[index + 1].of.i32,
-            res->data(),
-            res->size());
-        return hfResult(results, dataResult);
-    }
-    else if constexpr (std::is_same_v)
-    {
-        if (index < 0 || index + 1 >= params->size)
-            Throw(std::string(hfErrInternal));  // LCOV_EXCL_LINE
-
-        auto const dataResult = setData(
-            runtime,
-            params->data[index].of.i32,
-            params->data[index + 1].of.i32,
-            res->data(),
-            res->size());
-        return hfResult(results, dataResult);
-    }
-    else if constexpr (std::is_same_v)
-    {
-        return hfResult(results, res.value());
-    }
-    else if constexpr (std::is_same_v)
-    {
-        if (index < 0 || index + 1 >= params->size)
-            Throw(std::string(hfErrInternal));  // LCOV_EXCL_LINE
-
-        auto const resultValue = adjustWasmEndianess(res.value());
-        auto const dataResult = setData(
-            runtime,
-            params->data[index].of.i32,
-            params->data[index + 1].of.i32,
-            reinterpret_cast(&resultValue),
-            static_cast(sizeof(resultValue)));
-        return hfResult(results, dataResult);
-    }
-    else if constexpr (std::is_same_v)
-    {
-        if (index < 0 || index + 1 >= params->size)
-            Throw(std::string(hfErrInternal));  // LCOV_EXCL_LINE
-
-        auto const resultValue = adjustWasmEndianess(res.value());
-        auto const dataResult = setData(
-            runtime,
-            params->data[index].of.i32,
-            params->data[index + 1].of.i32,
-            reinterpret_cast(&resultValue),
-            static_cast(sizeof(resultValue)));
-        return hfResult(results, dataResult);
-    }
-    else if constexpr (std::is_same_v)
-    {
-        if (index < 0 || index + 3 >= params->size)
-            Throw(std::string(hfErrInternal));  // LCOV_EXCL_LINE
-
-        auto const mantissa = adjustWasmEndianess(res->first);
-        auto const r1 = setData(
-            runtime,
-            params->data[index].of.i32,
-            params->data[index + 1].of.i32,
-            reinterpret_cast(&mantissa),
-            static_cast(sizeof(mantissa)));
-        if (r1 < 0)
-            return hfResult(results, r1);
-
-        index += 2;
-        auto const exponent = adjustWasmEndianess(res->second);
-        auto const r2 = setData(
-            runtime,
-            params->data[index].of.i32,
-            params->data[index + 1].of.i32,
-            reinterpret_cast(&exponent),
-            static_cast(sizeof(exponent)));
-        if (r2 < 0)
-            return hfResult(results, r2);
-
-        return hfResult(results, r1 + r2);  // 12 bytes
-    }
-    else
-    {
-        static_assert([] { return false; }(), "Unhandled return type in returnResult");
-    }
-}
-
-//----------------------------------------------------------------------------------------------------------------------
-
-wasm_trap_t*
-HostFuncMain_wrap(WASM_CB_PARAMS_LIST)
-{
-    [[maybe_unused]] std::string_view hfName;
-
-    try
-    {
-        auto [hf, impFunc] = mainCheck(env, params, results);
-        hfName = impFunc.name;
-        auto* fWrap = reinterpret_cast(impFunc.wrap);
-        return fWrap(hf, params, results);
-    }
-    catch (std::exception const& e)
-    {
-#ifdef DEBUG_OUTPUT
-        std::cerr << "Hostfunction " << hfName << " exception: " << e.what() << std::endl;
-#endif
-        // Normalize to the two boundary signals: explicit out-of-gas, else any
-        // exception (including stray ones from helpers) is an internal fault.
-        bool const oog = std::string_view(e.what()) == hfErrOutOfGas;
-        wasm_trap_t* trap = reinterpret_cast(  // NOLINT
-            WasmEngine::instance().newTrap(std::string(oog ? hfErrOutOfGas : hfErrInternal)));
-        return trap;
-    }
-    catch (...)
-    {
-#ifdef DEBUG_OUTPUT
-        std::cerr << "Hostfunction " << hfName << " unknown exception." << std::endl;
-#endif
-        wasm_trap_t* trap = reinterpret_cast(               // NOLINT
-            WasmEngine::instance().newTrap(std::string(hfErrInternal)));  // LCOV_EXCL_LINE
-        return trap;
-    }
-
-    return nullptr;  // LCOV_EXCL_LINE
-}
-
-//----------------------------------------------------------------------------------------------------------------------
-wasm_trap_t*
-getLedgerSqn_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t const index = 0;
-    auto& runtime = hf.getRT();
-
-    return returnResult(runtime, params, results, hf.getLedgerSqn(), index);
-}
-
-wasm_trap_t*
-getParentLedgerTime_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t const index = 0;
-    auto& runtime = hf.getRT();
-
-    return returnResult(runtime, params, results, hf.getParentLedgerTime(), index);
-}
-
-wasm_trap_t*
-getParentLedgerHash_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t const index = 0;
-    auto& runtime = hf.getRT();
-
-    return returnResult(runtime, params, results, hf.getParentLedgerHash(), index);
-}
-
-wasm_trap_t*
-getBaseFee_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t const index = 0;
-    auto& runtime = hf.getRT();
-
-    return returnResult(runtime, params, results, hf.getBaseFee(), index);
-}
-
-wasm_trap_t*
-isAmendmentEnabled_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const slice = getDataSlice(runtime, params, index);
-    if (!slice)
-        return hfResult(results, slice.error());
-
-    if (slice->size() == uint256::size())
-    {
-        if (auto const ret = hf.isAmendmentEnabled(uint256::fromVoid(slice->data()));
-            ret && *ret == 1)
-            return returnResult(runtime, params, results, ret, index);
-        // Fall through to string lookup — the 32 bytes may be an amendment name
-    }
-
-    if (slice->size() > 64)
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-
-    auto const str = std::string_view(reinterpret_cast(slice->data()), slice->size());
-    return returnResult(runtime, params, results, hf.isAmendmentEnabled(str), index);
-}
-
-wasm_trap_t*
-cacheLedgerObj_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const id = getDataUInt256(runtime, params, index);
-    if (!id)
-        return hfResult(results, id.error());
-
-    auto const cache = getDataInt32(runtime, params, index);
-    if (!cache)
-        return hfResult(results, cache.error());  // LCOV_EXCL_LINE
-
-    return returnResult(runtime, params, results, hf.cacheLedgerObj(*id, *cache), index);
-}
-
-wasm_trap_t*
-getTxField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const fname = getDataSField(runtime, params, index);
-    if (!fname)
-        return hfResult(results, fname.error());
-
-    return returnResult(runtime, params, results, hf.getTxField(*fname), index);
-}
-
-wasm_trap_t*
-getCurrentLedgerObjField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const fname = getDataSField(runtime, params, index);
-    if (!fname)
-        return hfResult(results, fname.error());
-
-    return returnResult(runtime, params, results, hf.getCurrentLedgerObjField(*fname), index);
-}
-
-wasm_trap_t*
-getLedgerObjField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const cache = getDataInt32(runtime, params, index);
-    if (!cache)
-        return hfResult(results, cache.error());  // LCOV_EXCL_LINE
-
-    auto const fname = getDataSField(runtime, params, index);
-    if (!fname)
-        return hfResult(results, fname.error());
-
-    return returnResult(runtime, params, results, hf.getLedgerObjField(*cache, *fname), index);
-}
-
-wasm_trap_t*
-getTxNestedField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const locator = getDataLocator(runtime, params, index);
-    if (!locator)
-        return hfResult(results, locator.error());
-
-    return returnResult(runtime, params, results, hf.getTxNestedField(*locator), index);
-}
-
-wasm_trap_t*
-getCurrentLedgerObjNestedField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const locator = getDataLocator(runtime, params, index);
-    if (!locator)
-        return hfResult(results, locator.error());
-
-    return returnResult(
-        runtime, params, results, hf.getCurrentLedgerObjNestedField(*locator), index);
-}
-
-wasm_trap_t*
-getLedgerObjNestedField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const cache = getDataInt32(runtime, params, index);
-    if (!cache)
-        return hfResult(results, cache.error());  // LCOV_EXCL_LINE
-
-    auto const locator = getDataLocator(runtime, params, index);
-    if (!locator)
-        return hfResult(results, locator.error());
-
-    return returnResult(
-        runtime, params, results, hf.getLedgerObjNestedField(*cache, *locator), index);
-}
-
-wasm_trap_t*
-getTxArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const fname = getDataSField(runtime, params, index);
-    if (!fname)
-        return hfResult(results, fname.error());
-
-    return returnResult(runtime, params, results, hf.getTxArrayLen(*fname), index);
-}
-
-wasm_trap_t*
-getCurrentLedgerObjArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const fname = getDataSField(runtime, params, index);
-    if (!fname)
-        return hfResult(results, fname.error());
-
-    return returnResult(runtime, params, results, hf.getCurrentLedgerObjArrayLen(*fname), index);
-}
-
-wasm_trap_t*
-getLedgerObjArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const cache = getDataInt32(runtime, params, index);
-    if (!cache)
-        return hfResult(results, cache.error());  // LCOV_EXCL_LINE
-
-    auto const fname = getDataSField(runtime, params, index);
-    if (!fname)
-        return hfResult(results, fname.error());
-
-    return returnResult(runtime, params, results, hf.getLedgerObjArrayLen(*cache, *fname), index);
-}
-
-wasm_trap_t*
-getTxNestedArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const locator = getDataLocator(runtime, params, index);
-    if (!locator)
-        return hfResult(results, locator.error());
-
-    return returnResult(runtime, params, results, hf.getTxNestedArrayLen(*locator), index);
-}
-
-wasm_trap_t*
-getCurrentLedgerObjNestedArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const locator = getDataLocator(runtime, params, index);
-    if (!locator)
-        return hfResult(results, locator.error());
-
-    return returnResult(
-        runtime, params, results, hf.getCurrentLedgerObjNestedArrayLen(*locator), index);
-}
-wasm_trap_t*
-getLedgerObjNestedArrayLen_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const cache = getDataInt32(runtime, params, index);
-    if (!cache)
-        return hfResult(results, cache.error());  // LCOV_EXCL_LINE
-
-    auto const locator = getDataLocator(runtime, params, index);
-    if (!locator)
-        return hfResult(results, locator.error());
-
-    return returnResult(
-        runtime, params, results, hf.getLedgerObjNestedArrayLen(*cache, *locator), index);
-}
-
-wasm_trap_t*
-updateData_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const bytes = getDataSlice(runtime, params, index);
-    if (!bytes)
-        return hfResult(results, bytes.error());
-
-    return returnResult(runtime, params, results, hf.updateData(*bytes), index);
-}
-
-wasm_trap_t*
-checkSignature_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const message = getDataSlice(runtime, params, index);
-    if (!message)
-        return hfResult(results, message.error());
-
-    auto const signature = getDataSlice(runtime, params, index);
-    if (!signature)
-        return hfResult(results, signature.error());
-
-    auto const pubkey = getDataSlice(runtime, params, index);
-    if (!pubkey)
-        return hfResult(results, pubkey.error());
-
-    return returnResult(
-        runtime, params, results, hf.checkSignature(*message, *signature, *pubkey), index);
-}
-
-wasm_trap_t*
-computeSha512HalfHash_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const bytes = getDataSlice(runtime, params, index);
-    if (!bytes)
-        return hfResult(results, bytes.error());
-
-    return returnResult(runtime, params, results, hf.computeSha512HalfHash(*bytes), index);
-}
-
-wasm_trap_t*
-accountKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    return returnResult(runtime, params, results, hf.accountKeylet(*acc), index);
-}
-
-wasm_trap_t*
-ammKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const issue1 = getDataAsset(runtime, params, index);
-    if (!issue1)
-        return hfResult(results, issue1.error());
-
-    auto const issue2 = getDataAsset(runtime, params, index);
-    if (!issue2)
-        return hfResult(results, issue2.error());
-
-    return returnResult(
-        runtime, params, results, hf.ammKeylet(issue1.value(), issue2.value()), index);
-}
-
-wasm_trap_t*
-checkKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const seq = getDataUInt32(runtime, params, index);
-    if (!seq)
-        return hfResult(results, seq.error());
-
-    return returnResult(runtime, params, results, hf.checkKeylet(acc.value(), *seq), index);
-}
-
-wasm_trap_t*
-credentialKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const subj = getDataAccountID(runtime, params, index);
-    if (!subj)
-        return hfResult(results, subj.error());
-
-    auto const iss = getDataAccountID(runtime, params, index);
-    if (!iss)
-        return hfResult(results, iss.error());
-
-    auto const credType = getDataSlice(runtime, params, index);
-    if (!credType)
-        return hfResult(results, credType.error());
-
-    return returnResult(
-        runtime, params, results, hf.credentialKeylet(*subj, *iss, *credType), index);
-}
-
-wasm_trap_t*
-delegateKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const authorize = getDataAccountID(runtime, params, index);
-    if (!authorize)
-        return hfResult(results, authorize.error());
-
-    return returnResult(
-        runtime, params, results, hf.delegateKeylet(acc.value(), authorize.value()), index);
-}
-
-wasm_trap_t*
-depositPreauthKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const authorize = getDataAccountID(runtime, params, index);
-    if (!authorize)
-        return hfResult(results, authorize.error());
-
-    return returnResult(
-        runtime, params, results, hf.depositPreauthKeylet(acc.value(), authorize.value()), index);
-}
-
-wasm_trap_t*
-didKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    return returnResult(runtime, params, results, hf.didKeylet(acc.value()), index);
-}
-
-wasm_trap_t*
-escrowKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const seq = getDataUInt32(runtime, params, index);
-    if (!seq)
-        return hfResult(results, seq.error());
-
-    return returnResult(runtime, params, results, hf.escrowKeylet(*acc, *seq), index);
-}
-
-wasm_trap_t*
-trustLineKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc1 = getDataAccountID(runtime, params, index);
-    if (!acc1)
-        return hfResult(results, acc1.error());
-
-    auto const acc2 = getDataAccountID(runtime, params, index);
-    if (!acc2)
-        return hfResult(results, acc2.error());
-
-    auto const currency = getDataCurrency(runtime, params, index);
-    if (!currency)
-        return hfResult(results, currency.error());
-
-    return returnResult(
-        runtime,
-        params,
-        results,
-        hf.trustLineKeylet(acc1.value(), acc2.value(), currency.value()),
-        index);
-}
-
-wasm_trap_t*
-mptokenIssuanceKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const seq = getDataUInt32(runtime, params, index);
-    if (!seq)
-        return hfResult(results, seq.error());
-
-    return returnResult(
-        runtime, params, results, hf.mptokenIssuanceKeylet(acc.value(), seq.value()), index);
-}
-
-wasm_trap_t*
-mptokenKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const slice = getDataSlice(runtime, params, index);
-    if (!slice)
-        return hfResult(results, slice.error());
-
-    if (slice->size() != MPTID::size())
-        return hfResult(results, HostFunctionError::InvalidParams);
-    auto const mptid = MPTID::fromVoid(slice->data());
-
-    auto const holder = getDataAccountID(runtime, params, index);
-    if (!holder)
-        return hfResult(results, holder.error());
-
-    return returnResult(runtime, params, results, hf.mptokenKeylet(mptid, holder.value()), index);
-}
-
-wasm_trap_t*
-nftokenOfferKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const seq = getDataUInt32(runtime, params, index);
-    if (!seq)
-        return hfResult(results, seq.error());
-
-    return returnResult(
-        runtime, params, results, hf.nftokenOfferKeylet(acc.value(), seq.value()), index);
-}
-
-wasm_trap_t*
-offerKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const seq = getDataUInt32(runtime, params, index);
-    if (!seq)
-        return hfResult(results, seq.error());
-
-    return returnResult(runtime, params, results, hf.offerKeylet(acc.value(), seq.value()), index);
-}
-
-wasm_trap_t*
-oracleKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const documentId = getDataUInt32(runtime, params, index);
-    if (!documentId)
-        return hfResult(results, documentId.error());
-
-    return returnResult(runtime, params, results, hf.oracleKeylet(*acc, *documentId), index);
-}
-
-wasm_trap_t*
-paychannelKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const dest = getDataAccountID(runtime, params, index);
-    if (!dest)
-        return hfResult(results, dest.error());
-
-    auto const seq = getDataUInt32(runtime, params, index);
-    if (!seq)
-        return hfResult(results, seq.error());
-
-    return returnResult(
-        runtime,
-        params,
-        results,
-        hf.paychannelKeylet(acc.value(), dest.value(), seq.value()),
-        index);
-}
-
-wasm_trap_t*
-permissionedDomainKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const seq = getDataUInt32(runtime, params, index);
-    if (!seq)
-        return hfResult(results, seq.error());
-
-    return returnResult(
-        runtime, params, results, hf.permissionedDomainKeylet(acc.value(), seq.value()), index);
-}
-
-wasm_trap_t*
-signerListKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    return returnResult(runtime, params, results, hf.signerListKeylet(acc.value()), index);
-}
-
-wasm_trap_t*
-ticketKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const seq = getDataUInt32(runtime, params, index);
-    if (!seq)
-        return hfResult(results, seq.error());
-
-    return returnResult(runtime, params, results, hf.ticketKeylet(acc.value(), seq.value()), index);
-}
-
-wasm_trap_t*
-vaultKeylet_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const seq = getDataUInt32(runtime, params, index);
-    if (!seq)
-        return hfResult(results, seq.error());
-
-    return returnResult(runtime, params, results, hf.vaultKeylet(acc.value(), seq.value()), index);
-}
-
-wasm_trap_t*
-getNFT_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const acc = getDataAccountID(runtime, params, index);
-    if (!acc)
-        return hfResult(results, acc.error());
-
-    auto const nftId = getDataUInt256(runtime, params, index);
-    if (!nftId)
-        return hfResult(results, nftId.error());
-
-    return returnResult(runtime, params, results, hf.getNFT(*acc, *nftId), index);
-}
-
-wasm_trap_t*
-getNFTIssuer_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const nftId = getDataUInt256(runtime, params, index);
-    if (!nftId)
-        return hfResult(results, nftId.error());
-
-    return returnResult(runtime, params, results, hf.getNFTIssuer(*nftId), index);
-}
-
-wasm_trap_t*
-getNFTTaxon_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const nftId = getDataUInt256(runtime, params, index);
-    if (!nftId)
-        return hfResult(results, nftId.error());
-
-    return returnResult(runtime, params, results, hf.getNFTTaxon(*nftId), index);
-}
-
-wasm_trap_t*
-getNFTFlags_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const nftId = getDataUInt256(runtime, params, index);
-    if (!nftId)
-        return hfResult(results, nftId.error());
-
-    return returnResult(runtime, params, results, hf.getNFTFlags(*nftId), index);
-}
-
-wasm_trap_t*
-getNFTTransferFee_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const nftId = getDataUInt256(runtime, params, index);
-    if (!nftId)
-        return hfResult(results, nftId.error());
-
-    return returnResult(runtime, params, results, hf.getNFTTransferFee(*nftId), index);
-}
-
-wasm_trap_t*
-getNFTSequence_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t index = 0;
-    auto& runtime = hf.getRT();
-
-    auto const nftId = getDataUInt256(runtime, params, index);
-    if (!nftId)
-        return hfResult(results, nftId.error());
-
-    return returnResult(runtime, params, results, hf.getNFTSequence(*nftId), index);
-}
-
-// log() ignores the journal under DEBUG_OUTPUT, so the gate must not either.
-static inline bool
-traceActive([[maybe_unused]] HostFunctions const& hf)
-{
-#ifdef DEBUG_OUTPUT
-    return true;
-#else
-    return hf.getJournal().active(beast::Severity::Trace);
-#endif
-}
-
-// Not getDataUnsigned: that branches on pointer alignment, and trace must cost
-// the same regardless of how the guest laid out its buffer.
-template 
-static std::optional
-traceInt(Slice const& data)
-{
-    static_assert(std::is_integral_v);
-    if (data.size() != sizeof(T))
-        return std::nullopt;
-
-    T x;
-    memcpy(&x, data.data(), sizeof(T));
-    return adjustWasmEndianess(x);
-}
-
-// std::nullopt means the buffer does not match the type. May throw.
-static std::optional
-traceFormat(TraceDataType type, Slice const& data)
-{
-    switch (type)
-    {
-        case TraceDataType::Int64:
-            if (auto const x = traceInt(data))
-                return std::to_string(*x);
-            return std::nullopt;
-
-        case TraceDataType::Uint64:
-            if (auto const x = traceInt(data))
-                return std::to_string(*x);
-            return std::nullopt;
-
-        case TraceDataType::Xfloat:
-            return wasm_float::floatToString(data);
-
-        case TraceDataType::Account:
-            // Not getDataAccountID: it charges the transfer limit.
-            if (data.size() != AccountID::size())
-                return std::nullopt;
-            return toBase58(AccountID::fromVoid(data.data()));
-
-        case TraceDataType::Amount: {
-            auto serialIter = SerialIter(data);
-            STAmount const amount(serialIter, sfGeneric);  // may throw
-            return amount.getFullText();
-        }
-
-        case TraceDataType::AsHex: {
-            std::string hex;
-            hex.reserve(data.size() * 2);
-            boost::algorithm::hex(data.begin(), data.end(), std::back_inserter(hex));
-            return hex;
-        }
-
-        case TraceDataType::AsText:
-            // An empty Slice has a null data(), which std::string may not take.
-            if (data.empty())
-                return std::string();
-            return std::string(reinterpret_cast(data.data()), data.size());
-    }
-
-    return std::nullopt;  // unknown data_type
-}
-
-// trace's only effect is this node's local log, so nothing observable may depend
-// on the log level: gas is charged in mainCheck before this runs, no transfer
-// limit is charged, and errors are logged rather than trapped.
-wasm_trap_t*
-trace_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    if (!traceActive(hf))
-        return nullptr;
-
-    try
-    {
-        int32_t index = 0;
-        auto& runtime = hf.getRT();
-
-        auto const msg = getDataString(runtime, params, index);
-        if (!msg)
-        {
-            hf.getJournal().trace() << "WasmTrace: invalid message";
-            return nullptr;
-        }
-
-        auto const type = getDataInt32(runtime, params, index);
-        // LCOV_EXCL_START
-        if (!type)
-        {
-            hf.getJournal().trace() << "WasmTrace: invalid data type";
-            return nullptr;
-        }
-        // LCOV_EXCL_STOP
-
-        auto const data = getDataSlice(runtime, params, index);
-        if (!data)
-        {
-            hf.getJournal().trace() << "WasmTrace: invalid data";
-            return nullptr;
-        }
-
-        if (msg->size() + data->size() > kMaxWasmDataLength)
-        {
-            hf.getJournal().trace() << "WasmTrace: message and data too long";
-            return nullptr;
-        }
-
-        auto const text = traceFormat(static_cast(*type), *data);
-        if (!text)
-        {
-            hf.getJournal().trace() << "WasmTrace: data does not match the data type";
-            return nullptr;
-        }
-
-        hf.trace(*msg, *text);
-    }
-    catch (std::exception const& e)
-    {
-        hf.getJournal().trace() << "WasmTrace: error: " << e.what();
-    }
-    // LCOV_EXCL_START
-    catch (...)
-    {
-        hf.getJournal().trace() << "WasmTrace: unknown error";
-    }
-    // LCOV_EXCL_STOP
-    return nullptr;
-}
-
-wasm_trap_t*
-floatFromInt_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataInt64(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());  // LCOV_EXCL_LINE
-
-    i = 3;
-    auto const rounding = getDataInt32(runtime, params, i);
-    if (!rounding)
-        return hfResult(results, rounding.error());  // LCOV_EXCL_LINE
-
-    i = 1;
-    return returnResult(runtime, params, results, hf.floatFromInt(*x, *rounding), i);
-}
-
-wasm_trap_t*
-floatFromUint_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataUInt64(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());
-
-    i = 4;
-    auto const rounding = getDataInt32(runtime, params, i);
-    if (!rounding)
-        return hfResult(results, rounding.error());  // LCOV_EXCL_LINE
-
-    i = 2;
-    return returnResult(runtime, params, results, hf.floatFromUint(*x, *rounding), i);
-}
-
-wasm_trap_t*
-floatFromSTAmount_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataSlice(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());
-
-    auto serialIter = SerialIter(*x);
-    std::optional amount;
-    try
-    {
-        amount = STAmount(serialIter, sfGeneric);
-    }
-    catch (std::exception const&)
-    {
-        amount = std::nullopt;
-    }
-    if (!amount)
-        return hfResult(results, HostFunctionError::InvalidParams);
-
-    i = 4;
-    auto const rounding = getDataInt32(runtime, params, i);
-    if (!rounding)
-        return hfResult(results, rounding.error());  // LCOV_EXCL_LINE
-
-    i = 2;
-    return returnResult(runtime, params, results, hf.floatFromSTAmount(*amount, *rounding), i);
-}
-
-wasm_trap_t*
-floatFromSTNumber_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataSlice(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());
-
-    auto serialIter = SerialIter(*x);
-    std::optional num;
-    try
-    {
-        num = STNumber(serialIter, sfGeneric);
-    }
-    catch (std::exception const&)
-    {
-        num = std::nullopt;
-    }
-    if (!num)
-        return hfResult(results, HostFunctionError::InvalidParams);
-
-    i = 4;
-    auto const rounding = getDataInt32(runtime, params, i);
-    if (!rounding)
-        return hfResult(results, rounding.error());  // LCOV_EXCL_LINE
-
-    i = 2;
-    return returnResult(runtime, params, results, hf.floatFromSTNumber(*num, *rounding), i);
-}
-
-wasm_trap_t*
-floatToInt_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataSlice(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());
-
-    i = 4;
-    auto const rounding = getDataInt32(runtime, params, i);
-    if (!rounding)
-        return hfResult(results, rounding.error());  // LCOV_EXCL_LINE
-
-    i = 2;
-    return returnResult(runtime, params, results, hf.floatToInt(*x, *rounding), i);
-}
-
-wasm_trap_t*
-floatToMantExp_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataSlice(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());
-
-    i = 2;
-    return returnResult(runtime, params, results, hf.floatToMantExp(*x), i);
-}
-
-wasm_trap_t*
-floatFromMantExp_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const mant = getDataInt64(runtime, params, i);
-    if (!mant)
-        return hfResult(results, mant.error());  // LCOV_EXCL_LINE
-
-    auto const exp = getDataInt32(runtime, params, i);
-    if (!exp)
-        return hfResult(results, exp.error());  // LCOV_EXCL_LINE
-
-    i = 4;
-    auto const rounding = getDataInt32(runtime, params, i);
-    if (!rounding)
-        return hfResult(results, rounding.error());  // LCOV_EXCL_LINE
-
-    i = 2;
-    return returnResult(runtime, params, results, hf.floatFromMantExp(*mant, *exp, *rounding), i);
-}
-
-wasm_trap_t*
-floatCompare_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataSlice(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());
-
-    auto const y = getDataSlice(runtime, params, i);
-    if (!y)
-        return hfResult(results, y.error());
-
-    return returnResult(runtime, params, results, hf.floatCompare(*x, *y), i);
-}
-
-wasm_trap_t*
-floatAdd_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataSlice(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());
-
-    auto const y = getDataSlice(runtime, params, i);
-    if (!y)
-        return hfResult(results, y.error());
-
-    i = 6;
-    auto const rounding = getDataInt32(runtime, params, i);
-    if (!rounding)
-        return hfResult(results, rounding.error());  // LCOV_EXCL_LINE
-
-    i = 4;
-    return returnResult(runtime, params, results, hf.floatAdd(*x, *y, *rounding), i);
-}
-
-wasm_trap_t*
-floatSubtract_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataSlice(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());
-
-    auto const y = getDataSlice(runtime, params, i);
-    if (!y)
-        return hfResult(results, y.error());
-
-    i = 6;
-    auto const rounding = getDataInt32(runtime, params, i);
-    if (!rounding)
-        return hfResult(results, rounding.error());  // LCOV_EXCL_LINE
-
-    i = 4;
-    return returnResult(runtime, params, results, hf.floatSubtract(*x, *y, *rounding), i);
-}
-
-wasm_trap_t*
-floatMultiply_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataSlice(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());
-
-    auto const y = getDataSlice(runtime, params, i);
-    if (!y)
-        return hfResult(results, y.error());
-
-    i = 6;
-    auto const rounding = getDataInt32(runtime, params, i);
-    if (!rounding)
-        return hfResult(results, rounding.error());  // LCOV_EXCL_LINE
-
-    i = 4;
-    return returnResult(runtime, params, results, hf.floatMultiply(*x, *y, *rounding), i);
-}
-
-wasm_trap_t*
-floatDivide_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataSlice(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());
-
-    auto const y = getDataSlice(runtime, params, i);
-    if (!y)
-        return hfResult(results, y.error());
-
-    i = 6;
-    auto const rounding = getDataInt32(runtime, params, i);
-    if (!rounding)
-        return hfResult(results, rounding.error());  // LCOV_EXCL_LINE
-
-    i = 4;
-    return returnResult(runtime, params, results, hf.floatDivide(*x, *y, *rounding), i);
-}
-
-wasm_trap_t*
-floatRoot_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataSlice(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());
-
-    auto const n = getDataInt32(runtime, params, i);
-    if (!n)
-        return hfResult(results, n.error());  // LCOV_EXCL_LINE
-
-    i = 5;
-    auto const rounding = getDataInt32(runtime, params, i);
-    if (!rounding)
-        return hfResult(results, rounding.error());  // LCOV_EXCL_LINE
-
-    i = 3;
-    return returnResult(runtime, params, results, hf.floatRoot(*x, *n, *rounding), i);
-}
-
-wasm_trap_t*
-floatPower_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    int32_t i = 0;
-    auto& runtime = hf.getRT();
-
-    auto const x = getDataSlice(runtime, params, i);
-    if (!x)
-        return hfResult(results, x.error());
-
-    auto const n = getDataInt32(runtime, params, i);
-    if (!n)
-        return hfResult(results, n.error());  // LCOV_EXCL_LINE
-
-    i = 5;
-    auto const rounding = getDataInt32(runtime, params, i);
-    if (!rounding)
-        return hfResult(results, rounding.error());  // LCOV_EXCL_LINE
-
-    i = 3;
-    return returnResult(runtime, params, results, hf.floatPower(*x, *n, *rounding), i);
-}
-
-// Contract-specific host function wrappers
-
-wasm_trap_t*
-instanceParam_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[3].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const iindex = getDataInt32(rt, params, index);
-    if (!iindex)
-    {
-        return hfResult(results, iindex.error());
-    }
-
-    auto const stTypeId = getDataInt32(rt, params, index);
-    if (!stTypeId)
-    {
-        return hfResult(results, stTypeId.error());
-    }
-
-    return returnResult(rt, params, results, hf.instanceParam(*iindex, *stTypeId), index);
-}
-
-wasm_trap_t*
-functionParam_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[3].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const iindex = getDataInt32(rt, params, index);
-    if (!iindex)
-    {
-        return hfResult(results, iindex.error());
-    }
-
-    auto const stTypeId = getDataInt32(rt, params, index);
-    if (!stTypeId)
-    {
-        return hfResult(results, stTypeId.error());
-    }
-
-    return returnResult(rt, params, results, hf.functionParam(*iindex, *stTypeId), index);
-}
-
-wasm_trap_t*
-getDataObjectField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[1].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const acc = getDataAccountID(rt, params, index);
-    if (!acc)
-    {
-        return hfResult(results, acc.error());
-    }
-
-    if (params->data[3].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const key = getDataString(rt, params, index);
-    if (!key)
-    {
-        return hfResult(results, key.error());
-    }
-
-    if (params->data[5].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    return returnResult(rt, params, results, hf.getDataObjectField(*acc, *key), index);
-}
-
-wasm_trap_t*
-getDataNestedObjectField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[1].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const acc = getDataAccountID(rt, params, index);
-    if (!acc)
-    {
-        return hfResult(results, acc.error());
-    }
-
-    if (params->data[3].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const key = getDataString(rt, params, index);
-    if (!key)
-    {
-        return hfResult(results, key.error());
-    }
-
-    if (params->data[5].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const nested = getDataString(rt, params, index);
-    if (!nested)
-    {
-        return hfResult(results, nested.error());
-    }
-
-    if (params->data[7].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    return returnResult(
-        rt, params, results, hf.getDataNestedObjectField(*acc, *key, *nested), index);
-}
-
-wasm_trap_t*
-getDataArrayElementField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[1].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const acc = getDataAccountID(rt, params, index);
-    if (!acc)
-    {
-        return hfResult(results, acc.error());
-    }
-
-    if (params->data[3].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const key = getDataString(rt, params, index);
-    if (!key)
-    {
-        return hfResult(results, key.error());
-    }
-
-    auto const elemIndex = getDataInt32(rt, params, index);
-    if (!elemIndex)
-    {
-        return hfResult(results, elemIndex.error());
-    }
-
-    if (params->data[6].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    return returnResult(
-        rt, params, results, hf.getDataArrayElementField(*acc, *elemIndex, *key), index);
-}
-
-wasm_trap_t*
-getDataNestedArrayElementField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[1].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const acc = getDataAccountID(rt, params, index);
-    if (!acc)
-    {
-        return hfResult(results, acc.error());
-    }
-
-    if (params->data[3].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const key = getDataString(rt, params, index);
-    if (!key)
-    {
-        return hfResult(results, key.error());
-    }
-
-    auto const elemIndex = getDataInt32(rt, params, index);
-    if (!elemIndex)
-    {
-        return hfResult(results, elemIndex.error());
-    }
-
-    if (params->data[6].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const nested = getDataString(rt, params, index);
-    if (!nested)
-    {
-        return hfResult(results, nested.error());
-    }
-
-    if (params->data[8].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    return returnResult(
-        rt,
-        params,
-        results,
-        hf.getDataNestedArrayElementField(*acc, *key, *elemIndex, *nested),
-        index);
-}
-
-wasm_trap_t*
-setDataObjectField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[1].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const acc = getDataAccountID(rt, params, index);
-    if (!acc)
-    {
-        return hfResult(results, acc.error());
-    }
-
-    if (params->data[3].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const key = getDataString(rt, params, index);
-    if (!key)
-    {
-        return hfResult(results, key.error());
-    }
-
-    if (params->data[5].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const data = getDataSlice(rt, params, index);
-    if (!data)
-    {
-        return hfResult(results, data.error());
-    }
-
-    SerialIter valueSit(data->data(), data->size());
-    STJson::Value const value = STJson::makeValueFromVLWithType(valueSit);
-    return returnResult(rt, params, results, hf.setDataObjectField(*acc, *key, value), index);
-}
-
-wasm_trap_t*
-setDataNestedObjectField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[1].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const acc = getDataAccountID(rt, params, index);
-    if (!acc)
-    {
-        return hfResult(results, acc.error());
-    }
-
-    if (params->data[3].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const nested = getDataString(rt, params, index);
-    if (!nested)
-    {
-        return hfResult(results, nested.error());
-    }
-
-    if (params->data[5].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const key = getDataString(rt, params, index);
-    if (!key)
-    {
-        return hfResult(results, key.error());
-    }
-
-    if (params->data[7].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const data = getDataSlice(rt, params, index);
-    if (!data)
-    {
-        return hfResult(results, data.error());
-    }
-
-    SerialIter valueSit(data->data(), data->size());
-    STJson::Value const value = STJson::makeValueFromVLWithType(valueSit);
-    return returnResult(
-        rt, params, results, hf.setDataNestedObjectField(*acc, *nested, *key, value), index);
-}
-
-wasm_trap_t*
-setDataArrayElementField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[1].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const acc = getDataAccountID(rt, params, index);
-    if (!acc)
-    {
-        return hfResult(results, acc.error());
-    }
-
-    if (params->data[3].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const key = getDataString(rt, params, index);
-    if (!key)
-    {
-        return hfResult(results, key.error());
-    }
-
-    auto const elemIndex = getDataInt32(rt, params, index);
-    if (!elemIndex)
-    {
-        return hfResult(results, elemIndex.error());
-    }
-
-    if (params->data[6].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const data = getDataSlice(rt, params, index);
-    if (!data)
-    {
-        return hfResult(results, data.error());
-    }
-
-    SerialIter valueSit(data->data(), data->size());
-    STJson::Value const value = STJson::makeValueFromVLWithType(valueSit);
-    return returnResult(
-        rt, params, results, hf.setDataArrayElementField(*acc, *elemIndex, *key, value), index);
-}
-
-wasm_trap_t*
-setDataNestedArrayElementField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[1].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const acc = getDataAccountID(rt, params, index);
-    if (!acc)
-    {
-        return hfResult(results, acc.error());
-    }
-
-    if (params->data[3].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const key = getDataString(rt, params, index);
-    if (!key)
-    {
-        return hfResult(results, key.error());
-    }
-
-    auto const elemIndex = getDataInt32(rt, params, index);
-    if (!elemIndex)
-    {
-        return hfResult(results, elemIndex.error());
-    }
-
-    if (params->data[6].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const nested = getDataString(rt, params, index);
-    if (!nested)
-    {
-        return hfResult(results, nested.error());
-    }
-
-    if (params->data[8].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const data = getDataSlice(rt, params, index);
-    if (!data)
-    {
-        return hfResult(results, data.error());
-    }
-
-    SerialIter valueSit(data->data(), data->size());
-    STJson::Value const value = STJson::makeValueFromVLWithType(valueSit);
-    return returnResult(
-        rt,
-        params,
-        results,
-        hf.setDataNestedArrayElementField(*acc, *key, *elemIndex, *nested, value),
-        index);
-}
-
-wasm_trap_t*
-buildTxn_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-
-    auto const txnType = getDataInt32(rt, params, index);
-    if (!txnType)
-    {
-        return hfResult(results, txnType.error());
-    }
-
-    return returnResult(rt, params, results, hf.buildTxn(*txnType), index);
-}
-
-wasm_trap_t*
-addTxnField_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[3].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const txnIndex = getDataInt32(rt, params, index);
-    if (!txnIndex)
-    {
-        return hfResult(results, txnIndex.error());
-    }
-
-    auto const fname = getDataSField(rt, params, index);
-    if (!fname)
-    {
-        return hfResult(results, fname.error());
-    }
-
-    auto const data = getDataSlice(rt, params, index);
-    if (!data)
-    {
-        return hfResult(results, data.error());
-    }
-
-    return returnResult(rt, params, results, hf.addTxnField(*txnIndex, *fname, *data), index);
-}
-
-wasm_trap_t*
-emitBuiltTxn_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-
-    auto const txnIndex = getDataInt32(rt, params, index);
-    if (!txnIndex)
-    {
-        return hfResult(results, txnIndex.error());
-    }
-
-    return returnResult(rt, params, results, hf.emitBuiltTxn(*txnIndex), index);
-}
-
-wasm_trap_t*
-emitTxn_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[1].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const slice = getDataSlice(rt, params, index);
-    if (!slice)
-    {
-        return hfResult(results, slice.error());
-    }
-
-    std::shared_ptr stpTrans;
-    try
-    {
-        stpTrans = std::make_shared(SerialIter{*slice});
-    }
-    catch (std::exception&)
-    {
-        // The contract supplied bytes that do not decode as a transaction.
-        return hfResult(results, HostFunctionError::InvalidParams);
-    }
-
-    return returnResult(rt, params, results, hf.emitTxn(stpTrans), index);
-}
-
-wasm_trap_t*
-emitEvent_wrap(WASM_SECONDARY_CB_PARAMS_LIST)
-{
-    auto& rt = hf.getRT();
-    int32_t index = 0;
-    if (params->data[1].of.i32 > kMaxWasmDataLength)
-    {
-        return hfResult(results, HostFunctionError::DataFieldTooLarge);
-    }
-
-    auto const name = getDataString(rt, params, index);
-    if (!name)
-    {
-        return hfResult(results, name.error());
-    }
-
-    auto const data = getDataSlice(rt, params, index);
-    if (!data)
-    {
-        return hfResult(results, data.error());
-    }
-
-    std::shared_ptr parsed;
-    try
-    {
-        parsed = STJson::fromBlob(data->data(), data->size());
-    }
-    catch (std::exception const&)
-    {
-        return hfResult(results, HostFunctionError::InvalidParams);
-    }
-
-    if (!parsed)
-        return hfResult(results, HostFunctionError::InvalidParams);
-
-    return returnResult(rt, params, results, hf.emitEvent(*name, *parsed), index);
-}
-
-// LCOV_EXCL_START
-namespace test {
-
-class MockWasmRuntimeWrapper : public WasmRuntimeWrapper
-{
-    Wmem mem_;
-
-    std::int64_t gas_ = 1'000'000;
-    std::int64_t transferLimit_ = kWasmTransferLimit;
-
-public:
-    MockWasmRuntimeWrapper(Wmem memory) : mem_(memory)
-    {
-    }
-
-    // Mock methods to simulate the behavior of WasmRuntimeWrapper
-    [[nodiscard]] Wmem
-    getMem() override
-    {
-        return mem_;
-    }
-
-    std::int64_t
-    getGas() override
-    {
-        return gas_;
-    }
-
-    std::int64_t
-    setGas(std::int64_t gas) override
-    {
-        gas_ = gas;
-        return gas_;
-    }
-
-    std::int64_t
-    getTransferLimit() override
-    {
-        return transferLimit_;
-    }
-
-    std::int64_t
-    setTransferLimit(std::int64_t x) override
-    {
-        transferLimit_ = x;
-        return transferLimit_;
-    }
-};
-
-bool
-testGetDataIncrement()
-{
-    wasm_val_t values[4];
-
-    std::array buffer = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'};
-    MockWasmRuntimeWrapper runtime(Wmem(buffer.data(), buffer.size()));
-
-    {
-        // test int32_t
-        wasm_val_vec_t const params = {.size = 1, .data = &values[0]};
-
-        values[0] = WASM_I32_VAL(42);
-
-        int32_t index = 0;
-        auto const result = getDataInt32(runtime, ¶ms, index);
-        if (!result || result.value() != 42 || index != 1)
-            return false;
-    }
-
-    {
-        // test int64_t
-        wasm_val_vec_t const params = {.size = 1, .data = &values[0]};
-
-        values[0] = WASM_I64_VAL(1234);
-
-        int32_t index = 0;
-        auto const result = getDataInt64(runtime, ¶ms, index);
-        if (!result || result.value() != 1234 || index != 1)
-            return false;
-    }
-
-    {
-        // test SFieldCRef
-        wasm_val_vec_t const params = {.size = 1, .data = &values[0]};
-
-        values[0] = WASM_I32_VAL(sfAccount.getCode());
-
-        int32_t index = 0;
-        auto const result = getDataSField(runtime, ¶ms, index);
-        if (!result || result.value().get() != sfAccount || index != 1)
-            return false;
-    }
-
-    {
-        // test Slice
-        wasm_val_vec_t const params = {.size = 2, .data = &values[0]};
-
-        values[0] = WASM_I32_VAL(0);
-        values[1] = WASM_I32_VAL(3);
-
-        int32_t index = 0;
-        auto const result = getDataSlice(runtime, ¶ms, index);
-        if (!result || result.value() != Slice(buffer.data(), 3) || index != 2)
-            return false;
-    }
-
-    {
-        // test string
-        wasm_val_vec_t const params = {.size = 2, .data = &values[0]};
-
-        values[0] = WASM_I32_VAL(0);
-        values[1] = WASM_I32_VAL(5);
-
-        int32_t index = 0;
-        auto const result = getDataString(runtime, ¶ms, index);
-        if (!result ||
-            result.value() != std::string_view(reinterpret_cast(buffer.data()), 5) ||
-            index != 2)
-            return false;
-    }
-
-    {
-        // test account
-        AccountID const id(
-            calcAccountID(generateKeyPair(KeyType::Secp256k1, generateSeed("alice")).first));
-
-        wasm_val_vec_t const params = {.size = 2, .data = &values[0]};
-
-        values[0] = WASM_I32_VAL(0);
-        values[1] = WASM_I32_VAL(AccountID::size());
-        memcpy(&buffer[0], id.data(), AccountID::size());
-
-        int32_t index = 0;
-        auto const result = getDataAccountID(runtime, ¶ms, index);
-        if (!result || result.value() != id || index != 2)
-            return false;
-    }
-
-    {
-        // test uint256
-
-        Hash h1 = sha512Half(Slice(buffer.data(), 8));
-        wasm_val_vec_t const params = {.size = 2, .data = &values[0]};
-
-        values[0] = WASM_I32_VAL(0);
-        values[1] = WASM_I32_VAL(Hash::size());
-        memcpy(&buffer[0], h1.data(), Hash::size());
-
-        int32_t index = 0;
-        auto const result = getDataUInt256(runtime, ¶ms, index);
-        if (!result || result.value() != h1 || index != 2)
-            return false;
-    }
-
-    {
-        // test Currency
-
-        Currency const c = xrpCurrency();
-        wasm_val_vec_t const params = {.size = 2, .data = &values[0]};
-
-        values[0] = WASM_I32_VAL(0);
-        values[1] = WASM_I32_VAL(Currency::size());
-        memcpy(&buffer[0], c.data(), Currency::size());
-
-        int32_t index = 0;
-        auto const result = getDataCurrency(runtime, ¶ms, index);
-        if (!result || result.value() != c || index != 2)
-            return false;
-    }
-
-    return true;
-}
-
-}  // namespace test
-// LCOV_EXCL_STOP
-
-}  // namespace xrpl
diff --git a/src/libxrpl/tx/wasm/WasmVM.cpp b/src/libxrpl/tx/wasm/WasmVM.cpp
index d0ba3dcf0a..ee3ea84ced 100644
--- a/src/libxrpl/tx/wasm/WasmVM.cpp
+++ b/src/libxrpl/tx/wasm/WasmVM.cpp
@@ -1,237 +1,188 @@
 #include 
 
+#include 
 #include 
+#include 
 #include 
-#include   // IWYU pragma: keep
+#include 
+#include 
 #include 
-#include 
+
+#include 
+#include 
 
 #include 
 #include 
-#include 
-#include 
-#ifdef _DEBUG
-// #define DEBUG_OUTPUT 1
-#endif
-
-#include 
-#include 
-
-#include 
+#include 
+#include 
+#include 
 
 namespace xrpl {
-// WARNING: Per XLS-0102, the host functions registered here form a stable
-// ABI. Their name, semantics, parameters, and return types must NEVER be
-// changed, as there may always be a program that uses it. New host functions
-// may be added and existing gas costs may be adjusted, but every such change
-// must be gated by an amendment.
-// See XLS-0102 §6.5 (Future-Proofing):
-// https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0102-wasm-vm#65-future-proofing
-static void
-setCommonHostFunctions(HostFunctions& hfs, ImportVec& i)
+
+namespace {
+
+using RunStatus = rs::wasm_vm::RunStatus;
+using CheckStatus = rs::wasm_vm::CheckStatus;
+
+// The engine's outcome as the caller's: a value with its gas cost, or a TER with the gas cost
+// to record beside it.
+//
+// A `tecINTERNAL` reports no cost. It says the fault is the node's, and charging a
+// transaction for a node's defect would write that defect into the ledger.
+//
+// Exhaustive over the status enum, with no `default`: the enum is generated from the
+// engine's `RunError`, so an outcome added there fails this switch under -Wswitch -Werror
+// rather than quietly picking up a neighbour's TER. The return past the switch is for the
+// compilers that will not call an exhaustive switch exhaustive; it sits after the switch,
+// not in a `default`, so the coverage check above still holds.
+std::expected
+outcome(rs::wasm_vm::RunResult const& run)
 {
-    // clang-format off
-    WASM_IMPORT_FUNC2(i, getLedgerSqn, "ldgr_index", hfs,                                                      60);
-    WASM_IMPORT_FUNC2(i, getParentLedgerTime, "parent_ldgr_time", hfs,                                         60);
-    WASM_IMPORT_FUNC2(i, getParentLedgerHash, "parent_ldgr_hash", hfs,                                         60);
-    WASM_IMPORT_FUNC2(i, getBaseFee, "base_fee", hfs,                                                          60);
-    WASM_IMPORT_FUNC2(i, isAmendmentEnabled, "amendment_enabled", hfs,                                        100);
+    auto const cost = static_cast(run.gas_used);
 
-    WASM_IMPORT_FUNC2(i, cacheLedgerObj, "cache_le", hfs,                                                   5'000);
-    WASM_IMPORT_FUNC2(i, getTxField, "tx_field", hfs,                                                          70);
-    WASM_IMPORT_FUNC2(i, getCurrentLedgerObjField, "home_le_field", hfs,                                       70);
-    WASM_IMPORT_FUNC2(i, getLedgerObjField, "le_field", hfs,                                                   70);
-    WASM_IMPORT_FUNC2(i, getTxNestedField, "tx_inner", hfs,                                                   110);
-    WASM_IMPORT_FUNC2(i, getCurrentLedgerObjNestedField, "home_le_inner", hfs,                                110);
-    WASM_IMPORT_FUNC2(i, getLedgerObjNestedField, "le_inner", hfs,                                            110);
-    WASM_IMPORT_FUNC2(i, getTxArrayLen, "tx_arr_len", hfs,                                                     40);
-    WASM_IMPORT_FUNC2(i, getCurrentLedgerObjArrayLen, "home_le_arr_len", hfs,                                  40);
-    WASM_IMPORT_FUNC2(i, getLedgerObjArrayLen, "le_arr_len", hfs,                                              40);
-    WASM_IMPORT_FUNC2(i, getTxNestedArrayLen, "tx_inner_arr_len", hfs,                                         70);
-    WASM_IMPORT_FUNC2(i, getCurrentLedgerObjNestedArrayLen, "home_le_inner_arr_len", hfs,                      70);
-    WASM_IMPORT_FUNC2(i, getLedgerObjNestedArrayLen, "le_inner_arr_len", hfs,                                  70);
+    switch (run.status)
+    {
+        case RunStatus::Ok:
+            return EscrowResult{.result = run.result, .cost = cost};
 
-    WASM_IMPORT_FUNC2(i, checkSignature, "check_sig", hfs,                                                  35'000);
-    WASM_IMPORT_FUNC2(i, computeSha512HalfHash, "sha512_half", hfs,                                          2'000);
+        // The cost is the whole limit: XLS-0102 halts the guest the instant the meter runs
+        // out, and the run is charged for all of it.
+        case RunStatus::OutOfGas:
+            return std::unexpected{WasmTER{.ter = tecOUT_OF_GAS, .cost = cost}};
 
-    WASM_IMPORT_FUNC2(i, accountKeylet, "accountroot_id", hfs,                                                350);
-    WASM_IMPORT_FUNC2(i, ammKeylet, "amm_id", hfs,                                                            450);
-    WASM_IMPORT_FUNC2(i, checkKeylet, "check_id", hfs,                                                        350);
-    WASM_IMPORT_FUNC2(i, credentialKeylet, "credential_id", hfs,                                              350);
-    WASM_IMPORT_FUNC2(i, delegateKeylet, "delegate_id", hfs,                                                  350);
-    WASM_IMPORT_FUNC2(i, depositPreauthKeylet, "deposit_preauth_id", hfs,                                     350);
-    WASM_IMPORT_FUNC2(i, didKeylet, "did_id", hfs,                                                            350);
-    WASM_IMPORT_FUNC2(i, escrowKeylet, "escrow_id", hfs,                                                      350);
-    WASM_IMPORT_FUNC2(i, trustLineKeylet, "trustline_id", hfs,                                                     400);
-    WASM_IMPORT_FUNC2(i, mptokenIssuanceKeylet, "mpt_issuance_id", hfs,                                       350);
-    WASM_IMPORT_FUNC2(i, mptokenKeylet, "mptoken_id", hfs,                                                    500);
-    WASM_IMPORT_FUNC2(i, nftokenOfferKeylet, "nft_offer_id", hfs,                                                 350);
-    WASM_IMPORT_FUNC2(i, offerKeylet, "offer_id", hfs,                                                        350);
-    WASM_IMPORT_FUNC2(i, oracleKeylet, "oracle_id", hfs,                                                      350);
-    WASM_IMPORT_FUNC2(i, paychannelKeylet, "paychan_id", hfs,                                                 350);
-    WASM_IMPORT_FUNC2(i, permissionedDomainKeylet, "permissioned_domain_id", hfs,                             350);
-    WASM_IMPORT_FUNC2(i, signerListKeylet, "signers_id", hfs,                                                 350);
-    WASM_IMPORT_FUNC2(i, ticketKeylet, "ticket_id", hfs,                                                      350);
-    WASM_IMPORT_FUNC2(i, vaultKeylet, "vault_id", hfs,                                                        350);
+        // The contract's own fault - it trapped, or it never exported the linear memory
+        // its host calls need - so it is charged for what it burned reaching that point.
+        case RunStatus::Trap:
+        case RunStatus::NoMemory:
+        // A module that will not instantiate is the contract's fault too. Screening
+        // cannot see every way this happens - a linear memory the module keeps to itself
+        // is absent from its exports - so a module can pass preflight and still be
+        // refused here. It is a deterministic property of the code either way, and one
+        // this node's own conduct had no part in.
+        case RunStatus::Instantiate:
+            return std::unexpected{WasmTER{.ter = tecFAILED_PROCESSING, .cost = cost}};
 
-    WASM_IMPORT_FUNC2(i, getNFT, "nft_uri", hfs,                                                            5'000);
-    WASM_IMPORT_FUNC2(i, getNFTIssuer, "nft_issuer", hfs,                                                      70);
-    WASM_IMPORT_FUNC2(i, getNFTTaxon, "nft_taxon", hfs,                                                        60);
-    WASM_IMPORT_FUNC2(i, getNFTFlags, "nft_flags", hfs,                                                        60);
-    WASM_IMPORT_FUNC2(i, getNFTTransferFee, "nft_xfer_fee", hfs,                                               60);
-    WASM_IMPORT_FUNC2(i, getNFTSequence, "nft_serial", hfs,                                                    60);
-
-    WASM_IMPORT_FUNC (i, trace, hfs,                                                                           30);
-
-    WASM_IMPORT_FUNC2(i, floatFromInt, "float_from_int", hfs,                                                 100);
-    WASM_IMPORT_FUNC2(i, floatFromUint, "float_from_uint", hfs,                                               130);
-    WASM_IMPORT_FUNC2(i, floatFromSTAmount, "float_from_stamount", hfs,                                       150);
-    WASM_IMPORT_FUNC2(i, floatFromSTNumber, "float_from_stnumber", hfs,                                       150);
-    WASM_IMPORT_FUNC2(i, floatToInt, "float_to_int", hfs,                                                     130);
-    WASM_IMPORT_FUNC2(i, floatToMantExp, "float_to_mant_exp", hfs,                                            130);
-    WASM_IMPORT_FUNC2(i, floatFromMantExp, "float_from_mant_exp", hfs,                                        100);
-    WASM_IMPORT_FUNC2(i, floatCompare, "float_cmp", hfs,                                                       80);
-    WASM_IMPORT_FUNC2(i, floatAdd, "float_add", hfs,                                                          160);
-    WASM_IMPORT_FUNC2(i, floatSubtract, "float_sub", hfs,                                                     160);
-    WASM_IMPORT_FUNC2(i, floatMultiply, "float_mult", hfs,                                                    300);
-    WASM_IMPORT_FUNC2(i, floatDivide, "float_div", hfs,                                                       300);
-    WASM_IMPORT_FUNC2(i, floatRoot, "float_root", hfs,                                                      5'500);
-    WASM_IMPORT_FUNC2(i, floatPower, "float_pow", hfs,                                                      5'500);
-    // clang-format on
+        // A module that will not compile, or does not expose the entry point, should have
+        // been refused at preflight with `temINVALID_BYTECODE`: screening decides both from the
+        // same bytes and the same engine, so agreeing here is not a matter of degree.
+        // Reaching apply means the screening did not happen, which is a node-side fault
+        // rather than the transaction's.
+        case RunStatus::Compile:
+        case RunStatus::EntryPoint:
+        // The host could not serve a call, or it threw and `HostContext` caught it.
+        case RunStatus::Internal:
+        // The engine panicked: a defect in the engine, reported rather than fatal to the
+        // node.
+        case RunStatus::Panic:
+            return std::unexpected{WasmTER{.ter = tecINTERNAL, .cost = std::nullopt}};
+    }
+    UNREACHABLE("xrpl::outcome : unknown RunStatus");
+    return std::unexpected{WasmTER{.ter = tecINTERNAL, .cost = std::nullopt}};
 }
 
-ImportVec
-createWasmImport(HostFunctions& hfs)
+// A screening verdict as a TER.
+//
+// `temINVALID_BYTECODE` says the transaction carries something this engine cannot run: a
+// malformed transaction, refused before it can reach the ledger. A panic inside the
+// engine is different in kind - nothing was learned about the module - so the answer is
+// node-local rather than a claim about the transaction.
+//
+// Exhaustive over the status enum, with no `default`, for the same reason `outcome` is.
+NotTEC
+verdict(CheckStatus status)
 {
-    ImportVec i;
+    switch (status)
+    {
+        case CheckStatus::Ok:
+            return tesSUCCESS;
 
-    setCommonHostFunctions(hfs, i);
-    WASM_IMPORT_FUNC2(i, updateData, "set_data", hfs, 1000);
+        // The module will not compile, imports what no engine of this ABI serves or
+        // a host function at a signature none of them registers, does not export the
+        // entry point as `() -> i32`, or asks for more linear memory or table than it
+        // may have.
+        case CheckStatus::Compile:
+        case CheckStatus::Import:
+        case CheckStatus::Signature:
+        case CheckStatus::EntryPoint:
+        case CheckStatus::Memory:
+        case CheckStatus::Table:
+            return temINVALID_BYTECODE;
 
-    // clang-format off
-    // Contract-specific host functions
-    WASM_IMPORT_FUNC2(i, instanceParam, "instance_param", hfs,                                   100);
-    WASM_IMPORT_FUNC2(i, functionParam, "function_param", hfs,                                   100);
-
-    WASM_IMPORT_FUNC2(i, getDataObjectField, "get_data_object_field", hfs,                       500);
-    WASM_IMPORT_FUNC2(i, getDataNestedObjectField, "get_data_nested_object_field", hfs,          500);
-    WASM_IMPORT_FUNC2(i, getDataArrayElementField, "get_data_array_element_field", hfs,          500);
-    WASM_IMPORT_FUNC2(i, getDataNestedArrayElementField, "get_data_nested_array_element_field", hfs,  500);
-
-    WASM_IMPORT_FUNC2(i, setDataObjectField, "set_data_object_field", hfs,                       500);
-    WASM_IMPORT_FUNC2(i, setDataNestedObjectField, "set_data_nested_object_field", hfs,          500);
-    WASM_IMPORT_FUNC2(i, setDataArrayElementField, "set_data_array_element_field", hfs,          500);
-    WASM_IMPORT_FUNC2(i, setDataNestedArrayElementField, "set_data_nested_array_element_field", hfs,  500);
-
-    WASM_IMPORT_FUNC2(i, buildTxn, "build_txn", hfs,                                            200);
-    WASM_IMPORT_FUNC2(i, addTxnField, "add_txn_field", hfs,                                     200);
-    WASM_IMPORT_FUNC2(i, emitBuiltTxn, "emit_built_txn", hfs,                                   500);
-    WASM_IMPORT_FUNC2(i, emitTxn, "emit_txn", hfs,                                              500);
-    WASM_IMPORT_FUNC2(i, emitEvent, "emit_event", hfs,                                          500);
-    // clang-format on
-
-    return i;
+        // The engine panicked: a defect in the engine, reported rather than fatal to
+        // the node, and not the transaction's fault.
+        case CheckStatus::Panic:
+            return telFAILED_PROCESSING;
+    }
+    UNREACHABLE("xrpl::verdict : unknown CheckStatus");
+    return telFAILED_PROCESSING;
 }
 
+}  // namespace
+
 std::expected
 runEscrowWasm(
     Bytes const& wasmCode,
     HostFunctions& hfs,
-    int64_t gasLimit,
-    std::string_view funcName,
-    std::vector const& params)
+    std::int64_t gasLimit,
+    std::string_view funcName) noexcept
 {
-    //  create VM and set cost limit
-    auto& vm = WasmEngine::instance();
-    // vm.initMaxPages(MAX_PAGES);
+    XRPL_ASSERT(
+        gasLimit > 0,
+        "::xrpl::runEscrowWasm : gas limit is positive (should be checked in preflight)");
+    // A run needs a budget to spend. Refused here rather than in the engine because what a
+    // non-positive limit means is a transaction-validity rule; the engine's own budget is
+    // therefore an unsigned quantity with no invalid value to represent.
+    if (gasLimit <= 0)
+        return std::unexpected{WasmTER{.ter = temBAD_AMOUNT, .cost = std::nullopt}};
 
-    auto const ret =
-        vm.run(wasmCode, hfs, gasLimit, funcName, params, createWasmImport(hfs), hfs.getJournal());
+    auto const nodeSideFault = std::unexpected{WasmTER{.ter = tecINTERNAL, .cost = std::nullopt}};
 
-    if (!ret)
-    {
-#ifdef DEBUG_OUTPUT
-        std::cout << ", error: " << ret.error().ter << std::endl;
-#endif
-        // Carries the TER (tecOUT_OF_GAS / tecFAILED_PROCESSING / tecINTERNAL /
-        // temBAD_AMOUNT) and, when meaningful, the gas consumed. The caller is
-        // responsible for writing that gas to tx metadata.
-        return std::unexpected(ret.error());
-    }
+    return guarded(hfs.getJournal(), nodeSideFault, [&]() -> std::expected {
+        // The host caches the current ledger object, the slot table and the
+        // contract's data for the length of one run, so a reused one would answer a
+        // later contract out of an earlier contract's state.
+        XRPL_ASSERT(
+            hfs.checkSelf(), "::xrpl::runEscrowWasm : host functions not clean before the run");
+        if (!hfs.checkSelf())
+        {
+            throw std::runtime_error("host functions not clean before the run");
+        }
 
-#ifdef DEBUG_OUTPUT
-    std::cout << ", ret: " << ret->result << ", gas spent: " << ret->cost << std::endl;
-#endif
-    return EscrowResult{.result = ret->result, .cost = ret->cost};
+        HostContext const ctx{hfs};
+        auto const run = rs::wasm_vm::run_escrow(
+            ctx,
+            rust::Slice{wasmCode.data(), wasmCode.size()},
+            static_cast(gasLimit),
+            rust::Str{funcName.data(), funcName.size()});
+
+        auto const result = outcome(run);
+        if (!result)
+        {
+            JLOG(hfs.getJournal().warn())
+                << "wasm: " << std::string_view{run.detail.data(), run.detail.size()}
+                << ", ter: " << transToken(result.error().ter);
+        }
+        return result;
+    });
 }
 
 NotTEC
-preflightEscrowWasm(
-    Bytes const& wasmCode,
-    HostFunctions& hfs,
-    std::string_view funcName,
-    std::vector const& params)
+preflightEscrowWasm(Bytes const& wasmCode, beast::Journal j, std::string_view funcName) noexcept
 {
-    //  create VM and set cost limit
-    auto& vm = WasmEngine::instance();
-    // vm.initMaxPages(MAX_PAGES);
+    return guarded(j, NotTEC{telFAILED_PROCESSING}, [&]() {
+        auto const checked = rs::wasm_vm::check_escrow(
+            rust::Slice{wasmCode.data(), wasmCode.size()},
+            rust::Str{funcName.data(), funcName.size()});
 
-    auto const ret =
-        vm.check(wasmCode, hfs, funcName, params, createWasmImport(hfs), hfs.getJournal());
-
-    return ret;
+        auto const ter = verdict(checked.status);
+        if (!isTesSuccess(ter))
+        {
+            JLOG(j.warn()) << "wasm: "
+                           << std::string_view{checked.detail.data(), checked.detail.size()}
+                           << ", ter: " << transToken(ter);
+        }
+        return ter;
+    });
 }
 
-////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-WasmEngine::WasmEngine() : impl_(std::make_unique())
-{
-}
-
-WasmEngine&
-WasmEngine::instance()
-{
-    static WasmEngine e;
-    return e;
-}
-
-std::expected, WasmTER>
-WasmEngine::run(
-    Bytes const& wasmCode,
-    HostFunctions& hfs,
-    int64_t gasLimit,
-    std::string_view funcName,
-    std::vector const& params,
-    ImportVec const& imports,
-    beast::Journal j)
-{
-    return impl_->run(wasmCode, hfs, gasLimit, funcName, params, imports, j);
-}
-
-NotTEC
-WasmEngine::check(
-    Bytes const& wasmCode,
-    HostFunctions& hfs,
-    std::string_view funcName,
-    std::vector const& params,
-    ImportVec const& imports,
-    beast::Journal j)
-{
-    return impl_->check(wasmCode, hfs, funcName, params, imports, j);
-}
-
-void*
-WasmEngine::newTrap(std::string const& msg)
-{
-    return impl_->newTrap(msg);
-}
-
-// LCOV_EXCL_START
-beast::Journal
-WasmEngine::getJournal() const
-{
-    return impl_->getJournal();
-}
-// LCOV_EXCL_STOP
-
 }  // namespace xrpl
diff --git a/src/libxrpl/tx/wasm/WasmiVM.cpp b/src/libxrpl/tx/wasm/WasmiVM.cpp
deleted file mode 100644
index 7407bedec0..0000000000
--- a/src/libxrpl/tx/wasm/WasmiVM.cpp
+++ /dev/null
@@ -1,958 +0,0 @@
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-#include 
-
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#ifdef _DEBUG
-// #define DEBUG_OUTPUT 1
-#endif
-// #define SHOW_CALL_TIME 1
-
-namespace xrpl {
-
-wasm_trap_t*
-HostFuncMain_wrap(void* env, wasm_val_vec_t const* params, wasm_val_vec_t* results);
-
-namespace {
-
-void
-printWasmError(std::string_view msg, wasm_trap_t* trap, beast::Journal jlog)
-{
-#ifdef DEBUG_OUTPUT
-    auto& j = std::cerr;
-#else
-    auto j = jlog.warn();
-    if (jlog.active(beast::Severity::Warning))
-#endif
-    {
-        wasm_byte_vec_t errorMessage WASM_EMPTY_VEC;
-
-        if (trap != nullptr)
-            wasm_trap_message(trap, &errorMessage);
-
-        if (errorMessage.size != 0u)
-        {
-            j << "WASMI Error: " << msg << ", "
-              << std::string_view(errorMessage.data, errorMessage.size - 1);
-        }
-        else
-        {
-            j << "WASMI Error: " << msg;
-        }
-
-        if (errorMessage.size != 0u)
-            wasm_byte_vec_delete(&errorMessage);
-    }
-
-    if (trap != nullptr)
-        wasm_trap_delete(trap);
-
-#ifdef DEBUG_OUTPUT
-    j << std::endl;
-#endif
-}
-// LCOV_EXCL_STOP
-
-// Extract a trap's message into a std::string (the only signal the C API gives
-// for classification; see the trap-signal constants in WasmCommon.h). Does not
-// take ownership of `trap`.
-std::string
-trapMessage(wasm_trap_t* trap)
-{
-    if (trap == nullptr)
-        return {};  // LCOV_EXCL_LINE
-    wasm_byte_vec_t msg WASM_EMPTY_VEC;
-    wasm_trap_message(trap, &msg);
-    std::string out;
-    if (msg.size != 0u)
-    {
-        // wasm_trap_message NUL-terminates, so drop the trailing NUL.
-        out.assign(msg.data, msg.size - 1);
-        wasm_byte_vec_delete(&msg);
-    }
-    return out;
-}
-
-}  // namespace
-
-class WasmiRuntimeWrapper : public WasmRuntimeWrapper
-{
-    InstanceWrapper& iw_;
-
-public:
-    WasmiRuntimeWrapper(InstanceWrapper& iw) : iw_(iw)
-    {
-    }
-
-    Wmem
-    getMem() override
-    {
-        return iw_.getMem();
-    }
-
-    std::int64_t
-    getGas() override
-    {
-        return iw_.getGas();
-    }
-
-    std::int64_t
-    setGas(std::int64_t gas) override
-    {
-        return iw_.setGas(gas);
-    }
-
-    std::int64_t
-    getTransferLimit() override
-    {
-        return iw_.getTransferLimit();
-    }
-
-    std::int64_t
-    setTransferLimit(std::int64_t x) override
-    {
-        return iw_.setTransferLimit(x);
-    }
-};
-
-InstancePtr
-InstanceWrapper::init(
-    StorePtr& s,
-    ModulePtr& m,
-    WasmExternVec& expt,
-    WasmExternVec const& imports,
-    beast::Journal j)
-{
-    wasm_trap_t* trap = nullptr;
-    InstancePtr mi = InstancePtr(
-        wasm_instance_new(s.get(), m.get(), imports.get(), &trap), &wasm_instance_delete);
-
-    if (!mi || (trap != nullptr))
-    {
-        printWasmError("can't create instance", trap, j);
-        Throw("can't create instance");
-    }
-    wasm_instance_exports(mi.get(), expt.get());
-    return mi;
-}
-
-InstanceWrapper&
-InstanceWrapper::operator=(InstanceWrapper&& o)
-{
-    if (this == &o)
-        return *this;  // LCOV_EXCL_LINE
-
-    store_ = o.store_;
-    o.store_ = nullptr;
-    exports_ = std::move(o.exports_);
-    memIdx_ = o.memIdx_;
-    o.memIdx_ = -1;
-    instance_ = std::move(o.instance_);
-
-    j_ = o.j_;
-
-    return *this;
-}
-
-FuncInfo
-InstanceWrapper::getFunc(std::string_view funcName, WasmExporttypeVec const& exportTypes) const
-{
-    wasm_func_t const* f = nullptr;
-    wasm_functype_t const* ft = nullptr;
-
-    if (!instance_)
-        Throw("no instance");  // LCOV_EXCL_LINE
-
-    if (exportTypes.empty())
-        Throw("no export");  // LCOV_EXCL_LINE
-    if (exportTypes.size() != exports_.size())
-        Throw("invalid export");  // LCOV_EXCL_LINE
-
-    for (unsigned i = 0; i < exportTypes.size(); ++i)
-    {
-        auto const* expType(exportTypes[i]);
-
-        wasm_name_t const* name = wasm_exporttype_name(expType);
-        wasm_externtype_t const* exnType = wasm_exporttype_type(expType);
-        if (wasm_externtype_kind(exnType) == WASM_EXTERN_FUNC)
-        {
-            if (funcName != std::string_view(name->data, name->size))
-                continue;
-
-            auto const* exn(exports_[i]);
-            if (wasm_extern_kind(exn) != WASM_EXTERN_FUNC)
-                Throw("invalid export");  // LCOV_EXCL_LINE
-
-            ft = wasm_externtype_as_functype_const(exnType);
-            f = wasm_extern_as_func_const(exn);
-            break;
-        }
-    }
-
-    if ((f == nullptr) || (ft == nullptr))
-        Throw("can't find function <" + std::string(funcName) + ">");
-
-    return {f, ft};
-}
-
-Wmem
-InstanceWrapper::getMem() const
-{
-    if (memIdx_ >= 0)
-    {
-        auto* e(exports_[memIdx_]);
-        wasm_memory_t* mem = wasm_extern_as_memory(e);
-        return Wmem(wasm_memory_data(mem), wasm_memory_data_size(mem));
-    }
-
-    wasm_memory_t* mem = nullptr;
-    for (int i = 0; i < exports_.size(); ++i)
-    {
-        auto* e(exports_[i]);
-        if (wasm_extern_kind(e) == WASM_EXTERN_MEMORY)
-        {
-            memIdx_ = i;
-            mem = wasm_extern_as_memory(e);
-            break;
-        }
-    }
-
-    if (mem == nullptr)
-        return {};  // LCOV_EXCL_LINE
-
-    return Wmem(wasm_memory_data(mem), wasm_memory_data_size(mem));
-}
-
-std::int64_t
-InstanceWrapper::getGas() const
-{
-    if (store_ == nullptr)
-        return -1;  // LCOV_EXCL_LINE
-    std::uint64_t gas = 0;
-    wasm_store_get_fuel(store_, &gas);
-    return static_cast(gas);
-}
-
-std::int64_t
-InstanceWrapper::setGas(std::int64_t gas) const
-{
-    if (store_ == nullptr)
-        return -1;  // LCOV_EXCL_LINE
-
-    if (gas < 0)
-        gas = std::numeric_limits::max();
-    wasmi_error_t* err = wasm_store_set_fuel(store_, static_cast(gas));
-    if (err != nullptr)
-    {
-        // LCOV_EXCL_START
-        printWasmError("Can't set instance gas", nullptr, j_);
-        wasmi_error_delete(err);
-        return -1;
-        // LCOV_EXCL_STOP
-    }
-
-    return gas;
-}
-
-std::int64_t
-InstanceWrapper::getTransferLimit() const
-{
-    if (store_ == nullptr)
-        return -1;  // LCOV_EXCL_LINE
-
-    return transferLimit_;
-}
-
-std::int64_t
-InstanceWrapper::setTransferLimit(std::int64_t x)
-{
-    if (store_ == nullptr)
-        return -1;  // LCOV_EXCL_LINE
-    if (x < 0)
-    {
-        transferLimit_ = std::numeric_limits::max();
-    }
-    else
-    {
-        transferLimit_ = x;
-    }
-
-    return transferLimit_;
-}
-
-//////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-ModulePtr
-ModuleWrapper::init(StorePtr& s, Bytes const& wasmBin, beast::Journal j)
-{
-    wasm_byte_vec_t const code{
-        .size = wasmBin.size(),
-        // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
-        .data = const_cast(reinterpret_cast(wasmBin.data()))};
-    ModulePtr m = ModulePtr(wasm_module_new(s.get(), &code), &wasm_module_delete);
-    if (!m)
-        throw std::runtime_error("can't create module");
-
-    return m;
-}
-
-ModuleWrapper::ModuleWrapper(
-    StorePtr& s,
-    Bytes const& wasmBin,
-    bool instantiate,
-    ImportVec const& imports,
-    beast::Journal j)
-    : module_(init(s, wasmBin, j)), j_(j)
-{
-    wasm_module_exports(module_.get(), exportTypes_.get());
-    auto wimports = buildImports(s, imports);
-    if (instantiate)
-    {
-        addInstance(s, wimports);
-    }
-}
-
-// LCOV_EXCL_START
-ModuleWrapper&
-ModuleWrapper::operator=(ModuleWrapper&& o)
-{
-    if (this == &o)
-        return *this;
-
-    module_ = std::move(o.module_);
-    instanceWrap_ = std::move(o.instanceWrap_);
-    exportTypes_ = std::move(o.exportTypes_);
-    j_ = o.j_;
-
-    return *this;
-}
-
-// LCOV_EXCL_STOP
-
-static WasmValtypeVec
-makeImpParams(WasmImportFunc const& imp)
-{
-    auto const paramSize = imp.params.size();
-    if (paramSize == 0u)
-        return {};
-
-    WasmValtypeVec v(paramSize);
-
-    for (unsigned i = 0; i < paramSize; ++i)
-    {
-        auto const vt = imp.params[i];
-        switch (vt)
-        {
-            case WasmTypes::WtI32:
-                v[i] = wasm_valtype_new_i32();
-                break;
-            case WasmTypes::WtI64:
-                v[i] = wasm_valtype_new_i64();
-                break;
-                // LCOV_EXCL_START
-            default:
-                throw std::runtime_error("invalid import type");
-                // LCOV_EXCL_STOP
-        }
-    }
-    return v;
-}
-
-static WasmValtypeVec
-makeImpReturn(WasmImportFunc const& imp)
-{
-    if (!imp.result)
-        return {};  // LCOV_EXCL_LINE
-
-    WasmValtypeVec v(1);
-    switch (*imp.result)
-    {
-        case WasmTypes::WtI32:
-            v[0] = wasm_valtype_new_i32();
-            break;
-            // LCOV_EXCL_START
-        case WasmTypes::WtI64:
-            v[0] = wasm_valtype_new_i64();
-            break;
-        default:
-            throw std::runtime_error("invalid return type");
-            // LCOV_EXCL_STOP
-    }
-    return v;
-}
-
-WasmExternVec
-ModuleWrapper::buildImports(StorePtr& s, ImportVec const& imports) const
-{
-    WasmImporttypeVec importTypes;
-    wasm_module_imports(module_.get(), importTypes.get());
-
-    if (importTypes.empty())
-        return {};
-    if (imports.empty())
-        Throw("Empty imports");
-
-    WasmExternVec wimports(importTypes.size());
-
-    unsigned impCnt = 0;
-    for (unsigned i = 0; i < importTypes.size(); ++i)
-    {
-        wasm_importtype_t const* importType = importTypes[i];
-
-        // wasm_name_t const* mn = wasm_importtype_module(importtype);
-        // auto modName = std::string_view(mn->data, mn->num_elems);
-        wasm_name_t const* fn = wasm_importtype_name(importType);
-        auto fieldName = std::string_view(fn->data, fn->size);
-
-        wasm_externkind_t const itype = wasm_externtype_kind(wasm_importtype_type(importType));
-        if (itype != WASM_EXTERN_FUNC)
-        {
-            Throw(
-                "Invalid import type " + std::to_string(itype));  // LCOV_EXCL_LINE
-        }
-
-        // for multi-module support
-        // if ((W_ENV != modName) && (W_HOST_LIB != modName))
-        //     continue;
-
-        auto const it = imports.find(fieldName);
-        if (it == imports.end())
-        {
-            printWasmError("Import not found: " + std::string(fieldName), nullptr, j_);
-            continue;  // print all missed import
-        }
-
-        WasmUserData const& obj = it->second;
-        WasmImportFunc const& imp = obj.second;
-
-        WasmValtypeVec params(makeImpParams(imp));
-        WasmValtypeVec results(makeImpReturn(imp));
-
-        std::unique_ptr const ftype(
-            wasm_functype_new(params.get(), results.get()), &wasm_functype_delete);
-
-        params.release();
-        results.release();
-
-        wasm_func_t* func =
-            wasm_func_new_with_env(s.get(), ftype.get(), HostFuncMain_wrap, (void*)&obj, nullptr);
-        if (func == nullptr)
-        {
-            Throw(
-                "can't create import function " + std::string(imp.name));  // LCOV_EXCL_LINE
-        }
-
-        wimports[i] = wasm_func_as_extern(func);
-        ++impCnt;
-    }
-
-    if (impCnt != importTypes.size())
-    {
-        printWasmError(
-            std::string("Imports not finished: ") + std::to_string(impCnt) + "/" +
-                std::to_string(importTypes.size()),
-            nullptr,
-            j_);
-        Throw("Missing imports");
-    }
-
-    return wimports;
-}
-
-wasm_functype_t const*
-ModuleWrapper::getFuncType(std::string_view funcName) const
-{
-    for (size_t i = 0; i < exportTypes_.size(); i++)
-    {
-        auto const* expType(exportTypes_[i]);
-        wasm_name_t const* name = wasm_exporttype_name(expType);
-        wasm_externtype_t const* exnType = wasm_exporttype_type(expType);
-        if (wasm_externtype_kind(exnType) == WASM_EXTERN_FUNC &&
-            funcName == std::string_view(name->data, name->size))
-        {
-            return wasm_externtype_as_functype_const(exnType);
-        }
-    }
-
-    throw std::runtime_error("can't find function <" + std::string(funcName) + ">");
-}
-
-// int
-// my_module_t::delInstance(int i)
-// {
-//     if (i >= mod_inst.size())
-//         return -1;
-//     if (!mod_inst[i])
-//         mod_inst[i] = my_mod_inst_t();
-//     return i;
-// }
-
-//////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
-// void
-// WasmiEngine::clearModules()
-// {
-//     modules.clear();
-//     store.reset();  // to free the memory before creating new store
-//     store = {wasm_store_new(engine.get()), &wasm_store_delete};
-// }
-
-std::unique_ptr
-WasmiEngine::init()
-{
-    wasm_config_t* config = wasm_config_new();
-    if (config == nullptr)
-    {
-        return std::unique_ptr{
-            nullptr, &wasm_engine_delete};  // LCOV_EXCL_LINE
-    }
-    wasmi_config_consume_fuel_set(config, true);
-    wasmi_config_ignore_custom_sections_set(config, true);
-    wasmi_config_wasm_mutable_globals_set(config, false);
-    wasmi_config_wasm_multi_value_set(config, false);
-    wasmi_config_wasm_sign_extension_set(config, false);
-    wasmi_config_wasm_saturating_float_to_int_set(config, false);
-    wasmi_config_wasm_bulk_memory_set(config, false);
-    wasmi_config_wasm_reference_types_set(config, false);
-    wasmi_config_wasm_tail_call_set(config, false);
-    wasmi_config_wasm_extended_const_set(config, false);
-    wasmi_config_floats_set(config, false);
-    wasmi_config_wasm_multi_memory_set(config, false);
-    wasmi_config_wasm_custom_page_sizes_set(config, false);
-    wasmi_config_wasm_memory64_set(config, false);
-    wasmi_config_wasm_wide_arithmetic_set(config, false);
-
-    return std::unique_ptr(
-        wasm_engine_new_with_config(config), &wasm_engine_delete);
-}
-
-int
-WasmiEngine::addModule(
-    Bytes const& wasmCode,
-    bool instantiate,
-    ImportVec const& imports,
-    int64_t gas)
-{
-    moduleWrap_.reset();
-    store_.reset();  // to free the memory before creating new store
-    store_ = {wasm_store_new_with_memory_max_pages(engine_.get(), maxPages), &wasm_store_delete};
-
-    if (gas < 0)
-        gas = std::numeric_limits::max();
-    wasmi_error_t* err = wasm_store_set_fuel(store_.get(), static_cast(gas));
-    if (err != nullptr)
-    {
-        // LCOV_EXCL_START
-        printWasmError("Error setting gas", nullptr, j_);
-        wasmi_error_delete(err);
-        throw std::runtime_error("can't set gas");
-        // LCOV_EXCL_STOP
-    }
-
-    moduleWrap_ = std::make_unique(store_, wasmCode, instantiate, imports, j_);
-
-    if (!moduleWrap_)
-        throw std::runtime_error("can't create module wrapper");  // LCOV_EXCL_LINE
-
-    return moduleWrap_ ? 0 : -1;
-}
-
-// int
-// WasmiEngine::addInstance()
-// {
-//     return module->addInstance(store.get());
-// }
-
-std::vector
-WasmiEngine::convertParams(std::vector const& params)
-{
-    std::vector v;
-    v.reserve(params.size());
-    for (auto const& p : params)
-    {
-        switch (p.type)
-        {
-            case WasmTypes::WtI32:
-                v.push_back(WASM_I32_VAL(p.of.i32));
-                break;
-            // LCOV_EXCL_START
-            case WasmTypes::WtI64:
-                v.push_back(WASM_I64_VAL(p.of.i64));
-                break;
-            default:
-                throw std::runtime_error(
-                    "unknown parameter type: " + std::to_string(static_cast(p.type)));
-                break;
-                // LCOV_EXCL_STOP
-        }
-    }
-
-    return v;
-}
-
-int
-WasmiEngine::compareParamTypes(wasm_valtype_vec_t const* ftp, std::vector const& p)
-{
-    if (ftp->size != p.size())
-        return std::min(ftp->size, p.size());
-
-    for (unsigned i = 0; i < ftp->size; ++i)
-    {
-        auto const t1 = wasm_valtype_kind(ftp->data[i]);
-        auto const t2 = p[i].kind;
-        if (t1 != t2)
-            return i;
-    }
-
-    return -1;
-}
-
-// LCOV_EXCL_START
-void
-WasmiEngine::addParam(std::vector& in, int32_t p)
-{
-    in.emplace_back();
-    auto& el(in.back());
-    memset(&el, 0, sizeof(el));
-    el = WASM_I32_VAL(p);  // WASM_I32;
-}
-
-// LCOV_EXCL_STOP
-
-void
-WasmiEngine::addParam(std::vector& in, int64_t p)
-{
-    in.emplace_back();
-    auto& el(in.back());
-    el = WASM_I64_VAL(p);
-}
-
-template 
-WasmiResult
-WasmiEngine::call(std::string_view func, Types&&... args)
-{
-    // Lookup our export function
-    auto f = getFunc(func);
-    return call(f, std::forward(args)...);
-}
-
-template 
-WasmiResult
-WasmiEngine::call(FuncInfo const& f, Types&&... args)
-{
-    std::vector in;
-    return call(f, in, std::forward(args)...);
-}
-
-#ifdef SHOW_CALL_TIME
-static inline uint64_t
-usecs()
-{
-    uint64_t x = std::chrono::duration_cast(
-                     std::chrono::high_resolution_clock::now().time_since_epoch())
-                     .count();
-    return x;
-}
-#endif
-
-template 
-WasmiResult
-WasmiEngine::call(FuncInfo const& f, std::vector& in)
-{
-    WasmiResult ret(NR);
-    wasm_val_vec_t const inv = in.empty() ? wasm_val_vec_t WASM_EMPTY_VEC
-                                          : wasm_val_vec_t{.size = in.size(), .data = in.data()};
-
-#ifdef SHOW_CALL_TIME
-    auto const start = usecs();
-#endif
-
-    wasm_trap_t* trap = wasm_func_call(f.first, &inv, ret.r.get());
-
-#ifdef SHOW_CALL_TIME
-    auto const finish = usecs();
-    auto const delta_ms = (finish - start) / 1000;
-    std::cout << "wasm_func_call: " << delta_ms << "ms" << std::endl;
-#endif
-
-    if (trap)
-    {
-        // Classify the trap into a TER by matching tokens as substrings of the
-        // message (see the trap-signal constants in WasmCommon.h for why).
-        std::string const msg = trapMessage(trap);
-        auto const has = [&msg](std::string_view token) { return msg.contains(token); };
-        if (has(hfErrInternal))
-        {
-            ret.ter = tecINTERNAL;
-        }
-        else if (has(hfErrOutOfGas) || has(wasmiTrapOutOfFuel))
-        {
-            ret.ter = tecOUT_OF_GAS;
-        }
-        else
-        {
-            ret.ter = tecFAILED_PROCESSING;
-        }
-        printWasmError("failure to call func", trap, j_);
-    }
-
-    return ret;
-}
-
-template 
-WasmiResult
-WasmiEngine::call(FuncInfo const& f, std::vector& in, std::int32_t p, Types&&... args)
-{
-    addParam(in, p);
-    return call(f, in, std::forward(args)...);
-}
-
-template 
-WasmiResult
-WasmiEngine::call(FuncInfo const& f, std::vector& in, std::int64_t p, Types&&... args)
-{
-    addParam(in, p);
-    return call(f, in, std::forward(args)...);
-}
-
-template 
-WasmiResult
-WasmiEngine::call(FuncInfo const& f, std::vector& in, Bytes const& p, Types&&... args)
-{
-    return call(f, in, p.data(), p.size(), std::forward(args)...);
-}
-
-static inline void
-checkImports(ImportVec const& imports, HostFunctions* hfs)
-{
-    for (auto const& obj : imports)
-    {
-        if (hfs != &obj.second.first.get())
-            Throw("Imports hf unsync");
-    }
-}
-
-std::expected, WasmTER>
-WasmiEngine::run(
-    Bytes const& wasmCode,
-    HostFunctions& hfs,
-    int64_t gas,
-    std::string_view funcName,
-    std::vector const& params,
-    ImportVec const& imports,
-    beast::Journal j)
-{
-    if (gas <= 0)
-        return std::unexpected(WasmTER{.ter = temBAD_AMOUNT, .cost = std::nullopt});
-
-    try
-    {
-        checkImports(imports, &hfs);
-        return runHlp(wasmCode, hfs, gas, funcName, params, imports, j);
-    }
-    catch (std::exception const& e)
-    {
-        printWasmError(std::string("exception: ") + e.what(), nullptr, j);
-    }
-    // LCOV_EXCL_START
-    catch (...)
-    {
-        printWasmError(std::string("exception: unknown"), nullptr, j);
-    }
-    // LCOV_EXCL_STOP
-    // An exception escaping the engine is an xrpld-side fault -> tecINTERNAL,
-    // no gas. Genuine wasm faults don't throw; they surface as traps in runHlp.
-    return std::unexpected(WasmTER{.ter = tecINTERNAL, .cost = std::nullopt});
-}
-
-std::expected, WasmTER>
-WasmiEngine::runHlp(
-    Bytes const& wasmCode,
-    HostFunctions& hfs,
-    int64_t gas,
-    std::string_view funcName,
-    std::vector const& params,
-    ImportVec const& imports,
-    beast::Journal j)
-{
-    // currently only 1 module support, possible parallel UT run
-    std::scoped_lock const lg(m_);
-    j_ = j;
-
-    if (wasmCode.empty())
-        throw std::runtime_error("empty module");
-    if (!hfs.checkSelf())
-        throw std::runtime_error("hfs isn't clean");
-
-    // Create and instantiate the module.
-    [[maybe_unused]] int const m = addModule(wasmCode, true, imports, gas);
-
-    if (!moduleWrap_ || !moduleWrap_->getInstance())
-        throw std::runtime_error("no instance");  // LCOV_EXCL_LINE
-
-    auto clearRT = [](HostFunctions* p) { p->resetRT(); };
-    std::unique_ptr const clearGuard(&hfs, clearRT);
-    WasmiRuntimeWrapper iw(getRT());
-    hfs.setRT(iw);
-
-    // Call main
-    auto const f = getFunc(!funcName.empty() ? funcName : "_start");
-    auto const* ftp = wasm_functype_params(f.second);
-
-    // not const because passed directly to VM function (which accept non
-    // const)
-    auto p = convertParams(params);
-
-    if (int const comp = compareParamTypes(ftp, p); comp >= 0)
-        throw std::runtime_error("invalid parameter type #" + std::to_string(comp));
-
-    auto const res = call<1>(f, p);
-
-    if (gas == -1)
-        gas = std::numeric_limits::max();
-
-    if (res.ter.has_value())
-    {
-        // call() already classified the trap (see WasmiEngine::call).
-        // tecINTERNAL is an xrpld-side bug: report no gas.
-        if (*res.ter == tecINTERNAL)
-            return std::unexpected(WasmTER{.ter = tecINTERNAL, .cost = std::nullopt});
-
-        // Out-of-gas / wasm faults report gas (caller writes it to metadata).
-        // Force fuel to 0 on out-of-gas so cost is the full limit (wasmi leaves
-        // nonzero leftover fuel on its own out-of-fuel trap).
-        if (*res.ter == tecOUT_OF_GAS)
-            iw.setGas(0);
-
-        return std::unexpected(WasmTER{.ter = *res.ter, .cost = gas - moduleWrap_->getGas()});
-    }
-
-    if (res.r.empty())
-    {
-        Throw(
-            "<" + std::string(funcName) + "> return nothing");  // LCOV_EXCL_LINE
-    }
-
-    if (res.r[0].kind != WASM_I32)
-    {
-        Throw(
-            "<" + std::string(funcName) +
-            "> return type mismatch, ret: " + std::to_string(static_cast(res.r[0].kind)));
-    }
-
-    WasmResult const ret{.result = res.r[0].of.i32, .cost = gas - moduleWrap_->getGas()};
-
-    // #ifdef DEBUG_OUTPUT
-    //     auto& j = std::cerr;
-    // #else
-    //     auto j = j_.debug();
-    // #endif
-    // j << "WASMI Res: " << ret.result << " cost: " << ret.cost << std::endl;
-
-    return ret;
-}
-
-NotTEC
-WasmiEngine::check(
-    Bytes const& wasmCode,
-    HostFunctions& hfs,
-    std::string_view funcName,
-    std::vector const& params,
-    ImportVec const& imports,
-    beast::Journal j)
-{
-    try
-    {
-        checkImports(imports, &hfs);
-        return checkHlp(wasmCode, hfs, funcName, params, imports, j);
-    }
-    catch (std::exception const& e)
-    {
-        printWasmError(std::string("exception: ") + e.what(), nullptr, j);
-    }
-    // LCOV_EXCL_START
-    catch (...)
-    {
-        printWasmError(std::string("exception: unknown"), nullptr, j);
-    }
-    // LCOV_EXCL_STOP
-
-    return temINVALID_BYTECODE;
-}
-
-NotTEC
-WasmiEngine::checkHlp(
-    Bytes const& wasmCode,
-    HostFunctions& hfs,
-    std::string_view funcName,
-    std::vector const& params,
-    ImportVec const& imports,
-    beast::Journal j)
-{
-    // currently only 1 module support, possible parallel UT run
-    std::scoped_lock const lg(m_);
-    j_ = j;
-
-    // Create and instantiate the module.
-    if (wasmCode.empty())
-        throw std::runtime_error("empty module");
-
-    int const m = addModule(wasmCode, false, imports, -1);
-    if ((m < 0) || !moduleWrap_)
-        throw std::runtime_error("no module");  // LCOV_EXCL_LINE
-
-    // Looking for a func and compare parameter types
-    auto const f = moduleWrap_->getFuncType(!funcName.empty() ? funcName : "_start");
-    auto const* ftp = wasm_functype_params(f);
-    auto const p = convertParams(params);
-
-    if (int const comp = compareParamTypes(ftp, p); comp >= 0)
-        throw std::runtime_error("invalid parameter type #" + std::to_string(comp));
-
-    return tesSUCCESS;
-}
-
-wasm_trap_t*
-WasmiEngine::newTrap(std::string const& txt)
-{
-    static char empty[1] = {0};
-    wasm_message_t msg = {.size = 1, .data = empty};
-
-    if (!txt.empty())
-        wasm_name_new(&msg, txt.size() + 1, txt.c_str());  // include 0
-
-    wasm_trap_t* trap = wasm_trap_new(store_.get(), &msg);  // NOLINT
-
-    if (!txt.empty())
-        wasm_byte_vec_delete(&msg);
-
-    return trap;
-}
-
-}  // namespace xrpl
diff --git a/src/test/app/AMMCalc_test.cpp b/src/test/app/AMMCalc_test.cpp
index 74080e669c..23f251d57a 100644
--- a/src/test/app/AMMCalc_test.cpp
+++ b/src/test/app/AMMCalc_test.cpp
@@ -20,6 +20,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -188,8 +189,7 @@ class AMMCalc_test : public beast::unit_test::Suite
     static std::string
     toString(STAmount const& a)
     {
-        return (boost::format("%s/%s") % a.getText() % ::xrpl::to_string(a.get().currency))
-            .str();
+        return std::format("{}/{}", a.getText(), ::xrpl::to_string(a.get().currency));
     }
 
     static STAmount
diff --git a/src/test/app/AMMClawbackMPT_test.cpp b/src/test/app/AMMClawbackMPT_test.cpp
index 6facafde4a..44eb61395a 100644
--- a/src/test/app/AMMClawbackMPT_test.cpp
+++ b/src/test/app/AMMClawbackMPT_test.cpp
@@ -16,6 +16,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -137,7 +139,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
             AMM amm(env, gw, btc(100), usd(100));
             env.close();
             amm.deposit(alice, 1'000);
-            env.close();
 
             // can not clawback when tfMPTCanClawback is not enabled
             env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecNO_PERMISSION));
@@ -503,6 +504,150 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testAMMClawbackAmountRoundsToZero(FeatureBitset features)
+    {
+        // Ensure a clawback that rounds down to zero MPT fails with
+        // tecAMM_FAILED instead of silently burning the holder's LP.
+        testcase("test AMMClawback amount that rounds down to zero");
+        using namespace jtx;
+
+        Env env(*this, features);
+        Account const gw{"gateway"};
+        Account const alice{"alice"};
+        Account const bob{"bob"};
+        env.fund(XRP(10'000'000), gw, alice, bob);
+        env.close();
+
+        env(fset(gw, asfAllowTrustLineClawback));
+        env.close();
+
+        // The clawed asset (amountRounded) rounds to zero while its XRP
+        // counterpart is always large.
+        {
+            MPTTester const mptBtc(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {alice, bob},
+                 .pay = 1'000,
+                 .flags = tfMPTCanClawback | kMptDexFlags});
+            MPT const btc = mptBtc;
+
+            AMM amm(env, alice, btc(3), XRP(333'000));
+            amm.deposit(bob, btc(3), XRP(333'000));
+
+            [[maybe_unused]] auto const [poolBtcBefore, poolXrpBefore, lptBefore] = amm.balances();
+            BEAST_EXPECT(poolBtcBefore == btc(6));
+
+            auto const issuerOABefore = mptBtc.getBalance(gw);
+            auto const aliceLpBefore = amm.getLPTokensBalance(alice.id());
+            auto const bobLpBefore = amm.getLPTokensBalance(bob.id());
+
+            // Attempt to clawback 1/6th of the BTC pool. When the zero-rounding
+            // guard is active (gated by fixCleanup3_4_0) the rounded amount
+            // drops to 0 and should trigger tecAMM_FAILED.
+            env(amm::ammClawback(gw, alice, btc, XRP, btc(1)),
+                Ter(features[fixCleanup3_4_0] ? TER{tecAMM_FAILED} : TER{tesSUCCESS}));
+            env.close();
+
+            [[maybe_unused]] auto const [poolBtcAfter, poolXrpAfter, lptAfter] = amm.balances();
+            auto const issuerOAAfter = mptBtc.getBalance(gw);
+            auto const aliceLpAfter = amm.getLPTokensBalance(alice.id());
+            auto const bobLpAfter = amm.getLPTokensBalance(bob.id());
+
+            if (features[fixCleanup3_4_0])
+            {
+                // Post-fixCleanup3_4_0: Clawback fails because the BTC balance
+                // would round to zero. All balances must remain untouched.
+                BEAST_EXPECT(poolBtcAfter == poolBtcBefore);
+                BEAST_EXPECT(poolXrpAfter == poolXrpBefore);
+                BEAST_EXPECT(issuerOAAfter == issuerOABefore);
+                BEAST_EXPECT(aliceLpAfter == aliceLpBefore);
+                BEAST_EXPECT(bobLpAfter == bobLpBefore);
+            }
+            else
+            {
+                // Pre-fixCleanup3_4_0: BTC rounds to zero and the clawback
+                // silently burns alice's LP without clawing back any BTC.
+                BEAST_EXPECT(poolBtcAfter == poolBtcBefore);
+                BEAST_EXPECT(poolXrpAfter < poolXrpBefore);
+                BEAST_EXPECT(issuerOAAfter == issuerOABefore);
+                BEAST_EXPECT(aliceLpAfter < aliceLpBefore);
+                BEAST_EXPECT(bobLpAfter == bobLpBefore);
+            }
+        }
+
+        // The pool above only ever rounds the clawed asset (amountRounded) to
+        // zero; its XRP counterpart is always large. Exercise the other operand
+        // of the guard (amount2Rounded == 0) with an MPT/MPT pool where the
+        // *paired* asset is the tiny integer that floors to zero while the
+        // clawed asset still rounds non-zero.
+        {
+            Account const carol{"carol"};
+            Account const dan{"dan"};
+            env.fund(XRP(10'000'000), carol, dan);
+            env.close();
+
+            MPTTester const mptBtc(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {carol, dan},
+                 .pay = 100'000,
+                 .flags = tfMPTCanClawback | kMptDexFlags});
+            MPT const btc = mptBtc;
+
+            MPTTester const mptEth(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {carol, dan},
+                 .pay = 1'000,
+                 .flags = tfMPTCanClawback | kMptDexFlags});
+            MPT const eth = mptEth;
+
+            // btc pool dwarfs the eth pool, so a ~1/12th claw withdraws a
+            // non-zero btc amount while the eth counterpart rounds to zero.
+            AMM amm(env, carol, btc(3'000), eth(3));
+            amm.deposit(dan, btc(3'000), eth(3));
+
+            [[maybe_unused]] auto const [poolBtcBefore, poolEthBefore, lptBefore] = amm.balances();
+            BEAST_EXPECT(poolBtcBefore == btc(6'000));
+            BEAST_EXPECT(poolEthBefore == eth(6));
+
+            auto const carolLpBefore = amm.getLPTokensBalance(carol.id());
+            auto const danLpBefore = amm.getLPTokensBalance(dan.id());
+
+            env(amm::ammClawback(gw, carol, btc, eth, btc(500)),
+                Ter(features[fixCleanup3_4_0] ? TER{tecAMM_FAILED} : TER{tesSUCCESS}));
+            env.close();
+
+            [[maybe_unused]] auto const [poolBtcAfter, poolEthAfter, lptAfter] = amm.balances();
+            auto const carolLpAfter = amm.getLPTokensBalance(carol.id());
+            auto const danLpAfter = amm.getLPTokensBalance(dan.id());
+
+            if (features[fixCleanup3_4_0])
+            {
+                // Post-fixCleanup3_4_0: clawback fails because the ETH (Asset2)
+                // balance would round to zero (guard fires via
+                // amount2Rounded == 0). All balances must remain untouched.
+                BEAST_EXPECT(poolBtcAfter == poolBtcBefore);
+                BEAST_EXPECT(poolEthAfter == poolEthBefore);
+                BEAST_EXPECT(carolLpAfter == carolLpBefore);
+                BEAST_EXPECT(danLpAfter == danLpBefore);
+            }
+            else
+            {
+                // Pre-fixCleanup3_4_0: the asymmetric round-off goes through.
+                // btc is clawed (non-zero) but eth rounds to zero, so the eth
+                // pool is untouched while carol's LP is burned. This asymmetry
+                // proves amount2Rounded == 0 is the trigger.
+                BEAST_EXPECT(poolBtcAfter < poolBtcBefore);
+                BEAST_EXPECT(poolEthAfter == poolEthBefore);
+                BEAST_EXPECT(carolLpAfter < carolLpBefore);
+                BEAST_EXPECT(danLpAfter == danLpBefore);
+            }
+        }
+    }
+
     void
     testAMMClawbackAll(FeatureBitset features)
     {
@@ -543,7 +688,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
 
             // gw clawback all BTC from alice
             amm.deposit(bob, btc(1'000'000000), usd(2000));
-            env.close();
             BEAST_EXPECT(amm.expectBalances(btc(3'000'000000), usd(3000), IOUAmount(3000000)));
 
             auto aliceBTC = env.balance(alice, btc);
@@ -921,7 +1065,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
             BEAST_EXPECT(amm.expectBalances(btc(2'000'000000), usd(8'000), IOUAmount(4'000'000)));
 
             amm.deposit(bob, btc(1'000'000000), usd(4'000));
-            env.close();
             BEAST_EXPECT(amm.expectBalances(btc(3'000'000000), usd(12'000), IOUAmount(6'000'000)));
 
             auto aliceBTC = env.balance(alice, btc);
@@ -1335,6 +1478,60 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testClawbackCreatesMissingMPToken(FeatureBitset features)
+    {
+        testcase("test AMMClawback creates missing MPToken");
+        using namespace jtx;
+
+        auto test = [&](std::optional const clawAmount) {
+            Env env{*this, features};
+            Account const gw{"gateway"};
+            Account const alice{"alice"};
+            env.fund(XRP(1'000'000), gw, alice);
+            env.close();
+
+            MPTTester token(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {alice},
+                 .pay = 1'000,
+                 .flags = tfMPTCanClawback | tfMPTRequireAuth | kMptDexFlags,
+                 .authHolder = true});
+
+            AMM ammAlice(env, alice, token(1'000), XRP(1'000));
+            env.close();
+            BEAST_EXPECT(env.balance(alice, token) == token(0));
+
+            // The holder can delete the zero-balance MPToken while still
+            // holding LP tokens. A regular AMMWithdraw remains subject to
+            // RequireAuth and cannot recreate the missing token.
+            token.authorize({.account = alice, .flags = tfMPTUnauthorize});
+            env.close();
+            BEAST_EXPECT(!env.le(keylet::mptoken(token.issuanceID(), alice.id())));
+            ammAlice.withdrawAll(alice, std::nullopt, Ter(tecNO_AUTH));
+            env.close();
+            BEAST_EXPECT(!env.le(keylet::mptoken(token.issuanceID(), alice.id())));
+
+            // AMMClawback ignores authorization and must be able to recreate
+            // the holder MPToken so the issuer can recover MPT from the pool.
+            std::optional amount;
+            if (clawAmount)
+                amount = token(*clawAmount);
+            env(amm::ammClawback(gw, alice, token, XRP, amount));
+            env.close();
+
+            auto const sleMpt = env.le(keylet::mptoken(token.issuanceID(), alice.id()));
+            BEAST_EXPECT(sleMpt && sleMpt->isFlag(lsfMPTAuthorized));
+            env.require(Balance(alice, token(0)));
+
+            BEAST_EXPECT(clawAmount ? ammAlice.ammExists() : !ammAlice.ammExists());
+        };
+
+        test(std::nullopt);
+        test(400);
+    }
+
     void
     testSingleDepositAndClawback(FeatureBitset features)
     {
@@ -1361,7 +1558,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
             env.close();
             BEAST_EXPECT(amm.expectBalances(XRP(100), btc(400), IOUAmount(200000)));
             amm.deposit(alice, btc(400));
-            env.close();
             BEAST_EXPECT(amm.expectBalances(XRP(100), btc(800), IOUAmount{282842'712474619, -9}));
 
             auto aliceBTC = env.balance(alice, MPT(btc));
@@ -1407,7 +1603,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
             env.close();
             BEAST_EXPECT(amm.expectBalances(usd(100), btc(400), IOUAmount(200)));
             amm.deposit(alice, btc(400));
-            env.close();
             BEAST_EXPECT(amm.expectBalances(usd(100), btc(800), IOUAmount{282'842712474619, -12}));
 
             auto aliceBTC = env.balance(alice, MPT(btc));
@@ -1462,7 +1657,6 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
             env.close();
             BEAST_EXPECT(amm.expectBalances(usd(100), btc(400), IOUAmount(200)));
             amm.deposit(alice, btc(400));
-            env.close();
             BEAST_EXPECT(amm.expectBalances(usd(100), btc(800), IOUAmount{282'842712474619, -12}));
 
             auto aliceBTC = env.balance(alice, MPT(btc));
@@ -1669,7 +1863,7 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
             env(amm::ammClawback(gw, alice, btc, usd, std::nullopt), Ter(tecNO_PERMISSION));
 
             // Although USD is clawable with asfAllowTrustLineClawback.
-            // When tfClawTwoAssets is set, we will claw Asser2 as well.
+            // When tfClawTwoAssets is set, we will claw Asset2 as well.
             // But Asset2 is not clawable. tfMPTCanClawback was not set for BTC.
             env(amm::ammClawback(gw, alice, usd, btc, std::nullopt),
                 Txflags(tfClawTwoAssets),
@@ -1811,6 +2005,282 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
         }
     }
 
+    // Test that AMMClawback succeeds when the LP has previously deleted both
+    // zero-balance MPToken objects in an MPT/MPT pool.  The fix changes the
+    // ValidMPTIssuance invariant threshold from > 1 to > 2 so that the two
+    // MPToken creations triggered by the internal AMMWithdraw are permitted.
+    void
+    testClawbackAfterDeletingMPTokens(FeatureBitset features)
+    {
+        testcase("test AMMClawback after holder deletes zero-balance MPTokens");
+        using namespace jtx;
+
+        // Partial clawback (one asset): verify both MPTokens are recreated and
+        // the non-claw asset is returned to alice.
+        {
+            Env env(*this, features);
+            Account const gw{"gateway"};
+            Account const alice{"alice"};
+            env.fund(XRP(100'000), gw, alice);
+            env.close();
+
+            MPTTester btc(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {alice},
+                 .pay = 10'000,
+                 .flags = tfMPTCanClawback | kMptDexFlags});
+
+            MPTTester eth(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {alice},
+                 .pay = 10'000,
+                 .flags = tfMPTCanClawback | kMptDexFlags});
+
+            // Alice deposits everything into the MPT/MPT pool; her MPT
+            // balances drop to zero.
+            AMM const amm(env, alice, btc(10'000), eth(10'000));
+            env.close();
+            BEAST_EXPECT(amm.expectBalances(btc(10'000), eth(10'000), IOUAmount{10'000}));
+
+            auto aliceBTC = env.balance(alice, btc);
+            auto aliceETH = env.balance(alice, eth);
+            BEAST_EXPECT(aliceBTC == btc(0));
+            BEAST_EXPECT(aliceETH == eth(0));
+
+            // Alice deletes both zero-balance MPTokens to reclaim reserves.
+            btc.authorize({.account = alice, .flags = tfMPTUnauthorize});
+            eth.authorize({.account = alice, .flags = tfMPTUnauthorize});
+            BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice.id())));
+            BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice.id())));
+
+            // gw claws back some BTC from alice's share in the pool.
+            // AMMWithdraw internally creates both missing MPTokens
+            // (mptokensCreated_ == 2); the invariant (> 2) allows this.
+            env(amm::ammClawback(gw, alice, btc, eth, btc(1'000)));
+            env.close();
+
+            // Both MPToken objects must have been recreated.
+            BEAST_EXPECT(env.le(keylet::mptoken(btc.issuanceID(), alice.id())));
+            BEAST_EXPECT(env.le(keylet::mptoken(eth.issuanceID(), alice.id())));
+
+            // The non-claw asset (eth) was returned to alice.
+            BEAST_EXPECT(env.balance(alice, eth) > aliceETH);
+            // The claw asset (btc) was burned; alice's btc balance stays 0.
+            env.require(Balance(alice, aliceBTC));
+            BEAST_EXPECT(amm.ammExists());
+        }
+
+        // Full clawback (two assets, tfClawTwoAssets): verify both MPTokens
+        // are recreated and the AMM is deleted when fully drained.
+        {
+            Env env(*this, features);
+            Account const gw{"gateway"};
+            Account const alice{"alice"};
+            env.fund(XRP(100'000), gw, alice);
+            env.close();
+
+            MPTTester btc(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {alice},
+                 .pay = 10'000,
+                 .flags = tfMPTCanClawback | kMptDexFlags});
+
+            MPTTester eth(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {alice},
+                 .pay = 10'000,
+                 .flags = tfMPTCanClawback | kMptDexFlags});
+
+            AMM const amm(env, alice, btc(10'000), eth(10'000));
+            env.close();
+
+            auto aliceBTC = env.balance(alice, btc);
+            auto aliceETH = env.balance(alice, eth);
+
+            btc.authorize({.account = alice, .flags = tfMPTUnauthorize});
+            eth.authorize({.account = alice, .flags = tfMPTUnauthorize});
+            BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice.id())));
+            BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice.id())));
+
+            // Full two-asset clawback: both assets are clawed and alice
+            // receives nothing back.  The AMM should be empty and deleted.
+            env(amm::ammClawback(gw, alice, btc, eth, std::nullopt), Txflags(tfClawTwoAssets));
+            env.close();
+
+            BEAST_EXPECT(!amm.ammExists());
+            // Both assets were clawed; alice's balances remain at zero.
+            env.require(Balance(alice, aliceBTC));
+            env.require(Balance(alice, aliceETH));
+        }
+    }
+
+    void
+    testClawbackCrossIssuerPairedAssetAuth(FeatureBitset features)
+    {
+        testcase("test AMMClawback recreates paired-issuer MPToken unauthorized");
+        using namespace jtx;
+
+        // Cross-issuer MPT/MPT pool: btc is issued by gw, eth by gw2, and both
+        // require authorization. Alice deposits her entire balance of both and
+        // deletes the resulting zero-balance MPTokens. When gw claws back its
+        // own asset (btc), the two-asset withdrawal must recreate both of
+        // Alice's MPTokens so the pool can pay her the paired asset. The
+        // recreated MPToken may only be auto-authorized for the clawback
+        // issuer's own asset (btc); the paired asset's issuer (gw2) never
+        // consented, so eth must be recreated *unauthorized*, leaving gw2 in
+        // control of its own token and preserving its RequireAuth guarantee.
+        Env env(*this, features);
+        Account const gw{"gateway"};
+        Account const gw2{"gateway2"};
+        Account const alice{"alice"};
+        env.fund(XRP(100'000), gw, gw2, alice);
+        env.close();
+
+        MPTTester btc(
+            {.env = env,
+             .issuer = gw,
+             .holders = {alice},
+             .pay = 10'000,
+             .flags = tfMPTCanClawback | tfMPTRequireAuth | kMptDexFlags,
+             .authHolder = true});
+
+        MPTTester eth(
+            {.env = env,
+             .issuer = gw2,
+             .holders = {alice},
+             .pay = 10'000,
+             .flags = tfMPTCanClawback | tfMPTRequireAuth | kMptDexFlags,
+             .authHolder = true});
+
+        // Alice deposits everything into the pool; her MPT balances drop to 0.
+        AMM const amm(env, alice, btc(10'000), eth(10'000));
+        env.close();
+        BEAST_EXPECT(amm.expectBalances(btc(10'000), eth(10'000), IOUAmount{10'000}));
+        BEAST_EXPECT(env.balance(alice, btc) == btc(0));
+        BEAST_EXPECT(env.balance(alice, eth) == eth(0));
+
+        // Alice deletes both zero-balance MPTokens to reclaim reserves.
+        btc.authorize({.account = alice, .flags = tfMPTUnauthorize});
+        eth.authorize({.account = alice, .flags = tfMPTUnauthorize});
+        BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice.id())));
+        BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice.id())));
+
+        // gw (issuer of btc) claws back part of Alice's btc. This is a
+        // cross-issuer pool, so tfClawTwoAssets is not permitted: only btc is
+        // clawed back, while the paired eth is returned to Alice.
+        env(amm::ammClawback(gw, alice, btc, eth, btc(1'000)));
+        env.close();
+
+        // Both MPTokens were recreated so the withdrawal could pay Alice.
+        auto const sleBtc = env.le(keylet::mptoken(btc.issuanceID(), alice.id()));
+        auto const sleEth = env.le(keylet::mptoken(eth.issuanceID(), alice.id()));
+        BEAST_EXPECT(sleBtc);
+        BEAST_EXPECT(sleEth);
+
+        // The clawback issuer's own asset (btc) may be recreated authorized:
+        // gw has authority over its own token.
+        BEAST_EXPECT(sleBtc && sleBtc->isFlag(lsfMPTAuthorized));
+
+        // The paired asset (eth) is issued by gw2, who did not sign this
+        // transaction. It must be recreated *unauthorized* so gw2's RequireAuth
+        // is not bypassed. This is the core assertion for the cross-issuer fix.
+        BEAST_EXPECT(sleEth && !sleEth->isFlag(lsfMPTAuthorized));
+
+        // The clawback still completed: btc was clawed back (Alice keeps a zero
+        // btc balance) and the paired eth was delivered into Alice's now
+        // unauthorized, gw2-gated MPToken (non-zero raw balance).
+        BEAST_EXPECT(sleBtc && sleBtc->getFieldU64(sfMPTAmount) == 0);
+        BEAST_EXPECT(sleEth && sleEth->getFieldU64(sfMPTAmount) > 0);
+        BEAST_EXPECT(amm.ammExists());
+    }
+
+    void
+    testClawbackBypassesReserve(FeatureBitset features)
+    {
+        // Same as the IOU case, but the paired asset is an MPT alice does not
+        // hold yet. The reserve check is skipped on the clawback path while
+        // createMPToken() still runs, so alice's MPToken is created even though
+        // neither she nor the low-XRP issuer can cover the owner reserve.
+        testcase("test clawback bypasses recipient reserve (MPT)");
+        using namespace jtx;
+
+        Env env(*this, features);
+        Account const gw{"gateway"};    // IOU issuer + claw authority, low XRP
+        Account const gw2{"gateway2"};  // MPT issuer of the paired asset
+        Account const carol{"carol"};
+        Account const alice{"alice"};
+
+        auto const usd = gw["USD"];
+        auto const baseFee = env.current()->fees().base;
+
+        env.fund(XRP(1'000'000), gw2, carol);
+        // Low XRP so the legacy issuer-balance check cannot pass.
+        env.fund(env.current()->fees().accountReserve(0, 1) + baseFee * 10, gw);
+        // Reserve for the USD trustline and LP token trustline.
+        env.fund(env.current()->fees().accountReserve(2, 1) + baseFee * 5, alice);
+        env.close();
+
+        env(fset(gw, asfAllowTrustLineClawback));
+        env.close();
+
+        // The paired MPT: transferable so an AMM can hold it, and no
+        // RequireAuth so createMPToken()'s WeakAuth check passes.
+        MPT const btc = MPTTester(
+            {.env = env,
+             .issuer = gw2,
+             .holders = {carol},
+             .pay = 1'000'000,
+             .flags = kMptDexFlags});
+
+        env.trust(usd(1'000'000), carol);
+        env(pay(gw, carol, usd(100'000)));
+        env.close();
+        AMM amm(env, carol, usd(1'000), btc(1'000), Ter(tesSUCCESS));
+        env.close();
+
+        // alice holds a USD trustline and LP tokens, but no BTC MPToken.
+        env.trust(usd(100'000), alice);
+        env(pay(gw, alice, usd(1'000)));
+        env.close();
+        amm.deposit(alice, usd(100));
+
+        BEAST_EXPECT(env.ownerCount(alice) == 2);
+        BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID, alice.id())));
+
+        // AMMWithdraw still enforces the reserve check.
+        amm.withdrawAll(alice, std::nullopt, Ter(tecINSUFFICIENT_RESERVE));
+        BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID, alice.id())));
+        BEAST_EXPECT(env.ownerCount(alice) == 2);
+        // alice cannot afford a third owner object.
+        BEAST_EXPECT(env.balance(alice) < STAmount(env.current()->fees().accountReserve(3, 1)));
+
+        if (features[fixCleanup3_4_0])
+        {
+            // Reserve check skipped; the paired BTC returns to alice on a
+            // newly created MPToken.
+            env(amm::ammClawback(gw, alice, usd, btc, usd(10)), Ter(tesSUCCESS));
+            env.close();
+
+            BEAST_EXPECT(env.le(keylet::mptoken(btc.issuanceID, alice.id())));
+            BEAST_EXPECT(env.balance(alice, btc) > btc(0));
+            BEAST_EXPECT(env.ownerCount(alice) == 3);
+        }
+        else
+        {
+            // Legacy path: the check runs against max(issuer, holder) XRP,
+            // neither of which covers a third owner object.
+            env(amm::ammClawback(gw, alice, usd, btc, usd(10)), Ter(tecINSUFFICIENT_RESERVE));
+            env.close();
+
+            BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID, alice.id())));
+            BEAST_EXPECT(env.ownerCount(alice) == 2);
+        }
+    }
+
     void
     run() override
     {
@@ -1819,11 +2289,17 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
         testInvalidRequest(all);
         testFeatureDisabled(all);
         testAMMClawbackAmount(all);
+        testAMMClawbackAmount(all - fixCleanup3_4_0);
+        testAMMClawbackAmountRoundsToZero(all);
+        testAMMClawbackAmountRoundsToZero(all - fixCleanup3_4_0);
         testAMMClawbackAll(all);
         testAMMClawbackAmountSameIssuer(all);
         testAMMClawbackAllSameIssuer(all);
         testAMMClawbackIssuesEachOther(all);
         testAssetFrozenOrLocked(all);
+        testClawbackCreatesMissingMPToken(all);
+        testClawbackAfterDeletingMPTokens(all);
+        testClawbackCrossIssuerPairedAssetAuth(all);
         testSingleDepositAndClawback(all);
         testLastHolderLPTokenBalance(all);
         testLastHolderLPTokenBalance(all - fixAMMv1_3 - fixAMMClawbackRounding);
@@ -1832,6 +2308,8 @@ class AMMClawbackMPT_test : public beast::unit_test::Suite
             featureLendingProtocol);
         testLastHolderLPTokenBalance(all - fixAMMClawbackRounding);
         testClawAssetCheck(all);
+        testClawbackBypassesReserve(all);
+        testClawbackBypassesReserve(all - fixCleanup3_4_0);
     }
 };
 
diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp
index ba416d8192..4f025f08eb 100644
--- a/src/test/app/AMMClawback_test.cpp
+++ b/src/test/app/AMMClawback_test.cpp
@@ -13,7 +13,10 @@
 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -2155,6 +2158,209 @@ class AMMClawback_test : public beast::unit_test::Suite
             }
             BEAST_EXPECT(env.balance(carol, eur) == eur(7750));
         }
+
+        // gw (USD issuer) individually freezes the AMM-USD trust line.
+        // AMMClawback must still succeed because the freeze invariant
+        // short-circuits before reaching the AMM line check (no receivers in
+        // the USD issuer's change set). Behavior is identical with or without
+        // fixCleanup3_4_0.
+        {
+            Env env(*this, features);
+            Account const gw{"gateway"};
+            Account const gw2{"gateway2"};
+            Account const alice{"alice"};
+            env.fund(XRP(1000000), gw, gw2, alice);
+            env.close();
+
+            env(fset(gw, asfAllowTrustLineClawback));
+            env.close();
+            env.require(Flags(gw, asfAllowTrustLineClawback));
+
+            auto const usd = gw["USD"];
+            env.trust(usd(100000), alice);
+            env(pay(gw, alice, usd(3000)));
+            env.close();
+
+            auto const eur = gw2["EUR"];
+            env.trust(eur(100000), alice);
+            env(pay(gw2, alice, eur(3000)));
+            env.close();
+
+            AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS));
+            env.close();
+
+            BEAST_EXPECT(
+                amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12}));
+
+            // gw individually freezes the AMM-USD trust line (AMM pseudo-account
+            // <-> gw), not alice's trust line.
+            env(trust(gw, STAmount{Issue{usd.currency, amm.ammAccount()}, 0}, tfSetFreeze));
+            env.close();
+
+            env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS));
+            env.close();
+
+            env.require(Balance(alice, usd(1000)));
+            env.require(Balance(alice, eur(2500)));
+            BEAST_EXPECT(amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13}));
+            BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13}));
+        }
+
+        // gw2 (EUR issuer) individually freezes the AMM-EUR trust line.
+        // The EUR flow (AMM → alice) is a genuine P2P transfer checked by the
+        // freeze invariant. Pre-fixCleanup3_4_0 the isAMMNode guard incorrectly
+        // blocked AMMClawback's overrideFreeze privilege on that trust line.
+        {
+            Env env(*this, features);
+            Account const gw{"gateway"};
+            Account const gw2{"gateway2"};
+            Account const alice{"alice"};
+            env.fund(XRP(1000000), gw, gw2, alice);
+            env.close();
+
+            env(fset(gw, asfAllowTrustLineClawback));
+            env.close();
+            env.require(Flags(gw, asfAllowTrustLineClawback));
+
+            auto const usd = gw["USD"];
+            env.trust(usd(100000), alice);
+            env(pay(gw, alice, usd(3000)));
+            env.close();
+
+            auto const eur = gw2["EUR"];
+            env.trust(eur(100000), alice);
+            env(pay(gw2, alice, eur(3000)));
+            env.close();
+
+            AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS));
+            env.close();
+
+            BEAST_EXPECT(
+                amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12}));
+
+            // gw2 individually freezes the AMM-EUR trust line.
+            env(trust(gw2, STAmount{Issue{eur.currency, amm.ammAccount()}, 0}, tfSetFreeze));
+            env.close();
+
+            if (features[fixCleanup3_4_0])
+            {
+                // Post-fixCleanup3_4_0: overrideFreeze privilege applies to
+                // all freeze types on AMM trust lines.
+                env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS));
+                env.close();
+
+                env.require(Balance(alice, usd(1000)));
+                env.require(Balance(alice, eur(2500)));
+                BEAST_EXPECT(
+                    amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13}));
+                BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13}));
+            }
+            else
+            {
+                // Pre-fixCleanup3_4_0: the isAMMNode guard prevents the
+                // overrideFreeze privilege from applying to individually-frozen
+                // AMM trust lines, so the invariant blocks the clawback.
+                env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tecINVARIANT_FAILED));
+            }
+        }
+
+        // gw2 (EUR issuer) globally freezes its issued assets. AMMClawback
+        // must still be able to return EUR from the AMM to alice.
+        {
+            Env env(*this, features);
+            Account const gw{"gateway"};
+            Account const gw2{"gateway2"};
+            Account const alice{"alice"};
+            env.fund(XRP(1000000), gw, gw2, alice);
+            env.close();
+
+            env(fset(gw, asfAllowTrustLineClawback));
+            env.close();
+            env.require(Flags(gw, asfAllowTrustLineClawback));
+
+            auto const usd = gw["USD"];
+            env.trust(usd(100000), alice);
+            env(pay(gw, alice, usd(3000)));
+            env.close();
+
+            auto const eur = gw2["EUR"];
+            env.trust(eur(100000), alice);
+            env(pay(gw2, alice, eur(3000)));
+            env.close();
+
+            AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS));
+            env.close();
+
+            BEAST_EXPECT(
+                amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12}));
+
+            env(fset(gw2, asfGlobalFreeze));
+            env.close();
+
+            env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS));
+            env.close();
+
+            env.require(Balance(alice, usd(1000)));
+            env.require(Balance(alice, eur(2500)));
+            BEAST_EXPECT(amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13}));
+            BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13}));
+        }
+
+        // Same as above but gw2 deep-freezes the AMM-EUR trust line.
+        if (features[featureDeepFreeze])
+        {
+            Env env(*this, features);
+            Account const gw{"gateway"};
+            Account const gw2{"gateway2"};
+            Account const alice{"alice"};
+            env.fund(XRP(1000000), gw, gw2, alice);
+            env.close();
+
+            env(fset(gw, asfAllowTrustLineClawback));
+            env.close();
+            env.require(Flags(gw, asfAllowTrustLineClawback));
+
+            auto const usd = gw["USD"];
+            env.trust(usd(100000), alice);
+            env(pay(gw, alice, usd(3000)));
+            env.close();
+
+            auto const eur = gw2["EUR"];
+            env.trust(eur(100000), alice);
+            env(pay(gw2, alice, eur(3000)));
+            env.close();
+
+            AMM const amm(env, alice, eur(1000), usd(2000), Ter(tesSUCCESS));
+            env.close();
+
+            BEAST_EXPECT(
+                amm.expectBalances(usd(2000), eur(1000), IOUAmount{1414213562373095, -12}));
+
+            // gw2 deep-freezes the AMM-EUR trust line.
+            env(trust(
+                gw2,
+                STAmount{Issue{eur.currency, amm.ammAccount()}, 0},
+                tfSetFreeze | tfSetDeepFreeze));
+            env.close();
+
+            if (features[fixCleanup3_4_0])
+            {
+                env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tesSUCCESS));
+                env.close();
+
+                env.require(Balance(alice, usd(1000)));
+                env.require(Balance(alice, eur(2500)));
+                BEAST_EXPECT(
+                    amm.expectBalances(usd(1000), eur(500), IOUAmount{7071067811865475, -13}));
+                BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{7071067811865475, -13}));
+            }
+            else
+            {
+                // Pre-fixCleanup3_4_0: same isAMMNode guard issue blocks the
+                // clawback on deep-frozen AMM trust lines.
+                env(amm::ammClawback(gw, alice, usd, eur, usd(1000)), Ter(tecINVARIANT_FAILED));
+            }
+        }
     }
 
     void
@@ -2510,6 +2716,142 @@ class AMMClawback_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testClawbackBypassesReserve(FeatureBitset features)
+    {
+        // Clawback must not fail the holder-side reserve check: a holder could
+        // otherwise veto it by omitting the paired trustline. AMMWithdraw still
+        // enforces the check. Pre-fixCleanup3_4_0 the holder's reserve was
+        // compared against max(issuer pre-fee, holder current) XRP, so the
+        // clawback was blocked when neither balance covered it.
+        testcase("test clawback bypasses recipient reserve");
+        using namespace jtx;
+
+        Env env(*this, features);
+        Account const gw{"gateway"};
+        Account const carol{"carol"};
+        Account const alice{"alice"};
+
+        auto const usd = gw["USD"];
+        auto const eur = gw["EUR"];
+        auto const baseFee = env.current()->fees().base;
+
+        env.fund(XRP(1'000'000), carol);
+        // Low XRP so the legacy issuer-balance check cannot pass.
+        env.fund(env.current()->fees().accountReserve(0, 1) + baseFee * 10, gw);
+        // Reserve for the USD trustline and LP token trustline.
+        env.fund(env.current()->fees().accountReserve(2, 1) + baseFee * 5, alice);
+        env.close();
+
+        env(fset(gw, asfAllowTrustLineClawback));
+        env.close();
+
+        env.trust(usd(1'000'000), carol);
+        env.trust(eur(1'000'000), carol);
+        env(pay(gw, carol, usd(100'000)));
+        env(pay(gw, carol, eur(100'000)));
+        env.close();
+        AMM amm(env, carol, usd(1'000), eur(1'000), Ter(tesSUCCESS));
+        env.close();
+
+        // alice holds a USD trustline and LP tokens, but no EUR trustline.
+        env.trust(usd(100'000), alice);
+        env(pay(gw, alice, usd(1'000)));
+        env.close();
+        amm.deposit(alice, usd(100));
+
+        BEAST_EXPECT(env.ownerCount(alice) == 2);
+        // alice cannot afford a third owner object.
+        BEAST_EXPECT(env.balance(alice) < STAmount(env.current()->fees().accountReserve(3, 1)));
+
+        // AMMWithdraw still enforces the reserve check.
+        amm.withdraw(
+            WithdrawArg{
+                .account = alice, .asset1Out = eur(1), .err = Ter(tecINSUFFICIENT_RESERVE)});
+        BEAST_EXPECT(env.ownerCount(alice) == 2);
+
+        if (features[fixCleanup3_4_0])
+        {
+            // Reserve check skipped; the paired EUR returns to alice on a
+            // newly created EUR trustline.
+            env(amm::ammClawback(gw, alice, usd, eur, usd(10)), Ter(tesSUCCESS));
+            env.close();
+
+            BEAST_EXPECT(env.le(keylet::trustLine(alice.id(), eur.issue())));
+            BEAST_EXPECT(env.balance(alice, eur) > eur(0));
+            BEAST_EXPECT(env.ownerCount(alice) == 3);
+        }
+        else
+        {
+            // Legacy path: the check runs against max(issuer, holder) XRP,
+            // neither of which covers a third owner object.
+            env(amm::ammClawback(gw, alice, usd, eur, usd(10)), Ter(tecINSUFFICIENT_RESERVE));
+            env.close();
+            BEAST_EXPECT(env.ownerCount(alice) == 2);
+        }
+    }
+
+    void
+    testExactLPTokenEquality(FeatureBitset features)
+    {
+        using namespace jtx;
+
+        if (!features[fixAMMv1_3] || !features[fixAMMClawbackRounding])
+            return;
+
+        testcase("test exact LP token equality boundary");
+
+        Env env(*this, features);
+        Account const gw{"gateway"}, alice{"alice"}, bob{"bob"};
+        env.fund(XRP(100000), gw, alice, bob);
+        env.close();
+        env(fset(gw, asfAllowTrustLineClawback));
+        env.close();
+
+        auto const usd = gw["USD"];
+        env.trust(usd(100000), alice);
+        env(pay(gw, alice, usd(50000)));
+        env.trust(usd(100000), bob);
+        env(pay(gw, bob, usd(40000)));
+        env.close();
+
+        // bob keeps alice from being the sole LP, otherwise the clawback
+        // first rewrites the AMM's LP balance to alice's tokens and the
+        // boundary is no longer distinguishable.
+        AMM amm(env, alice, XRP(2), usd(1));
+        amm.deposit(alice, IOUAmount{1'876123487565916, -15});
+        amm.deposit(bob, IOUAmount{1'000'000});
+
+        auto const [amountBalance, amount2Balance, lptAMMBalance] = amm.balances(usd, XRP);
+        auto const aliceLP = amm.getLPTokensBalance(alice);
+        auto const holderLPTokens = STAmount{aliceLP, amm.lptIssue()};
+        BEAST_EXPECT(lptAMMBalance > holderLPTokens);
+
+        // Clawing alice's pro-rata share lands the transactor's computed LP
+        // amount exactly on her balance.
+        auto const amount = toSTAmount(usd, Number{amountBalance} * holderLPTokens / lptAMMBalance);
+        BEAST_EXPECT(
+            toSTAmount(lptAMMBalance.asset(), lptAMMBalance * (Number{amount} / amountBalance)) ==
+            holderLPTokens);
+
+        env(amm::ammClawback(gw, alice, usd, XRP, amount));
+        env.close();
+
+        auto const aliceLPAfter = amm.getLPTokensBalance(alice);
+        if (features[fixCleanup3_4_0])
+        {
+            // Equality takes the withdraw-all path, redeeming alice's tokens
+            // exactly.
+            BEAST_EXPECT(aliceLPAfter == IOUAmount(0));
+        }
+        else
+        {
+            // The fall-through re-rounds the LP amount against the much
+            // larger pool balance, leaving alice with dust.
+            BEAST_EXPECT(aliceLPAfter != IOUAmount(0) && aliceLPAfter < aliceLP);
+        }
+    }
+
     void
     run() override
     {
@@ -2530,6 +2872,7 @@ class AMMClawback_test : public beast::unit_test::Suite
               // precision loss caught in transaction layer -> tecPRECISION_LOSS
               all - fixAMMClawbackRounding - featureMPTokensV2,
               all - featureMPTokensV2,
+              all - fixCleanup3_4_0,
               all})
         {
             testAMMClawbackSpecificAmount(features);
@@ -2542,6 +2885,8 @@ class AMMClawback_test : public beast::unit_test::Suite
             testAssetFrozen(features);
             testSingleDepositAndClawback(features);
             testLastHolderLPTokenBalance(features);
+            testClawbackBypassesReserve(features);
+            testExactLPTokenEquality(features);
         }
     }
 };
diff --git a/src/test/app/AMMExtendedMPT_test.cpp b/src/test/app/AMMExtendedMPT_test.cpp
index f04ea39f2b..5059128d4b 100644
--- a/src/test/app/AMMExtendedMPT_test.cpp
+++ b/src/test/app/AMMExtendedMPT_test.cpp
@@ -188,20 +188,28 @@ private:
             {features});
 
         // tfPassive -- place the offer without crossing it.
-        testAMM(
-            [&](AMM& ammAlice, Env& env) {
-                // Carol creates a passive offer that could cross AMM.
-                // Carol's offer should stay in the ledger.
-                auto const& btc = MPT(ammAlice[1]);
-                env(offer(carol_, XRP(100), btc(100), tfPassive));
-                env.close();
-                BEAST_EXPECT(ammAlice.expectBalances(XRP(10'100), btc(10'000), ammAlice.tokens()));
-                BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), btc(100)}}}));
-            },
-            {{XRP(10'100), gAmmmpt(10'000)}},
-            0,
-            std::nullopt,
-            {features});
+        {
+            Env env{*this, features};
+            fund(env, gw_, {alice_, carol_}, XRP(30'000'000));
+
+            MPTTester const btc(
+                {.env = env,
+                 .issuer = gw_,
+                 .holders = {alice_, carol_},
+                 .pay = 30'000'000,
+                 .flags = kMptDexFlags});
+
+            AMM const ammAlice(env, alice_, XRP(10'100'000), btc(10'000'000));
+
+            // Scale the exact-quality fixture up so the visual relationship
+            // stays clear: the passive CLOB offer has the same 1:1 quality as
+            // the generated AMM offer, so it should not cross.
+            env(offer(carol_, XRP(100'000), btc(100'000), tfPassive));
+            env.close();
+            BEAST_EXPECT(
+                ammAlice.expectBalances(XRP(10'100'000), btc(10'000'000), ammAlice.tokens()));
+            BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100'000), btc(100'000)}}}));
+        }
 
         // tfPassive -- cross only offers of better quality.
         testAMM(
@@ -1084,9 +1092,9 @@ private:
 
         // AMM is consumed up to the first cam Offer quality
         BEAST_EXPECT(ammCarol.expectBalances(
-            aBux(3'093'541'659'651'604), bBux(3'200'215'509'984'418), ammCarol.tokens()));
+            aBux(3'093'541'659'651'603), bBux(3'200'215'509'984'419), ammCarol.tokens()));
         BEAST_EXPECT(expectOffers(
-            env, cam, 1, {{Amounts{bBux(200'215'509'984'418), aBux(200'215'509'984'419)}}}));
+            env, cam, 1, {{Amounts{bBux(200'215'509'984'419), aBux(200'215'509'984'419)}}}));
     }
 
     void
@@ -1241,7 +1249,7 @@ private:
         BEAST_EXPECT(sa == XRP(100'000'000));
         // Bob gets ~99.99e12ETH. This is the amount Bob
         // can get out of AMM for 100,000,000XRP.
-        BEAST_EXPECT(equal(da, eth(99'999'900'000'100)));
+        BEAST_EXPECT(equal(da, eth(99'999'900'000'099)));
     }
 
     // carol holds ETH, sells ETH for XRP
@@ -1505,6 +1513,96 @@ private:
         }
     }
 
+    void
+    pathFindMPTAMMExecutableSourceAmount()
+    {
+        testcase("Path Find: MPT AMM source amount is executable");
+        using namespace jtx;
+
+        auto const checkQuote = [&](std::int64_t usdPool,
+                                    std::int64_t eurPool,
+                                    std::int64_t deliverAmount,
+                                    std::int64_t expectedSourceAmount) {
+            Env env = pathTestEnv();
+            env.fund(XRP(30'000), gw_, alice_, bob_, carol_);
+            env.close();
+
+            MPTTester const usd(
+                {.env = env,
+                 .issuer = gw_,
+                 .holders = {alice_, bob_, carol_},
+                 .pay = usdPool,
+                 .flags = kMptDexFlags});
+
+            MPTTester const eur(
+                {.env = env,
+                 .issuer = gw_,
+                 .holders = {alice_, bob_, carol_},
+                 .pay = eurPool,
+                 .flags = kMptDexFlags});
+
+            AMM const ammCarol(env, carol_, usd(usdPool), eur(eurPool));
+            env.close();
+
+            STPathSet st;
+            STAmount sa, da;
+            auto const deliver = eur(deliverAmount);
+            std::tie(st, sa, da) = findPaths(
+                env,
+                alice_,
+                bob_,
+                deliver,
+                std::nullopt,
+                usd.issuanceID(),
+                std::nullopt,
+                std::nullopt);
+
+            // Each quote must execute when used as an exact-output SendMax.
+            BEAST_EXPECT(equal(da, deliver));
+            BEAST_EXPECT(equal(sa, usd(expectedSourceAmount)));
+            BEAST_EXPECT(!st.empty());
+
+            auto const before = eur.getBalance(bob_);
+            env(pay(alice_, bob_, deliver),
+                Json(jss::Paths, st.getJson(JsonOptions::Values::None)),
+                Sendmax(sa),
+                Txflags(tfNoRippleDirect));
+            BEAST_EXPECT(eur.getBalance(bob_) == before + deliverAmount);
+        };
+
+        struct TestCase
+        {
+            std::int64_t usdPool;
+            std::int64_t eurPool;
+            std::int64_t deliverAmount;
+            std::int64_t expectedSourceAmount;
+        };
+
+        // Cover the original 2:1 pool and the same pool scaled down by 1000.
+        // clang-format off
+        TestCase const testCases[] = {
+            {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 1,     .expectedSourceAmount = 3},
+            {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 2,     .expectedSourceAmount = 5},
+            {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 10,    .expectedSourceAmount = 21},
+            {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 100,   .expectedSourceAmount = 201},
+            {.usdPool = 2'000'000, .eurPool = 1'000'000, .deliverAmount = 1'000, .expectedSourceAmount = 2'003},
+            {.usdPool = 2'000,     .eurPool = 1'000,     .deliverAmount = 1,     .expectedSourceAmount = 3},
+            {.usdPool = 2'000,     .eurPool = 1'000,     .deliverAmount = 2,     .expectedSourceAmount = 5},
+            {.usdPool = 2'000,     .eurPool = 1'000,     .deliverAmount = 10,    .expectedSourceAmount = 21},
+            {.usdPool = 2'000,     .eurPool = 1'000,     .deliverAmount = 100,   .expectedSourceAmount = 223},
+        };
+        // clang-format on
+
+        for (auto const& testCase : testCases)
+        {
+            checkQuote(
+                testCase.usdPool,
+                testCase.eurPool,
+                testCase.deliverAmount,
+                testCase.expectedSourceAmount);
+        }
+    }
+
     void
     testFalseDry(FeatureBitset features)
     {
@@ -3583,6 +3681,7 @@ private:
         pathFind01();
         pathFind02();
         pathFind06();
+        pathFindMPTAMMExecutableSourceAmount();
     }
 
     void
diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp
index bb532b361a..971a540ff7 100644
--- a/src/test/app/AMMExtended_test.cpp
+++ b/src/test/app/AMMExtended_test.cpp
@@ -267,20 +267,39 @@ private:
             {features});
 
         // tfPassive -- place the offer without crossing it.
-        testAMM(
-            [&](AMM& ammAlice, Env& env) {
-                // Carol creates a passive offer that could cross AMM.
-                // Carol's offer should stay in the ledger.
-                env(offer(carol_, XRP(100), USD(100), tfPassive));
-                env.close();
-                BEAST_EXPECT(
-                    ammAlice.expectBalances(XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens()));
-                BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), STAmount{USD, 100}}}}));
-            },
-            {{XRP(10'100), USD(10'000)}},
-            0,
-            std::nullopt,
-            {features});
+        if (features[featureMPTokensV2])
+        {
+            Env env{*this, features};
+            fund(env, gw_, {alice_, carol_}, XRP(30'000'000), {USD(30'000'000)});
+
+            AMM const ammAlice(env, alice_, XRP(10'100'000), USD(10'000'000));
+
+            // Scale the exact-quality fixture up so the visual relationship
+            // stays clear: the passive CLOB offer has the same 1:1 quality as
+            // the generated AMM offer, so it should not cross.
+            env(offer(carol_, XRP(100'000), USD(100'000), tfPassive));
+            env.close();
+            BEAST_EXPECT(
+                ammAlice.expectBalances(XRP(10'100'000), USD(10'000'000), ammAlice.tokens()));
+            BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100'000), USD(100'000)}}}));
+        }
+        else
+        {
+            testAMM(
+                [&](AMM& ammAlice, Env& env) {
+                    // Carol creates a passive offer that could cross AMM.
+                    // Carol's offer should stay in the ledger.
+                    env(offer(carol_, XRP(100), USD(100), tfPassive));
+                    env.close();
+                    BEAST_EXPECT(ammAlice.expectBalances(
+                        XRP(10'100), STAmount{USD, 10'000}, ammAlice.tokens()));
+                    BEAST_EXPECT(expectOffers(env, carol_, 1, {{{XRP(100), STAmount{USD, 100}}}}));
+                },
+                {{XRP(10'100), USD(10'000)}},
+                0,
+                std::nullopt,
+                {features});
+        }
 
         // tfPassive -- cross only offers of better quality.
         testAMM(
@@ -1284,6 +1303,78 @@ private:
         BEAST_EXPECT(expectHolding(env, bob_, USD(0)));
     }
 
+    // Same shape as testRequireAuth, except the issuer never authorizes the AMM's own trust line.
+    // An AMM holds the asset for its liquidity providers and cannot sign a TrustSet for itself, so
+    // once pseudo-accounts are implicitly authorized the pool keeps trading. Before that the offer
+    // stream drops it and the taker's offer stays on the book.
+    void
+    testPseudoAccountRequireAuth(FeatureBitset features)
+    {
+        testcase("lsfRequireAuth, unauthorized AMM pseudo-account");
+
+        using namespace jtx;
+
+        bool const pseudoExempt = features[fixCleanup3_4_0];
+
+        Env env{*this, features};
+
+        auto const aliceUSD = alice_["USD"];
+        auto const bobUSD = bob_["USD"];
+
+        env.fund(XRP(400'000), gw_, alice_, bob_);
+        env.close();
+
+        env(fset(gw_, asfRequireAuth));
+        env.close();
+
+        env(trust(gw_, bobUSD(100)), Txflags(tfSetfAuth));
+        env(trust(bob_, USD(100)));
+        env(trust(gw_, aliceUSD(100)), Txflags(tfSetfAuth));
+        env(trust(alice_, USD(2'000)));
+        env(pay(gw_, alice_, USD(1'000)));
+        env.close();
+
+        AMM const ammAlice(env, alice_, USD(1'000), XRP(1'050));
+
+        // The pool's own line stays unauthorized: AMMCreate opens it without the flag, and the
+        // pseudo-account has no key to ask for one.
+        auto const ammLineAuthorized = [&]() -> bool {
+            auto const line =
+                env.le(keylet::trustLine(ammAlice.ammAccount(), USD.issue().account, USD.currency));
+            if (!BEAST_EXPECT(line))
+                return false;
+            return line->isFlag(
+                ammAlice.ammAccount() > USD.issue().account ? lsfLowAuth : lsfHighAuth);
+        };
+        BEAST_EXPECT(!ammLineAuthorized());
+
+        env(pay(gw_, bob_, USD(50)));
+        env.close();
+        BEAST_EXPECT(expectHolding(env, bob_, USD(50)));
+
+        // Bob sells USD into the pool, so the pool is the side that has to be authorized to hold
+        // the asset.
+        env(offer(bob_, XRP(50), USD(50)));
+        env.close();
+
+        if (pseudoExempt)
+        {
+            BEAST_EXPECT(ammAlice.expectBalances(USD(1'050), XRP(1'000), ammAlice.tokens()));
+            BEAST_EXPECT(expectOffers(env, bob_, 0));
+            BEAST_EXPECT(expectHolding(env, bob_, USD(0)));
+        }
+        else
+        {
+            // The pool is skipped, so nothing crosses and the offer rests on the book.
+            BEAST_EXPECT(ammAlice.expectBalances(USD(1'000), XRP(1'050), ammAlice.tokens()));
+            BEAST_EXPECT(expectOffers(env, bob_, 1));
+            BEAST_EXPECT(expectHolding(env, bob_, USD(50)));
+        }
+
+        // Either way the exemption skips the check rather than setting the flag.
+        BEAST_EXPECT(!ammLineAuthorized());
+    }
+
     void
     testMissingAuth(FeatureBitset features)
     {
@@ -1359,6 +1450,7 @@ private:
         testRmFundedOffer(all_ - fixAMMv1_1 - fixAMMv1_3);
         testEnforceNoRipple(all_);
         testFillModes(all_);
+        testFillModes(all_ - featureMPTokensV2);
         testOfferCrossWithXRP(all_);
         testOfferCrossWithLimitOverride(all_);
         testCurrencyConversionEntire(all_);
@@ -1380,6 +1472,8 @@ private:
         testDirectToDirectPath(all_);
         testDirectToDirectPath(all_ - fixAMMv1_1 - fixAMMv1_3);
         testRequireAuth(all_);
+        testPseudoAccountRequireAuth(all_);
+        testPseudoAccountRequireAuth(all_ - fixCleanup3_4_0);
         testMissingAuth(all_);
     }
 
diff --git a/src/test/app/AMMMPT_test.cpp b/src/test/app/AMMMPT_test.cpp
index 7078ea6769..37e0ed585d 100644
--- a/src/test/app/AMMMPT_test.cpp
+++ b/src/test/app/AMMMPT_test.cpp
@@ -15,6 +15,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -27,19 +28,24 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -3269,6 +3275,107 @@ private:
                     ammAlice.expectBalances(MPT(ammAlice[1])(1), XRP(10'000), IOUAmount{100000}));
             },
             {{XRP(10'000), gAmmmpt(10'000)}});
+
+        // MPT/MPT equal withdrawal after LP deletes both zero-balance MPTokens.
+        // AMMWithdraw must recreate both missing MPTokens; the invariant allows
+        // up to two MPToken creations per AMMWithdraw/AMMClawback (threshold > 2).
+        {
+            Env env{*this};
+            env.fund(XRP(30'000), gw_, alice_);
+            env.close();
+            MPTTester btc(
+                {.env = env,
+                 .issuer = gw_,
+                 .holders = {alice_},
+                 .pay = 10'000,
+                 .flags = kMptDexFlags});
+            MPTTester eth(
+                {.env = env,
+                 .issuer = gw_,
+                 .holders = {alice_},
+                 .pay = 10'000,
+                 .flags = kMptDexFlags});
+
+            // Alice deposits everything into the MPT/MPT pool; her MPT
+            // balances drop to zero.
+            AMM ammAlice(env, alice_, btc(10'000), eth(10'000));
+            BEAST_EXPECT(expectMPT(env, alice_, btc(0)));
+            BEAST_EXPECT(expectMPT(env, alice_, eth(0)));
+
+            // Alice deletes both zero-balance MPTokens to reclaim reserve.
+            btc.authorize({.account = alice_, .flags = tfMPTUnauthorize});
+            eth.authorize({.account = alice_, .flags = tfMPTUnauthorize});
+            BEAST_EXPECT(!env.le(keylet::mptoken(btc.issuanceID(), alice_.id())));
+            BEAST_EXPECT(!env.le(keylet::mptoken(eth.issuanceID(), alice_.id())));
+
+            // Equal withdrawal succeeds: both missing MPTokens are recreated
+            // (mptokensCreated_ == 2, which satisfies the > 2 invariant check).
+            ammAlice.withdrawAll(alice_);
+            BEAST_EXPECT(env.le(keylet::mptoken(btc.issuanceID(), alice_.id())));
+            BEAST_EXPECT(env.le(keylet::mptoken(eth.issuanceID(), alice_.id())));
+            BEAST_EXPECT(expectMPT(env, alice_, btc(10'000)));
+            BEAST_EXPECT(expectMPT(env, alice_, eth(10'000)));
+            BEAST_EXPECT(!ammAlice.ammExists());
+        }
+    }
+
+    void
+    testWithdrawReserveUsesLiveBalance()
+    {
+        testcase("Withdraw reserve check uses live balance");
+
+        using namespace jtx;
+
+        auto const test = [&](auto&& makeToken) {
+            Env env(*this);
+            env.fund(XRP(30'000), gw_, alice_, bob_);
+            env.close();
+
+            auto const token = makeToken(env);
+            AMM amm(env, gw_, XRP(100), token(100));
+
+            // The EUR trustline is an unrelated owner object. The XRP-only
+            // AMM deposit adds the LP token trustline, so Alice has 2 owners.
+            env.trust(gw_["EUR"](1), alice_);
+            amm.deposit(DepositArg{.account = alice_, .asset1In = XRP(10)});
+            BEAST_EXPECT(env.ownerCount(alice_) == 2);
+            env.require(Balance(alice_, token(kNone)));
+
+            // Drain Alice to one drop below the reserve for a third owner
+            // object, accounting for the fee on the drain payment.
+            auto const reserveForToken = reserve(env, 3);
+            auto const targetBalance = reserveForToken - XRPAmount{1};
+            auto const baseFee = env.current()->fees().base;
+            auto const currentBalance = env.balance(alice_).value().xrp();
+            auto const drainAmount = currentBalance - targetBalance - baseFee;
+            BEAST_EXPECT(drainAmount > XRPAmount{0});
+            env(pay(alice_, bob_, drops(drainAmount)));
+            env.close();
+
+            // AMMWithdraw captures priorBalance before the fee, then the XRP
+            // leg raises the live sandbox balance before the token leg.
+            // XRP(2) keeps the integral MPT side positive after rounding.
+            auto const xrpOut = XRP(2);
+            auto const tokenOut = token(2);
+            auto const priorBalance = env.balance(alice_).value().xrp();
+            auto const liveBalanceAfterXrpLeg = priorBalance - baseFee + xrpOut.value().xrp();
+            BEAST_EXPECT(priorBalance < reserveForToken);
+            BEAST_EXPECT(liveBalanceAfterXrpLeg > priorBalance);
+            BEAST_EXPECT(liveBalanceAfterXrpLeg >= reserveForToken);
+
+            // The XRP leg runs first, so the missing IOU trustline or MPToken
+            // is reserved against the updated sandbox balance.
+            amm.withdraw(
+                WithdrawArg{.account = alice_, .asset1Out = xrpOut, .asset2Out = tokenOut});
+
+            // The withdrawal succeeds only if the missing token holding can be
+            // reserved from the live balance after the XRP leg.
+            BEAST_EXPECT(env.ownerCount(alice_) == 3);
+            BEAST_EXPECT(env.balance(alice_, token).value().signum() > 0);
+        };
+
+        test([&](Env&) -> PrettyAsset { return gw_["USD"]; });
+        test([&](Env& env) -> PrettyAsset { return MPTTester({.env = env, .issuer = gw_}); });
     }
 
     void
@@ -3945,24 +4052,30 @@ private:
             [&](AMM& ammAlice, Env& env) {
                 // Bid a tiny amount
                 auto const tiny = Number{STAmount::kMinValue, STAmount::kMinOffset};
+                auto const cleanup340 = env.current()->rules().enabled(fixCleanup3_4_0);
+                auto const minBidPrice = IOUAmount{ammAuctionMinSlotPrice(ammAlice.tokens(), 1)};
+                auto const firstPrice = cleanup340 ? minBidPrice : IOUAmount{tiny};
                 env(ammAlice.bid({.account = alice_, .bidMin = IOUAmount{tiny}}));
-                // Auction slot purchase price is equal to the tiny amount
-                // since the minSlotPrice is 0 with no trading fee.
-                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny}));
-                // The purchase price is too small to affect the total tokens
+                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, firstPrice));
                 BEAST_EXPECT(ammAlice.expectBalances(
-                    MPT(ammAlice[0])(10'000'000'000), USD(10'000), ammAlice.tokens()));
+                    MPT(ammAlice[0])(10'000'000'000),
+                    USD(10'000),
+                    cleanup340 ? IOUAmount{Number{ammAlice.tokens()} - Number{minBidPrice}}
+                               : ammAlice.tokens()));
                 // Bid the tiny amount
                 env(ammAlice.bid({
                     .account = alice_,
                     .bidMin = IOUAmount{STAmount::kMinValue, STAmount::kMinOffset},
                 }));
                 // Pay slightly higher price
-                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny * Number{105, -2}}));
-                // The purchase price is still too small to affect the total
-                // tokens
+                BEAST_EXPECT(ammAlice.expectAuctionSlot(
+                    0, 0, IOUAmount{Number{firstPrice} * Number{105, -2}}));
                 BEAST_EXPECT(ammAlice.expectBalances(
-                    MPT(ammAlice[0])(10'000'000'000), USD(10'000), ammAlice.tokens()));
+                    MPT(ammAlice[0])(10'000'000'000),
+                    USD(10'000),
+                    cleanup340
+                        ? IOUAmount{Number{ammAlice.tokens()} - Number{minBidPrice} * Number{11, -1}}
+                        : ammAlice.tokens()));
             },
             {{gAmmmpt(10'000'000'000), USD(10'000)}});
 
@@ -4041,9 +4154,9 @@ private:
             {
                 auto jtx = env.jt(tx, Seq(1), Fee(10));
                 env.app().config().features.erase(featureMPTokensV2);
-                PreflightContext const pfctx(
+                PreflightContext const ctx(
                     env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
-                auto pf = AMMBid::checkExtraFeatures(pfctx);
+                auto pf = AMMBid::checkExtraFeatures(ctx);
                 BEAST_EXPECT(pf == false);
                 env.app().config().features.insert(featureMPTokensV2);
             }
@@ -4053,9 +4166,9 @@ private:
                 jtx.jv["Asset2"]["currency"] = "XRP";
                 jtx.jv["Asset2"].removeMember("mpt_issuance_id");
                 jtx.stx = env.ust(jtx);
-                PreflightContext const pfctx(
+                PreflightContext const ctx(
                     env.app(), *jtx.stx, env.current()->rules(), TapNone, env.journal);
-                auto pf = AMMBid::preflight(pfctx);
+                auto pf = AMMBid::preflight(ctx);
                 BEAST_EXPECT(pf == temBAD_AMM_TOKENS);
             }
         }
@@ -4901,7 +5014,7 @@ private:
                 XRP(10'100), MPT(ammAlice[1])(10'000'000000000001), ammAlice.tokens()));
             env.require(Balance(carol_, MPT(ammAlice[1])(30'199'999999999999)));
 
-            // Initial 30,000 - 10000(AMM pool LP) - 100(AMMoffer) -
+            // Initial 30,000 - 10000(AMM pool LP) - 100(AMM offer) -
             // - 100(offer) - 10(tx fee) - 10(tx fee of MPTTester init as
             // holder) - one reserve
             BEAST_EXPECT(expectLedgerEntryRoot(
@@ -5010,12 +5123,12 @@ private:
             env.close();
 
             BEAST_EXPECT(
-                amm.expectBalances(XRPAmount(909'090'909), btc(550'000000055001), amm.tokens()));
-            // Offer ~91XRP/49.99e12BTC
+                amm.expectBalances(XRPAmount(909'090'910), btc(549'999999450001), amm.tokens()));
+            // Offer ~91XRP/50e12BTC
             BEAST_EXPECT(expectOffers(
-                env, carol_, 1, {{Amounts{XRPAmount{9'090'909}, btc(4'999999950000)}}}));
-            // Carol pays 0.1% fee on 50'000000055000BTC = 50'000000055BTC
-            env.require(Balance(carol_, btc(29'949'949'999'944'943)));
+                env, carol_, 1, {{Amounts{XRPAmount{9'090'910}, btc(5'000000500000)}}}));
+            // Carol pays 0.1% fee on 49'999999450001BTC.
+            env.require(Balance(carol_, btc(29'949'950'000'550'548)));
         }
 
         {
@@ -5065,15 +5178,15 @@ private:
             env.close();
 
             BEAST_EXPECT(ammAlice.expectBalances(
-                btc(1'060'6848287928033), eth(1'037'0658372213574), ammAlice.tokens()));
+                btc(1'060'6848287928025), eth(1'037'0658372213582), ammAlice.tokens()));
             // Consumed offer ~72.93e13ETH/72.93e13BTC
             BEAST_EXPECT(expectOffers(
-                env, carol_, 1, {Amounts{eth(27'0658372213574), btc(27'0658372213575)}}));
+                env, carol_, 1, {Amounts{eth(27'0658372213582), btc(27'0658372213582)}}));
             BEAST_EXPECT(expectOffers(env, bob_, 0));
             BEAST_EXPECT(expectOffers(env, ed, 0));
 
-            env.require(Balance(carol_, btc(19'116'439'640'089'955)));
-            env.require(Balance(carol_, eth(20'729'341'627'786'426)));
+            env.require(Balance(carol_, btc(19'116'439'640'089'965)));
+            env.require(Balance(carol_, eth(20'729'341'627'786'418)));
             env.require(Balance(bob_, btc(20'100'000'000'000'000)));
             env.require(Balance(ed, eth(19'875'000'000'000'000)));
         }
@@ -5672,6 +5785,87 @@ private:
             });
     }
 
+    void
+    testAMMOfferGenerationPolicy(FeatureBitset features)
+    {
+        testcase("AMM payment offer generation picks economically coarser integral side");
+
+        using namespace jtx;
+
+        enum class GeneratedFirst { TakerPays, TakerGets };
+
+        auto const check = [&](std::uint64_t mptUnitsPerXRP, GeneratedFirst generatedFirst) {
+            TAmounts const pool{
+                XRPAmount{1'000'000}, MPTAmount{1'000'000'125}};
+            TAmounts const clobOffer{
+                kDropsPerXrp, MPTAmount{static_cast(mptUnitsPerXRP)}};
+            Quality const clobQuality{clobOffer};
+
+            auto const expectedAmounts = generatedFirst == GeneratedFirst::TakerGets
+                ? getAMMOfferStartWithTakerGets(pool, clobQuality, 0)
+                : getAMMOfferStartWithTakerPays(pool, clobQuality, 0);
+            auto const otherAmounts = generatedFirst == GeneratedFirst::TakerGets
+                ? getAMMOfferStartWithTakerPays(pool, clobQuality, 0)
+                : getAMMOfferStartWithTakerGets(pool, clobQuality, 0);
+            BEAST_EXPECT(expectedAmounts);
+            BEAST_EXPECT(otherAmounts);
+            if (!expectedAmounts || !otherAmounts)
+                return;
+
+            // Make the tested branch observable: these cases are chosen so the
+            // payment consumes different AMM amounts depending on which side
+            // is generated first.
+            BEAST_EXPECT(*expectedAmounts != *otherAmounts);
+
+            Env env(*this, features);
+            auto const gw = Account("gw");
+            auto const lp = Account("lp");
+            auto const maker = Account("maker");
+            auto const taker = Account("taker");
+            auto const dst = Account("dst");
+
+            env.fund(XRP(10'000), gw, lp, maker, taker, dst);
+            env.close();
+
+            MPTTester const token(
+                {.env = env, .issuer = gw, .holders = {lp, maker, dst}, .flags = kMptDexFlags});
+            env(pay(gw, lp, token(pool.out.value())));
+            env(pay(gw, maker, token(10'000'000)));
+            env.close();
+
+            AMM const amm(env, lp, drops(pool.in), token(pool.out.value()));
+            auto const makerOfferSeq = env.seq(maker);
+            env(offer(maker, XRP(1), token(mptUnitsPerXRP)), Txflags(tfPassive));
+            env.close();
+
+            env(pay(taker, dst, token(expectedAmounts->out.value())),
+                Sendmax(drops(expectedAmounts->in)));
+            env.close();
+
+            BEAST_EXPECT(amm.expectBalances(
+                drops(pool.in + expectedAmounts->in),
+                token((pool.out - expectedAmounts->out).value()),
+                amm.tokens()));
+            env.require(Balance(dst, token(expectedAmounts->out.value())));
+            BEAST_EXPECT(env.le(keylet::offer(maker.id(), SeqProxy::rawSequence(makerOfferSeq))));
+        };
+
+        // CLOB price: 10'000'000 MPT per 1 XRP, so one raw MPT unit is worth
+        // 0.1 drops. One drop is the economically coarser unit and the AMM
+        // offer is generated from takerPays.
+        check(10 * kDropsPerXrp.drops(), GeneratedFirst::TakerPays);
+
+        // CLOB price: 1'000'000 MPT per 1 XRP, so one raw MPT unit is worth
+        // one drop. Ties use takerGets to preserve the historical XRP-output
+        // behavior.
+        check(kDropsPerXrp.drops(), GeneratedFirst::TakerGets);
+
+        // CLOB price: 100'000 MPT per 1 XRP, so one raw MPT unit is worth
+        // 10 drops. MPT is the economically coarser unit and the AMM offer is
+        // generated from takerGets.
+        check(kDropsPerXrp.drops() / 10, GeneratedFirst::TakerGets);
+    }
+
     void
     testTradingFee(FeatureBitset features)
     {
@@ -7242,7 +7436,7 @@ private:
         // overflow. Deposit has no such bound, which is why only the deposit
         // path was exposed.
         //
-        // These mirror the deposit repros: the same oversized two-asset
+        // These mirror the deposit tests: the same oversized two-asset
         // request is rejected cleanly. If the preclaim bound is ever weakened,
         // equalWithdrawLimit would be reached with a huge frac and
         // Number::operator rep() would escape as tefEXCEPTION, failing this.
@@ -7295,6 +7489,57 @@ private:
         }
     }
 
+    void
+    testDanglingAMMMPTokenFreezeCheck()
+    {
+        testcase("Dangling AMM MPToken freeze check");
+
+        using namespace jtx;
+        FeatureBitset const all{testableAmendments()};
+
+        Env env(*this, all);
+
+        env.fund(XRP(1'000), gw_, alice_);
+        MPTTester usd({.env = env, .issuer = gw_});
+        MPTTester const btc({.env = env, .issuer = gw_});
+
+        AMM amm(env, gw_, usd(10'000), btc(10'000));
+        for (auto i = 0; i < kMaxDeletableAmmTrustLines + 10; ++i)
+        {
+            Account const a{std::to_string(i)};
+            env.fund(XRP(1'000), a);
+            env(trust(a, STAmount{amm.lptIssue(), 10'000}));
+            env.close();
+        }
+
+        // With too many LP-token trust lines to delete in one pass, the AMM
+        // remains in an empty state with zero-balance MPToken objects.
+        amm.withdrawAll(gw_);
+        BEAST_EXPECT(amm.ammExists());
+        BEAST_EXPECT(amm.expectBalances(usd(0), btc(0), IOUAmount{0}));
+
+        auto const ammToken = env.le(keylet::mptoken(usd.issuanceID(), amm.ammAccount()));
+        if (!BEAST_EXPECT(ammToken))
+            return;
+        BEAST_EXPECT((*ammToken)[sfMPTAmount] == 0);
+
+        usd.destroy();
+        BEAST_EXPECT(env.le(keylet::mptokenIssuance(usd.issuanceID())) == nullptr);
+        BEAST_EXPECT(!isFrozen(*env.current(), amm.ammAccount(), *ammToken));
+        // A Payment cannot cross this empty AMM because BookStep skips AMMs
+        // with zero LPTokenBalance. Probe the same ZeroIfFrozen balance read
+        // used by AMM accounting.
+        auto const balance = accountHolds(
+            *env.current(),
+            amm.ammAccount(),
+            MPTIssue{usd.issuanceID()},
+            FreezeHandling::ZeroIfFrozen,
+            AuthHandling::IgnoreAuth,
+            env.journal);
+
+        BEAST_EXPECT(balance == usd(0));
+    }
+
     void
     run() override
     {
@@ -7306,10 +7551,12 @@ private:
         testDeposit();
         testInvalidWithdraw();
         testWithdraw();
+        testWithdrawReserveUsesLiveBalance();
         testInvalidFeeVote();
         testFeeVote();
         testInvalidBid();
         testBid(all);
+        testBid(all - fixCleanup3_4_0);
         testClawback();
         testClawbackFromAMMAccount(all);
         testClawbackFromAMMAccount(all - featureSingleAssetVault);
@@ -7318,6 +7565,7 @@ private:
         testAMMTokens();
         testAmendment();
         testAMMAndCLOB(all);
+        testAMMOfferGenerationPolicy(all);
         testTradingFee(all);
         testTradingFee(all - fixAMMv1_3);
         testAdjustedTokens(all);
@@ -7334,6 +7582,7 @@ private:
         testDepositIntegralOverflowMPT(all);
         testDepositIntegralOverflowMPT(all - fixCleanup3_4_0);
         testWithdrawIntegralNoOverflowMPT();
+        testDanglingAMMMPTokenFreezeCheck();
     }
 };
 
diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp
index 77037cc67a..58d41c40b7 100644
--- a/src/test/app/AMM_test.cpp
+++ b/src/test/app/AMM_test.cpp
@@ -3127,27 +3127,59 @@ private:
             std::nullopt,
             {features});
 
+        // Zero-fee bid without an explicit price pays a floor with fixCleanup3_4_0.
+        testAMM(
+            [&](AMM& ammAlice, Env& env) {
+                auto const minBidPrice = IOUAmount{ammAuctionMinSlotPrice(ammAlice.tokens(), 1)};
+                auto const cleanup340 = features[fixCleanup3_4_0];
+                auto const expectedPrice = cleanup340 ? minBidPrice : IOUAmount{0};
+                auto const expectedTokens = cleanup340
+                    ? IOUAmount{Number{ammAlice.tokens()} - Number{minBidPrice}}
+                    : ammAlice.tokens();
+
+                env.close(seconds(kTotalTimeSlotSecs + 1));
+                env.close();
+                env(ammAlice.bid({.account = alice_}));
+                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, expectedPrice));
+                BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), expectedTokens));
+
+                ammAlice.vote(alice_, 1'000);
+                BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, expectedPrice));
+            },
+            std::nullopt,
+            0,
+            std::nullopt,
+            {features});
+
         // Bid tiny amount
         testAMM(
             [&](AMM& ammAlice, Env& env) {
                 // Bid a tiny amount
                 auto const tiny = Number{STAmount::kMinValue, STAmount::kMinOffset};
+                auto const cleanup340 = features[fixCleanup3_4_0];
+                auto const minBidPrice = IOUAmount{ammAuctionMinSlotPrice(ammAlice.tokens(), 1)};
+                auto const firstPrice = cleanup340 ? minBidPrice : IOUAmount{tiny};
                 env(ammAlice.bid({.account = alice_, .bidMin = IOUAmount{tiny}}));
-                // Auction slot purchase price is equal to the tiny amount
-                // since the minSlotPrice is 0 with no trading fee.
-                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny}));
-                // The purchase price is too small to affect the total tokens
-                BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
+                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, firstPrice));
+                BEAST_EXPECT(ammAlice.expectBalances(
+                    XRP(10'000),
+                    USD(10'000),
+                    cleanup340 ? IOUAmount{Number{ammAlice.tokens()} - Number{minBidPrice}}
+                               : ammAlice.tokens()));
                 // Bid the tiny amount
                 env(ammAlice.bid({
                     .account = alice_,
                     .bidMin = IOUAmount{STAmount::kMinValue, STAmount::kMinOffset},
                 }));
                 // Pay slightly higher price
-                BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{tiny * Number{105, -2}}));
-                // The purchase price is still too small to affect the total
-                // tokens
-                BEAST_EXPECT(ammAlice.expectBalances(XRP(10'000), USD(10'000), ammAlice.tokens()));
+                BEAST_EXPECT(ammAlice.expectAuctionSlot(
+                    0, 0, IOUAmount{Number{firstPrice} * Number{105, -2}}));
+                BEAST_EXPECT(ammAlice.expectBalances(
+                    XRP(10'000),
+                    USD(10'000),
+                    cleanup340
+                        ? IOUAmount{Number{ammAlice.tokens()} - Number{minBidPrice} * Number{11, -1}}
+                        : ammAlice.tokens()));
             },
             std::nullopt,
             0,
@@ -3778,6 +3810,21 @@ private:
                     BEAST_EXPECT(amm.expectBalances(XRP(1'000), USD(500), amm.tokens()));
                     BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}}));
                 }
+                else if (!features[featureMPTokensV2])
+                {
+                    BEAST_EXPECT(amm.expectBalances(
+                        XRPAmount(909'090'909),
+                        STAmount{USD, UINT64_C(550'000000055), -9},
+                        amm.tokens()));
+                    BEAST_EXPECT(expectOffers(
+                        env,
+                        carol_,
+                        1,
+                        {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
+                    BEAST_EXPECT(
+                        env.balance(carol_, USD) ==
+                        STAmount(USD, UINT64_C(29'949'94999999494), -11));
+                }
                 else
                 {
                     // Post-amendment the transfer fee is taken into account
@@ -3788,19 +3835,19 @@ private:
                     // quality.
                     // AMM offer ~50USD/91XRP
                     BEAST_EXPECT(amm.expectBalances(
-                        XRPAmount(909'090'909),
-                        STAmount{USD, UINT64_C(550'000000055), -9},
+                        XRPAmount(909'090'910),
+                        STAmount{USD, UINT64_C(549'99999945), -8},
                         amm.tokens()));
-                    // Offer ~91XRP/49.99USD
+                    // Offer ~91XRP/50USD
                     BEAST_EXPECT(expectOffers(
                         env,
                         carol_,
                         1,
-                        {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
+                        {{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
                     // Carol pays 0.1% fee on ~50USD =~ 0.05USD
                     BEAST_EXPECT(
                         env.balance(carol_, USD) ==
-                        STAmount(USD, UINT64_C(29'949'94999999494), -11));
+                        STAmount(USD, UINT64_C(29'949'95000060055), -11));
                 }
             },
             {{XRP(1'000), USD(500)}},
@@ -6451,7 +6498,7 @@ private:
                 BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
                 BEAST_EXPECT(expectOffers(env, carol_, 1, {{Amounts{XRP(100), USD(55)}}}));
             }
-            else
+            else if (!features[featureMPTokensV2])
             {
                 BEAST_EXPECT(amm.expectBalances(
                     XRPAmount(909'090'909),
@@ -6464,6 +6511,19 @@ private:
                     {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
                 BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
             }
+            else
+            {
+                BEAST_EXPECT(amm.expectBalances(
+                    XRPAmount(909'090'910),
+                    STAmount{USD, UINT64_C(549'99999945), -8},
+                    amm.tokens()));
+                BEAST_EXPECT(expectOffers(
+                    env,
+                    carol_,
+                    1,
+                    {{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
+                BEAST_EXPECT(expectOffers(env, bob_, 1, {{Amounts{USD(1), XRPAmount(500)}}}));
+            }
         }
 
         // There is no blocking offer, the same AMM liquidity is consumed
@@ -6475,10 +6535,30 @@ private:
             AMM const amm(env, alice_, XRP(1'000), USD(500));
             env(offer(carol_, XRP(100), USD(55)));
             env.close();
-            BEAST_EXPECT(amm.expectBalances(
-                XRPAmount(909'090'909), STAmount{USD, UINT64_C(550'000000055), -9}, amm.tokens()));
-            BEAST_EXPECT(expectOffers(
-                env, carol_, 1, {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
+            if (!features[featureMPTokensV2])
+            {
+                BEAST_EXPECT(amm.expectBalances(
+                    XRPAmount(909'090'909),
+                    STAmount{USD, UINT64_C(550'000000055), -9},
+                    amm.tokens()));
+                BEAST_EXPECT(expectOffers(
+                    env,
+                    carol_,
+                    1,
+                    {{Amounts{XRPAmount{9'090'909}, STAmount{USD, 4'99999995, -8}}}}));
+            }
+            else
+            {
+                BEAST_EXPECT(amm.expectBalances(
+                    XRPAmount(909'090'910),
+                    STAmount{USD, UINT64_C(549'99999945), -8},
+                    amm.tokens()));
+                BEAST_EXPECT(expectOffers(
+                    env,
+                    carol_,
+                    1,
+                    {{Amounts{XRPAmount{9'090'910}, STAmount{USD, 5'0000005, -7}}}}));
+            }
         }
     }
 
@@ -7388,6 +7468,7 @@ private:
         testFeeVote();
         testInvalidBid();
         testBid(all);
+        testBid(all - fixCleanup3_4_0);
         testBid(all - fixAMMv1_3);
         testBid(all - fixAMMv1_1 - fixAMMv1_3);
         testInvalidAMMPayment();
@@ -7400,6 +7481,7 @@ private:
         testFlags();
         testRippling();
         testAMMAndCLOB(all);
+        testAMMAndCLOB(all - featureMPTokensV2);
         testAMMAndCLOB(all - fixAMMv1_1 - fixAMMv1_3);
         testTradingFee(all);
         testTradingFee(all - fixAMMv1_3);
@@ -7419,8 +7501,10 @@ private:
         testOverflowOffer(all - fixAMMv1_1 - fixAMMv1_3);
         testSwapRounding();
         testFixChangeSpotPriceQuality(all);
+        testFixChangeSpotPriceQuality(all - featureMPTokensV2);
         testFixChangeSpotPriceQuality(all - fixAMMv1_1 - fixAMMv1_3);
         testFixAMMOfferBlockedByLOB(all);
+        testFixAMMOfferBlockedByLOB(all - featureMPTokensV2);
         testFixAMMOfferBlockedByLOB(all - fixAMMv1_1 - fixAMMv1_3);
         testLPTokenBalance(all);
         testLPTokenBalance(all - fixAMMv1_3);
diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp
index 15668d4d71..aa7fe898e3 100644
--- a/src/test/app/AccountDelete_test.cpp
+++ b/src/test/app/AccountDelete_test.cpp
@@ -29,6 +29,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -67,7 +69,8 @@ private:
         // We can't use env.meta() here, because meta() doesn't include
         // delivered_amount.
         env.close();
-        json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta];
+        json::Value const txResult = env.rpc("tx", txHash)[jss::result];
+        json::Value const meta = txResult[jss::meta];
 
         // Expect there to be a DeliveredAmount field.
         if (!BEAST_EXPECT(meta.isMember(sfDeliveredAmount.jsonName)))
@@ -78,6 +81,21 @@ private:
         json::Value const jsonExpect{amount.getJson(JsonOptions::Values::None)};
         BEAST_EXPECT(meta[sfDeliveredAmount.jsonName] == jsonExpect);
         BEAST_EXPECT(meta[jss::delivered_amount] == jsonExpect);
+
+        // The `ledger` RPC (with expanded transactions) should also report
+        // delivered_amount for this transaction, matching the `tx` RPC.
+        json::Value ledgerParams;
+        ledgerParams[jss::ledger_index] = txResult[jss::ledger_index].asUInt();
+        ledgerParams[jss::transactions] = true;
+        ledgerParams[jss::expand] = true;
+
+        auto const ledgerResult = env.rpc("json", "ledger", to_string(ledgerParams));
+        auto const& ledgerTx = ledgerResult[jss::result][jss::ledger][jss::transactions][0u];
+        BEAST_EXPECT(ledgerTx[jss::hash].asString() == txHash);
+
+        json::Value const& ledgerMeta = ledgerTx[jss::metaData];
+        BEAST_EXPECT(ledgerMeta[sfDeliveredAmount.jsonName] == jsonExpect);
+        BEAST_EXPECT(ledgerMeta[jss::delivered_amount] == jsonExpect);
     }
 
     // Helper function to create a payment channel.
diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp
index c332b26a5b..7e6ecfb8ca 100644
--- a/src/test/app/Batch_test.cpp
+++ b/src/test/app/Batch_test.cpp
@@ -3169,7 +3169,12 @@ class Batch_test : public beast::unit_test::Suite
         auto const debtMaximumValue = asset(25'000).value();
         auto const coverDepositValue = asset(1000).value();
 
-        auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset});
+        // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
+        // accepts closed-ended vaults, so build one with a subscription
+        // window that lets the lender deposit now, then advance the clock
+        // past SubscriptionDate before creating loans.
+        auto [tx, vaultKeylet, subscriptionDate] =
+            vault.createClosedEnded({.owner = lender, .asset = asset});
         env(tx);
         env.close();
         BEAST_EXPECT(env.le(vaultKeylet));
@@ -3177,6 +3182,9 @@ class Batch_test : public beast::unit_test::Suite
         env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit}));
         env.close();
 
+        // Move into the Investment phase before creating loans.
+        vault.closePastSubscription(subscriptionDate);
+
         auto const brokerKeylet =
             keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender)));
 
diff --git a/src/test/app/ConfidentialMPTKeyRotation_test.cpp b/src/test/app/ConfidentialMPTKeyRotation_test.cpp
new file mode 100644
index 0000000000..c4e8e607da
--- /dev/null
+++ b/src/test/app/ConfidentialMPTKeyRotation_test.cpp
@@ -0,0 +1,634 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class ConfidentialMPTKeyRotation_test : public ConfidentialTransferTestBase
+{
+    void
+    testMPTokenIssuanceSetRotateIssuerKey(FeatureBitset features)
+    {
+        testcase("MPTokenIssuanceSet rotate issuer key");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+        });
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+
+        // First-time registration.
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(alice),
+        });
+
+        // Verify that no epochs are set when registering for the first time.
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
+
+        // Rotating the issuer key requires the key rotation amendment
+        bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(bob),
+            .err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
+        });
+
+        // A rotation replaces the issuer key and bumps its epoch. The auditor
+        // key was never registered, so it and its epoch stay absent.
+        if (rotationEnabled)
+        {
+            BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, std::nullopt));
+            BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
+        }
+        else
+        {
+            BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, std::nullopt));
+            BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
+        }
+
+        if (rotationEnabled)
+        {
+            // A second rotation increments the epoch again
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+            });
+
+            BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, std::nullopt));
+        }
+    }
+
+    void
+    testMPTokenIssuanceSetRotateBothKeys(FeatureBitset features)
+    {
+        testcase("MPTokenIssuanceSet rotate both issuer and auditor keys");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const auditor("auditor");
+        MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+        });
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(auditor);
+
+        // Register both keys together.
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(alice),
+            .auditorPubKey = mptAlice.getPubKey(auditor),
+        });
+
+        // Verify that no epochs are set when registering for the first time.
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
+
+        // Rotating both keys requires the amendment
+        bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(bob),
+            .auditorPubKey = mptAlice.getPubKey(alice),
+            .err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
+        });
+
+        if (rotationEnabled)
+        {
+            BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, alice));
+            BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, 1u));
+        }
+        else
+        {
+            BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
+            BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
+        }
+
+        if (rotationEnabled)
+        {
+            // Rotating the issuer key to its current value fails.
+            // Current issuer key is bob, duplicate.
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(bob),
+                .err = tecDUPLICATE,
+            });
+
+            // Rotating the auditor key to its current value fails.
+            // Current auditor key is alice, duplicate.
+            mptAlice.set({
+                .account = alice,
+                .auditorPubKey = mptAlice.getPubKey(alice),
+                .err = tecDUPLICATE,
+            });
+
+            // The whole transaction fails when one key is unchanged, even if
+            // the other key is rotated to a new value.
+            // Current issuer key is bob, duplicate.
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(bob),
+                .auditorPubKey = mptAlice.getPubKey(auditor),
+                .err = tecDUPLICATE,
+            });
+
+            // Current auditor key is alice, duplicate.
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(auditor),
+                .auditorPubKey = mptAlice.getPubKey(alice),
+                .err = tecDUPLICATE,
+            });
+
+            // Nothing changed: keys and epochs are untouched
+            BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, 1u));
+
+            // A second rotation increments both epochs again
+            mptAlice.set({
+                .account = alice,
+                .issuerPubKey = mptAlice.getPubKey(alice),
+                .auditorPubKey = mptAlice.getPubKey(auditor),
+            });
+
+            BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, 2u));
+        }
+    }
+
+    void
+    testMPTokenIssuanceSetRotateAuditorKeyOnly(FeatureBitset features)
+    {
+        testcase("MPTokenIssuanceSet rotate auditor key only");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const auditor("auditor");
+        MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+        });
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(auditor);
+
+        // Register both keys together.
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(alice),
+            .auditorPubKey = mptAlice.getPubKey(auditor),
+        });
+
+        // A transaction carrying only the auditor key fails preflight
+        // pre-ConfidentialMPTKeyRotation; post-ConfidentialMPTKeyRotation it rotates the auditor
+        // key
+        bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
+        mptAlice.set({
+            .account = alice,
+            .auditorPubKey = mptAlice.getPubKey(bob),
+            .err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
+        });
+
+        // The issuer key keeps unchanged, and rotating only the auditor key
+        // bumps only its epoch.
+        if (rotationEnabled)
+        {
+            BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, bob));
+            BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u));
+        }
+        else
+        {
+            BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
+            BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
+        }
+
+        if (rotationEnabled)
+        {
+            // A second rotation increments the epoch again
+            mptAlice.set({
+                .account = alice,
+                .auditorPubKey = mptAlice.getPubKey(auditor),
+            });
+
+            // The issuer key epoch is still untouched.
+            BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 2u));
+        }
+    }
+
+    void
+    testMPTokenIssuanceSetRegisterAuditorKeyLater(FeatureBitset features)
+    {
+        testcase("MPTokenIssuanceSet register auditor key after issuer key");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const auditor("auditor");
+        MPTTester mptAlice(env, alice);
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+        });
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(auditor);
+
+        // Register the issuer key first. We'll register the auditor key in a separate transaction.
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(alice),
+        });
+
+        // Register the auditor key separately.
+        // pre-ConfidentialMPTKeyRotation it fails preflight; post-ConfidentialMPTKeyRotation it
+        // succeeds without touching any epoch because it's a first-time registration.
+        bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
+        mptAlice.set({
+            .account = alice,
+            .auditorPubKey = mptAlice.getPubKey(auditor),
+            .err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
+        });
+
+        BEAST_EXPECT(mptAlice.checkEncryptionKeys(
+            alice, rotationEnabled ? std::optional(auditor) : std::nullopt));
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
+    }
+
+    void
+    testMPTokenIssuanceSetRegisterAuditorKeyLaterWithCOA(FeatureBitset features)
+    {
+        testcase("MPTokenIssuanceSet register auditor key later with circulating supply");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const auditor("auditor");
+        MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+        });
+
+        mptAlice.authorize({.account = bob});
+        mptAlice.pay(alice, bob, 100);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(auditor);
+
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(alice),
+        });
+
+        // Convert some of bob's balance so that COA > 0
+        mptAlice.convert({
+            .account = bob,
+            .amt = 50,
+            .holderPubKey = mptAlice.getPubKey(bob),
+        });
+
+        auto const sleIssuanceBefore = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
+        if (!BEAST_EXPECT(sleIssuanceBefore))
+            return;
+        auto const coaBefore = (*sleIssuanceBefore)[~sfConfidentialOutstandingAmount].value_or(0);
+        BEAST_EXPECT(coaBefore > 0);
+
+        // Registering the auditor key for the first time while confidential
+        // supply is circulating: pre-ConfidentialMPTKeyRotation an auditor-only
+        // transaction fails preflight; post-ConfidentialMPTKeyRotation it
+        // succeeds as a first-time late-registration even COA > 0.
+        bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
+        mptAlice.set({
+            .account = alice,
+            .auditorPubKey = mptAlice.getPubKey(auditor),
+            .err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
+        });
+
+        auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
+        if (!BEAST_EXPECT(sleIssuance))
+            return;
+        BEAST_EXPECT(mptAlice.checkEncryptionKeys(
+            alice, rotationEnabled ? std::optional(auditor) : std::nullopt));
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
+
+        // The circulating supply itself is not affected.
+        BEAST_EXPECT((*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) == coaBefore);
+    }
+
+    void
+    testMPTokenIssuanceSetAuditorKeyWithoutIssuerKey(FeatureBitset features)
+    {
+        testcase("MPTokenIssuanceSet auditor key requires issuer key");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const auditor("auditor");
+        MPTTester mptAlice(env, alice);
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+        });
+
+        mptAlice.generateKeyPair(auditor);
+        // The issuer key was never registered. pre-ConfidentialMPTKeyRotation an auditor-only
+        // transaction fails preflight; post-ConfidentialMPTKeyRotation it passes preflight
+        // but preclaim rejects registering an auditor key on an issuance
+        // without an issuer key.
+        bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
+        mptAlice.set({
+            .account = alice,
+            .auditorPubKey = mptAlice.getPubKey(auditor),
+            .err = rotationEnabled ? TER(tecNO_PERMISSION) : TER(temMALFORMED),
+        });
+
+        // The rejected transaction leaves the issuance without either key.
+        BEAST_EXPECT(mptAlice.checkEncryptionKeys(std::nullopt, std::nullopt));
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
+    }
+
+    void
+    testMPTokenIssuanceSetRotateWithCOA(FeatureBitset features)
+    {
+        testcase("MPTokenIssuanceSet rotate with circulating confidential supply");
+        using namespace test::jtx;
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        MPTTester mptAlice(env, alice, {.holders = {bob}});
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+        });
+
+        mptAlice.authorize({.account = bob});
+        mptAlice.pay(alice, bob, 100);
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(carol);
+
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(alice),
+        });
+
+        // Convert some of bob's balance to confidential spending, so that the
+        // issuance has confidential supply. COA > 0.
+        mptAlice.convert({
+            .account = bob,
+            .amt = 50,
+            .holderPubKey = mptAlice.getPubKey(bob),
+        });
+
+        auto const sleIssuanceBeforeRotation =
+            env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
+        if (!BEAST_EXPECT(sleIssuanceBeforeRotation))
+            return;
+        auto const coaBeforeRotation =
+            (*sleIssuanceBeforeRotation)[~sfConfidentialOutstandingAmount].value_or(0);
+        BEAST_EXPECT(coaBeforeRotation > 0);
+
+        // Rotating key requires the
+        // amendment.
+        bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(carol),
+            .err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
+        });
+
+        auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
+        if (!BEAST_EXPECT(sleIssuance))
+            return;
+        if (rotationEnabled)
+        {
+            BEAST_EXPECT(mptAlice.checkEncryptionKeys(carol, std::nullopt));
+            BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
+        }
+        else
+        {
+            BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, std::nullopt));
+            BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
+        }
+
+        // The confidential outstanding amount is not affected by the rotation
+        BEAST_EXPECT(
+            (*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) == coaBeforeRotation);
+
+        // Re-enabling confidential balances while supply is circulating is
+        // rejected regardless of the ConfidentialMPTKeyRotation amendment.
+        mptAlice.set({
+            .account = alice,
+            .flags = tfMPTSetCanHoldConfidentialBalance,
+            .err = tecNO_PERMISSION,
+        });
+    }
+
+    void
+    testMPTokenIssuanceSetKeyEpochAtMax(FeatureBitset features)
+    {
+        using namespace test::jtx;
+        if (!features[featureConfidentialMPTKeyRotation])
+            return;
+
+        testcase("MPTokenIssuanceSet key epoch cannot wrap");
+
+        Env env{*this, features};
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const auditor("auditor");
+
+        // Keep the ledger open so that we can write the key epochs directly into it.
+        MPTTester mptAlice(env, alice, {.holders = {bob}, .close = false});
+
+        mptAlice.create({
+            .ownerCount = 1,
+            .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
+        });
+
+        mptAlice.generateKeyPair(alice);
+        mptAlice.generateKeyPair(bob);
+        mptAlice.generateKeyPair(carol);
+        mptAlice.generateKeyPair(auditor);
+
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(alice),
+            .auditorPubKey = mptAlice.getPubKey(auditor),
+        });
+
+        auto const issuanceKeylet = keylet::mptokenIssuance(mptAlice.issuanceID());
+
+        // Writes the supplied key epochs straight into the open ledger so that
+        // the maximum epoch is reachable without submitting four billion
+        // rotations.
+        auto setEpochs = [&](std::optional const& issuerKeyEpoch,
+                             std::optional const& auditorKeyEpoch) {
+            env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
+                auto const sle = view.read(issuanceKeylet);
+                if (!sle)
+                    return false;  // LCOV_EXCL_LINE
+
+                auto replacement = std::make_shared(*sle);
+                if (issuerKeyEpoch)
+                    (*replacement)[sfIssuerKeyEpoch] = *issuerKeyEpoch;
+                if (auditorKeyEpoch)
+                    (*replacement)[sfAuditorKeyEpoch] = *auditorKeyEpoch;
+                view.rawReplace(replacement);
+                return true;
+            });
+        };
+
+        BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
+
+        // Increment the auditor epoch to kMaxKeyEpoch - 1, leaving the issuer epoch absent.
+        setEpochs(std::nullopt, kMaxKeyEpoch - 1);
+        BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch - 1));
+
+        // Rotating the auditor key to kMaxKeyEpoch succeeds.
+        mptAlice.set({
+            .account = alice,
+            .auditorPubKey = mptAlice.getPubKey(carol),
+        });
+
+        BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, carol));
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch));
+
+        // A further auditor rotation is rejected because the epoch is exhausted.
+        mptAlice.set({
+            .account = alice,
+            .auditorPubKey = mptAlice.getPubKey(bob),
+            .err = tecNO_PERMISSION,
+        });
+
+        // Rotating both keys at once is rejected as a whole because the auditor
+        // epoch is exhausted.
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(auditor),
+            .auditorPubKey = mptAlice.getPubKey(bob),
+            .err = tecNO_PERMISSION,
+        });
+
+        // Both rejections leave every key and epoch as it was.
+        BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, carol));
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch));
+
+        // The issuer key is unaffected by the exhausted auditor epoch.
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(bob),
+        });
+
+        BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, carol));
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, kMaxKeyEpoch));
+
+        // Increment the issuer epoch to kMaxKeyEpoch - 1.
+        setEpochs(kMaxKeyEpoch - 1, std::nullopt);
+        BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, carol));
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch - 1, kMaxKeyEpoch));
+
+        // Rotating the issuer key to kMaxKeyEpoch succeeds.
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(auditor),
+        });
+
+        BEAST_EXPECT(mptAlice.checkEncryptionKeys(auditor, carol));
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch, kMaxKeyEpoch));
+
+        // With both epochs exhausted neither key can be rotated again.
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(alice),
+            .err = tecNO_PERMISSION,
+        });
+        mptAlice.set({
+            .account = alice,
+            .auditorPubKey = mptAlice.getPubKey(bob),
+            .err = tecNO_PERMISSION,
+        });
+        mptAlice.set({
+            .account = alice,
+            .issuerPubKey = mptAlice.getPubKey(alice),
+            .auditorPubKey = mptAlice.getPubKey(bob),
+            .err = tecNO_PERMISSION,
+        });
+
+        BEAST_EXPECT(mptAlice.checkEncryptionKeys(auditor, carol));
+        BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch, kMaxKeyEpoch));
+    }
+
+    void
+    testMPTokenIssuanceSetWithFeats(FeatureBitset features)
+    {
+        testMPTokenIssuanceSetRotateIssuerKey(features);
+        testMPTokenIssuanceSetRotateBothKeys(features);
+        testMPTokenIssuanceSetRotateAuditorKeyOnly(features);
+        testMPTokenIssuanceSetRegisterAuditorKeyLater(features);
+        testMPTokenIssuanceSetRegisterAuditorKeyLaterWithCOA(features);
+        testMPTokenIssuanceSetAuditorKeyWithoutIssuerKey(features);
+        testMPTokenIssuanceSetRotateWithCOA(features);
+        testMPTokenIssuanceSetKeyEpochAtMax(features);
+    }
+
+public:
+    void
+    run() override
+    {
+        using namespace test::jtx;
+        FeatureBitset const all{testableAmendments()};
+
+        testMPTokenIssuanceSetWithFeats(all);
+        testMPTokenIssuanceSetWithFeats(all - featureConfidentialMPTKeyRotation);
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(ConfidentialMPTKeyRotation, app, xrpl);
+
+}  // namespace xrpl
diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp
index 6450ceeb61..a964193c1a 100644
--- a/src/test/app/ConfidentialTransfer_test.cpp
+++ b/src/test/app/ConfidentialTransfer_test.cpp
@@ -736,12 +736,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
                 .err = temMALFORMED,
             });
 
-            // Cannot set auditor key without issuer key
-            mptAlice.set({
-                .account = alice,
-                .auditorPubKey = mptAlice.getPubKey(alice),
-                .err = temMALFORMED,
-            });
+            // Note: "auditor key without issuer key" (temMALFORMED before
+            // ConfidentialMPTKeyRotation) is covered in ConfidentialMPTKeyRotation_test
 
             // Cannot set Holder and issuer Keys in the same transaction
             mptAlice.set({
@@ -787,9 +783,9 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
             });
         }
 
-        // Cannot update issuer public key once set
+        // Cannot update issuer public key once set (pre-ConfidentialMPTKeyRotation behavior)
         {
-            Env env{*this, features};
+            Env env{*this, features - featureConfidentialMPTKeyRotation};
             Account const alice("alice");
             Account const bob("bob");
             MPTTester mptAlice(env, alice, {.holders = {bob}});
@@ -819,8 +815,9 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
         // Cannot update issuer and auditor public keys once set
         // Note: trying to set only auditor key fails in preflight (temMALFORMED)
         // so we must provide both keys, which fails on issuer key check first
+        // (pre-ConfidentialMPTKeyRotation behavior)
         {
-            Env env{*this, features};
+            Env env{*this, features - featureConfidentialMPTKeyRotation};
             Account const alice("alice");
             Account const bob("bob");
             Account const auditor("auditor");
@@ -900,8 +897,9 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
         }
 
         // Set issuer key first, then auditor key in a separate tx
+        // (pre-ConfidentialMPTKeyRotation behavior)
         {
-            Env env{*this, features};
+            Env env{*this, features - featureConfidentialMPTKeyRotation};
             Account const alice("alice");
             Account const auditor("auditor");
             MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
diff --git a/src/test/app/Contract_test.cpp b/src/test/app/Contract_test.cpp
index 6902942598..94acdfba94 100644
--- a/src/test/app/Contract_test.cpp
+++ b/src/test/app/Contract_test.cpp
@@ -1,4 +1,3 @@
-#include 
 #include 
 #include 
 #include 
@@ -61,6 +60,8 @@
 #include 
 #include 
 
+#include 
+
 namespace xrpl {
 namespace test {
 
@@ -1495,7 +1496,7 @@ class Contract_test : public beast::unit_test::Suite
         std::string const& dir = "e2e-tests";
         std::string const name = "/Users/darkmatter/projects/ledger-works/xrpl-wasm-std/" + dir +
             "/" + contract_name + "/target/wasm32v1-none/release/" + contract_name + ".wasm";
-        if (!boost::filesystem::exists(name))
+        if (!std::filesystem::exists(name))
         {
             std::cout << "File does not exist: " << name << "\n";
             return "";
diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp
index c69e95a654..61f5746397 100644
--- a/src/test/app/Delegate_test.cpp
+++ b/src/test/app/Delegate_test.cpp
@@ -47,6 +47,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -2718,19 +2719,24 @@ class Delegate_test : public beast::unit_test::Suite
 
         std::size_t delegableCount = 0;
 
+#pragma push_macro("UNWRAP")
+#undef UNWRAP
 #pragma push_macro("TRANSACTION")
 #undef TRANSACTION
 
-#define TRANSACTION(tag, value, name, txDelegable, ...) \
-    if (txDelegable == Delegation::Delegable)           \
-    {                                                   \
-        delegableCount++;                               \
+#define UNWRAP(...) __VA_ARGS__
+#define TRANSACTION(tag, value, name, settings, ...)                                 \
+    if ((xrpl::TxSettings UNWRAP settings).delegable == xrpl::Delegation::Delegable) \
+    {                                                                                \
+        delegableCount++;                                                            \
     }
 
 #include 
 
 #undef TRANSACTION
 #pragma pop_macro("TRANSACTION")
+#undef UNWRAP
+#pragma pop_macro("UNWRAP")
 
         // ====================================================================
         // IMPORTANT NOTICE:
@@ -2750,7 +2756,7 @@ class Delegate_test : public beast::unit_test::Suite
         // DO NOT modify expectedDelegableCount unless all scenarios, including
         // edge cases, have been fully tested and verified.
         // ====================================================================
-        std::size_t const expectedDelegableCount = 63;
+        std::size_t const expectedDelegableCount = 62;
 
         BEAST_EXPECTS(
             delegableCount == expectedDelegableCount,
diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp
index 881441e0f9..c987e603be 100644
--- a/src/test/app/DepositAuth_test.cpp
+++ b/src/test/app/DepositAuth_test.cpp
@@ -934,6 +934,46 @@ struct DepositPreauth_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testZeroCredentialID(FeatureBitset features)
+    {
+        testcase("Zero credential ID");
+
+        using namespace jtx;
+
+        char const credType[] = "abcde";
+        Account const issuer{"issuer"};
+        Account const alice{"alice"};
+        Account const bob{"bob"};
+
+        Env env(*this, features);
+
+        env.fund(XRP(5000), issuer, alice, bob);
+        env.close();
+
+        env(credentials::create(alice, issuer, credType));
+        env.close();
+        env(credentials::accept(alice, issuer, credType));
+        env.close();
+
+        auto const jv = credentials::ledgerEntry(env, alice, issuer, credType);
+        std::string const credIdx = jv[jss::result][jss::index].asString();
+
+        std::string const zeroIdx(64, '0');
+
+        // post-fixCleanup3_4_0: a zero ID is rejected by checkFields in
+        // preflight; pre-fixCleanup3_4_0, it will trigger assertion, so it is not testable.
+        env(pay(alice, bob, XRP(100)), credentials::Ids({zeroIdx}), Ter(temMALFORMED));
+        env.close();
+
+        env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx, zeroIdx}), Ter(temMALFORMED));
+        env.close();
+
+        // A valid credential succeeds
+        env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx}));
+        env.close();
+    }
+
     void
     testCredentialsCreation()
     {
@@ -1446,6 +1486,7 @@ struct DepositPreauth_test : public beast::unit_test::Suite
         testPayment(supported - featureCredentials);
         testPayment(supported);
         testCredentialsPayment();
+        testZeroCredentialID(supported);
         testCredentialsCreation();
         testExpiredCreds();
         testSortingCredentials();
diff --git a/src/test/app/EscrowSmart_test.cpp b/src/test/app/EscrowSmart_test.cpp
deleted file mode 100644
index 118fa3e41e..0000000000
--- a/src/test/app/EscrowSmart_test.cpp
+++ /dev/null
@@ -1,1333 +0,0 @@
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-namespace xrpl::test {
-
-struct EscrowSmart_test : public beast::unit_test::Suite
-{
-    void
-    testCreateBytecodePreflight(FeatureBitset features)
-    {
-        testcase("Test preflight checks involving Bytecode");
-
-        using namespace jtx;
-        using namespace std::chrono;
-
-        Account const alice{"alice"};
-        Account const carol{"carol"};
-
-        // Tests whether the ledger index is >= 5
-        // getLedgerSqn() >= 5}
-
-        {
-            // featureSmartEscrow disabled
-            Env env(*this, features - featureSmartEscrow);
-            env.fund(XRP(5000), alice, carol);
-            XRPAmount const txnFees = env.current()->fees().base + 1000;
-            auto const escrowCreate = escrow::create(alice, carol, XRP(1000));
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCancelTime(env.now() + 100s),
-                Fee(txnFees),
-                Ter(temDISABLED));
-            env.close();
-
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCancelTime(env.now() + 100s),
-                escrow::Data("00112233"),
-                Fee(txnFees),
-                Ter(temDISABLED));
-            env.close();
-        }
-
-        {
-            // Bytecode > max length
-            Env env(
-                *this,
-                envconfig([](std::unique_ptr cfg) {
-                    cfg->fees.bytecodeSizeLimit = 10;  // 10 bytes
-                    return cfg;
-                }),
-                features);
-            XRPAmount const txnFees = env.current()->fees().base + 1000;
-            // create escrow
-            env.fund(XRP(5000), alice, carol);
-
-            auto const escrowCreate = escrow::create(alice, carol, XRP(500));
-
-            // 11-byte string
-            std::string const longWasmHex = "00112233445566778899AA";
-            env(escrowCreate,
-                escrow::Bytecode(longWasmHex),
-                escrow::kCancelTime(env.now() + 100s),
-                Fee(txnFees),
-                Ter(temMALFORMED));
-            env.close();
-        }
-
-        {
-            // compute limit set to 0
-            Env env(
-                *this,
-                envconfig([](std::unique_ptr cfg) {
-                    // WASM runtime disabled
-                    cfg->fees.gasLimit = 0;
-                    return cfg;
-                }),
-                features);
-            XRPAmount const txnFees = env.current()->fees().base + 1000;
-            // create escrow
-            env.fund(XRP(5000), alice, carol);
-
-            auto const escrowCreate = escrow::create(alice, carol, XRP(500));
-
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCancelTime(env.now() + 100s),
-                escrow::Gas(100),
-                Fee(txnFees),
-                Ter(temMALFORMED));
-            env.close();
-        }
-
-        {
-            // size limit set to 0
-            Env env(
-                *this,
-                envconfig([](std::unique_ptr cfg) {
-                    cfg->fees.bytecodeSizeLimit = 0;  // WASM upload disabled
-                    return cfg;
-                }),
-                features);
-            XRPAmount const txnFees = env.current()->fees().base + 1000;
-            // create escrow
-            env.fund(XRP(5000), alice, carol);
-
-            auto const escrowCreate = escrow::create(alice, carol, XRP(500));
-
-            // 2-byte string
-            env(escrowCreate,
-                escrow::Bytecode("AA"),
-                escrow::kCancelTime(env.now() + 100s),
-                Fee(txnFees),
-                Ter(temTEMP_DISABLED));
-            env.close();
-
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCancelTime(env.now() + 100s),
-                Fee(txnFees),
-                Ter(temTEMP_DISABLED));
-            env.close();
-        }
-
-        {
-            // Data without Bytecode
-            Env env(*this, features);
-            XRPAmount const txnFees = env.current()->fees().base + 100000;
-            // create escrow
-            env.fund(XRP(5000), alice, carol);
-
-            auto const escrowCreate = escrow::create(alice, carol, XRP(500));
-
-            std::string const longData(4, 'A');
-            env(escrowCreate,
-                escrow::Data(longData),
-                escrow::kFinishTime(env.now() + 100s),
-                Fee(txnFees),
-                Ter(temMALFORMED));
-            env.close();
-        }
-
-        {
-            // Data > max length
-            Env env(*this, features);
-            XRPAmount const txnFees = env.current()->fees().base + 100000;
-            // create escrow
-            env.fund(XRP(5000), alice, carol);
-
-            auto const escrowCreate = escrow::create(alice, carol, XRP(500));
-
-            // string of length kMaxWasmDataLength * 2 + 2
-            std::string const longData((kMaxWasmDataLength + 1) * 2, 'B');
-            env(escrowCreate,
-                escrow::Data(longData),
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCancelTime(env.now() + 100s),
-                Fee(txnFees),
-                Ter(temMALFORMED));
-            env.close();
-        }
-
-        Env env(
-            *this,
-            envconfig([](std::unique_ptr cfg) {
-                cfg->startUp = StartUpType::Fresh;
-                return cfg;
-            }),
-            features);
-        XRPAmount const txnFees =
-            env.current()->fees().base * 10 + kLedgerSqnWasmHex.size() / 2 * 5;
-        // create escrow
-        env.fund(XRP(5000), alice, carol);
-
-        auto escrowCreate = escrow::create(alice, carol, XRP(500));
-
-        // Success situations
-        {
-            // Bytecode + CancelAfter
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCancelTime(env.now() + 20s),
-                Fee(txnFees));
-            env.close();
-        }
-        {
-            // Bytecode + Condition + CancelAfter
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCancelTime(env.now() + 30s),
-                escrow::kCondition(escrow::kCb1),
-                Fee(txnFees));
-            env.close();
-        }
-        {
-            // Bytecode + FinishAfter + CancelAfter
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCancelTime(env.now() + 40s),
-                escrow::kFinishTime(env.now() + 2s),
-                Fee(txnFees));
-            env.close();
-        }
-        {
-            // Bytecode + FinishAfter + Condition + CancelAfter
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCancelTime(env.now() + 50s),
-                escrow::kCondition(escrow::kCb1),
-                escrow::kFinishTime(env.now() + 2s),
-                Fee(txnFees));
-            env.close();
-        }
-
-        // Failure situations (i.e. all other combinations)
-        {
-            // only Bytecode
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                Fee(txnFees),
-                Ter(temBAD_EXPIRATION));
-            env.close();
-        }
-        {
-            // Bytecode + FinishAfter
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kFinishTime(env.now() + 2s),
-                Fee(txnFees),
-                Ter(temBAD_EXPIRATION));
-            env.close();
-        }
-        {
-            // Bytecode + Condition
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCondition(escrow::kCb1),
-                Fee(txnFees),
-                Ter(temBAD_EXPIRATION));
-            env.close();
-        }
-        {
-            // Bytecode + FinishAfter + Condition
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCondition(escrow::kCb1),
-                escrow::kFinishTime(env.now() + 2s),
-                Fee(txnFees),
-                Ter(temBAD_EXPIRATION));
-            env.close();
-        }
-        {
-            // Bytecode 0 length
-            env(escrowCreate,
-                escrow::Bytecode(""),
-                escrow::kCancelTime(env.now() + 60s),
-                Fee(txnFees),
-                Ter(temMALFORMED));
-            env.close();
-        }
-        {
-            // Not enough fees
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCancelTime(env.now() + 70s),
-                Fee(txnFees - 1),
-                Ter(telINSUF_FEE_P));
-            env.close();
-        }
-
-        {
-            // Bytecode nonexistent host function
-            // pub fn finish() -> bool {
-            //     unsafe { host_lib::bad() >= 5 }
-            // }
-            auto const badWasmHex =
-                "0061736d010000000105016000017f02100108686f73745f6c696203626164"
-                "00000302010005030100100611027f00418080c0000b7f00418080c0000b07"
-                "2e04066d656d6f727902000666696e69736800010a5f5f646174615f656e64"
-                "03000b5f5f686561705f6261736503010a09010700100041044a0b004d0970"
-                "726f64756365727302086c616e6775616765010452757374000c70726f6365"
-                "737365642d6279010572757374631d312e38352e3120283465623136313235"
-                "3020323032352d30332d31352900490f7461726765745f6665617475726573"
-                "042b0f6d757461626c652d676c6f62616c732b087369676e2d6578742b0f72"
-                "65666572656e63652d74797065732b0a6d756c746976616c7565";
-            env(escrowCreate,
-                escrow::Bytecode(badWasmHex),
-                escrow::kCancelTime(env.now() + 100s),
-                Fee(txnFees),
-                Ter(temINVALID_BYTECODE));
-            env.close();
-        }
-    }
-
-    void
-    testFinishWasmFailures(FeatureBitset features)
-    {
-        testcase("EscrowFinish Smart Escrow failures");
-
-        using namespace jtx;
-        using namespace std::chrono;
-
-        Account const alice{"alice"};
-        Account const carol{"carol"};
-
-        // Tests whether the ledger index is >= 5
-        // getLedgerSqn() >= 5}
-
-        {
-            // featureSmartEscrow disabled
-            Env env(*this, features - featureSmartEscrow);
-            env.fund(XRP(5000), alice, carol);
-            XRPAmount const txnFees =
-                env.current()->fees().base * 10 + kLedgerSqnWasmHex.size() / 2 * 5;
-            env(escrow::finish(carol, alice, 1), Fee(txnFees), escrow::Gas(4), Ter(temDISABLED));
-            env.close();
-        }
-
-        {
-            // Gas > max compute limit
-            Env env(
-                *this,
-                envconfig([](std::unique_ptr cfg) {
-                    cfg->fees.gasLimit = 1'000;  // in gas
-                    return cfg;
-                }),
-                features);
-            env.fund(XRP(5000), alice, carol);
-            // Run past the flag ledger so that a Fee change vote occurs and
-            // updates FeeSettings. (It also activates all supported
-            // amendments.)
-            for (auto i = env.current()->seq(); i <= 257; ++i)
-                env.close();
-
-            auto const allowance = 1'001;
-            env(escrow::finish(carol, alice, 1),
-                Fee(env.current()->fees().base + allowance),
-                escrow::Gas(allowance),
-                Ter(temBAD_LIMIT));
-        }
-
-        {
-            // WASM compute disabled
-            using namespace test::jtx;
-            using namespace std::chrono;
-            Env env{*this, envconfig([](std::unique_ptr cfg) {
-                        cfg->fees.gasLimit = 0;
-                        return cfg;
-                    })};
-
-            Account const alice{"alice"};
-            env.fund(XRP(1000), alice);
-            env.close();
-
-            auto const seq = env.seq(alice);
-            auto const keylet = keylet::escrow(alice.id(), SeqProxy::rawSequence(seq));
-            env(noop(alice));  // to align sequence numbers
-
-            // This adds the Escrow ledger object by hand, bypassing normal
-            // transaction processing This is necessary because the config
-            // cannot be updated in the middle of a test, and we cannot easily
-            // create a Smart Escrow while the compute limit is set to 0
-            env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
-                auto sle = std::make_shared(keylet);
-
-                sle->setAccountID(sfAccount, alice.id());
-                sle->setFieldAmount(sfAmount, XRP(100));
-                sle->setFieldU32(sfCancelAfter, 110);
-                sle->setAccountID(sfDestination, alice.id());
-                sle->setFieldVL(sfBytecode, strUnHex(kLedgerSqnWasmHex).value());
-                sle->setFieldU32(sfFlags, 0);
-                sle->setFieldU64(sfOwnerNode, 0);
-                uint256 tmp;
-                BEAST_EXPECT(tmp.parseHex(
-                    "F63D1A452A96C19EFD77901FB37D236C59EAA746771A6"
-                    "85D1BBA57A2238B9401"));
-                sle->setFieldH256(sfPreviousTxnID, tmp);
-                sle->setFieldU32(sfPreviousTxnLgrSeq, 4);
-                sle->setFieldU32(sfSequence, seq);
-
-                view.rawInsert(sle);
-                return true;
-            });
-            BEAST_EXPECT(env.le(keylet));
-
-            env(escrow::finish(alice, alice, seq),
-                escrow::Gas(1000),
-                Fee(env.current()->fees().base + 1000),
-                Ter(temTEMP_DISABLED));
-        }
-
-        Env env(*this, features);
-
-        // Run past the flag ledger so that a Fee change vote occurs and
-        // updates FeeSettings. (It also activates all supported
-        // amendments.)
-        for (auto i = env.current()->seq(); i <= 257; ++i)
-            env.close();
-
-        XRPAmount const txnFees =
-            env.current()->fees().base * 10 + kLedgerSqnWasmHex.size() / 2 * 5;
-        env.fund(XRP(5000), alice, carol);
-
-        // create escrow
-        auto const seq = env.seq(alice);
-        env(escrow::create(alice, carol, XRP(500)),
-            escrow::Bytecode(kLedgerSqnWasmHex),
-            escrow::kCancelTime(env.now() + 100s),
-            Fee(txnFees));
-        env.close();
-
-        {
-            // no Gas field
-            env(escrow::finish(carol, alice, seq), Ter(tefBYTECODE_NOT_INCLUDED));
-        }
-
-        {
-            // Gas value of 0
-            env(escrow::finish(carol, alice, seq), escrow::Gas(0), Ter(temBAD_LIMIT));
-        }
-
-        {
-            // not enough fees
-            // This function takes 4 gas
-            // In testing, 1 gas costs 1 drop
-            auto const finishFee = env.current()->fees().base + 3;
-            env(escrow::finish(carol, alice, seq),
-                Fee(finishFee),
-                escrow::Gas(4),
-                Ter(telINSUF_FEE_P));
-        }
-
-        {
-            // not enough gas
-            // This function takes 4 gas
-            // In testing, 1 gas costs 1 drop
-            auto const finishFee = env.current()->fees().base + 4;
-            env(escrow::finish(carol, alice, seq),
-                Fee(finishFee),
-                escrow::Gas(2),
-                Ter(tecOUT_OF_GAS));
-
-            // Running out of gas still reports the gas consumed, which is the
-            // whole allowance. The function did not run to completion, so
-            // there is no return code to report.
-            auto const txMeta = env.meta();
-            if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
-            {
-                BEAST_EXPECTS(
-                    txMeta->getFieldU32(sfGasUsed) == 2,
-                    std::to_string(txMeta->getFieldU32(sfGasUsed)));
-            }
-            BEAST_EXPECT(txMeta && !txMeta->isFieldPresent(sfVMReturnCode));
-        }
-
-        {
-            // Gas field included w/no Bytecode on
-            // escrow
-            auto const seq2 = env.seq(alice);
-            env(escrow::create(alice, carol, XRP(500)),
-                escrow::kFinishTime(env.now() + 10s),
-                escrow::kCancelTime(env.now() + 100s));
-            env.close();
-
-            auto const allowance = 100;
-            env(escrow::finish(carol, alice, seq2),
-                Fee(env.current()->fees().base +
-                    (allowance * env.current()->fees().gasPrice) / microDropsPerDrop + 1),
-                escrow::Gas(allowance),
-                Ter(tefNO_BYTECODE));
-        }
-
-        {
-            // a trap in the wasm code reports the gas it burned, which is only
-            // part of the allowance
-            auto const trapSeq = env.seq(alice);
-            env(escrow::create(alice, carol, XRP(500)),
-                escrow::Bytecode(kTrapUnreachableHex),
-                escrow::kCancelTime(env.now() + 100s),
-                Fee(env.current()->fees().base * 10 + kTrapUnreachableHex.size() / 2 * 5));
-            env.close();
-
-            std::uint32_t const allowance = 1000;
-            env(escrow::finish(carol, alice, trapSeq),
-                Fee(env.current()->fees().base +
-                    (allowance * env.current()->fees().gasPrice) / microDropsPerDrop + 1),
-                escrow::Gas(allowance),
-                Ter(tecFAILED_PROCESSING));
-
-            auto const txMeta = env.meta();
-            if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
-            {
-                auto const gasUsed = txMeta->getFieldU32(sfGasUsed);
-                BEAST_EXPECTS(gasUsed < allowance, std::to_string(gasUsed));
-            }
-            BEAST_EXPECT(txMeta && !txMeta->isFieldPresent(sfVMReturnCode));
-        }
-    }
-
-    void
-    testBytecode(FeatureBitset features)
-    {
-        testcase("Example escrow function");
-
-        using namespace jtx;
-        using namespace std::chrono;
-
-        Account const alice{"alice"};
-        Account const carol{"carol"};
-
-        // Tests whether the ledger index is >= 5
-        // getLedgerSqn() >= 5}
-        std::uint32_t const allowance = 467;
-        auto escrowCreate = escrow::create(alice, carol, XRP(1000));
-        auto [createFee, finishFee] = [&]() {
-            Env const env(*this, features);
-            auto createFee = env.current()->fees().base * 10 + kLedgerSqnWasmHex.size() / 2 * 5;
-            auto finishFee = env.current()->fees().base +
-                (allowance * env.current()->fees().gasPrice) / microDropsPerDrop + 1;
-            return std::make_pair(createFee, finishFee);
-        }();
-
-        {
-            // basic Bytecode situation
-            Env env(*this, features);
-            // create escrow
-            env.fund(XRP(5000), alice, carol);
-            auto const seq = env.seq(alice);
-            BEAST_EXPECT(env.ownerCount(alice) == 0);
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCancelTime(env.now() + 100s),
-                Fee(createFee));
-            env.close();
-
-            if (BEAST_EXPECT(env.ownerCount(alice) == 2))
-            {
-                env.require(Balance(alice, XRP(4000) - createFee));
-                env.require(Balance(carol, XRP(5000)));
-
-                env(escrow::finish(carol, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee),
-                    Ter(tecBYTECODE_REJECTED));
-                env(escrow::finish(alice, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee),
-                    Ter(tecBYTECODE_REJECTED));
-                env(escrow::finish(alice, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee),
-                    Ter(tecBYTECODE_REJECTED));
-                env(escrow::finish(carol, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee),
-                    Ter(tecBYTECODE_REJECTED));
-                env(escrow::finish(carol, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee),
-                    Ter(tecBYTECODE_REJECTED));
-                env.close();
-
-                {
-                    auto const txMeta = env.meta();
-                    if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
-                    {
-                        BEAST_EXPECTS(
-                            env.meta()->getFieldU32(sfGasUsed) == allowance,
-                            std::to_string(env.meta()->getFieldU32(sfGasUsed)));
-                    }
-                }
-
-                env(escrow::finish(alice, alice, seq),
-                    Fee(finishFee),
-                    escrow::Gas(allowance),
-                    Ter(tesSUCCESS));
-
-                auto const txMeta = env.meta();
-                if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
-                {
-                    BEAST_EXPECTS(
-                        txMeta->getFieldU32(sfGasUsed) == allowance,
-                        std::to_string(txMeta->getFieldU32(sfGasUsed)));
-                }
-                if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
-                {
-                    BEAST_EXPECTS(
-                        txMeta->getFieldI32(sfVMReturnCode) == 5,
-                        std::to_string(txMeta->getFieldI32(sfVMReturnCode)));
-                }
-
-                BEAST_EXPECT(env.ownerCount(alice) == 0);
-            }
-        }
-
-        {
-            // Bytecode + Condition
-            Env env(*this, features);
-            env.fund(XRP(5000), alice, carol);
-            BEAST_EXPECT(env.ownerCount(alice) == 0);
-            auto const seq = env.seq(alice);
-            // create escrow
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCondition(escrow::kCb1),
-                escrow::kCancelTime(env.now() + 100s),
-                Fee(createFee));
-            env.close();
-            auto const conditionFinishFee =
-                finishFee + env.current()->fees().base * (32 + (escrow::kFb1.size() / 16));
-
-            if (BEAST_EXPECT(env.ownerCount(alice) == 2))
-            {
-                env.require(Balance(alice, XRP(4000) - createFee));
-                env.require(Balance(carol, XRP(5000)));
-
-                // no fulfillment provided, function fails
-                env(escrow::finish(carol, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee),
-                    Ter(tecCRYPTOCONDITION_ERROR));
-                // fulfillment provided, function fails
-                env(escrow::finish(carol, alice, seq),
-                    escrow::kCondition(escrow::kCb1),
-                    escrow::kFulfillment(escrow::kFb1),
-                    escrow::Gas(allowance),
-                    Fee(conditionFinishFee),
-                    Ter(tecBYTECODE_REJECTED));
-                if (BEAST_EXPECT(env.meta()->isFieldPresent(sfGasUsed)))
-                {
-                    BEAST_EXPECTS(
-                        env.meta()->getFieldU32(sfGasUsed) == allowance,
-                        std::to_string(env.meta()->getFieldU32(sfGasUsed)));
-                }
-                env.close();
-                // no fulfillment provided, function succeeds
-                env(escrow::finish(alice, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(conditionFinishFee),
-                    Ter(tecCRYPTOCONDITION_ERROR));
-                // wrong fulfillment provided, function succeeds
-                env(escrow::finish(alice, alice, seq),
-                    escrow::kCondition(escrow::kCb1),
-                    escrow::kFulfillment(escrow::kFb2),
-                    escrow::Gas(allowance),
-                    Fee(conditionFinishFee),
-                    Ter(tecCRYPTOCONDITION_ERROR));
-                // fulfillment provided, function succeeds, tx succeeds
-                env(escrow::finish(alice, alice, seq),
-                    escrow::kCondition(escrow::kCb1),
-                    escrow::kFulfillment(escrow::kFb1),
-                    escrow::Gas(allowance),
-                    Fee(conditionFinishFee),
-                    Ter(tesSUCCESS));
-
-                auto const txMeta = env.meta();
-                if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
-                {
-                    BEAST_EXPECTS(
-                        txMeta->getFieldU32(sfGasUsed) == allowance,
-                        std::to_string(txMeta->getFieldU32(sfGasUsed)));
-                }
-                if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
-                {
-                    BEAST_EXPECTS(
-                        txMeta->getFieldI32(sfVMReturnCode) == 5,
-                        std::to_string(txMeta->getFieldI32(sfVMReturnCode)));
-                }
-
-                env.close();
-                BEAST_EXPECT(env.ownerCount(alice) == 0);
-            }
-        }
-
-        {
-            // Bytecode + FinishAfter
-            Env env(*this, features);
-            // create escrow
-            env.fund(XRP(5000), alice, carol);
-            auto const seq = env.seq(alice);
-            BEAST_EXPECT(env.ownerCount(alice) == 0);
-            auto const ts = env.now() + 97s;
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kFinishTime(ts),
-                escrow::kCancelTime(env.now() + 1000s),
-                Fee(createFee));
-            env.close();
-
-            if (BEAST_EXPECT(env.ownerCount(alice) == 2))
-            {
-                env.require(Balance(alice, XRP(4000) - createFee));
-                env.require(Balance(carol, XRP(5000)));
-
-                // finish time hasn't passed, function fails
-                env(escrow::finish(carol, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee + 1),
-                    Ter(tecNO_PERMISSION));
-                env.close();
-                // finish time hasn't passed, function succeeds
-                for (; env.now() < ts; env.close())
-                {
-                    env(escrow::finish(carol, alice, seq),
-                        escrow::Gas(allowance),
-                        Fee(finishFee + 2),
-                        Ter(tecNO_PERMISSION));
-                }
-
-                env(escrow::finish(carol, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee + 1),
-                    Ter(tesSUCCESS));
-
-                auto const txMeta = env.meta();
-                if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
-                    BEAST_EXPECT(txMeta->getFieldU32(sfGasUsed) == allowance);
-                if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
-                {
-                    BEAST_EXPECTS(
-                        txMeta->getFieldI32(sfVMReturnCode) == 5,
-                        std::to_string(txMeta->getFieldI32(sfVMReturnCode)));
-                }
-
-                BEAST_EXPECT(env.ownerCount(alice) == 0);
-            }
-        }
-
-        {
-            // Bytecode + FinishAfter #2
-            Env env(*this, features);
-            // create escrow
-            env.fund(XRP(5000), alice, carol);
-            auto const seq = env.seq(alice);
-            BEAST_EXPECT(env.ownerCount(alice) == 0);
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kFinishTime(env.now() + 2s),
-                escrow::kCancelTime(env.now() + 100s),
-                Fee(createFee));
-            // Don't close the ledger here
-
-            if (BEAST_EXPECT(env.ownerCount(alice) == 2))
-            {
-                env.require(Balance(alice, XRP(4000) - createFee));
-                env.require(Balance(carol, XRP(5000)));
-
-                // finish time hasn't passed, function fails
-                env(escrow::finish(carol, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee),
-                    Ter(tecNO_PERMISSION));
-                env.close();
-
-                // finish time has passed, function fails
-                env(escrow::finish(carol, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee),
-                    Ter(tecBYTECODE_REJECTED));
-                if (BEAST_EXPECT(env.meta()->isFieldPresent(sfGasUsed)))
-                {
-                    BEAST_EXPECTS(
-                        env.meta()->getFieldU32(sfGasUsed) == allowance,
-                        std::to_string(env.meta()->getFieldU32(sfGasUsed)));
-                }
-                env.close();
-                // finish time has passed, function succeeds, tx succeeds
-                env(escrow::finish(carol, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee),
-                    Ter(tesSUCCESS));
-
-                auto const txMeta = env.meta();
-                if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
-                    BEAST_EXPECT(txMeta->getFieldU32(sfGasUsed) == allowance);
-                if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
-                {
-                    BEAST_EXPECTS(
-                        txMeta->getFieldI32(sfVMReturnCode) == 5,
-                        std::to_string(txMeta->getFieldI32(sfVMReturnCode)));
-                }
-
-                env.close();
-                BEAST_EXPECT(env.ownerCount(alice) == 0);
-            }
-        }
-    }
-
-    void
-    testUpdateDataOnFailure(FeatureBitset features)
-    {
-        testcase("Update escrow data on failure");
-
-        using namespace jtx;
-        using namespace std::chrono;
-
-        // wasm that always fails
-        Account const alice{"alice"};
-        Account const carol{"carol"};
-
-        Env env(*this, features);
-        // create escrow
-        env.fund(XRP(5000), alice);
-        auto const seq = env.seq(alice);
-        BEAST_EXPECT(env.ownerCount(alice) == 0);
-        auto escrowCreate = escrow::create(alice, alice, XRP(1000));
-        XRPAmount const txnFees =
-            env.current()->fees().base * 10 + kUpdateDataWasmHex.size() / 2 * 5;
-        env(escrowCreate,
-            escrow::Bytecode(kUpdateDataWasmHex),
-            escrow::kFinishTime(env.now() + 2s),
-            escrow::kCancelTime(env.now() + 100s),
-            Fee(txnFees));
-        env.close();
-        env.close();
-        env.close();
-
-        if (BEAST_EXPECT(env.ownerCount(alice) == (1 + (kUpdateDataWasmHex.size() / 2 / 500))))
-        {
-            env.require(Balance(alice, XRP(4000) - txnFees));
-
-            auto const allowance = 1420;
-            XRPAmount const finishFee = env.current()->fees().base +
-                (allowance * env.current()->fees().gasPrice) / microDropsPerDrop + 1;
-
-            // FinishAfter time hasn't passed
-            env(escrow::finish(alice, alice, seq),
-                escrow::Gas(allowance),
-                Fee(finishFee),
-                Ter(tecBYTECODE_REJECTED));
-
-            auto const txMeta = env.meta();
-            if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
-            {
-                BEAST_EXPECTS(
-                    txMeta->getFieldU32(sfGasUsed) == allowance,
-                    std::to_string(txMeta->getFieldU32(sfGasUsed)));
-            }
-            if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
-            {
-                BEAST_EXPECTS(
-                    txMeta->getFieldI32(sfVMReturnCode) == -256,
-                    std::to_string(txMeta->getFieldI32(sfVMReturnCode)));
-            }
-
-            auto const sle = env.le(keylet::escrow(alice, SeqProxy::rawSequence(seq)));
-            if (BEAST_EXPECT(sle && sle->isFieldPresent(sfData)))
-                BEAST_EXPECTS(checkVL(sle, sfData, "Data"), strHex(sle->getFieldVL(sfData)));
-        }
-    }
-
-    void
-    testFees(FeatureBitset features)
-    {
-        testcase("Fees");
-
-        using namespace jtx;
-        using namespace std::chrono;
-
-        Account const alice{"alice"};
-        Account const carol{"carol"};
-
-        // Tests whether the ledger index is >= 5
-        // getLedgerSqn() >= 5}
-        uint64_t const allowance = 467;
-        auto escrowCreate = escrow::create(alice, carol, XRP(1000));
-        auto createFee = [&]() {
-            Env const env(*this, features);
-            auto createFee = env.current()->fees().base * 10 + kLedgerSqnWasmHex.size() / 2 * 5;
-            return createFee;
-        }();
-
-        {
-            // ensure fees don't overflow
-            Env env(
-                *this,
-                envconfig([](std::unique_ptr cfg) {
-                    cfg->fees.gasPrice = 1'000'000;  // in gas
-                    return cfg;
-                }),
-                features);
-            // Run past the flag ledger so that a Fee change vote occurs and
-            // updates FeeSettings. (It also activates all supported
-            // amendments.)
-            for (auto i = env.current()->seq(); i <= 257; ++i)
-                env.close();
-
-            // create escrow
-            env.fund(XRP(5000), alice, carol);
-            auto const seq = env.seq(alice);
-            BEAST_EXPECT(env.ownerCount(alice) == 0);
-            env(escrowCreate,
-                escrow::Bytecode(kLedgerSqnWasmHex),
-                escrow::kCancelTime(env.now() + 100s),
-                Fee(createFee));
-            env.close();
-
-            if (BEAST_EXPECT(env.ownerCount(alice) == 2))
-            {
-                env.require(Balance(alice, XRP(4000) - createFee));
-                env.require(Balance(carol, XRP(5000)));
-                env.close();
-
-                auto const bigAllowance = 996'433;
-                uint64_t const partialFeeCalc =
-                    ((static_cast(bigAllowance) * 1'000'000) / microDropsPerDrop) + 1;
-                auto finishFee = env.current()->fees().base + partialFeeCalc;
-                BEAST_EXPECT(finishFee.drops() > bigAllowance);
-
-                // Intentional low value to test overflow handling
-                auto finishFeeOverflow = drops(30);
-
-                env(escrow::finish(alice, alice, seq),
-                    Fee(finishFeeOverflow),  // enough if there's an overflow
-                    escrow::Gas(bigAllowance),
-                    Ter(telINSUF_FEE_P));
-
-                env(escrow::finish(alice, alice, seq),
-                    Fee(finishFee - 1),
-                    escrow::Gas(bigAllowance),
-                    Ter(telINSUF_FEE_P));
-
-                env(escrow::finish(alice, alice, seq),
-                    Fee(finishFee),
-                    escrow::Gas(bigAllowance),
-                    Ter(tesSUCCESS));
-
-                auto const txMeta = env.meta();
-                if (BEAST_EXPECT(txMeta->isFieldPresent(sfGasUsed)))
-                {
-                    BEAST_EXPECTS(
-                        txMeta->getFieldU32(sfGasUsed) == allowance,
-                        std::to_string(txMeta->getFieldU32(sfGasUsed)));
-                }
-                if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
-                {
-                    BEAST_EXPECTS(
-                        txMeta->getFieldI32(sfVMReturnCode) == 5,
-                        std::to_string(txMeta->getFieldI32(sfVMReturnCode)));
-                }
-
-                BEAST_EXPECT(env.ownerCount(alice) == 0);
-            }
-        }
-    }
-
-    void
-    testAllHostFunctions(FeatureBitset features)
-    {
-        testcase("Test all host functions");
-
-        using namespace jtx;
-        using namespace std::chrono;
-
-        Account const alice{"alice"};
-        Account const carol{"carol"};
-
-        {
-            Env env(*this, features);
-            // create escrow
-            env.fund(XRP(5000), alice, carol);
-            auto const seq = env.seq(alice);
-            BEAST_EXPECT(env.ownerCount(alice) == 0);
-            auto escrowCreate = escrow::create(alice, carol, XRP(1000));
-            XRPAmount const txnFees =
-                env.current()->fees().base * 10 + kAllHostFunctionsWasmHex.size() / 2 * 5;
-            env(escrowCreate,
-                escrow::Bytecode(kAllHostFunctionsWasmHex),
-                escrow::kFinishTime(env.now() + 11s),
-                escrow::kCancelTime(env.now() + 100s),
-                escrow::Data("1000000000"),  // 1000 XRP in drops
-                Fee(txnFees));
-            env.close();
-
-            if (BEAST_EXPECT(
-                    env.ownerCount(alice) == (1 + (kAllHostFunctionsWasmHex.size() / 2 / 500))))
-            {
-                env.require(Balance(alice, XRP(4000) - txnFees));
-                env.require(Balance(carol, XRP(5000)));
-
-                auto const allowance = 1'000'000;
-                XRPAmount const finishFee = env.current()->fees().base +
-                    (allowance * env.current()->fees().gasPrice) / microDropsPerDrop + 1;
-
-                // FinishAfter time hasn't passed
-                env(escrow::finish(carol, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee),
-                    Ter(tecNO_PERMISSION));
-                env.close();
-                env.close();
-                env.close();
-
-                // reduce the destination balance
-                env(pay(carol, alice, XRP(4500)));
-                env.close();
-                env.close();
-
-                env(escrow::finish(alice, alice, seq),
-                    escrow::Gas(allowance),
-                    Fee(finishFee),
-                    Ter(tesSUCCESS));
-
-                auto const txMeta = env.meta();
-                if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
-                {
-                    BEAST_EXPECTS(
-                        txMeta->getFieldU32(sfGasUsed) == 48'433,
-                        std::to_string(txMeta->getFieldU32(sfGasUsed)));
-                }
-                if (BEAST_EXPECT(txMeta->isFieldPresent(sfVMReturnCode)))
-                    BEAST_EXPECT(txMeta->getFieldI32(sfVMReturnCode) == 1);
-
-                env.close();
-                BEAST_EXPECT(env.ownerCount(alice) == 0);
-            }
-        }
-    }
-
-    // TODO: this test is disabled until the all_keylets fixture is
-    // regenerated; the call in run() is commented out.
-    //
-    // kAllKeyletsWasmHex was built against the old trace ABI, where the trace_*
-    // host functions returned i32 rather than void, so the module is rejected
-    // with temINVALID_BYTECODE and the escrow below is never created.
-    //
-    // Regenerating it is not just a rebuild: all_keylets/ is still pinned to
-    // xrpl-wasm-stdlib @ "renames" and uses modules that moved on
-    // xrpl-common-stdlib @ "error-and-trace" (core::keylets,
-    // core::ledger_objects::*, core::types::*, and trace_data/DataRepr). The
-    // fixture source has to be ported first, along with float_tests/ and
-    // float_0/, which are stale for the same reason. The gas expectations here
-    // will also need rechecking once the module runs again.
-    //
-    // Keylet coverage is not lost meanwhile: HostFuncImpl_test.cpp exercises
-    // every keylet host function directly. What is missing is the end-to-end
-    // path through a live escrow.
-    void
-    testKeyletHostFunctions(FeatureBitset features)
-    {
-        testcase("Test all keylet host functions");
-
-        using namespace jtx;
-        using namespace std::chrono;
-
-        // TODO: create wasm module for all host functions
-        Account const alice{"alice"};
-        Account const carol{"carol"};
-
-        {
-            Env env{*this};
-            env.fund(XRP(10000), alice, carol);
-
-            BEAST_EXPECT(env.seq(alice) == 4);
-            BEAST_EXPECT(env.ownerCount(alice) == 0);
-
-            // base objects that need to be created first
-            auto const tokenId = token::getNextID(env, alice, 0, tfTransferable);
-            env(token::mint(alice, 0u), Txflags(tfTransferable));
-            env(trust(alice, carol["USD"](1'000'000)));
-            env.close();
-            BEAST_EXPECT(env.seq(alice) == 6);
-            BEAST_EXPECT(env.ownerCount(alice) == 2);
-
-            // set up a bunch of objects to check their keylets
-            AMM const amm(env, carol, XRP(10), carol["USD"](1000));
-            env(check::create(alice, carol, XRP(100)));
-            env(credentials::create(alice, alice, "termsandconditions"));
-            env(delegate::set(alice, carol, {"TrustSet"}));
-            env(deposit::auth(alice, carol));
-            env(did::set(alice), did::Data("alice_did"));
-            env(escrow::create(alice, carol, XRP(100)), escrow::kFinishTime(env.now() + 100s));
-            MPTTester mptTester{env, alice, {.fund = false}};
-            mptTester.create();
-            mptTester.authorize({.account = carol});
-            env(token::createOffer(carol, tokenId, XRP(100)), token::Owner(alice));
-            env(offer(alice, carol["GBP"](0.1), XRP(100)));
-            env(paychan::create(alice, carol, XRP(1000), 100s, alice.pk()));
-            pdomain::Credentials const credentials{
-                {.issuer = alice, .credType = "first credential"}};
-            env(pdomain::setTx(alice, credentials));
-            env(signers(alice, 1, {{carol, 1}}));
-            env(ticket::create(alice, 1));
-            Vault const vault{env};
-            auto [tx, _keylet] = vault.create({.owner = alice, .asset = xrpIssue()});
-            env(tx);
-            env.close();
-
-            BEAST_EXPECTS(env.ownerCount(alice) == 17, std::to_string(env.ownerCount(alice)));
-            if (BEAST_EXPECTS(env.seq(alice) == 20, std::to_string(env.seq(alice))))
-            {
-                auto const seq = env.seq(alice);
-                XRPAmount const txnFees =
-                    env.current()->fees().base * 10 + kAllKeyletsWasmHex.size() / 2 * 5;
-                env(escrow::create(alice, carol, XRP(1000)),
-                    escrow::Bytecode(kAllKeyletsWasmHex),
-                    escrow::kFinishTime(env.now() + 2s),
-                    escrow::kCancelTime(env.now() + 100s),
-                    Fee(txnFees));
-                env.close();
-                env.close();
-                env.close();
-
-                auto const allowance = 184'375;
-                auto const finishFee = env.current()->fees().base +
-                    (allowance * env.current()->fees().gasPrice) / microDropsPerDrop + 1;
-                env(escrow::finish(carol, alice, seq), escrow::Gas(allowance), Fee(finishFee));
-                env.close();
-
-                auto const txMeta = env.meta();
-                if (BEAST_EXPECT(txMeta && txMeta->isFieldPresent(sfGasUsed)))
-                {
-                    auto const gasUsed = txMeta->getFieldU32(sfGasUsed);
-                    BEAST_EXPECTS(gasUsed == allowance, std::to_string(gasUsed));
-                }
-                BEAST_EXPECTS(env.ownerCount(alice) == 17, std::to_string(env.ownerCount(alice)));
-            }
-        }
-    }
-
-    void
-    testLargeWasmModules(FeatureBitset features)
-    {
-        testcase("Test large wasm modules");
-
-        using namespace jtx;
-        using namespace std::chrono;
-        using namespace wasm_constants;
-
-        enum class ExpectedStatus { Success, Malformed, Crash };
-
-        auto runTest = [&](std::vector const& wasm,
-                           std::optional sizeLimit,
-                           ExpectedStatus expectedStatus,
-                           std::source_location const& loc = std::source_location::current()) {
-            auto makeEnv = [&]() -> Env {
-                if (sizeLimit)
-                {
-                    return Env(
-                        *this,
-                        envconfig([&sizeLimit](std::unique_ptr cfg) {
-                            cfg->fees.bytecodeSizeLimit = *sizeLimit;
-                            return cfg;
-                        }),
-                        features);
-                }
-                return Env(*this, features);
-            };
-            Env env = makeEnv();
-
-            auto const alice = Account("alice");
-            env.fund(XRP(1'000'000), alice);
-            env.close();
-
-            auto const wasmHex = strHex(wasm);
-            try
-            {
-                env(escrow::create(alice, alice, XRP(1000)),
-                    escrow::Bytecode(wasmHex),
-                    escrow::kCancelTime(env.now() + 100s),
-                    Fee(env.current()->fees().base * 10 + wasmHex.size() / 2 * 5),
-                    Ter(expectedStatus == ExpectedStatus::Success ? TER{tesSUCCESS}
-                                                                  : TER{temMALFORMED}));
-                if (expectedStatus == ExpectedStatus::Crash)
-                {
-                    fail("Expected crash", loc.file_name(), loc.line());
-                }
-                else
-                {
-                    pass();
-                }
-            }
-            catch (std::exception const& e)
-            {
-                if (expectedStatus == ExpectedStatus::Crash)
-                {
-                    pass();
-                }
-                else
-                {
-                    fail(e.what(), loc.file_name(), loc.line());
-                }
-            }
-        };
-
-        // Table-driven test cases
-        struct TestCase
-        {
-            enum class BlobType { Code, Data };
-            BlobType type;
-            uint32_t size;
-            std::optional sizeLimit;
-            ExpectedStatus expected;
-        };
-
-        std::vector const testCases = {
-            // Code blob tests
-            {.type = TestCase::BlobType::Code,
-             .size = 99'950,
-             .sizeLimit = std::nullopt,
-             .expected = ExpectedStatus::Success},  // just under 100kb
-            {.type = TestCase::BlobType::Code,
-             .size = 99'955,
-             .sizeLimit = std::nullopt,
-             .expected = ExpectedStatus::Malformed},  // just over 100kb
-            {.type = TestCase::BlobType::Code,
-             .size = 200'000,
-             .sizeLimit = 10'000'000,
-             .expected = ExpectedStatus::Success},  // ~200kb
-            {.type = TestCase::BlobType::Code,
-             .size = 490'000,
-             .sizeLimit = 10'000'000,
-             .expected = ExpectedStatus::Success},  // just under 1MB JSON
-            {.type = TestCase::BlobType::Code,
-             .size = 999'999,
-             .sizeLimit = 10'000'000,
-             .expected = ExpectedStatus::Crash},  // just over 1MB JSON
-            // Data blob tests
-            {.type = TestCase::BlobType::Data,
-             .size = 99'939,
-             .sizeLimit = std::nullopt,
-             .expected = ExpectedStatus::Success},  // just under 100kb
-            {.type = TestCase::BlobType::Data,
-             .size = 99'941,
-             .sizeLimit = std::nullopt,
-             .expected = ExpectedStatus::Malformed},  // just over 100kb
-            {.type = TestCase::BlobType::Data,
-             .size = 200'000,
-             .sizeLimit = 10'000'000,
-             .expected = ExpectedStatus::Success},  // ~200kb
-            {.type = TestCase::BlobType::Data,
-             .size = 490'000,
-             .sizeLimit = 10'000'000,
-             .expected = ExpectedStatus::Success},  // just under 1MB JSON
-            {.type = TestCase::BlobType::Data,
-             .size = 999'950,
-             .sizeLimit = 10'000'000,
-             .expected = ExpectedStatus::Crash},  // just over 1MB JSON
-        };
-
-        for (auto const& tc : testCases)
-        {
-            auto const wasm = tc.type == TestCase::BlobType::Code ? generateCodeBlob(tc.size)
-                                                                  : generateDataBlob(tc.size);
-            runTest(wasm, tc.sizeLimit, tc.expected);
-        }
-    }
-
-    void
-    testWithFeats(FeatureBitset features)
-    {
-        testCreateBytecodePreflight(features);
-        testFinishWasmFailures(features);
-        testBytecode(features);
-        testUpdateDataOnFailure(features);
-        testFees(features);
-
-        // TODO: Update module with new host functions
-        testAllHostFunctions(features);
-        // TODO: re-enable once the all_keylets fixture is regenerated (see
-        // testKeyletHostFunctions)
-        // testKeyletHostFunctions(features);
-
-        testLargeWasmModules(features);
-    }
-
-public:
-    void
-    run() override
-    {
-        using namespace test::jtx;
-        FeatureBitset const all{testableAmendments()};
-        testWithFeats(all);
-    }
-};
-
-BEAST_DEFINE_TESTSUITE(EscrowSmart, app, xrpl);
-
-}  // namespace xrpl::test
diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp
index 7e7509c3b7..3a2bc14183 100644
--- a/src/test/app/EscrowToken_test.cpp
+++ b/src/test/app/EscrowToken_test.cpp
@@ -951,6 +951,99 @@ struct EscrowToken_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testIOUCancelReserveRecycle(FeatureBitset features)
+    {
+        testcase("IOU Cancel Reserve Recycle");
+        using namespace jtx;
+        using namespace std::literals;
+
+        // Escrowing the whole IOU balance lets the owner delete the now-zero
+        // trust line, so cancelling has to re-create it: one object destroyed,
+        // one created, and the reserve requirement unchanged.
+        Env env{*this, features};
+        bool const fixEnabled = env.current()->rules().enabled(fixCleanup3_4_0);
+
+        auto const baseFee = env.current()->fees().base;
+        auto const alice = Account("alice");
+        auto const bob = Account("bob");
+        auto const gw = Account("gw");
+        auto const usd = gw["USD"];
+
+        env.fund(XRP(10'000), alice, bob, gw);
+        env.close();
+
+        env(fset(gw, asfAllowTrustLineLocking));
+        env.close();
+
+        env.trust(usd(10'000), alice);
+        env.close();
+
+        env(pay(gw, alice, usd(10'000)));
+        env.close();
+        BEAST_EXPECT(env.ownerCount(alice) == 1);
+
+        auto const cancelAfter = env.now() + 100s;
+        auto const seq = env.seq(alice);
+        env(escrow::create(alice, bob, usd(10'000)),
+            escrow::kFinishTime(env.now() + 1s),
+            escrow::kCancelTime(cancelAfter),
+            Fee(baseFee));
+        env.close();
+        BEAST_EXPECT(env.ownerCount(alice) == 2);
+
+        auto const trustLineKey = keylet::trustLine(alice.id(), gw.id(), usd.currency);
+        env(trust(alice, usd(0)));
+        env.close();
+        BEAST_EXPECT(!env.current()->exists(trustLineKey));
+        BEAST_EXPECT(env.ownerCount(alice) == 1);
+
+        // Leave alice holding the reserve for exactly one owned object. That
+        // is the escrow now and the re-created trust line after the cancel.
+        auto const oneObject = env.current()->fees().accountReserve(1, 1);
+        auto const twoObjects = env.current()->fees().accountReserve(2, 1);
+        auto const balance = env.balance(alice).value().xrp();
+        auto const feeCushion = baseFee.drops() * 20;
+        env(pay(alice, bob, drops(balance.drops() - oneObject.drops() - feeCushion)));
+        env.close();
+        BEAST_EXPECT(env.balance(alice).value().xrp() >= oneObject);
+        BEAST_EXPECT(env.balance(alice).value().xrp() < twoObjects);
+
+        for (; env.now() < cancelAfter; env.close())
+        {
+        }
+        env.close();
+        env.close();
+
+        auto const expectedResult = fixEnabled ? Ter(tesSUCCESS) : Ter(tecNO_LINE_INSUF_RESERVE);
+        env(escrow::cancel(alice, alice, seq), Fee(baseFee), expectedResult);
+        env.close();
+
+        auto const escrowKey = keylet::escrow(alice.id(), SeqProxy::rawSequence(seq));
+        if (fixEnabled)
+        {
+            BEAST_EXPECT(!env.le(escrowKey));
+            BEAST_EXPECT(env.current()->exists(trustLineKey));
+            BEAST_EXPECT(env.balance(alice, usd) == usd(10'000));
+            BEAST_EXPECT(env.ownerCount(alice) == 1);
+        }
+        else
+        {
+            // The tec keeps the escrow, so one more owner reserve lets the
+            // retry through.
+            BEAST_EXPECT(env.le(escrowKey) != nullptr);
+            BEAST_EXPECT(!env.current()->exists(trustLineKey));
+            BEAST_EXPECT(env.ownerCount(alice) == 1);
+
+            env(pay(bob, alice, drops(twoObjects.drops() - oneObject.drops())));
+            env.close();
+            env(escrow::cancel(alice, alice, seq), Fee(baseFee), Ter(tesSUCCESS));
+            env.close();
+            BEAST_EXPECT(!env.le(escrowKey));
+            BEAST_EXPECT(env.balance(alice, usd) == usd(10'000));
+        }
+    }
+
     void
     testIOUBalances(FeatureBitset features)
     {
@@ -3749,6 +3842,186 @@ struct EscrowToken_test : public beast::unit_test::Suite
         BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
     }
 
+    void
+    testMPTLargeLockedRate(FeatureBitset features)
+    {
+        testcase("MPT large locked rate");
+        using namespace test::jtx;
+        using namespace std::literals;
+
+        auto constexpr escrowAmount = 200'000'000'000'000'000LL;
+        auto constexpr noOverflowEscrowAmount = 186'000'000'000'000'000LL;
+        auto const alice = Account("alice");
+        auto const bob = Account("bob");
+        auto const gw = Account("gw");
+
+        for (auto const testFeatures :
+             {features - featureMPTokensV2 - fixCleanup3_4_0,
+              features - featureMPTokensV2,
+              (features | featureMPTokensV2) - fixCleanup3_4_0,
+              features | featureMPTokensV2})
+        {
+            bool const mptV2 = testFeatures[featureMPTokensV2];
+            bool const tokenEscrowV1 = testFeatures[fixTokenEscrowV1];
+            // The transfer-fee split in EscrowFinish only overflows on the
+            // legacy divideRound(amount, lockedRate, ...) path, which runs when
+            // fixCleanup3_4_0 is disabled. With fixCleanup3_4_0 the split uses
+            // mulRatio (128-bit intermediate), which cannot overflow. Without
+            // it, this large amount overflows unless the MPTokensV2 Number path
+            // is active. So the finish succeeds when either amendment is enabled.
+            bool const cleanup340 = testFeatures[fixCleanup3_4_0];
+            bool const noOverflow = cleanup340 || mptV2;
+            auto const expectedErr = noOverflow ? Ter(tesSUCCESS) : Ter(tefEXCEPTION);
+
+            // Finish with a large MPT amount and non-zero transfer fee. When the
+            // computation overflows (legacy divideRound path, no MPTokensV2) the
+            // finish fails with tefEXCEPTION and the escrow is untouched;
+            // otherwise it unlocks the escrow.
+            {
+                Env env{*this, testFeatures};
+                env.fund(XRP(1'000), alice, bob, gw);
+                auto const baseFee = env.current()->fees().base;
+
+                MPTTester const mpt(
+                    {.env = env,
+                     .issuer = gw,
+                     .holders = {alice, bob},
+                     .transferFee = 1'000,
+                     .flags = tfMPTCanEscrow | tfMPTCanTransfer});
+                env(pay(gw, alice, mpt(escrowAmount)));
+                env.close();
+
+                auto const preAlice = env.balance(alice, mpt);
+                auto const preBob = env.balance(bob, mpt);
+                auto const seq = env.seq(alice);
+                env(escrow::create(alice, bob, mpt(escrowAmount)),
+                    escrow::kCondition(escrow::kCb1),
+                    escrow::kFinishTime(env.now() + 1s),
+                    escrow::kCancelTime(env.now() + 500s),
+                    Fee(baseFee * 150));
+                env.close();
+
+                BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowAmount);
+                BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowAmount);
+
+                env(escrow::finish(bob, alice, seq),
+                    escrow::kCondition(escrow::kCb1),
+                    escrow::kFulfillment(escrow::kFb1),
+                    Fee(baseFee * 150),
+                    expectedErr);
+                env.close();
+
+                if (noOverflow)
+                {
+                    BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
+                    BEAST_EXPECT(env.balance(alice, mpt) == preAlice - mpt(escrowAmount));
+                    auto const postBob = env.balance(bob, mpt);
+                    BEAST_EXPECT(postBob.value() > preBob.value());
+                    BEAST_EXPECT(postBob.value() < (preBob + mpt(escrowAmount)).value());
+                    auto const xferFee = escrowAmount - (postBob.value() - preBob.value());
+                    auto const expectedEscrow = tokenEscrowV1 ? 0 : xferFee;
+                    BEAST_EXPECT(mptEscrowed(env, alice, mpt) == expectedEscrow);
+                    BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == expectedEscrow);
+                }
+                else
+                {
+                    BEAST_EXPECT(env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
+                    BEAST_EXPECT(env.balance(alice, mpt) == preAlice - mpt(escrowAmount));
+                    BEAST_EXPECT(env.balance(bob, mpt) == preBob);
+                    BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowAmount);
+                    BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowAmount);
+                }
+            }
+
+            // Control: a still-large amount below the legacy overflow boundary
+            // finishes successfully in both feature modes.
+            {
+                Env env{*this, testFeatures};
+                env.fund(XRP(1'000), alice, bob, gw);
+                auto const baseFee = env.current()->fees().base;
+
+                MPTTester const mpt(
+                    {.env = env,
+                     .issuer = gw,
+                     .holders = {alice, bob},
+                     .transferFee = 1'000,
+                     .flags = tfMPTCanEscrow | tfMPTCanTransfer});
+                env(pay(gw, alice, mpt(noOverflowEscrowAmount)));
+                env.close();
+
+                auto const preAlice = env.balance(alice, mpt);
+                auto const preBob = env.balance(bob, mpt);
+                auto const seq = env.seq(alice);
+                env(escrow::create(alice, bob, mpt(noOverflowEscrowAmount)),
+                    escrow::kCondition(escrow::kCb1),
+                    escrow::kFinishTime(env.now() + 1s),
+                    escrow::kCancelTime(env.now() + 500s),
+                    Fee(baseFee * 150));
+                env.close();
+
+                BEAST_EXPECT(mptEscrowed(env, alice, mpt) == noOverflowEscrowAmount);
+                BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == noOverflowEscrowAmount);
+
+                env(escrow::finish(bob, alice, seq),
+                    escrow::kCondition(escrow::kCb1),
+                    escrow::kFulfillment(escrow::kFb1),
+                    Fee(baseFee * 150),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
+                BEAST_EXPECT(env.balance(alice, mpt) == preAlice - mpt(noOverflowEscrowAmount));
+                auto const postBob = env.balance(bob, mpt);
+                BEAST_EXPECT(postBob.value() > preBob.value());
+                BEAST_EXPECT(postBob.value() < (preBob + mpt(noOverflowEscrowAmount)).value());
+                auto const xferFee = noOverflowEscrowAmount - (postBob.value() - preBob.value());
+                auto const expectedEscrow = tokenEscrowV1 ? 0 : xferFee;
+                BEAST_EXPECT(mptEscrowed(env, alice, mpt) == expectedEscrow);
+                BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == expectedEscrow);
+            }
+
+            // Cancel returns the escrow to the owner using parity rate, so it
+            // does not hit the transfer-rate division in either feature mode.
+            {
+                Env env{*this, testFeatures};
+                env.fund(XRP(1'000), alice, bob, gw);
+                auto const baseFee = env.current()->fees().base;
+
+                MPTTester const mpt(
+                    {.env = env,
+                     .issuer = gw,
+                     .holders = {alice, bob},
+                     .transferFee = 1'000,
+                     .flags = tfMPTCanEscrow | tfMPTCanTransfer});
+                env(pay(gw, alice, mpt(escrowAmount)));
+                env.close();
+
+                auto const preAlice = env.balance(alice, mpt);
+                auto const preBob = env.balance(bob, mpt);
+                auto const seq = env.seq(alice);
+                env(escrow::create(alice, bob, mpt(escrowAmount)),
+                    escrow::kCondition(escrow::kCb1),
+                    escrow::kFinishTime(env.now() + 1s),
+                    escrow::kCancelTime(env.now() + 3s),
+                    Fee(baseFee * 150));
+                env.close();
+
+                BEAST_EXPECT(mptEscrowed(env, alice, mpt) == escrowAmount);
+                BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == escrowAmount);
+
+                env(escrow::cancel(alice, alice, seq), Fee(baseFee), Ter(tesSUCCESS));
+                env.close();
+
+                BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
+                BEAST_EXPECT(env.balance(alice, mpt) == preAlice);
+                BEAST_EXPECT(env.balance(bob, mpt) == preBob);
+                BEAST_EXPECT(env.balance(gw, mpt) == -mpt(escrowAmount));
+                BEAST_EXPECT(mptEscrowed(env, alice, mpt) == 0);
+                BEAST_EXPECT(issuerMPTEscrowed(env, mpt) == 0);
+            }
+        }
+    }
+
     void
     testMPTRequireAuth(FeatureBitset features)
     {
@@ -4047,6 +4320,7 @@ struct EscrowToken_test : public beast::unit_test::Suite
         testMPTMetaAndOwnership(features);
         testMPTGateway(features);
         testMPTLockedRate(features);
+        testMPTLargeLockedRate(features);
         testMPTRequireAuth(features);
         testMPTLock(features);
         testMPTCanTransfer(features);
@@ -4069,6 +4343,8 @@ public:
         }
         testMPTSplitEscrowTransferFee(all - fixCleanup3_4_0);
         testMPTSplitEscrowTransferFee(all);
+        testIOUCancelReserveRecycle(all - fixCleanup3_4_0);
+        testIOUCancelReserveRecycle(all);
     }
 };
 
diff --git a/src/test/app/FlowMPT_test.cpp b/src/test/app/FlowMPT_test.cpp
index a94834eb28..0f88814d4f 100644
--- a/src/test/app/FlowMPT_test.cpp
+++ b/src/test/app/FlowMPT_test.cpp
@@ -26,8 +26,10 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -637,6 +639,149 @@ struct FlowMPT_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testMPTEndpointTransferRateOverflow(FeatureBitset features)
+    {
+        testcase("MPT Endpoint transfer rate overflow");
+
+        using namespace jtx;
+
+        Account const iouGW("iou_gateway");
+        Account const mptGW("mpt_gateway");
+        Account const alice("alice");
+        Account const bob("bob");
+
+        {
+            // Control: the same issuer-owned offer path works when the
+            // transfer-fee-adjusted input amount remains representable.
+            Env env(*this, features);
+
+            std::int64_t constexpr deliverAmount = 1'000'000'000'000'000'000LL;
+            std::int64_t constexpr offerAmount = deliverAmount + (deliverAmount / 2);
+
+            env.fund(XRP(10'000), iouGW, mptGW, alice, bob);
+            env.close();
+
+            auto const usd = iouGW["USD"];
+            env.trust(usd(offerAmount), alice);
+            env.trust(usd(offerAmount), mptGW);
+            env(pay(iouGW, alice, usd(offerAmount)));
+
+            MPTTester const mpt(
+                {.env = env, .issuer = mptGW, .holders = {bob}, .transferFee = kMaxTransferFee});
+
+            env(offer(mptGW, usd(offerAmount), mpt(offerAmount)));
+
+            env(pay(alice, bob, mpt(deliverAmount)),
+                Path(~mpt),
+                Sendmax(usd(offerAmount)),
+                Txflags(tfNoRippleDirect | tfPartialPayment));
+
+            env.require(Balance(alice, usd(0)), Balance(bob, mpt(deliverAmount)));
+            BEAST_EXPECT(!isOffer(env, mptGW, usd(offerAmount), mpt(offerAmount)));
+        }
+        {
+            // Regression: an extreme transfer-fee-adjusted MPT amount used to
+            // throw from MPTAmount::mulRatio during the endpoint reverse pass.
+            // The reverse pass now caps srcToDst at the largest amount whose
+            // transfer-fee-adjusted input is representable, so the offer limits
+            // the strand and a partial payment goes through.
+            Env env(*this, features);
+
+            std::int64_t constexpr overflowAmount = 7'000'000'000'000'000'000LL;
+            // The offer caps the input at overflowAmount, which the maximum
+            // transfer rate of 1.5 scales down to 7e18 * 2 / 3, rounded down
+            std::int64_t constexpr deliveredAmount = 4'666'666'666'666'666'666LL;
+
+            env.fund(XRP(10'000), iouGW, mptGW, alice, bob);
+            env.close();
+
+            auto const usd = iouGW["USD"];
+            env.trust(usd(overflowAmount), alice);
+            env.trust(usd(overflowAmount), mptGW);
+            env(pay(iouGW, alice, usd(overflowAmount)));
+
+            MPTTester const mpt(
+                {.env = env, .issuer = mptGW, .holders = {bob}, .transferFee = kMaxTransferFee});
+
+            env(offer(mptGW, usd(overflowAmount), mpt(overflowAmount)));
+
+            env(pay(alice, bob, mpt(overflowAmount)),
+                Path(~mpt),
+                Sendmax(usd(overflowAmount)),
+                Txflags(tfNoRippleDirect | tfPartialPayment));
+
+            env.require(Balance(alice, usd(0)), Balance(bob, mpt(deliveredAmount)));
+            BEAST_EXPECT(!isOffer(env, mptGW, usd(overflowAmount), mpt(overflowAmount)));
+        }
+    }
+
+    void
+    testMPTEndpointRipplingInputOverflow(FeatureBitset features)
+    {
+        // A payment between the holders of an MPT with a transfer fee ripples
+        // through the issuer, and the issuing step has to charge the transfer
+        // rate on the amount it receives. maxPaymentFlow() returns the issuance
+        // maximum for that step, so srcToDst * transferRate is not necessarily
+        // representable as an MPT amount. The reverse pass must cap the flow at
+        // the largest representable input instead of declaring the strand dry,
+        // otherwise a deliverable partial payment fails with tecPATH_DRY.
+        //
+        // Same defect as the case above, reached without an offer: holder ->
+        // issuer -> holder, one case per branch of the pre-fix revImp.
+        testcase("MPT Endpoint rippling input overflow");
+
+        using namespace jtx;
+
+        Account const gw("gateway");
+        Account const alice("alice");
+        Account const bob("bob");
+
+        // The maximum transfer fee gives a transfer rate of 1.5, so an input of
+        // kMaxMpTokenAmount covers at most kMaxMpTokenAmount * 2 / 3 of output.
+        std::int64_t constexpr maxRepresentable = 6'148'914'691'236'517'204LL;
+        std::int64_t constexpr aliceBalance = 1'000;
+        // The forward pass rounds the delivered amount down: 1000 / 1.5
+        std::int64_t constexpr bobBalance = 666;
+
+        auto const test =
+            [&](std::uint64_t maxAmt, std::int64_t deliver, std::string const& label) {
+                Env env(*this, features);
+                env.fund(XRP(10'000), gw, alice, bob);
+                env.close();
+
+                auto mpt = MPTTester(
+                    {.env = env,
+                     .issuer = gw,
+                     .holders = {alice, bob},
+                     .transferFee = kMaxTransferFee,
+                     .maxAmt = maxAmt});
+
+                env(pay(gw, alice, mpt(aliceBalance)));
+                env.close();
+
+                // alice asks to deliver more than the transfer rate can scale,
+                // so the issuing step caps the flow and her balance limits it
+                // further
+                env(pay(alice, bob, mpt(deliver)),
+                    Sendmax(mpt(kMaxMpTokenAmount)),
+                    Txflags(tfPartialPayment));
+                BEAST_EXPECTS(env.ter() == tesSUCCESS, label);
+                BEAST_EXPECTS(env.balance(alice, mpt) == mpt(0), label);
+                BEAST_EXPECTS(env.balance(bob, mpt) == mpt(bobBalance), label);
+                BEAST_EXPECTS(mpt.checkMPTokenOutstandingAmount(bobBalance), label);
+            };
+
+        // The requested amount is below MaximumAmount, so the reverse pass
+        // takes the non-limiting branch and overflows on the requested amount
+        test(kMaxMpTokenAmount, maxRepresentable + 1, "non-limiting");
+
+        // MaximumAmount is below the requested amount but still large enough
+        // that scaling it by the transfer rate is not representable, so the
+        // reverse pass takes the limiting branch and overflows on the maximum
+        test(maxRepresentable + 1, kMaxMpTokenAmount, "limiting");
+    }
+
     void
     testFalseDry(FeatureBitset features)
     {
@@ -742,6 +887,164 @@ struct FlowMPT_test : public beast::unit_test::Suite
         return result;
     }
 
+    void
+    testOfferOwnerMPTCreation(FeatureBitset features)
+    {
+        using namespace jtx;
+        Account const alice("alice");
+        Account const bob("bob");
+        Account const carol("carol");
+        Account const gw("gw");
+
+        {
+            testcase("Reserve-edge offer owner cannot create another object");
+
+            Env env(*this, features);
+
+            auto const baseFee = env.current()->fees().base;
+            auto const ownerIncrement = reserve(env, 1) - reserve(env, 0);
+            auto const xrpOffer = ownerIncrement - drops(1);
+            auto const bobStart = reserve(env, 2) - drops(1) + baseFee;
+
+            env.fund(XRP(10'000), alice, gw);
+            env.fund(bobStart, bob);
+            env.close();
+
+            MPTTester const usd({.env = env, .issuer = gw, .maxAmt = 10});
+
+            env(offer(bob, usd(1), xrpOffer));
+            env.close();
+
+            env.require(Balance(bob, reserve(env, 2) - drops(1)), Owners(bob, 1));
+
+            // This mirrors the full-crossing setup below. Bob has enough XRP
+            // for the resting offer, but not enough to pay a fee and add
+            // another owner-count object while the offer remains on ledger.
+            env(check::create(bob, alice, drops(1)), Ter(tecINSUFFICIENT_RESERVE));
+            env.close();
+
+            env.require(Owners(bob, 1));
+            BEAST_EXPECT(offersOnAccount(env, bob).size() == 1);
+        }
+
+        {
+            testcase("Reserve-edge offer owner creates MPToken during consume");
+
+            Env env(*this, features);
+
+            auto const baseFee = env.current()->fees().base;
+            auto const ownerIncrement = reserve(env, 1) - reserve(env, 0);
+            auto const xrpOffer = ownerIncrement - drops(1);
+            auto const bobStart = reserve(env, 2) - drops(1) + baseFee;
+
+            env.fund(XRP(10'000), alice, carol, gw);
+            env.fund(bobStart, bob);
+            env.close();
+
+            MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10});
+
+            env(pay(gw, alice, usd(1)));
+            env(offer(bob, usd(1), xrpOffer));
+            env.close();
+
+            env.require(Balance(bob, reserve(env, 2) - drops(1)), Owners(bob, 1));
+            BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
+            auto const carolXRP = env.balance(carol);
+
+            // Bob has enough XRP for the resting offer but is close to
+            // reserve. The payment should not create Bob's USD MPToken until
+            // the offer is actually consumed, otherwise the temporary owner
+            // count increase can make the offer look underfunded during path
+            // execution.
+            env(pay(alice, carol, xrpOffer),
+                Path(~XRP),
+                Sendmax(usd(1)),
+                Txflags(tfNoRippleDirect));
+            env.close();
+
+            env.require(Balance(carol, carolXRP + xrpOffer));
+            env.require(Balance(bob, usd(1)));
+            env.require(Balance(bob, reserve(env, 1)), Owners(bob, 1));
+            BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
+            BEAST_EXPECT(offersOnAccount(env, bob).empty());
+        }
+
+        {
+            testcase("Partial offer owner creates MPToken during consume");
+
+            Env env(*this, features);
+
+            auto const baseFee = env.current()->fees().base;
+            auto const ownerIncrement = reserve(env, 1) - reserve(env, 0);
+            auto const bobStart = reserve(env, 3) + baseFee;
+
+            env.fund(XRP(10'000), alice, carol, gw);
+            env.fund(bobStart, bob);
+            env.close();
+
+            MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10});
+
+            env(pay(gw, alice, usd(1)));
+            env(offer(bob, usd(2), drops(2 * ownerIncrement)));
+            env.close();
+
+            env.require(Balance(bob, reserve(env, 3)), Owners(bob, 1));
+            BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
+            auto const carolXRP = env.balance(carol);
+
+            // Partial consumption leaves Bob's offer on the ledger, so he ends
+            // up owning both the remaining offer and a newly created MPToken.
+            // The MPToken is created regardless of reserve; this setup simply
+            // funds Bob enough that he still meets reserve(2) afterward (the
+            // under-reserved case is covered in OfferMPT_test's no-reserve-check
+            // testcase).
+            env(pay(alice, carol, drops(ownerIncrement)),
+                Path(~XRP),
+                Sendmax(usd(1)),
+                Txflags(tfNoRippleDirect));
+            env.close();
+
+            env.require(Balance(carol, carolXRP + drops(ownerIncrement)));
+            env.require(Balance(bob, usd(1)));
+            env.require(Balance(bob, reserve(env, 2)), Owners(bob, 2));
+            BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
+            BEAST_EXPECT(offersOnAccount(env, bob).size() == 1);
+            BEAST_EXPECT(isOffer(env, bob, usd(1), drops(ownerIncrement)));
+        }
+
+        {
+            testcase("Issuer-owned offer does not create issuer MPToken");
+
+            Env env(*this, features);
+
+            env.fund(XRP(10'000), alice, carol, gw);
+            env.close();
+
+            MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10});
+
+            env(pay(gw, alice, usd(1)));
+            env(offer(gw, usd(1), drops(1'000)));
+            env.close();
+
+            BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), gw.id())));
+            auto const carolXRP = env.balance(carol);
+
+            // The issuer can own an offer that receives its own MPT without an
+            // MPToken. Consuming that offer should keep the issuer side
+            // tokenless.
+            env(pay(alice, carol, drops(1'000)),
+                Path(~XRP),
+                Sendmax(usd(1)),
+                Txflags(tfNoRippleDirect));
+            env.close();
+
+            env.require(Balance(alice, usd(0)));
+            env.require(Balance(carol, carolXRP + drops(1'000)));
+            BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), gw.id())));
+            BEAST_EXPECT(offersOnAccount(env, gw).empty());
+        }
+    }
+
     void
     testSelfPayment1(FeatureBitset features)
     {
@@ -2112,6 +2415,103 @@ struct FlowMPT_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testLockedMidPathHolder(FeatureBitset features)
+    {
+        // Regression: a cross-currency strand whose second book step
+        // consumes the offer of a holder that is locked on the step's
+        // in-asset (an MPT). The strand is XRP -> [book1: XRP/USD] ->
+        // USD -> [book2: USD/EUR] -> EUR, so book2 has book_.in == USD
+        // (an MPT) and its previous step is another BookStep. That is
+        // exactly the checkMPTDEX() branch that trusts the preceding
+        // BookStep and no longer re-checks isFrozen(owner, book_.in).
+        //
+        // The bypass the branch might appear to open does not exist:
+        // for MPT, isDeepFrozen() == isFrozen() (frozen MPTs can neither
+        // send nor receive), and OfferStream gates every offer through
+        // isDeepFrozen(owner, assetIn) before it can reach checkMPTDEX().
+        // So a locked mid-path holder's offer is removed by the liquidity
+        // source and the strand simply finds no liquidity at book2.
+        testcase("Locked mid-path holder behind a BookStep");
+
+        using namespace jtx;
+
+        Account const gw("gw");
+        Account const alice("alice");  // book1 (XRP/USD) offer owner
+        Account const mid("mid");      // book2 (USD/EUR) offer owner
+        Account const sam("sam");      // source
+        Account const bill("bill");    // destination
+
+        auto const test = [&](bool lock) {
+            Env env(*this, features);
+            env.fund(XRP(1'000), gw, alice, mid, sam, bill);
+            env.close();
+
+            auto usd = MPTTester(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {alice, mid},
+                 .flags = kMptDexFlags | tfMPTCanLock,
+                 .maxAmt = 1'000});
+            auto const eur = gw["EUR"];
+
+            // alice funds book1 (sells USD for XRP); mid funds book2
+            // (sells EUR for USD, i.e. receives the mid-path USD).
+            env(pay(gw, alice, usd(100)));
+            env(trust(mid, eur(100)));
+            env(pay(gw, mid, eur(100)));
+            env(trust(bill, eur(100)));
+            env.close();
+
+            env(offer(alice, XRP(100), usd(100)));  // XRP/USD, sells USD
+            env.close();
+            env(offer(mid, usd(100), eur(100)));  // USD/EUR, sells EUR
+            env.close();
+            BEAST_EXPECT(expectOffers(env, alice, 1));
+            BEAST_EXPECT(expectOffers(env, mid, 1));
+
+            // Lock mid on USD *after* its offer is already on the book:
+            // the reviewer's "frozen holder's offer sits behind a
+            // BookStep" scenario.
+            if (lock)
+            {
+                usd.set({.holder = mid, .flags = tfMPTLock});
+                env.close();
+            }
+
+            env(pay(sam, bill, eur(100)),
+                Sendmax(XRP(100)),
+                Path(~usd, ~eur),
+                Txflags(tfNoRippleDirect),
+                // book1 (XRP/USD) still has liquidity, so the strand is
+                // not fully dry; it just cannot cross book2 once mid's
+                // offer is removed, hence PARTIAL rather than DRY.
+                Ter(lock ? TER(tecPATH_PARTIAL) : TER(tesSUCCESS)));
+            env.close();
+
+            if (lock)
+            {
+                // No liquidity reached book2: mid neither received USD
+                // nor delivered EUR, so bill received nothing.
+                BEAST_EXPECT(env.balance(bill, eur) == eur(0));
+                BEAST_EXPECT(env.balance(mid, usd) == usd(0));
+            }
+            else
+            {
+                // The strand crosses both books: mid receives the
+                // mid-path USD and bill receives EUR.
+                BEAST_EXPECT(env.balance(bill, eur) == eur(100));
+                BEAST_EXPECT(env.balance(mid, usd) == usd(100));
+                BEAST_EXPECT(env.balance(alice, usd) == usd(0));
+                BEAST_EXPECT(expectOffers(env, alice, 0));
+                BEAST_EXPECT(expectOffers(env, mid, 0));
+            }
+        };
+
+        test(false);  // baseline: unlocked strand succeeds
+        test(true);   // locked mid-path holder: strand finds no liquidity
+    }
+
     void
     testWithFeats(FeatureBitset features)
     {
@@ -2121,7 +2521,10 @@ struct FlowMPT_test : public beast::unit_test::Suite
         testFalseDry(features);
         testDirectStep(features);
         testBookStep(features);
+        testOfferOwnerMPTCreation(features);
         testTransferRate(features);
+        testMPTEndpointTransferRateOverflow(features);
+        testMPTEndpointRipplingInputOverflow(features);
         testSelfPayment1(features);
         testSelfPayment2(features);
         testSelfFundedXRPEndpoint(false, features);
@@ -2129,6 +2532,7 @@ struct FlowMPT_test : public beast::unit_test::Suite
         testUnfundedOffer(features);
         testReExecuteDirectStep(features);
         testSelfPayLowQualityOffer(features);
+        testLockedMidPathHolder(features);
     }
 
     void
diff --git a/src/test/app/GRPCServerTLS_test.cpp b/src/test/app/GRPCServerTLS_test.cpp
index a48986d004..58ccf33959 100644
--- a/src/test/app/GRPCServerTLS_test.cpp
+++ b/src/test/app/GRPCServerTLS_test.cpp
@@ -1,13 +1,12 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
 #include 
 
-#include 
-
 #include 
 #include 
 #include 
@@ -17,6 +16,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -254,10 +254,8 @@ public:
 
     TemporaryTLSCertificates()
     {
-        auto tmpDir = std::filesystem::temp_directory_path();
-        auto uniqueDirName =
-            boost::filesystem::unique_path(std::string(kCertsDirPrefix) + "%%%%%%%%");
-        tempDir_ = tmpDir / uniqueDirName.string();
+        tempDir_ = xrpl::uniqueRandomPath(
+            std::filesystem::temp_directory_path(), std::string(kCertsDirPrefix));
         std::filesystem::create_directories(tempDir_);
 
         writeFile(tempDir_ / kCaCertFilename, kCaCertContent);
diff --git a/src/test/app/HostFuncImpl_test.cpp b/src/test/app/HostFuncImpl_test.cpp
deleted file mode 100644
index 58d4eb4419..0000000000
--- a/src/test/app/HostFuncImpl_test.cpp
+++ /dev/null
@@ -1,6438 +0,0 @@
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-namespace xrpl::test {
-
-static Bytes
-toBytes(std::uint8_t value)
-{
-    return {value};
-}
-
-static Bytes
-toBytes(std::uint16_t value)
-{
-    auto const* b = reinterpret_cast(&value);
-    auto const* e = reinterpret_cast(&value + 1);
-    return Bytes{b, e};
-}
-
-static Bytes
-toBytes(std::uint32_t value)
-{
-    auto const* b = reinterpret_cast(&value);
-    auto const* e = reinterpret_cast(&value + 1);
-    return Bytes{b, e};
-}
-
-static Bytes
-toBytes(uint256 const& value)
-{
-    return Bytes{value.begin(), value.end()};
-}
-
-static Bytes
-toBytes(Issue const& issue)
-{
-    Serializer s;
-    s.addBitString(issue.currency);
-    if (!isXRP(issue.currency))
-        s.addBitString(issue.account);
-    auto const data = s.getData();
-    return data;
-}
-
-static Bytes
-toBytes(Asset const& asset)
-{
-    if (asset.holds())
-        return toBytes(asset.get());
-
-    auto const& mptIssue = asset.get();
-    auto const& mptID = mptIssue.getMptID();
-    return Bytes{mptID.cbegin(), mptID.cend()};
-}
-
-static Bytes
-toBytes(STAmount const& amount)
-{
-    Serializer msg;
-    amount.add(msg);
-    auto const data = msg.getData();
-
-    return data;
-}
-
-static Bytes
-toBytes(STNumber const& number)
-{
-    Serializer msg;
-    number.add(msg);
-    auto const data = msg.getData();
-
-    return data;
-}
-
-static ApplyContext
-createApplyContext(
-    test::jtx::Env& env,
-    OpenView& ov,
-    beast::Journal j,
-    STTx const& tx = STTx(ttESCROW_FINISH, [](STObject&) {}))
-{
-    ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, j};
-    return ac;
-}
-
-static ApplyContext
-createApplyContext(
-    test::jtx::Env& env,
-    OpenView& ov,
-    STTx const& tx = STTx(ttESCROW_FINISH, [](STObject&) {}))
-{
-    return createApplyContext(env, ov, env.journal, tx);
-}
-
-class VirtualRuntime : public WasmRuntimeWrapper
-{
-    Bytes buffer_;
-    std::int64_t gas_ = 1'000'000;
-    std::int64_t transferLimit_ = kWasmTransferLimit;
-
-public:
-    static constexpr std::int64_t transferDiff = 1024;
-
-    VirtualRuntime() : buffer_(1024 * 1024)
-    {
-    }
-
-    Wmem
-    getMem() override
-    {
-        return Wmem(buffer_.data(), buffer_.size());
-    }
-
-    std::int64_t
-    getGas() override
-    {
-        gas_ -= 100;
-        return gas_;
-    }
-
-    std::int64_t
-    setGas(std::int64_t gas) override
-    {
-        if (gas == -2)
-            return -1;
-
-        if (gas < 0)
-        {
-            gas_ = std::numeric_limits::max();
-        }
-        else
-        {
-            gas_ = gas;
-        }
-
-        return gas_;
-    }
-
-    std::int64_t
-    getTransferLimit() override
-    {
-        transferLimit_ -= transferDiff;
-        return transferLimit_;
-    }
-
-    [[nodiscard]] std::int64_t
-    getTestTransferLimit() const
-    {
-        return transferLimit_;
-    }
-
-    std::int64_t
-    setTransferLimit(std::int64_t x) override
-    {
-        if (x == -2)
-            return -1;
-
-        if (x < 0)
-        {
-            transferLimit_ = std::numeric_limits::max();
-        }
-        else
-        {
-            transferLimit_ = x;
-        }
-
-        return transferLimit_;
-    }
-
-    void
-    checkIdx(WasmValVec const& params, size_t i) const
-    {
-        if (i + 1 >= params.size())
-            Throw("Out of bounds");
-        if (params[i].kind != WASM_I32 || params[i + 1].kind != WASM_I32)
-            Throw("Invalid params");
-        std::int32_t const ptr = params[i].of.i32;
-        std::int32_t const size = params[i + 1].of.i32;
-        std::int64_t const offset = (std::int64_t)ptr + size;
-        if (ptr < 0 || size < 0 || std::cmp_greater_equal(offset, buffer_.size()))
-            Throw("Out of bounds");
-    }
-
-    [[nodiscard]] Slice
-    getBuffer(WasmValVec const& params, size_t i) const
-    {
-        checkIdx(params, i);
-        std::int32_t const ptr = params[i].of.i32;
-        std::int32_t const size = params[i + 1].of.i32;
-        return {&buffer_[ptr], static_cast(size)};
-    }
-
-    [[nodiscard]] Bytes
-    getBytes(WasmValVec const& params, size_t i) const
-    {
-        checkIdx(params, i);
-        std::int32_t const ptr = params[i].of.i32;
-        std::int32_t const size = params[i + 1].of.i32;
-        return {&buffer_[ptr], &buffer_[ptr + size]};
-    }
-
-    void
-    setBytes(size_t ptr, void const* bytes, size_t size)
-    {
-        if (ptr + size >= buffer_.size())
-            Throw("Out of bounds");
-        memcpy(&buffer_[ptr], bytes, size);
-    }
-
-    template 
-    [[nodiscard]] [[nodiscard]] [[nodiscard]] [[nodiscard]] T
-    getInt(WasmValVec const& params, size_t i) const
-    {
-        checkIdx(params, i);
-        std::int32_t const ptr = params[i].of.i32;
-        std::int32_t const size = params[i + 1].of.i32;
-        if (size != sizeof(T))
-            Throw("Invalid size");
-        return *reinterpret_cast(&buffer_[ptr]);
-    }
-
-    [[nodiscard]] std::int32_t
-    getInt32(WasmValVec const& params, size_t i) const
-    {
-        return getInt(params, i);
-    }
-
-    [[nodiscard]] std::uint32_t
-    getUint32(WasmValVec const& params, size_t i) const
-    {
-        return getInt(params, i);
-    }
-
-    [[nodiscard]] std::int64_t
-    getInt64(WasmValVec const& params, size_t i) const
-    {
-        return getInt(params, i);
-    }
-
-    [[nodiscard]] std::uint64_t
-    getUint64(WasmValVec const& params, size_t i) const
-    {
-        return getInt(params, i);
-    }
-};
-
-template 
-void
-ww_hlp(size_t& idx, E&& e, P&& params, Arg&& arg)
-{
-    if constexpr (std::is_integral_v)
-    {
-        params[idx++] = std::is_same_v || std::is_same_v
-            ? wasm_val_t WASM_I64_VAL(static_cast(arg))
-            : wasm_val_t WASM_I32_VAL(static_cast(arg));
-    }
-    else if constexpr (std::is_same_v)
-    {
-        auto const* udata = reinterpret_cast(e);
-        HostFunctions const& hf = udata->first;
-        auto& vrt = reinterpret_cast(hf.getRT());
-
-        auto const data = toBytes(std::forward(arg));
-
-        size_t const ptr = (idx << 10);
-        vrt.setBytes(ptr, data.data(), data.size());
-        params[idx++] = wasm_val_t WASM_I32_VAL(static_cast(ptr));
-        params[idx++] = wasm_val_t WASM_I32_VAL(static_cast(data.size()));
-    }
-    else
-    {
-        auto const* udata = reinterpret_cast(e);
-        HostFunctions const& hf = udata->first;
-        auto& vrt = reinterpret_cast(hf.getRT());
-
-        size_t const ptr = (idx << 10);
-        vrt.setBytes(ptr, arg.data(), arg.size());
-        params[idx++] = wasm_val_t WASM_I32_VAL(static_cast(ptr));
-        params[idx++] = wasm_val_t WASM_I32_VAL(static_cast(arg.size()));
-    }
-}
-
-// Helper wrapper to call WASM wrapper functions with automatic parameter packing
-template 
-wasm_trap_t*
-ww(E&& e, P&& params, P&& result, Args... args)
-{
-    size_t idx = 0;
-    (ww_hlp(idx, e, params, std::forward(args)), ...);                   // NOLINT
-    return HostFuncMain_wrap(std::forward(e), params.get(), result.get());  // NOLINT
-}
-
-// ww() packs only integral args as wasm params, so the scoped enum needs widening.
-constexpr int32_t
-traceDataTypeToInt(TraceDataType t)
-{
-    return static_cast(t);
-}
-
-constexpr int64_t min64 = std::numeric_limits::min();
-constexpr int64_t max64 = std::numeric_limits::max();
-constexpr int32_t floatSize = 12;
-
-struct HostFuncImpl_test : public beast::unit_test::Suite
-{
-    void
-    testGetLedgerSqn()
-    {
-        testcase("getLedgerSqn");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.getLedgerSqn();
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("ldgr_index"), params, result, 0, sizeof(std::uint32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == sizeof(std::uint32_t)) &&
-                BEAST_EXPECT(vrt.getUint32(params, 0) == env.current()->header().seq);
-        }
-    }
-
-    void
-    testGetParentLedgerTime()
-    {
-        testcase("getParentLedgerTime");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.getParentLedgerTime();
-            WasmValVec params(2), result(1);
-            auto* trap =
-                ww(&import.at("parent_ldgr_time"), params, result, 0, sizeof(std::uint32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == sizeof(std::uint32_t)) &&
-                BEAST_EXPECT(
-                    vrt.getUint32(params, 0) ==
-                    env.current()->parentCloseTime().time_since_epoch().count());
-        }
-    }
-
-    void
-    testGetParentLedgerHash()
-    {
-        testcase("getParentLedgerHash");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.getParentLedgerHash();
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("parent_ldgr_hash"), params, result, 0, uint256::size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == uint256::size());
-            auto const resultBytes = vrt.getBytes(params, 0);
-            auto const expectedHash = env.current()->header().parentHash;
-            BEAST_EXPECT(
-                resultBytes.size() == uint256::size() &&
-                std::memcmp(resultBytes.data(), expectedHash.data(), uint256::size()) == 0);
-        }
-    }
-
-    void
-    testGetBaseFee()
-    {
-        testcase("getBaseFee");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // hfs.getBaseFee();
-        {
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("base_fee"), params, result, 0, sizeof(std::uint32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == sizeof(std::uint32_t)) &&
-                BEAST_EXPECT(vrt.getUint32(params, 0) == env.current()->fees().base.drops());
-        }
-    }
-
-    void
-    testIsAmendmentEnabled()
-    {
-        testcase("isAmendmentEnabled");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // Use featureTokenEscrow for testing
-        auto const amendmentId = featureTokenEscrow;
-
-        // hfs.isAmendmentEnabled(amendmentId);
-        {
-            WasmValVec params(2), result(1);
-            vrt.setBytes(0, amendmentId.data(), uint256::size());
-            auto* trap = ww(&import.at("amendment_enabled"), params, result, 0, uint256::size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 1);
-        }
-
-        std::string const amendmentName = "TokenEscrow";
-        // hfs.isAmendmentEnabled(amendmentName);
-        {
-            WasmValVec params(2), result(1);
-            vrt.setBytes(0, amendmentName.data(), amendmentName.size());
-            auto* trap =
-                ww(&import.at("amendment_enabled"), params, result, 0, amendmentName.size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 1);
-        }
-
-        uint256 const fakeId;
-        // hfs.isAmendmentEnabled(fakeId);
-        {
-            WasmValVec params(2), result(1);
-            vrt.setBytes(0, fakeId.data(), uint256::size());
-            auto* trap = ww(&import.at("amendment_enabled"), params, result, 0, uint256::size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 0);
-        }
-
-        std::string const fakeName = "FakeAmendment";
-        // hfs.isAmendmentEnabled(fakeName);
-        {
-            WasmValVec params(2), result(1);
-            vrt.setBytes(0, fakeName.data(), fakeName.size());
-            auto* trap = ww(&import.at("amendment_enabled"), params, result, 0, fakeName.size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 0);
-        }
-    }
-
-    void
-    testCacheLedgerObj()
-    {
-        testcase("cacheLedgerObj");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow = keylet::escrow(env.master, SeqProxy::rawSequence(2));
-        auto const accountKeylet = keylet::account(env.master);
-        {
-            VirtualRuntime vrt;
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            // hfs.cacheLedgerObj(accountKeylet.key, -1);
-            {
-                WasmValVec params(3), result(1);
-                vrt.setBytes(0, accountKeylet.key.data(), uint256::size());
-                auto* trap = ww(&import.at("cache_le"), params, result, 0, uint256::size(), -1);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(
-                        result[0].of.i32 == static_cast(HostFunctionError::SlotOutRange));
-            }
-
-            // hfs.cacheLedgerObj(accountKeylet.key, 257);
-            {
-                WasmValVec params(3), result(1);
-                vrt.setBytes(0, accountKeylet.key.data(), uint256::size());
-                auto* trap = ww(&import.at("cache_le"), params, result, 0, uint256::size(), 257);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(
-                        result[0].of.i32 == static_cast(HostFunctionError::SlotOutRange));
-            }
-
-            // hfs.cacheLedgerObj(dummyEscrow.key, 0);
-            {
-                WasmValVec params(3), result(1);
-                vrt.setBytes(0, dummyEscrow.key.data(), uint256::size());
-                auto* trap = ww(&import.at("cache_le"), params, result, 0, uint256::size(), 0);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(
-                        result[0].of.i32 ==
-                        static_cast(HostFunctionError::LedgerObjNotFound));
-            }
-
-            // hfs.cacheLedgerObj(accountKeylet.key, 0);
-            {
-                WasmValVec params(3), result(1);
-                vrt.setBytes(0, accountKeylet.key.data(), uint256::size());
-                auto* trap = ww(&import.at("cache_le"), params, result, 0, uint256::size(), 0);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 == 1);
-            }
-
-            vrt.setGas(2'000'000);
-            for (int i = 1; i <= 256; ++i)
-            {
-                // hfs.cacheLedgerObj(accountKeylet.key, i);
-                WasmValVec params(3), result(1);
-                vrt.setBytes(0, accountKeylet.key.data(), uint256::size());
-                auto* trap = ww(&import.at("cache_le"), params, result, 0, uint256::size(), i);
-
-                if (!(BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                      BEAST_EXPECTS(
-                          result[0].of.i32 == i,
-                          "result: " + std::to_string(result[0].of.i32) +
-                              ", expected: " + std::to_string(i))))
-                    break;
-            }
-
-            // hfs.cacheLedgerObj(accountKeylet.key, 0);
-            {
-                WasmValVec params(3), result(1);
-                vrt.setBytes(0, accountKeylet.key.data(), uint256::size());
-                auto* trap = ww(&import.at("cache_le"), params, result, 0, uint256::size(), 0);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(
-                        result[0].of.i32 == static_cast(HostFunctionError::SlotsFull));
-            }
-        }
-
-        {
-            VirtualRuntime vrt;
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            vrt.setGas(2'000'000);
-            for (int i = 1; i <= 256; ++i)
-            {
-                // hfs.cacheLedgerObj(accountKeylet.key, 0);
-                WasmValVec params(3), result(1);
-                vrt.setBytes(0, accountKeylet.key.data(), uint256::size());
-                auto* trap = ww(&import.at("cache_le"), params, result, 0, uint256::size(), 0);
-
-                if (!(BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                      BEAST_EXPECTS(
-                          result[0].of.i32 == i,
-                          "result: " + std::to_string(result[0].of.i32) +
-                              ", expected: " + std::to_string(i))))
-                    break;
-            }
-
-            // hfs.cacheLedgerObj(accountKeylet.key, 0);
-            {
-                WasmValVec params(3), result(1);
-                vrt.setBytes(0, accountKeylet.key.data(), uint256::size());
-                auto* trap = ww(&import.at("cache_le"), params, result, 0, uint256::size(), 0);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(
-                        result[0].of.i32 == static_cast(HostFunctionError::SlotsFull));
-            }
-        }
-    }
-
-    void
-    testGetTxField()
-    {
-        testcase("getTxField");
-        using namespace test::jtx;
-
-        std::string const credIdHex =
-            "0011223344556677889900112233445566778899001122334455667788990011";
-        uint256 credId;
-        BEAST_EXPECT(credId.parseHex(credIdHex));
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        STTx const stx = STTx(ttESCROW_FINISH, [&](auto& obj) {
-            obj.setAccountID(sfAccount, env.master.id());
-            obj.setAccountID(sfOwner, env.master.id());
-            obj.setFieldU32(sfOfferSequence, env.seq(env.master));
-            obj.setFieldArray(sfMemos, STArray{});
-            STVector256 credIds;
-            credIds.pushBack(credId);
-            obj.setFieldV256(sfCredentialIDs, credIds);
-        });
-        ApplyContext ac = createApplyContext(env, ov, stx);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-
-        {
-            VirtualRuntime vrt;
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            // hfs.getTxField(sfAccount);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap =
-                    ww(&import.at("tx_field"),
-                       params,
-                       result,
-                       sfAccount.getCode(),
-                       0,
-                       AccountID::size());
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 == AccountID::size());
-                auto const accountBytes = vrt.getBytes(params, 1);
-                BEAST_EXPECT(std::ranges::equal(accountBytes, env.master.id()));
-            }
-
-            // hfs.getTxField(sfOwner);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap =
-                    ww(&import.at("tx_field"),
-                       params,
-                       result,
-                       sfOwner.getCode(),
-                       0,
-                       AccountID::size());
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 == AccountID::size());
-                auto const ownerBytes = vrt.getBytes(params, 1);
-                BEAST_EXPECT(std::ranges::equal(ownerBytes, env.master.id()));
-            }
-
-            // hfs.getTxField(sfTransactionType);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap =
-                    ww(&import.at("tx_field"), params, result, sfTransactionType.getCode(), 0, 256);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 > 0);
-                auto txTypeBytes = vrt.getBytes(params, 1);
-                txTypeBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(txTypeBytes == toBytes(ttESCROW_FINISH));
-            }
-
-            // hfs.getTxField(sfOfferSequence);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap =
-                    ww(&import.at("tx_field"), params, result, sfOfferSequence.getCode(), 0, 256);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 > 0);
-                auto offerSeqBytes = vrt.getBytes(params, 1);
-                offerSeqBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(offerSeqBytes == toBytes(env.seq(env.master)));
-            }
-
-            // hfs.getTxField(sfDestination);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap =
-                    ww(&import.at("tx_field"), params, result, sfDestination.getCode(), 0, 256);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(
-                        result[0].of.i32 == static_cast(HostFunctionError::FieldNotFound));
-            }
-
-            // hfs.getTxField(sfMemos);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap = ww(&import.at("tx_field"), params, result, sfMemos.getCode(), 0, 256);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(
-                        result[0].of.i32 == static_cast(HostFunctionError::NotLeafField));
-            }
-
-            // hfs.getTxField(sfCredentialIDs);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap =
-                    ww(&import.at("tx_field"), params, result, sfCredentialIDs.getCode(), 0, 256);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-                BEAST_EXPECTS(
-                    result[0].of.i32 == static_cast(HostFunctionError::NotLeafField),
-                    std::to_string(result[0].of.i32));
-            }
-
-            // hfs.getTxField(sfInvalid);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap =
-                    ww(&import.at("tx_field"), params, result, sfInvalid.getCode(), 0, 256);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(
-                        result[0].of.i32 == static_cast(HostFunctionError::FieldNotFound));
-            }
-
-            // hfs.getTxField(sfGeneric);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap =
-                    ww(&import.at("tx_field"), params, result, sfGeneric.getCode(), 0, 256);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(
-                        result[0].of.i32 == static_cast(HostFunctionError::FieldNotFound));
-            }
-        }
-
-        {
-            auto const iouAsset = env.master["USD"];
-            STTx const stx2 = STTx(ttAMM_DEPOSIT, [&](auto& obj) {
-                obj.setAccountID(sfAccount, env.master.id());
-                obj.setFieldIssue(sfAsset, STIssue{sfAsset, xrpIssue()});
-                obj.setFieldIssue(sfAsset2, STIssue{sfAsset2, iouAsset.issue()});
-            });
-            ApplyContext ac2 = createApplyContext(env, ov, stx2);
-            VirtualRuntime vrt;
-            WasmHostFunctionsImpl hfs(ac2, dummyEscrow);
-
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            // hfs.getTxField(sfAsset);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap = ww(&import.at("tx_field"), params, result, sfAsset.getCode(), 0, 256);
-
-                std::vector const expectedAsset(20, 0);
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 > 0);
-                auto assetBytes = vrt.getBytes(params, 1);
-                assetBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(assetBytes == expectedAsset);
-            }
-
-            // hfs.getTxField(sfAsset2);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap = ww(&import.at("tx_field"), params, result, sfAsset2.getCode(), 0, 256);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 > 0);
-                auto asset2Bytes = vrt.getBytes(params, 1);
-                asset2Bytes.resize(result[0].of.i32);
-                BEAST_EXPECT(asset2Bytes == toBytes(Asset(iouAsset)));
-            }
-        }
-
-        {
-            auto const iouAsset = env.master["GBP"];
-            auto const mptId = makeMptID(1, env.master);
-            STTx const stx2 = STTx(ttAMM_DEPOSIT, [&](auto& obj) {
-                obj.setAccountID(sfAccount, env.master.id());
-                obj.setFieldIssue(sfAsset, STIssue{sfAsset, iouAsset.issue()});
-                obj.setFieldIssue(sfAsset2, STIssue{sfAsset2, MPTIssue{mptId}});
-            });
-            ApplyContext ac2 = createApplyContext(env, ov, stx2);
-            VirtualRuntime vrt;
-            WasmHostFunctionsImpl hfs(ac2, dummyEscrow);
-
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            // hfs.getTxField(sfAsset);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap = ww(&import.at("tx_field"), params, result, sfAsset.getCode(), 0, 256);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-                if (BEAST_EXPECT(result[0].of.i32 > 0))
-                {
-                    auto assetBytes = vrt.getBytes(params, 1);
-                    assetBytes.resize(result[0].of.i32);
-                    BEAST_EXPECT(assetBytes == toBytes(Asset(iouAsset)));
-                }
-            }
-
-            // hfs.getTxField(sfAsset2);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap = ww(&import.at("tx_field"), params, result, sfAsset2.getCode(), 0, 256);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-                if (BEAST_EXPECT(result[0].of.i32 > 0))
-                {
-                    auto assetBytes = vrt.getBytes(params, 1);
-                    assetBytes.resize(result[0].of.i32);
-                    BEAST_EXPECT(assetBytes == toBytes(Asset(mptId)));
-                }
-            }
-        }
-
-        {
-            std::uint8_t const expectedScale = 8;
-            STTx const stx2 = STTx(ttMPTOKEN_ISSUANCE_CREATE, [&](auto& obj) {
-                obj.setAccountID(sfAccount, env.master.id());
-                obj.setFieldU8(sfAssetScale, expectedScale);
-            });
-            ApplyContext ac2 = createApplyContext(env, ov, stx2);
-            VirtualRuntime vrt;
-            WasmHostFunctionsImpl hfs(ac2, dummyEscrow);
-
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            // hfs.getTxField(sfAssetScale);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap =
-                    ww(&import.at("tx_field"), params, result, sfAssetScale.getCode(), 0, 256);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-                if (BEAST_EXPECT(result[0].of.i32 > 0))
-                {
-                    auto assetBytes = vrt.getBytes(params, 1);
-                    assetBytes.resize(result[0].of.i32);
-                    BEAST_EXPECT(std::ranges::equal(assetBytes, toBytes(expectedScale)));
-                }
-            }
-        }
-    }
-
-    void
-    testGetCurrentLedgerObjField()
-    {
-        testcase("getCurrentLedgerObjField");
-        using namespace test::jtx;
-        using namespace std::chrono;
-
-        Env env{*this};
-
-        // Fund the account and create an escrow so the ledger object exists
-        env(escrow::create(env.master, env.master, XRP(100)), escrow::kFinishTime(env.now() + 1s));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        // Find the escrow ledger object
-        auto const escrowKeylet =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master) - 1));
-        BEAST_EXPECT(env.le(escrowKeylet));
-
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, escrowKeylet);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // hfs.getCurrentLedgerObjField(sfAccount);
-        {
-            WasmValVec params(3), result(1);
-            auto* trap =
-                ww(&import.at("home_le_field"), params, result, sfAccount.getCode(), 0, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECTS(result[0].of.i32 > 0, std::to_string(result[0].of.i32)))
-            {
-                auto accountBytes = vrt.getBytes(params, 1);
-                accountBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(std::ranges::equal(accountBytes, env.master.id()));
-            }
-        }
-
-        // hfs.getCurrentLedgerObjField(sfAmount);
-        {
-            WasmValVec params(3), result(1);
-            auto* trap =
-                ww(&import.at("home_le_field"), params, result, sfAmount.getCode(), 0, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECT(result[0].of.i32 > 0))
-            {
-                auto amountBytes = vrt.getBytes(params, 1);
-                amountBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(amountBytes == toBytes(XRP(100)));
-            }
-        }
-
-        // hfs.getCurrentLedgerObjField(sfPreviousTxnID);
-        {
-            WasmValVec params(3), result(1);
-            auto* trap =
-                ww(&import.at("home_le_field"), params, result, sfPreviousTxnID.getCode(), 0, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECT(result[0].of.i32 > 0))
-            {
-                auto previousTxnIdBytes = vrt.getBytes(params, 1);
-                previousTxnIdBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(previousTxnIdBytes == toBytes(env.tx()->getTransactionID()));
-            }
-        }
-
-        // hfs.getCurrentLedgerObjField(sfOwner);
-        {
-            WasmValVec params(3), result(1);
-            auto* trap = ww(&import.at("home_le_field"), params, result, sfOwner.getCode(), 0, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == static_cast(HostFunctionError::FieldNotFound));
-        }
-
-        {
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master) + 5));
-            VirtualRuntime vrt2;
-            WasmHostFunctionsImpl hfs2(ac, dummyEscrow);
-
-            auto import2 = xrpl::createWasmImport(hfs2);
-            hfs2.setRT(vrt2);
-
-            // hfs2.getCurrentLedgerObjField(sfAccount);
-            {
-                WasmValVec params(3), result(1);
-                auto* trap =
-                    ww(&import2.at("home_le_field"), params, result, sfAccount.getCode(), 0, 256);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(
-                        result[0].of.i32 ==
-                        static_cast(HostFunctionError::LedgerObjNotFound));
-            }
-        }
-    }
-
-    void
-    testGetLedgerObjField()
-    {
-        testcase("getLedgerObjField");
-        using namespace test::jtx;
-        using namespace std::chrono;
-
-        Env env{*this};
-        // Fund the account and create an escrow so the ledger object exists
-        env(escrow::create(env.master, env.master, XRP(100)), escrow::kFinishTime(env.now() + 1s));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const accountKeylet = keylet::account(env.master.id());
-        auto const escrowKeylet =
-            keylet::escrow(env.master.id(), SeqProxy::rawSequence(env.seq(env.master) - 1));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, escrowKeylet);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // hfs.cacheLedgerObj(accountKeylet.key, 1);
-        {
-            WasmValVec params(3), result(1);
-            vrt.setBytes(0, accountKeylet.key.data(), uint256::size());
-            auto* trap = ww(&import.at("cache_le"), params, result, 0, uint256::size(), 1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 1);
-        }
-
-        // hfs.getLedgerObjField(1, sfAccount);
-        {
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("le_field"), params, result, 1, sfAccount.getCode(), 0, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECTS(result[0].of.i32 > 0, std::to_string(result[0].of.i32)))
-            {
-                auto accountBytes = vrt.getBytes(params, 2);
-                accountBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(std::ranges::equal(accountBytes, env.master.id()));
-            }
-        }
-
-        // hfs.getLedgerObjField(1, sfBalance);
-        {
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("le_field"), params, result, 1, sfBalance.getCode(), 0, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECT(result[0].of.i32 > 0))
-            {
-                auto balanceBytes = vrt.getBytes(params, 2);
-                balanceBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(balanceBytes == toBytes(env.balance(env.master)));
-            }
-        }
-
-        // hfs.getLedgerObjField(0, sfAccount);
-        {
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("le_field"), params, result, 0, sfAccount.getCode(), 0, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == static_cast(HostFunctionError::SlotOutRange));
-        }
-
-        // hfs.getLedgerObjField(257, sfAccount);
-        {
-            WasmValVec params(4), result(1);
-            auto* trap =
-                ww(&import.at("le_field"), params, result, 257, sfAccount.getCode(), 0, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == static_cast(HostFunctionError::SlotOutRange));
-        }
-
-        // hfs.getLedgerObjField(2, sfAccount);
-        {
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("le_field"), params, result, 2, sfAccount.getCode(), 0, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == static_cast(HostFunctionError::EmptySlot));
-        }
-
-        // hfs.getLedgerObjField(1, sfOwner);
-        {
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("le_field"), params, result, 1, sfOwner.getCode(), 0, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == static_cast(HostFunctionError::FieldNotFound));
-        }
-    }
-
-    void
-    testGetTxNestedField()
-    {
-        testcase("getTxNestedField");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-
-        std::string const credIdHex =
-            "0011223344556677889900112233445566778899001122334455667788990011";
-        uint256 credId;
-        BEAST_EXPECT(credId.parseHex(credIdHex));
-
-        // Create a transaction with a nested array field
-        STTx const stx = STTx(ttESCROW_FINISH, [&](auto& obj) {
-            obj.setAccountID(sfAccount, env.master.id());
-            STArray memos;
-            STObject memoObj(sfMemo);
-            memoObj.setFieldVL(sfMemoData, Slice("hello", 5));
-            memos.push_back(memoObj);
-            obj.setFieldArray(sfMemos, memos);
-            STVector256 credIds;
-            credIds.pushBack(credId);
-            obj.setFieldV256(sfCredentialIDs, credIds);
-        });
-
-        ApplyContext ac = createApplyContext(env, ov, stx);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // hfs.getTxNestedField(locator);
-        {
-            // Locator for sfMemos[0].sfMemo.sfMemoData
-            // Locator is a sequence of int32_t codes:
-            // [sfMemos.getCode(), 0, sfMemoData.getCode()]
-            std::vector const locatorVec = {sfMemos.getCode(), 0, sfMemoData.getCode()};
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-
-            WasmValVec params(4), result(1);
-            auto* trap =
-                ww(&import.at("tx_inner"),
-                   params,
-                   result,
-                   0,
-                   locatorVec.size() * sizeof(int32_t),
-                   256,
-                   256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECTS(result[0].of.i32 > 0, std::to_string(result[0].of.i32)))
-            {
-                auto memoDataBytes = vrt.getBytes(params, 2);
-                memoDataBytes.resize(result[0].of.i32);
-                std::string const memoData(memoDataBytes.begin(), memoDataBytes.end());
-                BEAST_EXPECT(memoData == "hello");
-            }
-        }
-
-        // hfs.getTxNestedField(locator);
-        {
-            // Locator for sfCredentialIDs[0]
-            std::vector locatorVec = {sfCredentialIDs.getCode(), 0};
-            vrt.setBytes(
-                0,
-                reinterpret_cast(locatorVec.data()),
-                locatorVec.size() * sizeof(int32_t));
-
-            WasmValVec params(4), result(1);
-            auto* trap =
-                ww(&import.at("tx_inner"),
-                   params,
-                   result,
-                   0,
-                   locatorVec.size() * sizeof(int32_t),
-                   256,
-                   256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECTS(result[0].of.i32 > 0, std::to_string(result[0].of.i32)))
-            {
-                auto credIdBytes = vrt.getBytes(params, 2);
-                credIdBytes.resize(result[0].of.i32);
-                std::string const credIdResult(credIdBytes.begin(), credIdBytes.end());
-                BEAST_EXPECT(strHex(credIdResult) == credIdHex);
-            }
-        }
-
-        // hfs.getTxNestedField(locator);
-        {
-            // can use the nested locator for base fields too
-            std::vector locatorVec = {sfAccount.getCode()};
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-
-            WasmValVec params(4), result(1);
-            auto* trap =
-                ww(&import.at("tx_inner"),
-                   params,
-                   result,
-                   0,
-                   locatorVec.size() * sizeof(int32_t),
-                   256,
-                   256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECTS(result[0].of.i32 > 0, std::to_string(result[0].of.i32)))
-            {
-                auto accountBytes = vrt.getBytes(params, 2);
-                accountBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(std::ranges::equal(accountBytes, env.master.id()));
-            }
-        }
-
-        // hfs.getTxNestedField(locator);
-        {
-            // unaligned locator
-            std::vector locatorVec(sizeof(int32_t) + 1);
-            auto const accountFieldCode = sfAccount.getCode();
-            memcpy(locatorVec.data() + 1, &accountFieldCode, sizeof(int32_t));
-            vrt.setBytes(0, locatorVec.data(), sizeof(int32_t) + 1);
-
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("tx_inner"), params, result, 1, sizeof(int32_t), 256, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECTS(result[0].of.i32 > 0, std::to_string(result[0].of.i32)))
-            {
-                auto accountBytes = vrt.getBytes(params, 2);
-                accountBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(std::ranges::equal(accountBytes, env.master.id()));
-            }
-        }
-
-        auto expectError = [&](std::vector const& locatorVec,
-                               HostFunctionError expectedError) {
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-
-            WasmValVec params(4), result(1);
-            // hfs.getTxNestedField(locator);
-            auto* trap =
-                ww(&import.at("tx_inner"),
-                   params,
-                   result,
-                   0,
-                   locatorVec.size() * sizeof(int32_t),
-                   256,
-                   256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECTS(
-                result[0].of.i32 == hfErrorToInt(expectedError), std::to_string(result[0].of.i32));
-        };
-
-        // hfs.getTxNestedField(locator);
-        // Locator for non-existent base field
-        expectError(
-            {sfSigners.getCode(),  // sfSigners does not exist
-             0,
-             sfAccount.getCode()},
-            HostFunctionError::FieldNotFound);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for non-existent index
-        expectError(
-            {sfMemos.getCode(),
-             1,  // index 1 does not exist
-             sfMemoData.getCode()},
-            HostFunctionError::IndexOutOfBounds);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for non-existent index
-        expectError(
-            {sfCredentialIDs.getCode(), 1},  // index 1 does not exist
-            HostFunctionError::IndexOutOfBounds);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for negative index (STArray)
-        expectError(
-            {sfMemos.getCode(),
-             -1,  // negative index
-             sfMemoData.getCode()},
-            HostFunctionError::IndexOutOfBounds);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for negative index (STVector256)
-        expectError(
-            {sfCredentialIDs.getCode(), -1},  // negative index
-            HostFunctionError::IndexOutOfBounds);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for non-existent nested field
-        expectError(
-            {sfMemos.getCode(), 0, sfURI.getCode()},  // sfURI does not exist in the memo
-            HostFunctionError::FieldNotFound);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for non-existent base sfield
-        expectError(
-            {fieldCode(20000, 20000),  // nonexistent SField code
-             0,
-             sfAccount.getCode()},
-            HostFunctionError::InvalidField);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for non-existent nested sfield
-        expectError(
-            {sfMemos.getCode(),  // nonexistent SField code
-             0,
-             fieldCode(20000, 20000)},
-            HostFunctionError::InvalidField);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for negative base sfield code (-1 = sfInvalid, exists in map but not in tx)
-        expectError(
-            {-1,  // sfInvalid's field code
-             0,
-             sfAccount.getCode()},
-            HostFunctionError::FieldNotFound);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for zero base sfield code (0 = sfGeneric, exists in map but not in tx)
-        expectError(
-            {0,  // sfGeneric's field code
-             0,
-             sfAccount.getCode()},
-            HostFunctionError::FieldNotFound);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for very negative base sfield code (not in knownCodeToField map)
-        expectError(
-            {std::numeric_limits::min(), 0, sfAccount.getCode()},
-            HostFunctionError::InvalidField);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for negative nested sfield code in STObject context
-        // (sfMemos[0] is an STObject, then -1 is looked up as SField)
-        expectError(
-            {sfMemos.getCode(), 0, -1},  // -1 = sfInvalid, exists in map but not in memo object
-            HostFunctionError::FieldNotFound);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for STArray
-        expectError({sfMemos.getCode()}, HostFunctionError::NotLeafField);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for STVector256
-        expectError({sfCredentialIDs.getCode()}, HostFunctionError::NotLeafField);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for nesting into non-array/object field
-        expectError(
-            {sfAccount.getCode(),  // sfAccount is not an array or object
-             0,
-             sfAccount.getCode()},
-            HostFunctionError::LocatorMalformed);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for empty locator
-        expectError({}, HostFunctionError::LocatorMalformed);
-
-        // hfs.getTxNestedField(locator);
-        // Locator for malformed locator (not multiple of 4)
-        {
-            std::vector locatorVec = {sfMemos.getCode()};
-            vrt.setBytes(0, locatorVec.data(), 3);
-
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("tx_inner"), params, result, 0, 3, 256, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == static_cast(HostFunctionError::LocatorMalformed));
-        }
-    }
-
-    void
-    testGetCurrentLedgerObjNestedField()
-    {
-        testcase("getCurrentLedgerObjNestedField");
-        using namespace test::jtx;
-
-        Env env{*this};
-        Account const alice("alice");
-        Account const becky("becky");
-        // Create a SignerList for env.master
-        env(signers(env.master, 2, {{alice, 1}, {becky, 1}}));
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        // Find the signer ledger object
-        auto const signerKeylet = keylet::signerList(env.master.id());
-        BEAST_EXPECT(env.le(signerKeylet));
-
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, signerKeylet);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // hfs.getCurrentLedgerObjNestedField(baseLocatorSlice);
-        // Locator for base field
-        {
-            std::vector baseLocator = {sfSignerQuorum.getCode()};
-            vrt.setBytes(0, baseLocator.data(), baseLocator.size() * sizeof(int32_t));
-
-            WasmValVec params(4), result(1);
-            auto* trap =
-                ww(&import.at("home_le_inner"),
-                   params,
-                   result,
-                   0,
-                   baseLocator.size() * sizeof(int32_t),
-                   256,
-                   256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECTS(result[0].of.i32 > 0, std::to_string(result[0].of.i32)))
-            {
-                auto signerQuorumBytes = vrt.getBytes(params, 2);
-                signerQuorumBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(signerQuorumBytes == toBytes(static_cast(2)));
-            }
-        }
-
-        auto expectError = [&](std::vector const& locatorVec,
-                               HostFunctionError expectedError) {
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-
-            WasmValVec params(4), result(1);
-            // hfs.getCurrentLedgerObjNestedField(locator);
-            auto* trap =
-                ww(&import.at("home_le_inner"),
-                   params,
-                   result,
-                   0,
-                   locatorVec.size() * sizeof(int32_t),
-                   256,
-                   256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECTS(
-                result[0].of.i32 == hfErrorToInt(expectedError), std::to_string(result[0].of.i32));
-        };
-        // hfs.getCurrentLedgerObjNestedField(locator);
-        // Locator for non-existent base field
-        expectError(
-            {sfSigners.getCode(),  // sfSigners does not exist
-             0,
-             sfAccount.getCode()},
-            HostFunctionError::FieldNotFound);
-
-        // hfs.getCurrentLedgerObjNestedField(locator);
-        // Locator for nesting into non-array/object field
-        expectError(
-            {sfSignerQuorum.getCode(),  // sfSignerQuorum is not an array or object
-             0,
-             sfAccount.getCode()},
-            HostFunctionError::LocatorMalformed);
-
-        // hfs.getCurrentLedgerObjNestedField(emptyLocator);
-        // Locator for empty locator
-        {
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("home_le_inner"), params, result, 0, 0, 256, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == static_cast(HostFunctionError::LocatorMalformed));
-        }
-
-        // hfs.getCurrentLedgerObjNestedField(malformedLocator);
-        // Locator for malformed locator (not multiple of 4)
-        {
-            std::vector malformedLocatorVec = {sfMemos.getCode()};
-            vrt.setBytes(0, malformedLocatorVec.data(), 3);
-
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("home_le_inner"), params, result, 0, 3, 256, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == static_cast(HostFunctionError::LocatorMalformed));
-        }
-
-        // hfs.getCurrentLedgerObjNestedField(locator);
-        {
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master) + 5));
-            VirtualRuntime vrt2;
-            WasmHostFunctionsImpl dummyHfs(ac, dummyEscrow);
-
-            auto import2 = xrpl::createWasmImport(dummyHfs);
-            dummyHfs.setRT(vrt2);
-
-            std::vector const locatorVec = {sfAccount.getCode()};
-            vrt2.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-
-            WasmValVec params(4), result(1);
-            auto* trap =
-                ww(&import2.at("home_le_inner"),
-                   params,
-                   result,
-                   0,
-                   locatorVec.size() * sizeof(int32_t),
-                   256,
-                   256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECTS(
-                result[0].of.i32 == static_cast(HostFunctionError::LedgerObjNotFound),
-                std::to_string(result[0].of.i32));
-        }
-    }
-
-    void
-    testGetLedgerObjNestedField()
-    {
-        testcase("getLedgerObjNestedField");
-        using namespace test::jtx;
-
-        Env env{*this};
-        Account const alice("alice");
-        Account const becky("becky");
-        // Create a SignerList for env.master
-        env(signers(env.master, 2, {{alice, 1}, {becky, 1}}));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // Cache the SignerList ledger object in slot 1
-        auto const signerListKeylet = keylet::signerList(env.master.id());
-        // hfs.cacheLedgerObj(signerListKeylet.key, 1);
-        {
-            WasmValVec params(3), result(1);
-            vrt.setBytes(0, signerListKeylet.key.data(), uint256::size());
-            auto* trap = ww(&import.at("cache_le"), params, result, 0, uint256::size(), 1);
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 1);
-        }
-
-        // Locator for sfSignerEntries[0].sfAccount
-        {
-            std::vector const locatorVec = {
-                sfSignerEntries.getCode(), 0, sfAccount.getCode()};
-            // hfs.getLedgerObjNestedField(1, locator);
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("le_inner"),
-                   params,
-                   result,
-                   1,
-                   0,
-                   locatorVec.size() * sizeof(int32_t),
-                   256,
-                   256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECTS(result[0].of.i32 > 0, std::to_string(result[0].of.i32)))
-            {
-                auto aliceIdBytes = vrt.getBytes(params, 3);
-                aliceIdBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(std::ranges::equal(aliceIdBytes, alice.id()));
-            }
-        }
-
-        // Locator for sfSignerEntries[1].sfAccount
-        {
-            std::vector const locatorVec = {
-                sfSignerEntries.getCode(), 1, sfAccount.getCode()};
-            // hfs.getLedgerObjNestedField(1, locator);
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("le_inner"),
-                   params,
-                   result,
-                   1,
-                   0,
-                   locatorVec.size() * sizeof(int32_t),
-                   256,
-                   256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECTS(result[0].of.i32 > 0, std::to_string(result[0].of.i32)))
-            {
-                auto beckyIdBytes = vrt.getBytes(params, 3);
-                beckyIdBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(std::ranges::equal(beckyIdBytes, becky.id()));
-            }
-        }
-
-        // Locator for sfSignerEntries[0].sfSignerWeight
-        {
-            std::vector const locatorVec = {
-                sfSignerEntries.getCode(), 0, sfSignerWeight.getCode()};
-            // hfs.getLedgerObjNestedField(1, locator);
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("le_inner"),
-                   params,
-                   result,
-                   1,
-                   0,
-                   locatorVec.size() * sizeof(int32_t),
-                   256,
-                   256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECTS(result[0].of.i32 > 0, std::to_string(result[0].of.i32)))
-            {
-                // Should be 1
-                auto const expected = toBytes(static_cast(1));
-                auto weightBytes = vrt.getBytes(params, 3);
-                weightBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(weightBytes == expected);
-            }
-        }
-
-        // Locator for base field sfSignerQuorum
-        {
-            std::vector const locatorVec = {sfSignerQuorum.getCode()};
-            // hfs.getLedgerObjNestedField(1, locator);
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("le_inner"),
-                   params,
-                   result,
-                   1,
-                   0,
-                   locatorVec.size() * sizeof(int32_t),
-                   256,
-                   256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECTS(result[0].of.i32 > 0, std::to_string(result[0].of.i32)))
-            {
-                auto const expected = toBytes(static_cast(2));
-                auto quorumBytes = vrt.getBytes(params, 3);
-                quorumBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(quorumBytes == expected);
-            }
-        }
-
-        // Helper for error checks
-        auto expectError = [&](std::vector const& locatorVec,
-                               HostFunctionError expectedError,
-                               int slot = 1) {
-            // hfs.getLedgerObjNestedField(slot, locator);
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("le_inner"),
-                   params,
-                   result,
-                   slot,
-                   0,
-                   locatorVec.size() * sizeof(int32_t),
-                   256,
-                   256);
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECTS(
-                result[0].of.i32 == hfErrorToInt(expectedError), std::to_string(result[0].of.i32));
-        };
-
-        // Error: base field not found
-        expectError(
-            {sfSigners.getCode(),  // sfSigners does not exist
-             0,
-             sfAccount.getCode()},
-            HostFunctionError::FieldNotFound);
-
-        // Error: index out of bounds
-        expectError(
-            {sfSignerEntries.getCode(),
-             2,  // index 2 does not exist
-             sfAccount.getCode()},
-            HostFunctionError::IndexOutOfBounds);
-
-        // Error: nested field not found
-        expectError(
-            {
-                sfSignerEntries.getCode(),
-                0,
-                sfDestination.getCode()  // sfDestination does not exist
-            },
-            HostFunctionError::FieldNotFound);
-
-        // Error: invalid field code
-        expectError(
-            {fieldCode(99999, 99999), 0, sfAccount.getCode()}, HostFunctionError::InvalidField);
-
-        // Error: invalid nested field code
-        expectError(
-            {sfSignerEntries.getCode(), 0, fieldCode(99999, 99999)},
-            HostFunctionError::InvalidField);
-
-        // Error: slot out of range
-        expectError({sfSignerQuorum.getCode()}, HostFunctionError::SlotOutRange, 0);
-        expectError({sfSignerQuorum.getCode()}, HostFunctionError::SlotOutRange, 257);
-
-        // Error: empty slot
-        expectError({sfSignerQuorum.getCode()}, HostFunctionError::EmptySlot, 2);
-
-        // Error: locator for STArray (not leaf field)
-        expectError({sfSignerEntries.getCode()}, HostFunctionError::NotLeafField);
-
-        // Error: nesting into non-array/object field
-        expectError(
-            {sfSignerQuorum.getCode(), 0, sfAccount.getCode()},
-            HostFunctionError::LocatorMalformed);
-
-        // Error: empty locator
-        expectError({}, HostFunctionError::LocatorMalformed);
-
-        // Error: locator malformed (not multiple of 4)
-        {
-            std::vector const locatorVec = {sfSignerEntries.getCode()};
-            // hfs.getLedgerObjNestedField(1, locator);
-            vrt.setBytes(0, locatorVec.data(), 3);
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("le_inner"), params, result, 1, 0, 3, 256, 256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == static_cast(HostFunctionError::LocatorMalformed));
-        }
-    }
-
-    void
-    testGetTxArrayLen()
-    {
-        testcase("getTxArrayLen");
-        using namespace test::jtx;
-
-        std::string const credIdHex =
-            "0011223344556677889900112233445566778899001122334455667788990011";
-        uint256 credId;
-        BEAST_EXPECT(credId.parseHex(credIdHex));
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-
-        // Transaction with an array field
-        STTx const stx = STTx(ttESCROW_FINISH, [&](auto& obj) {
-            obj.setAccountID(sfAccount, env.master.id());
-            STArray memos;
-            {
-                STObject memoObj(sfMemo);
-                memoObj.setFieldVL(sfMemoData, Slice("hello", 5));
-                memos.push_back(memoObj);
-            }
-            {
-                STObject memoObj(sfMemo);
-                memoObj.setFieldVL(sfMemoData, Slice("world", 5));
-                memos.push_back(memoObj);
-            }
-            obj.setFieldArray(sfMemos, memos);
-            STVector256 credIds;
-            credIds.pushBack(credId);
-            obj.setFieldV256(sfCredentialIDs, credIds);
-        });
-
-        ApplyContext ac = createApplyContext(env, ov, stx);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // Should return 2 for sfMemos
-        // hfs.getTxArrayLen(sfMemos);
-        {
-            WasmValVec params(1), result(1);
-            auto* trap = ww(&import.at("tx_arr_len"), params, result, sfMemos.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECT(result[0].of.i32 > 0))
-                BEAST_EXPECT(result[0].of.i32 == 2);
-        }
-
-        // Should return error for non-array field
-        // hfs.getTxArrayLen(sfAccount);
-        {
-            WasmValVec params(1), result(1);
-            auto* trap = ww(&import.at("tx_arr_len"), params, result, sfAccount.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECT(result[0].of.i32 == static_cast(HostFunctionError::NoArray));
-        }
-
-        // Should return error for missing array field
-        // hfs.getTxArrayLen(sfSigners);
-        {
-            WasmValVec params(1), result(1);
-            auto* trap = ww(&import.at("tx_arr_len"), params, result, sfSigners.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECT(
-                result[0].of.i32 == static_cast(HostFunctionError::FieldNotFound));
-        }
-
-        // Should return 1 for sfCredentialIDs
-        // hfs.getTxArrayLen(sfCredentialIDs);
-        {
-            WasmValVec params(1), result(1);
-            auto* trap = ww(&import.at("tx_arr_len"), params, result, sfCredentialIDs.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECT(result[0].of.i32 > 0))
-                BEAST_EXPECT(result[0].of.i32 == 1);
-        }
-    }
-
-    void
-    testGetCurrentLedgerObjArrayLen()
-    {
-        testcase("getCurrentLedgerObjArrayLen");
-        using namespace test::jtx;
-
-        Env env{*this};
-        Account const alice("alice");
-        Account const becky("becky");
-        // Create a SignerList for env.master
-        env(signers(env.master, 2, {{alice, 1}, {becky, 1}}));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const signerKeylet = keylet::signerList(env.master.id());
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, signerKeylet);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // hfs.getCurrentLedgerObjArrayLen(sfSignerEntries);
-        {
-            WasmValVec params(1), result(1);
-            auto* trap =
-                ww(&import.at("home_le_arr_len"), params, result, sfSignerEntries.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECT(result[0].of.i32 > 0))
-                BEAST_EXPECT(result[0].of.i32 == 2);
-        }
-
-        // hfs.getCurrentLedgerObjArrayLen(sfMemos);
-        {
-            WasmValVec params(1), result(1);
-            auto* trap = ww(&import.at("home_le_arr_len"), params, result, sfMemos.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECT(
-                result[0].of.i32 == static_cast(HostFunctionError::FieldNotFound));
-        }
-
-        // Should return NO_ARRAY for non-array field
-        // hfs.getCurrentLedgerObjArrayLen(sfAccount);
-        {
-            WasmValVec params(1), result(1);
-            auto* trap = ww(&import.at("home_le_arr_len"), params, result, sfAccount.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECT(result[0].of.i32 == static_cast(HostFunctionError::NoArray));
-        }
-
-        {
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master) + 5));
-            VirtualRuntime vrt2;
-            WasmHostFunctionsImpl dummyHfs(ac, dummyEscrow);
-
-            auto import2 = xrpl::createWasmImport(dummyHfs);
-            dummyHfs.setRT(vrt2);
-
-            // auto const len = dummyHfs.getCurrentLedgerObjArrayLen(sfMemos);
-            WasmValVec params(1), result(1);
-            auto* trap = ww(&import2.at("home_le_arr_len"), params, result, sfMemos.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECT(
-                result[0].of.i32 == static_cast(HostFunctionError::LedgerObjNotFound));
-        }
-    }
-
-    void
-    testGetLedgerObjArrayLen()
-    {
-        testcase("getLedgerObjArrayLen");
-        using namespace test::jtx;
-
-        Env env{*this};
-        Account const alice("alice");
-        Account const becky("becky");
-        // Create a SignerList for env.master
-        env(signers(env.master, 2, {{alice, 1}, {becky, 1}}));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        auto const signerListKeylet = keylet::signerList(env.master.id());
-        // hfs.cacheLedgerObj(signerListKeylet.key, 1);
-        {
-            WasmValVec params(3), result(1);
-            vrt.setBytes(0, signerListKeylet.key.data(), uint256::size());
-            auto* trap = ww(&import.at("cache_le"), params, result, 0, uint256::size(), 1);
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 1);
-        }
-
-        {
-            // hfs.getLedgerObjArrayLen(1, sfSignerEntries);
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("le_arr_len"), params, result, 1, sfSignerEntries.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECT(result[0].of.i32 > 0))
-            {
-                // Should return 2 for sfSignerEntries
-                BEAST_EXPECT(result[0].of.i32 == 2);
-            }
-        }
-        {
-            // hfs.getLedgerObjArrayLen(0, sfSignerEntries);
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("le_arr_len"), params, result, 0, sfSignerEntries.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECT(result[0].of.i32 == static_cast(HostFunctionError::SlotOutRange));
-        }
-
-        {
-            // Should return error for non-array field
-            // hfs.getLedgerObjArrayLen(1, sfAccount);
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("le_arr_len"), params, result, 1, sfAccount.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECT(result[0].of.i32 == static_cast(HostFunctionError::NoArray));
-        }
-
-        {
-            // Should return error for empty slot
-            // hfs.getLedgerObjArrayLen(2, sfSignerEntries);
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("le_arr_len"), params, result, 2, sfSignerEntries.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECT(result[0].of.i32 == static_cast(HostFunctionError::EmptySlot));
-        }
-
-        {
-            // Should return error for missing array field
-            // hfs.getLedgerObjArrayLen(1, sfMemos);
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("le_arr_len"), params, result, 1, sfMemos.getCode());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECT(
-                result[0].of.i32 == static_cast(HostFunctionError::FieldNotFound));
-        }
-    }
-
-    void
-    testGetTxNestedArrayLen()
-    {
-        testcase("getTxNestedArrayLen");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-
-        STTx const stx = STTx(ttESCROW_FINISH, [&](auto& obj) {
-            STArray memos;
-            STObject memoObj(sfMemo);
-            memoObj.setFieldVL(sfMemoData, Slice("hello", 5));
-            memos.push_back(memoObj);
-            obj.setFieldArray(sfMemos, memos);
-        });
-
-        ApplyContext ac = createApplyContext(env, ov, stx);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // Helper for error checks
-        auto expectError = [&](std::vector const& locatorVec,
-                               HostFunctionError expectedError) {
-            // hfs.getTxNestedArrayLen(locator);
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-            WasmValVec params(2), result(1);
-            auto* trap =
-                ww(&import.at("tx_inner_arr_len"),
-                   params,
-                   result,
-                   0,
-                   locatorVec.size() * sizeof(int32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECTS(
-                result[0].of.i32 == hfErrorToInt(expectedError), std::to_string(result[0].of.i32));
-        };
-
-        // Locator for sfMemos
-        {
-            std::vector locatorVec = {sfMemos.getCode()};
-            // hfs.getTxNestedArrayLen(locator);
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-            WasmValVec params(2), result(1);
-            auto* trap =
-                ww(&import.at("tx_inner_arr_len"),
-                   params,
-                   result,
-                   0,
-                   locatorVec.size() * sizeof(int32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECT(result[0].of.i32 == 1);
-        }
-
-        // Error: non-array field
-        expectError({sfAccount.getCode()}, HostFunctionError::NoArray);
-
-        // Error: missing field
-        expectError({sfSigners.getCode()}, HostFunctionError::FieldNotFound);
-    }
-
-    void
-    testGetCurrentLedgerObjNestedArrayLen()
-    {
-        testcase("getCurrentLedgerObjNestedArrayLen");
-        using namespace test::jtx;
-
-        Env env{*this};
-        Account const alice("alice");
-        Account const becky("becky");
-        // Create a SignerList for env.master
-        env(signers(env.master, 2, {{alice, 1}, {becky, 1}}));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const signerKeylet = keylet::signerList(env.master.id());
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, signerKeylet);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // Helper for error checks
-        auto expectError = [&](std::vector const& locatorVec,
-                               HostFunctionError expectedError) {
-            // hfs.getCurrentLedgerObjNestedArrayLen(locator);
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-            WasmValVec params(2), result(1);
-            auto* trap =
-                ww(&import.at("home_le_inner_arr_len"),
-                   params,
-                   result,
-                   0,
-                   locatorVec.size() * sizeof(int32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECTS(
-                result[0].of.i32 == hfErrorToInt(expectedError), std::to_string(result[0].of.i32));
-        };
-
-        // Locator for sfSignerEntries
-        {
-            std::vector locatorVec = {sfSignerEntries.getCode()};
-            // hfs.getCurrentLedgerObjNestedArrayLen(locator);
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-            WasmValVec params(2), result(1);
-            auto* trap =
-                ww(&import.at("home_le_inner_arr_len"),
-                   params,
-                   result,
-                   0,
-                   locatorVec.size() * sizeof(int32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECT(result[0].of.i32 == 2);
-        }
-
-        // Error: non-array field
-        expectError({sfSignerQuorum.getCode()}, HostFunctionError::NoArray);
-
-        // Error: missing field
-        expectError({sfSigners.getCode()}, HostFunctionError::FieldNotFound);
-
-        {
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master) + 5));
-            VirtualRuntime vrt2;
-            WasmHostFunctionsImpl dummyHfs(ac, dummyEscrow);
-
-            auto import2 = xrpl::createWasmImport(dummyHfs);
-            dummyHfs.setRT(vrt2);
-
-            std::vector locatorVec = {sfAccount.getCode()};
-            // auto const result = dummyHfs.getCurrentLedgerObjNestedArrayLen(locator);
-            vrt2.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-            WasmValVec params(2), result(1);
-            auto* trap =
-                ww(&import2.at("home_le_inner_arr_len"),
-                   params,
-                   result,
-                   0,
-                   locatorVec.size() * sizeof(int32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECTS(
-                result[0].of.i32 == static_cast(HostFunctionError::LedgerObjNotFound),
-                std::to_string(result[0].of.i32));
-        }
-    }
-
-    void
-    testGetLedgerObjNestedArrayLen()
-    {
-        testcase("getLedgerObjNestedArrayLen");
-        using namespace test::jtx;
-
-        Env env{*this};
-        Account const alice("alice");
-        Account const becky("becky");
-        env(signers(env.master, 2, {{alice, 1}, {becky, 1}}));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        auto const signerListKeylet = keylet::signerList(env.master.id());
-        // hfs.cacheLedgerObj(signerListKeylet.key, 1);
-        {
-            WasmValVec params(3), result(1);
-            vrt.setBytes(0, signerListKeylet.key.data(), uint256::size());
-            auto* trap = ww(&import.at("cache_le"), params, result, 0, uint256::size(), 1);
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 1);
-        }
-
-        // Locator for sfSignerEntries
-        std::vector locatorVec = {sfSignerEntries.getCode()};
-        // hfs.getLedgerObjNestedArrayLen(1, locator);
-        {
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-            WasmValVec params(3), result(1);
-            auto* trap =
-                ww(&import.at("le_inner_arr_len"),
-                   params,
-                   result,
-                   1,
-                   0,
-                   locatorVec.size() * sizeof(int32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            if (BEAST_EXPECT(result[0].of.i32 > 0))
-                BEAST_EXPECT(result[0].of.i32 == 2);
-        }
-
-        // Helper for error checks
-        auto expectError = [&](std::vector const& locatorVec,
-                               HostFunctionError expectedError,
-                               int slot = 1) {
-            // hfs.getLedgerObjNestedArrayLen(slot, locator);
-            vrt.setBytes(0, locatorVec.data(), locatorVec.size() * sizeof(int32_t));
-            WasmValVec params(3), result(1);
-            auto* trap =
-                ww(&import.at("le_inner_arr_len"),
-                   params,
-                   result,
-                   slot,
-                   0,
-                   locatorVec.size() * sizeof(int32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32);
-            BEAST_EXPECTS(
-                result[0].of.i32 == hfErrorToInt(expectedError), std::to_string(result[0].of.i32));
-        };
-
-        // Error: non-array field
-        expectError({sfSignerQuorum.getCode()}, HostFunctionError::NoArray);
-
-        // Error: missing field
-        expectError({sfSigners.getCode()}, HostFunctionError::FieldNotFound);
-
-        // Slot out of range
-        expectError(locatorVec, HostFunctionError::SlotOutRange, 0);
-        expectError(locatorVec, HostFunctionError::SlotOutRange, 257);
-
-        // Empty slot
-        expectError(locatorVec, HostFunctionError::EmptySlot, 2);
-
-        // Error: empty locator
-        expectError({}, HostFunctionError::LocatorMalformed);
-
-        // Error: locator malformed (not multiple of 4)
-        {
-            // hfs.getLedgerObjNestedArrayLen(1, malformedLocator);
-            vrt.setBytes(0, locatorVec.data(), 3);
-            WasmValVec params(3), result(1);
-            auto* trap = ww(&import.at("le_inner_arr_len"), params, result, 1, 0, 3);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == static_cast(HostFunctionError::LocatorMalformed));
-        }
-
-        // Error: locator for non-STArray field
-        expectError(
-            {sfSignerQuorum.getCode(), 0, sfAccount.getCode()},
-            HostFunctionError::LocatorMalformed);
-    }
-
-    void
-    testUpdateData()
-    {
-        testcase("updateData");
-        using namespace test::jtx;
-
-        Env env{*this};
-        env(escrow::create(env.master, env.master, XRP(100)),
-            escrow::kFinishTime(env.now() + std::chrono::seconds(1)));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const escrowKeylet =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master) - 1));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, escrowKeylet);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // Should succeed for small data
-        Bytes data(10, 0x42);
-        // hfs.updateData(Slice(data.data(), data.size()));
-        {
-            vrt.setBytes(0, data.data(), data.size());
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("set_data"), params, result, 0, data.size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == data.size());
-            BEAST_EXPECT(hfs.getData() && *hfs.getData() == data);
-        }
-
-        // Should fail for too large data
-        Bytes bigData(kMaxWasmDataLength + 1, 0x42);
-        // hfs.updateData(Slice(bigData.data(), bigData.size()));
-        {
-            vrt.setBytes(0, bigData.data(), bigData.size());
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("set_data"), params, result, 0, bigData.size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == hfErrorToInt(HostFunctionError::DataFieldTooLarge));
-        }
-    }
-
-    void
-    testCheckSignature()
-    {
-        testcase("checkSignature");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // Generate a keypair and sign a message
-        auto const kp = generateKeyPair(KeyType::Secp256k1, randomSeed());
-        PublicKey const& pk = kp.first;
-        SecretKey const& sk = kp.second;
-        std::string const& message = "hello signature";
-        auto const sig = sign(pk, sk, Slice(message.data(), message.size()));
-
-        // Should succeed for valid signature
-        {
-            // hfs.checkSignature(
-            //     Slice(message.data(), message.size()),
-            //     Slice(sig.data(), sig.size()),
-            //     Slice(pk.data(), pk.size()));
-            vrt.setBytes(0, message.data(), message.size());
-            vrt.setBytes(256, sig.data(), sig.size());
-            vrt.setBytes(512, pk.data(), pk.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("check_sig"),
-                   params,
-                   result,
-                   0,
-                   message.size(),
-                   256,
-                   sig.size(),
-                   512,
-                   pk.size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 1);
-        }
-
-        // Should fail for invalid signature
-        {
-            std::string badSig(sig.size(), 0xFF);
-            // hfs.checkSignature(
-            //     Slice(message.data(), message.size()),
-            //     Slice(badSig.data(), badSig.size()),
-            //     Slice(pk.data(), pk.size()));
-            vrt.setBytes(0, message.data(), message.size());
-            vrt.setBytes(256, badSig.data(), badSig.size());
-            vrt.setBytes(512, pk.data(), pk.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("check_sig"),
-                   params,
-                   result,
-                   0,
-                   message.size(),
-                   256,
-                   badSig.size(),
-                   512,
-                   pk.size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 0);
-        }
-
-        // Should fail for invalid public key
-        {
-            std::string badPk(pk.size(), 0x00);
-            // hfs.checkSignature(
-            //     Slice(message.data(), message.size()),
-            //     Slice(sig.data(), sig.size()),
-            //     Slice(badPk.data(), badPk.size()));
-            vrt.setBytes(0, message.data(), message.size());
-            vrt.setBytes(256, sig.data(), sig.size());
-            vrt.setBytes(512, badPk.data(), badPk.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("check_sig"),
-                   params,
-                   result,
-                   0,
-                   message.size(),
-                   256,
-                   sig.size(),
-                   512,
-                   badPk.size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == hfErrorToInt(HostFunctionError::InvalidParams));
-        }
-
-        // Should fail for empty public key
-        {
-            // hfs.checkSignature(
-            //     Slice(message.data(), message.size()),
-            //     Slice(sig.data(), sig.size()),
-            //     Slice(nullptr, 0));
-            vrt.setBytes(0, message.data(), message.size());
-            vrt.setBytes(256, sig.data(), sig.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("check_sig"),
-                   params,
-                   result,
-                   0,
-                   message.size(),
-                   256,
-                   sig.size(),
-                   512,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == hfErrorToInt(HostFunctionError::InvalidParams));
-        }
-
-        // Should fail for empty signature
-        {
-            // hfs.checkSignature(
-            //     Slice(message.data(), message.size()),
-            //     Slice(nullptr, 0),
-            //     Slice(pk.data(), pk.size()));
-            vrt.setBytes(0, message.data(), message.size());
-            vrt.setBytes(512, pk.data(), pk.size());
-            WasmValVec params(6), result(1);
-            auto* trap = ww(
-                &import.at("check_sig"), params, result, 0, message.size(), 256, 0, 512, pk.size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 0);
-        }
-
-        // Should fail for empty message
-        {
-            // hfs.checkSignature(
-            //     Slice(nullptr, 0), Slice(sig.data(), sig.size()), Slice(pk.data(), pk.size()));
-            vrt.setBytes(256, sig.data(), sig.size());
-            vrt.setBytes(512, pk.data(), pk.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("check_sig"), params, result, 0, 0, 256, sig.size(), 512, pk.size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 0);
-        }
-    }
-
-    void
-    testComputeSha512HalfHash()
-    {
-        testcase("computeSha512HalfHash");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        std::string data = "hello world";
-        // hfs.computeSha512HalfHash(Slice(data.data(), data.size()));
-        {
-            vrt.setBytes(0, data.data(), data.size());
-            WasmValVec params(4), result(1);
-            auto* trap =
-                ww(&import.at("sha512_half"), params, result, 0, data.size(), 256, uint256::size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == uint256::size());
-
-            // Should match direct call to sha512Half
-            auto expected = sha512Half(Slice(data.data(), data.size()));
-            auto hashBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(std::ranges::equal(hashBytes, expected));
-        }
-    }
-
-    void
-    testKeyletFunctions()
-    {
-        testcase("keylet functions");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-        VirtualRuntime vrt;
-
-        auto const usdIssue = env.master["USD"].issue();
-        auto const masterID = env.master.id();
-        auto const baseMpt = makeMptID(1, masterID);
-
-        auto imp = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // Lambda to compare a Bytes (std::vector) to a keylet
-        auto compareKeylet = [](std::vector const& bytes, Keylet const& kl) {
-            return std::ranges::equal(bytes, kl.key);
-        };
-
-        {
-            auto const expected = keylet::account(masterID);
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&imp.at("accountroot_id"), params, result, masterID, 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 2);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 = ww(&imp.at("accountroot_id"), params, result, xrpAccount(), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::amm(xrpIssue(), usdIssue);
-            WasmValVec params(6), result(1);
-
-            auto* trap = ww(&imp.at("amm_id"), params, result, xrpIssue(), usdIssue, 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 = ww(&imp.at("amm_id"), params, result, xrpIssue(), xrpIssue(), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidParams));
-
-            auto* trap3 = ww(&imp.at("amm_id"), params, result, baseMpt, xrpIssue(), 1024, 32);
-            BEAST_EXPECT(
-                !trap3 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidParams));
-        }
-
-        {
-            auto const expected = keylet::check(masterID, SeqProxy::rawSequence(1u));
-            WasmValVec params(6), result(1);
-            auto* trap = ww(&imp.at("check_id"), params, result, masterID, toBytes(1u), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 =
-                ww(&imp.at("check_id"), params, result, xrpAccount(), toBytes(1u), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        std::string const credTypeStr = "test";
-        Slice const credType(credTypeStr.data(), credTypeStr.size());
-        Account const alice("alice");
-        {
-            auto const expected = keylet::credential(masterID, masterID, credType);
-            WasmValVec params(8), result(1);
-            auto* trap = ww(
-                &imp.at("credential_id"), params, result, masterID, masterID, credType, 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 6);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            std::string_view constexpr longCredTypeStr =
-                "abcdefghijklmnopqrstuvwxyz01234567890qwertyuiop[]"
-                "asdfghjkl;'zxcvbnm8237tr28weufwldebvfv8734t07p";
-            Slice const longCredType(longCredTypeStr.data(), longCredTypeStr.size());
-            static_assert(longCredTypeStr.size() > kMaxCredentialTypeLength);
-            auto* trap2 =
-                ww(&imp.at("credential_id"),
-                   params,
-                   result,
-                   masterID,
-                   alice.id(),
-                   longCredType,
-                   1024,
-                   32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidParams));
-
-            auto* trap3 =
-                ww(&imp.at("credential_id"),
-                   params,
-                   result,
-                   xrpAccount(),
-                   alice.id(),
-                   credType,
-                   1024,
-                   32);
-            BEAST_EXPECT(
-                !trap3 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-
-            auto* trap4 =
-                ww(&imp.at("credential_id"),
-                   params,
-                   result,
-                   masterID,
-                   xrpAccount(),
-                   credType,
-                   1024,
-                   32);
-            BEAST_EXPECT(
-                !trap4 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::did(masterID);
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&imp.at("did_id"), params, result, masterID, 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 2);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 = ww(&imp.at("did_id"), params, result, xrpAccount(), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::delegate(masterID, alice.id());
-            WasmValVec params(6), result(1);
-            auto* trap = ww(&imp.at("delegate_id"), params, result, masterID, alice.id(), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 = ww(&imp.at("delegate_id"), params, result, masterID, masterID, 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidParams));
-
-            auto* trap3 =
-                ww(&imp.at("delegate_id"), params, result, masterID, xrpAccount(), 1024, 32);
-            BEAST_EXPECT(
-                !trap3 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-
-            auto* trap4 =
-                ww(&imp.at("delegate_id"), params, result, xrpAccount(), masterID, 1024, 32);
-            BEAST_EXPECT(
-                !trap4 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::depositPreauth(masterID, alice.id());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&imp.at("deposit_preauth_id"), params, result, masterID, alice.id(), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 =
-                ww(&imp.at("deposit_preauth_id"), params, result, masterID, masterID, 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidParams));
-
-            auto* trap3 =
-                ww(&imp.at("deposit_preauth_id"), params, result, masterID, xrpAccount(), 1024, 32);
-            BEAST_EXPECT(
-                !trap3 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-
-            auto* trap4 =
-                ww(&imp.at("deposit_preauth_id"), params, result, xrpAccount(), masterID, 1024, 32);
-            BEAST_EXPECT(
-                !trap4 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::escrow(masterID, SeqProxy::rawSequence(1u));
-            WasmValVec params(6), result(1);
-            auto* trap = ww(&imp.at("escrow_id"), params, result, masterID, toBytes(1u), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 =
-                ww(&imp.at("escrow_id"), params, result, xrpAccount(), toBytes(1u), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        Currency const usd = toCurrency("USD");
-        {
-            auto const expected = keylet::trustLine(masterID, alice.id(), usd);
-            WasmValVec params(8), result(1);
-            auto* trap =
-                ww(&imp.at("trustline_id"), params, result, masterID, alice.id(), usd, 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 6);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 =
-                ww(&imp.at("trustline_id"), params, result, masterID, masterID, usd, 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidParams));
-
-            auto* trap3 =
-                ww(&imp.at("trustline_id"), params, result, masterID, xrpAccount(), usd, 1024, 32);
-            BEAST_EXPECT(
-                !trap3 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-
-            auto* trap4 =
-                ww(&imp.at("trustline_id"), params, result, xrpAccount(), masterID, usd, 1024, 32);
-            BEAST_EXPECT(
-                !trap4 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-
-            auto* trap5 =
-                ww(&imp.at("trustline_id"),
-                   params,
-                   result,
-                   masterID,
-                   alice.id(),
-                   toCurrency(""),
-                   1024,
-                   32);
-            BEAST_EXPECT(
-                !trap5 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidParams));
-        }
-
-        {
-            auto const expected = keylet::mptokenIssuance(makeMptID(1u, masterID));
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&imp.at("mpt_issuance_id"), params, result, masterID, toBytes(1u), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 =
-                ww(&imp.at("mpt_issuance_id"), params, result, xrpAccount(), toBytes(1u), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::mptoken(baseMpt, alice.id());
-            WasmValVec params(6), result(1);
-            auto* trap = ww(&imp.at("mptoken_id"), params, result, baseMpt, alice.id(), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 = ww(&imp.at("mptoken_id"), params, result, MPTID{}, alice.id(), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidParams));
-
-            auto* trap3 =
-                ww(&imp.at("mptoken_id"), params, result, baseMpt, xrpAccount(), 1024, 32);
-            BEAST_EXPECT(
-                !trap3 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::nftokenOffer(masterID, SeqProxy::rawSequence(1u));
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&imp.at("nft_offer_id"), params, result, masterID, toBytes(1u), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 =
-                ww(&imp.at("nft_offer_id"), params, result, xrpAccount(), toBytes(1u), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::offer(masterID, SeqProxy::rawSequence(1u));
-            WasmValVec params(6), result(1);
-            auto* trap = ww(&imp.at("offer_id"), params, result, masterID, toBytes(1u), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 =
-                ww(&imp.at("offer_id"), params, result, xrpAccount(), toBytes(1u), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::oracle(masterID, 1u);
-            WasmValVec params(6), result(1);
-            auto* trap = ww(&imp.at("oracle_id"), params, result, masterID, toBytes(1u), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 =
-                ww(&imp.at("oracle_id"), params, result, xrpAccount(), toBytes(1u), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected =
-                keylet::payChannel(masterID, alice.id(), SeqProxy::rawSequence(1u));
-            WasmValVec params(8), result(1);
-            auto* trap = ww(
-                &imp.at("paychan_id"), params, result, masterID, alice.id(), toBytes(1u), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 6);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 = ww(
-                &imp.at("paychan_id"), params, result, masterID, masterID, toBytes(1u), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidParams));
-
-            auto* trap3 =
-                ww(&imp.at("paychan_id"),
-                   params,
-                   result,
-                   masterID,
-                   xrpAccount(),
-                   toBytes(1u),
-                   1024,
-                   32);
-            BEAST_EXPECT(
-                !trap3 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-
-            auto* trap4 =
-                ww(&imp.at("paychan_id"),
-                   params,
-                   result,
-                   xrpAccount(),
-                   masterID,
-                   toBytes(1u),
-                   1024,
-                   32);
-            BEAST_EXPECT(
-                !trap4 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::permissionedDomain(masterID, SeqProxy::rawSequence(1u));
-            WasmValVec params(6), result(1);
-            auto* trap = ww(
-                &imp.at("permissioned_domain_id"), params, result, masterID, toBytes(1u), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 =
-                ww(&imp.at("permissioned_domain_id"),
-                   params,
-                   result,
-                   xrpAccount(),
-                   toBytes(1u),
-                   1024,
-                   32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::signerList(masterID);
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&imp.at("signers_id"), params, result, masterID, 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 2);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 = ww(&imp.at("signers_id"), params, result, xrpAccount(), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::ticket(masterID, SeqProxy::rawTicket(1u));
-            WasmValVec params(6), result(1);
-            auto* trap = ww(&imp.at("ticket_id"), params, result, masterID, toBytes(1u), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 =
-                ww(&imp.at("ticket_id"), params, result, xrpAccount(), toBytes(1u), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-
-        {
-            auto const expected = keylet::vault(masterID, SeqProxy::rawSequence(1u));
-            WasmValVec params(6), result(1);
-            auto* trap = ww(&imp.at("vault_id"), params, result, masterID, toBytes(1u), 1024, 32);
-            if (BEAST_EXPECT(!trap && result[0].kind == WASM_I32 && result[0].of.i32 == 32))
-            {
-                auto const actual = vrt.getBytes(params, 4);
-                BEAST_EXPECT(compareKeylet(actual, expected));
-            }
-
-            auto* trap2 =
-                ww(&imp.at("vault_id"), params, result, xrpAccount(), toBytes(1u), 1024, 32);
-            BEAST_EXPECT(
-                !trap2 && result[0].kind == WASM_I32 &&
-                result[0].of.i32 == static_cast(HostFunctionError::InvalidAccount));
-        }
-    }
-
-    void
-    testGetNFT()
-    {
-        testcase("getNFT");
-        using namespace test::jtx;
-
-        Env env{*this};
-        Account const alice("alice");
-        env.fund(XRP(1000), alice);
-        env.close();
-
-        // Mint NFT for alice
-        uint256 const nftId = token::getNextID(env, alice, 0u, 0u);
-        std::string const uri = "https://example.com/nft";
-        env(token::mint(alice), token::Uri(uri));
-        env.close();
-        uint256 const nftId2 = token::getNextID(env, alice, 0u, 0u);
-        env(token::mint(alice));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const dummyEscrow = keylet::escrow(alice, SeqProxy::rawSequence(env.seq(alice)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // Should succeed for valid NFT
-        {
-            // hfs.getNFT(alice.id(), nftId);
-            vrt.setBytes(0, alice.id().data(), AccountID::size());
-            vrt.setBytes(256, nftId.data(), uint256::size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("nft_uri"),
-                   params,
-                   result,
-                   0,
-                   AccountID::size(),
-                   256,
-                   uint256::size(),
-                   512,
-                   256);
-
-            if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 > 0))
-            {
-                auto uriBytes = vrt.getBytes(params, 4);
-                uriBytes.resize(result[0].of.i32);
-                BEAST_EXPECT(std::ranges::equal(uriBytes, uri));
-            }
-        }
-
-        // Should fail for invalid account
-        {
-            // hfs.getNFT(xrpAccount(), nftId);
-            vrt.setBytes(0, xrpAccount().data(), AccountID::size());
-            vrt.setBytes(256, nftId.data(), uint256::size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("nft_uri"),
-                   params,
-                   result,
-                   0,
-                   AccountID::size(),
-                   256,
-                   uint256::size(),
-                   512,
-                   256);
-
-            if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32))
-                BEAST_EXPECT(result[0].of.i32 == hfErrorToInt(HostFunctionError::InvalidAccount));
-        }
-
-        // Should fail for invalid nftId
-        {
-            // hfs.getNFT(alice.id(), uint256());
-            uint256 zeroId;
-            vrt.setBytes(0, alice.id().data(), AccountID::size());
-            vrt.setBytes(256, zeroId.data(), uint256::size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("nft_uri"),
-                   params,
-                   result,
-                   0,
-                   AccountID::size(),
-                   256,
-                   uint256::size(),
-                   512,
-                   256);
-
-            if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32))
-                BEAST_EXPECT(result[0].of.i32 == hfErrorToInt(HostFunctionError::InvalidParams));
-        }
-
-        // Should fail for invalid nftId
-        {
-            auto const badId = token::getNextID(env, alice, 0u, 1u);
-            // hfs.getNFT(alice.id(), badId);
-            vrt.setBytes(0, alice.id().data(), AccountID::size());
-            vrt.setBytes(256, badId.data(), uint256::size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("nft_uri"),
-                   params,
-                   result,
-                   0,
-                   AccountID::size(),
-                   256,
-                   uint256::size(),
-                   512,
-                   256);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == hfErrorToInt(HostFunctionError::LedgerObjNotFound));
-        }
-
-        {
-            // hfs.getNFT(alice.id(), nftId2);
-            vrt.setBytes(0, alice.id().data(), AccountID::size());
-            vrt.setBytes(256, nftId2.data(), uint256::size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("nft_uri"),
-                   params,
-                   result,
-                   0,
-                   AccountID::size(),
-                   256,
-                   uint256::size(),
-                   512,
-                   256);
-
-            if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32))
-                BEAST_EXPECT(result[0].of.i32 == hfErrorToInt(HostFunctionError::FieldNotFound));
-        }
-    }
-
-    void
-    testGetNFTIssuer()
-    {
-        testcase("getNFTIssuer");
-        using namespace test::jtx;
-
-        Env env{*this};
-        // Mint NFT for env.master
-        uint32_t const taxon = 12345;
-        uint256 const nftId = token::getNextID(env, env.master, taxon);
-        env(token::mint(env.master, taxon));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // Should succeed for valid NFT id
-        {
-            // hfs.getNFTIssuer(nftId);
-            vrt.setBytes(0, nftId.data(), uint256::size());
-            WasmValVec params(4), result(1);
-            auto* trap =
-                ww(&import.at("nft_issuer"),
-                   params,
-                   result,
-                   0,
-                   uint256::size(),
-                   256,
-                   AccountID::size());
-
-            if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == AccountID::size()))
-            {
-                auto issuerBytes = vrt.getBytes(params, 2);
-                BEAST_EXPECT(std::ranges::equal(issuerBytes, env.master.id()));
-            }
-        }
-
-        // Should fail for zero NFT id
-        {
-            // hfs.getNFTIssuer(uint256());
-            uint256 zeroId;
-            vrt.setBytes(0, zeroId.data(), uint256::size());
-            WasmValVec params(4), result(1);
-            auto* trap =
-                ww(&import.at("nft_issuer"),
-                   params,
-                   result,
-                   0,
-                   uint256::size(),
-                   256,
-                   AccountID::size());
-
-            if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32))
-                BEAST_EXPECT(result[0].of.i32 == hfErrorToInt(HostFunctionError::InvalidParams));
-        }
-    }
-
-    void
-    testGetNFTTaxon()
-    {
-        testcase("getNFTTaxon");
-        using namespace test::jtx;
-
-        Env env{*this};
-
-        uint32_t const taxon = 54321;
-        uint256 const nftId = token::getNextID(env, env.master, taxon);
-        env(token::mint(env.master, taxon));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // hfs.getNFTTaxon(nftId);
-        vrt.setBytes(0, nftId.data(), uint256::size());
-        WasmValVec params(4), result(1);
-        auto* trap =
-            ww(&import.at("nft_taxon"), params, result, 0, uint256::size(), 256, sizeof(uint32_t));
-
-        if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-            BEAST_EXPECT(result[0].of.i32 == sizeof(uint32_t)))
-        {
-            BEAST_EXPECT(vrt.getUint32(params, 2) == taxon);
-        }
-    }
-
-    void
-    testGetNFTFlags()
-    {
-        testcase("getNFTFlags");
-        using namespace test::jtx;
-
-        Env env{*this};
-
-        // Mint NFT with default flags
-        uint256 const nftId = token::getNextID(env, env.master, 0u, tfTransferable);
-        env(token::mint(env.master, 0), Txflags(tfTransferable));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.getNFTFlags(nftId);
-            vrt.setBytes(0, nftId.data(), uint256::size());
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("nft_flags"), params, result, 0, uint256::size());
-
-            if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32))
-                BEAST_EXPECT(result[0].of.i32 == tfTransferable);
-        }
-
-        // Should return 0 for zero NFT id
-        {
-            // hfs.getNFTFlags(uint256());
-            uint256 zeroId;
-            vrt.setBytes(0, zeroId.data(), uint256::size());
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("nft_flags"), params, result, 0, uint256::size());
-
-            if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32))
-                BEAST_EXPECT(result[0].of.i32 == 0);
-        }
-    }
-
-    void
-    testGetNFTTransferFee()
-    {
-        testcase("getNFTTransferFee");
-        using namespace test::jtx;
-
-        Env env{*this};
-
-        uint16_t const transferFee = 250;
-        uint256 const nftId = token::getNextID(env, env.master, 0u, tfTransferable, transferFee);
-        env(token::mint(env.master, 0), token::XferFee(transferFee), Txflags(tfTransferable));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.getNFTTransferFee(nftId);
-            vrt.setBytes(0, nftId.data(), uint256::size());
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("nft_xfer_fee"), params, result, 0, uint256::size());
-
-            if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32))
-                BEAST_EXPECT(result[0].of.i32 == transferFee);
-        }
-
-        // Should return 0 for zero NFT id
-        {
-            // hfs.getNFTTransferFee(uint256());
-            uint256 zeroId;
-            vrt.setBytes(0, zeroId.data(), uint256::size());
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("nft_xfer_fee"), params, result, 0, uint256::size());
-
-            if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32))
-                BEAST_EXPECT(result[0].of.i32 == 0);
-        }
-    }
-
-    void
-    testGetNFTSerial()
-    {
-        testcase("getNFTSequence");
-        using namespace test::jtx;
-
-        Env env{*this};
-
-        // Mint NFT with serial 0
-        uint256 const nftId = token::getNextID(env, env.master, 0u);
-        auto const serial = env.seq(env.master);
-        env(token::mint(env.master));
-        env.close();
-
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.getNFTSequence(nftId);
-            vrt.setBytes(0, nftId.data(), uint256::size());
-            WasmValVec params(4), result(1);
-            auto* trap =
-                ww(&import.at("nft_serial"),
-                   params,
-                   result,
-                   0,
-                   uint256::size(),
-                   256,
-                   sizeof(uint32_t));
-
-            if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == sizeof(uint32_t)))
-            {
-                BEAST_EXPECT(vrt.getUint32(params, 2) == serial);
-            }
-        }
-
-        // Should return 0 for zero NFT id
-        {
-            // hfs.getNFTSequence(uint256());
-            uint256 zeroId;
-            vrt.setBytes(0, zeroId.data(), uint256::size());
-            WasmValVec params(4), result(1);
-            auto* trap =
-                ww(&import.at("nft_serial"),
-                   params,
-                   result,
-                   0,
-                   uint256::size(),
-                   256,
-                   sizeof(uint32_t));
-
-            if (BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == sizeof(uint32_t)))
-            {
-                BEAST_EXPECT(vrt.getUint32(params, 2) == 0);
-            }
-        }
-    }
-
-    void
-    testTrace()
-    {
-        testcase("trace");
-        using namespace test::jtx;
-
-        {
-            Env env(*this);
-            OpenView ov{*env.current()};
-            test::StreamSink sink{beast::Severity::Trace};
-            beast::Journal const jlog{sink};
-            ApplyContext ac = createApplyContext(env, ov, jlog);
-
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            VirtualRuntime vrt;
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            std::string const msg = "test trace";
-            std::string data = "abc";
-            auto const slice = Slice(data.data(), data.size());
-
-            // AsText: data printed verbatim (was trace with as_hex = 0)
-            {
-                vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-                vrt.setBytes(256, slice.data(), slice.size());
-                WasmValVec params(5), result(0);
-                auto* trap =
-                    ww(&import.at("trace"),
-                       params,
-                       result,
-                       0,
-                       msg.size(),
-                       traceDataTypeToInt(TraceDataType::AsText),
-                       256,
-                       slice.size());
-
-                if (BEAST_EXPECT(!trap))
-                {
-                    auto const messages = sink.messages().str();
-                    BEAST_EXPECT(messages.contains(msg));
-                    BEAST_EXPECT(messages.contains(data));
-                }
-            }
-
-            // AsHex: host hex-encodes data (was trace with as_hex = 1)
-            {
-                vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-                vrt.setBytes(256, slice.data(), slice.size());
-                WasmValVec params(5), result(0);
-                auto* trap =
-                    ww(&import.at("trace"),
-                       params,
-                       result,
-                       0,
-                       msg.size(),
-                       traceDataTypeToInt(TraceDataType::AsHex),
-                       256,
-                       slice.size());
-
-                if (BEAST_EXPECT(!trap))
-                {
-                    auto const messages = sink.messages().str();
-                    std::string hex;
-                    hex.reserve(data.size() * 2);
-                    boost::algorithm::hex(data.begin(), data.end(), std::back_inserter(hex));
-                    BEAST_EXPECT(messages.contains(msg));
-                    BEAST_EXPECT(messages.contains(hex));
-                }
-            }
-
-            // Unknown data_type: logged as invalid, never a trap
-            {
-                vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-                vrt.setBytes(256, slice.data(), slice.size());
-                WasmValVec params(5), result(0);
-                auto* trap =
-                    ww(&import.at("trace"), params, result, 0, msg.size(), 9999, 256, slice.size());
-                BEAST_EXPECT(!trap);
-            }
-
-            // msg and data each fit, but their combined size exceeds
-            // kMaxWasmDataLength, so nothing is logged
-            {
-                std::string const longMsg(kMaxWasmDataLength, 'x');
-                vrt.setBytes(0, reinterpret_cast(longMsg.data()), longMsg.size());
-                vrt.setBytes(2048, slice.data(), slice.size());
-                WasmValVec params(5), result(0);
-                auto* trap =
-                    ww(&import.at("trace"),
-                       params,
-                       result,
-                       0,
-                       longMsg.size(),
-                       traceDataTypeToInt(TraceDataType::AsText),
-                       2048,
-                       slice.size());
-
-                if (BEAST_EXPECT(!trap))
-                {
-                    auto const messages = sink.messages().str();
-                    BEAST_EXPECT(messages.contains("message and data too long"));
-                    BEAST_EXPECT(!messages.contains(longMsg));
-                }
-            }
-        }
-
-        {
-            // logs disabled (trace < error)
-            Env env(*this);
-            OpenView ov{*env.current()};
-            test::StreamSink sink{beast::Severity::Error};
-            beast::Journal const jlog{sink};
-            ApplyContext ac = createApplyContext(env, ov, jlog);
-
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            VirtualRuntime vrt;
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            std::string const msg = "test trace";
-            std::string data = "abc";
-            auto const slice = Slice(data.data(), data.size());
-
-            vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-            vrt.setBytes(256, slice.data(), slice.size());
-            WasmValVec params(5), result(0);
-            auto* trap =
-                ww(&import.at("trace"),
-                   params,
-                   result,
-                   0,
-                   msg.size(),
-                   traceDataTypeToInt(TraceDataType::AsText),
-                   256,
-                   slice.size());
-
-            BEAST_EXPECT(!trap);
-            auto const messages = sink.messages().str();
-            BEAST_EXPECT(messages.empty());
-        }
-    }
-
-    void
-    testTraceNum()
-    {
-        testcase("traceNum");
-        using namespace test::jtx;
-
-        {
-            Env env(*this);
-            OpenView ov{*env.current()};
-            test::StreamSink sink{beast::Severity::Trace};
-            beast::Journal const jlog{sink};
-            ApplyContext ac = createApplyContext(env, ov, jlog);
-
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            VirtualRuntime vrt;
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            std::string const msg = "trace number";
-
-            // adjustWasmEndianess is its own inverse, so writing the adjusted value
-            // lets the wrapper's adjustment recover it on either endianness.
-            auto const traceNum = [&](TraceDataType type, auto value) {
-                auto const wire = adjustWasmEndianess(value);
-                vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-                vrt.setBytes(256, reinterpret_cast(&wire), sizeof(wire));
-                WasmValVec params(5), result(0);
-                auto* trap =
-                    ww(&import.at("trace"),
-                       params,
-                       result,
-                       0,
-                       msg.size(),
-                       traceDataTypeToInt(type),
-                       256,
-                       sizeof(wire));
-
-                if (BEAST_EXPECT(!trap))
-                {
-                    auto const messages = sink.messages().str();
-                    BEAST_EXPECT(messages.contains(msg));
-                    BEAST_EXPECT(messages.contains(std::to_string(value)));
-                }
-            };
-
-            traceNum(TraceDataType::Int64, int64_t{123456789});
-            traceNum(TraceDataType::Int64, int64_t{-42});
-            // Above int64 max -- unreachable through the old trace_num
-            traceNum(TraceDataType::Uint64, std::numeric_limits::max());
-
-            // Wrong buffer length for the type: logged as invalid, no trap
-            {
-                std::int32_t const tooShort = 7;
-                vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-                vrt.setBytes(256, reinterpret_cast(&tooShort), sizeof(tooShort));
-                WasmValVec params(5), result(0);
-                auto* trap =
-                    ww(&import.at("trace"),
-                       params,
-                       result,
-                       0,
-                       msg.size(),
-                       traceDataTypeToInt(TraceDataType::Int64),
-                       256,
-                       sizeof(tooShort));
-                BEAST_EXPECT(!trap);
-            }
-        }
-
-        {
-            // logs disabled
-            Env env(*this);
-            OpenView ov{*env.current()};
-            test::StreamSink sink{beast::Severity::Error};
-            beast::Journal const jlog{sink};
-            ApplyContext ac = createApplyContext(env, ov, jlog);
-
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            VirtualRuntime vrt;
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            std::string const msg = "trace number";
-            auto const wire = adjustWasmEndianess(int64_t{123456789});
-
-            vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-            vrt.setBytes(256, reinterpret_cast(&wire), sizeof(wire));
-            WasmValVec params(5), result(0);
-            auto* trap =
-                ww(&import.at("trace"),
-                   params,
-                   result,
-                   0,
-                   msg.size(),
-                   traceDataTypeToInt(TraceDataType::Int64),
-                   256,
-                   sizeof(wire));
-
-            BEAST_EXPECT(!trap);
-            auto const messages = sink.messages().str();
-            BEAST_EXPECT(messages.empty());
-        }
-    }
-
-    void
-    testTraceAccount()
-    {
-        testcase("traceAccount");
-        using namespace test::jtx;
-
-        {
-            Env env(*this);
-            OpenView ov{*env.current()};
-            test::StreamSink sink{beast::Severity::Trace};
-            beast::Journal const jlog{sink};
-            ApplyContext ac = createApplyContext(env, ov, jlog);
-
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            VirtualRuntime vrt;
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            std::string const msg = "trace account";
-            auto const& accountId = env.master.id();
-
-            vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-            vrt.setBytes(256, accountId.data(), accountId.size());
-            WasmValVec params(5), result(0);
-            auto* trap =
-                ww(&import.at("trace"),
-                   params,
-                   result,
-                   0,
-                   msg.size(),
-                   traceDataTypeToInt(TraceDataType::Account),
-                   256,
-                   accountId.size());
-
-            if (BEAST_EXPECT(!trap))
-            {
-                auto const messages = sink.messages().str();
-                BEAST_EXPECT(messages.contains(msg));
-                BEAST_EXPECT(messages.contains(env.master.human()));
-            }
-        }
-
-        {
-            // logs disabled
-            Env env(*this);
-            OpenView ov{*env.current()};
-            test::StreamSink sink{beast::Severity::Error};
-            beast::Journal const jlog{sink};
-            ApplyContext ac = createApplyContext(env, ov, jlog);
-
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            VirtualRuntime vrt;
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            std::string msg = "trace account";
-            auto const& accountId = env.master.id();
-
-            vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-            vrt.setBytes(256, accountId.data(), accountId.size());
-            WasmValVec params(5), result(0);
-            auto* trap =
-                ww(&import.at("trace"),
-                   params,
-                   result,
-                   0,
-                   msg.size(),
-                   traceDataTypeToInt(TraceDataType::Account),
-                   256,
-                   accountId.size());
-
-            BEAST_EXPECT(!trap);
-            auto const messages = sink.messages().str();
-            BEAST_EXPECT(messages.empty());
-        }
-    }
-
-    void
-    testTraceAmount()
-    {
-        testcase("traceAmount");
-        using namespace test::jtx;
-
-        {
-            Env env(*this);
-            OpenView ov{*env.current()};
-            test::StreamSink sink{beast::Severity::Trace};
-            beast::Journal const jlog{sink};
-            ApplyContext ac = createApplyContext(env, ov, jlog);
-
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            VirtualRuntime vrt;
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            std::string const msg = "trace amount";
-            STAmount const amount = XRP(12345);
-            {
-                Bytes amountBytes = toBytes(amount);
-                vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-                vrt.setBytes(256, amountBytes.data(), amountBytes.size());
-                WasmValVec params(5), result(0);
-                auto* trap =
-                    ww(&import.at("trace"),
-                       params,
-                       result,
-                       0,
-                       msg.size(),
-                       traceDataTypeToInt(TraceDataType::Amount),
-                       256,
-                       amountBytes.size());
-
-                if (BEAST_EXPECT(!trap))
-                {
-                    auto const messages = sink.messages().str();
-                    BEAST_EXPECT(messages.contains(msg));
-                    BEAST_EXPECT(messages.contains(amount.getFullText()));
-                }
-            }
-
-            // IOU amount
-            Account const alice("alice");
-            env.fund(XRP(1000), alice);
-            env.close();
-            STAmount const iouAmount = env.master["USD"](100);
-            {
-                Bytes amountBytes = toBytes(iouAmount);
-                vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-                vrt.setBytes(256, amountBytes.data(), amountBytes.size());
-                WasmValVec params(5), result(0);
-                auto* trap =
-                    ww(&import.at("trace"),
-                       params,
-                       result,
-                       0,
-                       msg.size(),
-                       traceDataTypeToInt(TraceDataType::Amount),
-                       256,
-                       amountBytes.size());
-
-                BEAST_EXPECT(!trap);
-            }
-
-            // MPT amount
-            {
-                auto const mptId = makeMptID(42, env.master.id());
-                Asset const mptAsset = Asset(mptId);
-                STAmount const mptAmount(mptAsset, 123456);
-
-                Bytes amountBytes = toBytes(mptAmount);
-                vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-                vrt.setBytes(256, amountBytes.data(), amountBytes.size());
-                WasmValVec params(5), result(0);
-                auto* trap =
-                    ww(&import.at("trace"),
-                       params,
-                       result,
-                       0,
-                       msg.size(),
-                       traceDataTypeToInt(TraceDataType::Amount),
-                       256,
-                       amountBytes.size());
-
-                BEAST_EXPECT(!trap);
-            }
-        }
-
-        {
-            // logs disabled
-            Env env(*this);
-            OpenView ov{*env.current()};
-            test::StreamSink sink{beast::Severity::Error};
-            beast::Journal const jlog{sink};
-            ApplyContext ac = createApplyContext(env, ov, jlog);
-
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            VirtualRuntime vrt;
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            std::string const msg = "trace amount";
-            STAmount const amount = XRP(12345);
-
-            Bytes amountBytes = toBytes(amount);
-            vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-            vrt.setBytes(256, amountBytes.data(), amountBytes.size());
-            WasmValVec params(5), result(0);
-            auto* trap =
-                ww(&import.at("trace"),
-                   params,
-                   result,
-                   0,
-                   msg.size(),
-                   traceDataTypeToInt(TraceDataType::Amount),
-                   256,
-                   amountBytes.size());
-
-            BEAST_EXPECT(!trap);
-            auto const messages = sink.messages().str();
-            BEAST_EXPECT(messages.empty());
-        }
-    }
-
-    // clang-format off
-
-    int const normalExp = 18;
-
-    Bytes const floatIntMin        =  {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};  // -2^63 (rounds to nearest: -(2^63-1))
-    Bytes const floatIntZero       =  {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00};  // 0
-    Bytes const floatIntMax        =  {0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00};  // 2^63-1
-    Bytes const floatUIntMax       =  {0x19, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A, 0x00, 0x00, 0x00, 0x01};  // 2^64-1
-
-    Bytes const floatMaxExp        =  {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00};  // 1e(Number::kMaxExponent + normalExp)
-    Bytes const floatPreMaxExp     =  {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF};  // 1e(Number::kMaxExponent + normalExp - 1)
-    Bytes const floatMinusMaxExp   =  {0xF2, 0x1F, 0x49, 0x4C, 0x58, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00};  // -1e(Number::kMaxExponent + normalExp)
-    Bytes const floatMinExp        =  {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00};  // 1e(Number::kMinExponent - normalExp)
-    Bytes const floatMax           =  {0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x00};  // Number::kMaxRep e(Number::kMaxExponent - normalExp)
-
-    Bytes const floatMaxIOU        =  {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x63, 0xFF, 0x9C, 0x00, 0x00, 0x00, 0x4E};  // 9999999999999999e(96)
-    Bytes const floatMinIOU        =  {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x9D};  // 1e(-96 - 3 + normalExp = -81)
-
-    Bytes const float1             =  {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE};  // 1
-    Bytes const floatMinus1        =  {0xF2, 0x1F, 0x49, 0x4C, 0x58, 0x9C, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE};  // -1
-    Bytes const float1More         =  {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x03, 0xE8, 0xFF, 0xFF, 0xFF, 0xEE};  // 1.000 000 000 000 001
-    Bytes const float2             =  {0x1B, 0xC1, 0x6D, 0x67, 0x4E, 0xC8, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE};  // 2
-    Bytes const float10            =  {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEF};  // 10
-    Bytes const floatPi            =  {0x2B, 0x99, 0x2D, 0xDF, 0xA2, 0x32, 0x48, 0xE8, 0xFF, 0xFF, 0xFF, 0xEE};  // 3.141592653589793
-    Bytes const floatInvalidZero   =  {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00};  // INVALID
-    Bytes const floatMinus3        =  {0xD6, 0x5D, 0xDB, 0xE5, 0x09, 0xD4, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE};  // -3
-
-    std::string const invalid = "invalid_data";
-
-    // clang-format on
-
-    template 
-    void
-    printFloats(std::string_view descr, T m, int e)
-    {
-        Serializer msg;
-        Number n;
-
-        if constexpr (std::is_signed_v)
-        {
-            n = Number(static_cast(m), e);
-        }
-        else
-        {
-            n = Number(static_cast(m), e, Number::Normalized{});
-        }
-
-        STNumber(sfNumber, n).add(msg);
-        auto const& data = msg.modData();
-        std::cout << std::setw(24) << descr << " m: " << std::setw(20) << n.mantissa()
-                  << ", e: " << std::setw(8) << n.exponent() << ", hex: ";
-        std::cout << std::hex << std::uppercase << std::setfill('0');
-        for (auto const& c : data)
-            std::cout << std::setw(2) << (unsigned)c << " ";
-        std::cout << std::dec << std::setfill(' ') << std::endl;
-    }
-
-    void
-    printNumbersBin()
-    {
-        printFloats("int64.min", std::numeric_limits::min(), 0);
-        printFloats("zero", 0, 0);
-        printFloats("int64.max", std::numeric_limits::max(), 0);
-        printFloats("uint64.max", std::numeric_limits::max(), 0);
-
-        printFloats("Number 1 max exp", 1, Number::kMaxExponent + normalExp);
-        printFloats("Number (max exp - 1)", 1, Number::kMaxExponent + normalExp - 1);
-        printFloats("Number -1 max exp", -1, Number::kMaxExponent + normalExp);
-
-        printFloats("Number.max", Number::kMaxRep, Number::kMaxExponent);
-        printFloats("Number min positive", 1, Number::kMinExponent + normalExp);
-        printFloats(
-            "Number.min", std::numeric_limits::min(), Number::kMaxExponent - normalExp);
-        printFloats("STAmount.max", STAmount::kMaxValue, STAmount::kMaxOffset);
-        printFloats("STAmount min positive", STAmount::kMinValue, STAmount::kMinOffset);
-
-        printFloats("one", 1, 0);
-        printFloats("-one", -1, 0);
-        printFloats("1,00...01", 1'000'000'000'000'001, -15);
-        printFloats("two", 2, 0);
-        printFloats("ten", 10, 0);
-        printFloats("pi", 3141592653589793, -15);
-        printFloats("-three", -3, 0);
-    }
-
-    void
-    testTraceFloat()
-    {
-        testcase("traceFloat");
-        using namespace test::jtx;
-
-        {
-            Env env{*this};
-            OpenView ov{*env.current()};
-            ApplyContext ac = createApplyContext(env, ov);
-
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            VirtualRuntime vrt;
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            std::string const msg = "trace float";
-
-            {
-                vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-                vrt.setBytes(256, reinterpret_cast(invalid.data()), invalid.size());
-                WasmValVec params(5), result(0);
-                auto* trap =
-                    ww(&import.at("trace"),
-                       params,
-                       result,
-                       0,
-                       msg.size(),
-                       traceDataTypeToInt(TraceDataType::Xfloat),
-                       256,
-                       invalid.size());
-
-                BEAST_EXPECT(!trap);
-            }
-
-            {
-                vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-                vrt.setBytes(256, floatMaxExp.data(), floatMaxExp.size());
-                WasmValVec params(5), result(0);
-                auto* trap =
-                    ww(&import.at("trace"),
-                       params,
-                       result,
-                       0,
-                       msg.size(),
-                       traceDataTypeToInt(TraceDataType::Xfloat),
-                       256,
-                       floatMaxExp.size());
-
-                BEAST_EXPECT(!trap);
-            }
-        }
-
-        {
-            // logs disabled
-            Env env(*this);
-            OpenView ov{*env.current()};
-            test::StreamSink sink{beast::Severity::Error};
-            beast::Journal const jlog{sink};
-            ApplyContext ac = createApplyContext(env, ov, jlog);
-
-            auto const dummyEscrow =
-                keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-            WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-            VirtualRuntime vrt;
-            auto import = xrpl::createWasmImport(hfs);
-            hfs.setRT(vrt);
-
-            std::string const msg = "trace float";
-
-            vrt.setBytes(0, reinterpret_cast(msg.data()), msg.size());
-            vrt.setBytes(256, reinterpret_cast(invalid.data()), invalid.size());
-            WasmValVec params(5), result(0);
-            auto* trap =
-                ww(&import.at("trace"),
-                   params,
-                   result,
-                   0,
-                   msg.size(),
-                   traceDataTypeToInt(TraceDataType::Xfloat),
-                   256,
-                   invalid.size());
-
-            BEAST_EXPECT(!trap);
-            auto const messages = sink.messages().str();
-            BEAST_EXPECT(messages.empty());
-        }
-    }
-
-    void
-    testFloatFromInt()
-    {
-        testcase("floatFromInt");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.floatFromInt(min64, -1);
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("float_from_int"), params, result, min64, 0, floatSize, -1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatFromInt(min64, 4);
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("float_from_int"), params, result, min64, 0, floatSize, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatFromInt(min64, 0);
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("float_from_int"), params, result, min64, 0, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 1);
-            BEAST_EXPECT(resultBytes == floatIntMin);
-        }
-
-        {
-            // hfs.floatFromInt(0, 0);
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("float_from_int"), params, result, 0ll, 0, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 1);
-            BEAST_EXPECT(resultBytes == floatIntZero);
-        }
-
-        {
-            // hfs.floatFromInt(max64, 0);
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("float_from_int"), params, result, max64, 0, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 1);
-            BEAST_EXPECT(resultBytes == floatIntMax);
-        }
-    }
-
-    void
-    testFloatFromUint()
-    {
-        testcase("floatFromUint");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.floatFromUint(std::numeric_limits::min(), -1);
-            WasmValVec params(5), result(1);
-            uint64_t val = std::numeric_limits::min();
-            vrt.setBytes(0, &val, sizeof(val));
-            auto* trap = ww(&import.at("float_from_uint"), params, result, 0, 8, 16, floatSize, -1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatFromUint(std::numeric_limits::min(), 4);
-            WasmValVec params(5), result(1);
-            uint64_t val = std::numeric_limits::min();
-            vrt.setBytes(0, &val, sizeof(val));
-            auto* trap = ww(&import.at("float_from_uint"), params, result, 0, 8, 16, floatSize, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatFromUint(0, 0);
-            WasmValVec params(5), result(1);
-            uint64_t val = 0;
-            vrt.setBytes(0, &val, sizeof(val));
-            auto* trap = ww(&import.at("float_from_uint"), params, result, 0, 8, 16, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatIntZero);
-        }
-
-        {
-            // hfs.floatFromUint(std::numeric_limits::max(), 0);
-            WasmValVec params(5), result(1);
-            uint64_t val = std::numeric_limits::max();
-            vrt.setBytes(0, &val, sizeof(val));
-            auto* trap = ww(&import.at("float_from_uint"), params, result, 0, 8, 16, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatUIntMax);
-        }
-    }
-
-    void
-    testfloatFromMantExp()
-    {
-        testcase("floatFromMantExp");
-        using namespace test::jtx;
-        using namespace wasm_float;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.floatFromMantExp(1, 0, -1);
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_mant_exp"), params, result, 1ll, 0, 0, floatSize, -1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatFromMantExp(1, 0, 4);
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_mant_exp"), params, result, 1ll, 0, 0, floatSize, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatFromMantExp(1, Number::kMaxExponent + normalExp + 1, 0);
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_mant_exp"),
-                   params,
-                   result,
-                   1ll,
-                   Number::kMaxExponent + normalExp + 1,
-                   0,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatFromMantExp(1, Number::kMinExponent + normalExp - 1, 0);
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_mant_exp"),
-                   params,
-                   result,
-                   1ll,
-                   Number::kMinExponent + normalExp - 1,
-                   0,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatIntZero);
-        }
-
-        {
-            // hfs.floatFromMantExp(1, Number::kMaxExponent + normalExp, 0);
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_mant_exp"),
-                   params,
-                   result,
-                   1ll,
-                   Number::kMaxExponent + normalExp,
-                   0,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatMaxExp);
-        }
-
-        {
-            // hfs.floatFromMantExp(-1, Number::kMaxExponent + normalExp, 0);
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_mant_exp"),
-                   params,
-                   result,
-                   -1ll,
-                   Number::kMaxExponent + normalExp,
-                   0,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatMinusMaxExp);
-        }
-
-        {
-            // hfs.floatFromMantExp(1, Number::kMaxExponent + normalExp - 1, 0);
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_mant_exp"),
-                   params,
-                   result,
-                   1ll,
-                   Number::kMaxExponent + normalExp - 1,
-                   0,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatPreMaxExp);
-        }
-
-        {
-            // hfs.floatFromMantExp(STAmount::kMaxValue, STAmount::kMaxOffset, 0);
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_mant_exp"),
-                   params,
-                   result,
-                   static_cast(STAmount::kMaxValue),
-                   STAmount::kMaxOffset,
-                   0,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatMaxIOU);
-        }
-
-        {
-            // hfs.floatFromMantExp(1, Number::kMinExponent + normalExp, 0);
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_mant_exp"),
-                   params,
-                   result,
-                   1ll,
-                   Number::kMinExponent - normalExp,
-                   0,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatMinExp);
-        }
-
-        {
-            // hfs.floatFromMantExp(10, -1, 0);
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_mant_exp"), params, result, 10ll, -1, 0, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == float1);
-        }
-
-        {
-            // hfs.floatFromMantExp(1, Number::kMaxExponent + normalExp + 1, 0);
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_mant_exp"),
-                   params,
-                   result,
-                   1ll,
-                   Number::kMaxExponent + normalExp + 1,
-                   0,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-    }
-
-    void
-    testFloatCompare()
-    {
-        testcase("floatCompare");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.floatCompare(Slice(), Slice());
-            WasmValVec params(4), result(1);
-            auto* trap = ww(&import.at("float_cmp"), params, result, 0, 0, 0, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatCompare(makeSlice(floatInvalidZero), Slice());
-            WasmValVec params(4), result(1);
-            vrt.setBytes(0, floatInvalidZero.data(), floatInvalidZero.size());
-            auto* trap = ww(&import.at("float_cmp"), params, result, 0, floatSize, 0, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatCompare(makeSlice(float1), makeSlice(invalid));
-            WasmValVec params(4), result(1);
-            vrt.setBytes(0, float1.data(), float1.size());
-            vrt.setBytes(floatSize, invalid.data(), invalid.size());
-            auto* trap = ww(
-                &import.at("float_cmp"), params, result, 0, floatSize, floatSize, invalid.size());
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatCompare(makeSlice(floatIntMin), makeSlice(floatIntZero));
-            WasmValVec params(4), result(1);
-            vrt.setBytes(0, floatIntMin.data(), floatIntMin.size());
-            vrt.setBytes(floatSize, floatIntZero.data(), floatIntZero.size());
-            auto* trap =
-                ww(&import.at("float_cmp"), params, result, 0, floatSize, floatSize, floatSize);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 2);
-        }
-
-        {
-            // hfs.floatCompare(makeSlice(floatIntMax), makeSlice(floatIntZero));
-            WasmValVec params(4), result(1);
-            vrt.setBytes(0, floatIntMax.data(), floatIntMax.size());
-            vrt.setBytes(floatSize, floatIntZero.data(), floatIntZero.size());
-            auto* trap =
-                ww(&import.at("float_cmp"), params, result, 0, floatSize, floatSize, floatSize);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 1);
-        }
-
-        {
-            // hfs.floatCompare(makeSlice(float1), makeSlice(float1));
-            WasmValVec params(4), result(1);
-            vrt.setBytes(0, float1.data(), float1.size());
-            vrt.setBytes(floatSize, float1.data(), float1.size());
-            auto* trap =
-                ww(&import.at("float_cmp"), params, result, 0, floatSize, floatSize, floatSize);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 0);
-        }
-    }
-
-    void
-    testFloatAdd()
-    {
-        testcase("floatAdd");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.floatAdd(Slice(), Slice(), -1);
-            WasmValVec params(7), result(1);
-            auto* trap = ww(&import.at("float_add"), params, result, 0, 0, 0, 0, 0, floatSize, -1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatAdd(Slice(), Slice(), 0);
-            WasmValVec params(7), result(1);
-            auto* trap = ww(&import.at("float_add"), params, result, 0, 0, 0, 0, 0, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatAdd(makeSlice(float1), makeSlice(invalid), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, float1.data(), float1.size());
-            vrt.setBytes(floatSize, invalid.data(), invalid.size());
-            auto* trap =
-                ww(&import.at("float_add"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   invalid.size(),
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatAdd(makeSlice(floatMaxIOU), makeSlice(floatMaxExp), 0);
-            // max IOU is too small to make any change
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, floatMaxIOU.data(), floatMaxIOU.size());
-            vrt.setBytes(floatSize, floatMaxExp.data(), floatMaxExp.size());
-            auto* trap =
-                ww(&import.at("float_add"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 4);
-            BEAST_EXPECT(resultBytes == floatMaxExp);
-        }
-
-        {
-            // hfs.floatAdd(makeSlice(floatIntMin), makeSlice(floatIntZero), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, floatIntMin.data(), floatIntMin.size());
-            vrt.setBytes(floatSize, floatIntZero.data(), floatIntZero.size());
-            auto* trap =
-                ww(&import.at("float_add"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 4);
-            BEAST_EXPECT(resultBytes == floatIntMin);
-        }
-
-        {
-            // hfs.floatAdd(makeSlice(floatIntMax), makeSlice(floatIntMin), 0);//
-            //  int64.min is rounded to nearest: -(2^63-1), so max + min == 0
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, floatIntMax.data(), floatIntMax.size());
-            vrt.setBytes(floatSize, floatIntMin.data(), floatIntMin.size());
-            auto* trap =
-                ww(&import.at("float_add"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 4);
-            BEAST_EXPECT(resultBytes == floatIntZero);
-        }
-    }
-
-    void
-    testFloatSubtract()
-    {
-        testcase("floatSubtract");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.floatSubtract(Slice(), Slice(), -1);
-            WasmValVec params(7), result(1);
-            auto* trap = ww(&import.at("float_sub"), params, result, 0, 0, 0, 0, 0, floatSize, -1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatSubtract(Slice(), Slice(), 0);
-            WasmValVec params(7), result(1);
-            auto* trap = ww(&import.at("float_sub"), params, result, 0, 0, 0, 0, 0, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatSubtract(makeSlice(float1), makeSlice(invalid), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, float1.data(), float1.size());
-            vrt.setBytes(floatSize, invalid.data(), invalid.size());
-            auto* trap =
-                ww(&import.at("float_sub"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   invalid.size(),
-                   floatSize * 2,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatSubtract(makeSlice(floatMinusMaxExp), makeSlice(floatMaxIOU), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, floatMinusMaxExp.data(), floatMinusMaxExp.size());
-            vrt.setBytes(floatSize, floatMaxIOU.data(), floatMaxIOU.size());
-            auto* trap =
-                ww(&import.at("float_sub"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 4);
-            BEAST_EXPECT(resultBytes == floatMinusMaxExp);
-        }
-
-        {
-            // hfs.floatSubtract(makeSlice(floatIntMin), makeSlice(floatIntZero), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, floatIntMin.data(), floatIntMin.size());
-            vrt.setBytes(floatSize, floatIntZero.data(), floatIntZero.size());
-            auto* trap =
-                ww(&import.at("float_sub"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 4);
-            BEAST_EXPECT(resultBytes == floatIntMin);
-        }
-
-        {
-            // hfs.floatSubtract(makeSlice(floatIntZero), makeSlice(float1), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, floatIntZero.data(), floatIntZero.size());
-            vrt.setBytes(floatSize, float1.data(), float1.size());
-            auto* trap =
-                ww(&import.at("float_sub"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 4);
-            BEAST_EXPECT(resultBytes == floatMinus1);
-        }
-    }
-
-    void
-    testFloatMultiply()
-    {
-        testcase("floatMultiply");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.floatMultiply(Slice(), Slice(), -1);
-            WasmValVec params(7), result(1);
-            auto* trap = ww(&import.at("float_mult"), params, result, 0, 0, 0, 0, 0, floatSize, -1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatMultiply(Slice(), Slice(), 0);
-            WasmValVec params(7), result(1);
-            auto* trap = ww(&import.at("float_mult"), params, result, 0, 0, 0, 0, 0, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatMultiply(makeSlice(float1), makeSlice(invalid), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, float1.data(), float1.size());
-            vrt.setBytes(floatSize, invalid.data(), invalid.size());
-            auto* trap =
-                ww(&import.at("float_mult"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   invalid.size(),
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatMultiply(makeSlice(floatMax), makeSlice(float1More), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, floatMax.data(), floatMax.size());
-            vrt.setBytes(floatSize, float1More.data(), float1More.size());
-            auto* trap =
-                ww(&import.at("float_mult"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatComputationError));
-        }
-
-        {
-            // hfs.floatMultiply(makeSlice(float1), makeSlice(float1), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, float1.data(), float1.size());
-            vrt.setBytes(floatSize, float1.data(), float1.size());
-            auto* trap =
-                ww(&import.at("float_mult"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 4);
-            BEAST_EXPECT(resultBytes == float1);
-        }
-
-        {
-            // hfs.floatMultiply(makeSlice(floatIntZero), makeSlice(floatMaxIOU), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, floatIntZero.data(), floatIntZero.size());
-            vrt.setBytes(floatSize, floatMaxIOU.data(), floatMaxIOU.size());
-            auto* trap =
-                ww(&import.at("float_mult"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 4);
-            BEAST_EXPECT(resultBytes == floatIntZero);
-        }
-
-        {
-            // hfs.floatMultiply(makeSlice(float10), makeSlice(floatPreMaxExp), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, float10.data(), float10.size());
-            vrt.setBytes(floatSize, floatPreMaxExp.data(), floatPreMaxExp.size());
-            auto* trap =
-                ww(&import.at("float_mult"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 4);
-            BEAST_EXPECT(resultBytes == floatMaxExp);
-        }
-    }
-
-    void
-    testFloatDivide()
-    {
-        testcase("floatDivide");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.floatDivide(Slice(), Slice(), -1);
-            WasmValVec params(7), result(1);
-            auto* trap = ww(&import.at("float_div"), params, result, 0, 0, 0, 0, 0, floatSize, -1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatDivide(Slice(), Slice(), 0);
-            WasmValVec params(7), result(1);
-            auto* trap = ww(&import.at("float_div"), params, result, 0, 0, 0, 0, 0, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {  // hfs.floatDivide(makeSlice(float1), makeSlice(invalid), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, float1.data(), float1.size());
-            vrt.setBytes(floatSize, invalid.data(), invalid.size());
-            auto* trap =
-                ww(&import.at("float_div"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   invalid.size(),
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {  // hfs.floatDivide(makeSlice(float1), makeSlice(floatIntZero), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, float1.data(), float1.size());
-            vrt.setBytes(floatSize, floatIntZero.data(), floatIntZero.size());
-            auto* trap =
-                ww(&import.at("float_div"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatComputationError));
-        }
-
-        {  // hfs.floatDivide(makeSlice(floatMax), makeSlice(*y), 0);
-            auto const y =
-                hfs.floatFromMantExp(STAmount::kMaxValue, -normalExp - 1, 0);  // 0.9999999...
-            if (BEAST_EXPECT(y))
-            {
-                WasmValVec params(7), result(1);
-                vrt.setBytes(0, floatMax.data(), floatMax.size());
-                vrt.setBytes(floatSize, y->data(), y->size());
-                auto* trap =
-                    ww(&import.at("float_div"),
-                       params,
-                       result,
-                       0,
-                       floatSize,
-                       floatSize,
-                       floatSize,
-                       2 * floatSize,
-                       floatSize,
-                       0);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(
-                        result[0].of.i32 ==
-                        static_cast(HostFunctionError::FloatComputationError));
-            }
-        }
-
-        {  // hfs.floatDivide(makeSlice(floatIntZero), makeSlice(float1), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, floatIntZero.data(), floatIntZero.size());
-            vrt.setBytes(floatSize, float1.data(), float1.size());
-            auto* trap =
-                ww(&import.at("float_div"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 4);
-            BEAST_EXPECT(resultBytes == floatIntZero);
-        }
-
-        {  // hfs.floatDivide(makeSlice(floatMaxExp), makeSlice(float10), 0);
-            WasmValVec params(7), result(1);
-            vrt.setBytes(0, floatMaxExp.data(), floatMaxExp.size());
-            vrt.setBytes(floatSize, float10.data(), float10.size());
-            auto* trap =
-                ww(&import.at("float_div"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   floatSize,
-                   floatSize,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 4);
-            BEAST_EXPECT(resultBytes == floatPreMaxExp);
-        }
-    }
-
-    void
-    testFloatRoot()
-    {
-        testcase("floatRoot");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {  // hfs.floatRoot(Slice(), 2, -1);
-            WasmValVec params(6), result(1);
-            auto* trap = ww(&import.at("float_root"), params, result, 0, 0, 2, 0, floatSize, -1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {  // hfs.floatRoot(makeSlice(invalid), 3, 0);
-            WasmValVec params(6), result(1);
-            vrt.setBytes(0, invalid.data(), invalid.size());
-            auto* trap =
-                ww(&import.at("float_root"),
-                   params,
-                   result,
-                   0,
-                   invalid.size(),
-                   3,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {  // hfs.floatRoot(makeSlice(float1), -2, 0);
-            WasmValVec params(6), result(1);
-            vrt.setBytes(0, float1.data(), float1.size());
-            auto* trap =
-                ww(&import.at("float_root"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   -2,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {  // hfs.floatRoot(makeSlice(floatIntZero), 2, 0);
-            WasmValVec params(6), result(1);
-            vrt.setBytes(0, floatIntZero.data(), floatIntZero.size());
-            auto* trap =
-                ww(&import.at("float_root"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   2,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 3);
-            BEAST_EXPECT(resultBytes == floatIntZero);
-        }
-
-        {  // hfs.floatRoot(makeSlice(floatMaxIOU), 1, 0);
-            WasmValVec params(6), result(1);
-            vrt.setBytes(0, floatMaxIOU.data(), floatMaxIOU.size());
-            auto* trap =
-                ww(&import.at("float_root"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   1,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 3);
-            BEAST_EXPECT(resultBytes == floatMaxIOU);
-        }
-
-        {
-            // hfs.floatRoot(makeSlice(*x), 2, 0);
-            auto const x = hfs.floatFromMantExp(100, 0, 0);  // 100
-            if (BEAST_EXPECT(x))
-            {
-                WasmValVec params(6), result(1);
-                vrt.setBytes(0, x->data(), x->size());
-                auto* trap =
-                    ww(&import.at("float_root"),
-                       params,
-                       result,
-                       0,
-                       floatSize,
-                       2,
-                       2 * floatSize,
-                       floatSize,
-                       0);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 == floatSize);
-                auto const resultBytes = vrt.getBytes(params, 3);
-                BEAST_EXPECT(resultBytes == float10);
-            }
-        }
-
-        {
-            // hfs.floatRoot(makeSlice(*x), 3, 0);
-            auto const x = hfs.floatFromMantExp(1000, 0, 0);  // 1000
-            if (BEAST_EXPECT(x))
-            {
-                WasmValVec params(6), result(1);
-                vrt.setBytes(0, x->data(), x->size());
-                auto* trap =
-                    ww(&import.at("float_root"),
-                       params,
-                       result,
-                       0,
-                       floatSize,
-                       3,
-                       2 * floatSize,
-                       floatSize,
-                       0);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 == floatSize);
-                auto const resultBytes = vrt.getBytes(params, 3);
-                BEAST_EXPECT(resultBytes == float10);
-            }
-        }
-
-        {
-            // hfs.floatRoot(makeSlice(*x), 2, 0);
-            auto const x = hfs.floatFromMantExp(1, -2, 0);  // 0.01
-            auto const y = hfs.floatFromMantExp(1, -1, 0);  // 0.1
-            if (BEAST_EXPECT(x && y))
-            {
-                WasmValVec params(6), result(1);
-                vrt.setBytes(0, x->data(), x->size());
-                auto* trap =
-                    ww(&import.at("float_root"),
-                       params,
-                       result,
-                       0,
-                       floatSize,
-                       2,
-                       2 * floatSize,
-                       floatSize,
-                       0);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 == floatSize);
-                auto const resultBytes = vrt.getBytes(params, 3);
-                BEAST_EXPECT(resultBytes == *y);
-            }
-        }
-    }
-
-    void
-    testFloatPower()
-    {
-        testcase("floatPower");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {  // hfs.floatPower(Slice(), 2, -1);
-            WasmValVec params(6), result(1);
-            auto* trap = ww(&import.at("float_pow"), params, result, 0, 0, 2, 0, floatSize, -1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {  // hfs.floatPower(makeSlice(invalid), 3, 0);
-            WasmValVec params(6), result(1);
-            vrt.setBytes(0, invalid.data(), invalid.size());
-            auto* trap =
-                ww(&import.at("float_pow"),
-                   params,
-                   result,
-                   0,
-                   invalid.size(),
-                   3,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {  // hfs.floatPower(makeSlice(float1), -2, 0);
-            WasmValVec params(6), result(1);
-            vrt.setBytes(0, float1.data(), float1.size());
-            auto* trap =
-                ww(&import.at("float_pow"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   -2,
-                   2 * floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatPower(makeSlice(floatMax), 2, 0);
-            WasmValVec params(6), result(1);
-            vrt.setBytes(0, floatMax.data(), floatMax.size());
-            auto* trap = ww(
-                &import.at("float_pow"), params, result, 0, floatSize, 2, floatSize, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatComputationError));
-        }
-
-        {
-            // hfs.floatPower(makeSlice(floatMax), Number::kMaxExponent + 1, 0);
-            WasmValVec params(6), result(1);
-            vrt.setBytes(0, floatMax.data(), floatMax.size());
-            auto* trap =
-                ww(&import.at("float_pow"),
-                   params,
-                   result,
-                   0,
-                   floatSize,
-                   Number::kMaxExponent + 1,
-                   floatSize,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatPower(makeSlice(floatMaxIOU), 0, 0);
-            WasmValVec params(6), result(1);
-            vrt.setBytes(0, floatMaxIOU.data(), floatMaxIOU.size());
-            auto* trap = ww(
-                &import.at("float_pow"), params, result, 0, floatSize, 0, floatSize, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 3);
-            BEAST_EXPECT(resultBytes == float1);
-        }
-
-        {  // hfs.floatPower(makeSlice(floatMaxIOU), 1, 0);
-            WasmValVec params(6), result(1);
-            vrt.setBytes(0, floatMaxIOU.data(), floatMaxIOU.size());
-            auto* trap = ww(
-                &import.at("float_pow"), params, result, 0, floatSize, 1, floatSize, floatSize, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 3);
-            BEAST_EXPECT(resultBytes == floatMaxIOU);
-        }
-
-        {
-            // hfs.floatPower(makeSlice(float10), 2, 0);
-            auto const x = hfs.floatFromMantExp(100, 0, 0);  // 100
-            if (BEAST_EXPECT(x))
-            {
-                WasmValVec params(6), result(1);
-                vrt.setBytes(0, float10.data(), float10.size());
-                auto* trap =
-                    ww(&import.at("float_pow"),
-                       params,
-                       result,
-                       0,
-                       floatSize,
-                       2,
-                       2 * floatSize,
-                       floatSize,
-                       0);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 == floatSize);
-                auto const resultBytes = vrt.getBytes(params, 3);
-                BEAST_EXPECT(resultBytes == *x);
-            }
-        }
-
-        {
-            // hfs.floatPower(makeSlice(*x), 2, 0);
-            auto const x = hfs.floatFromMantExp(1, -1, 0);  // 0.1
-            auto const y = hfs.floatFromMantExp(1, -2, 0);  // 0.01
-            if (BEAST_EXPECT(x && y))
-            {
-                WasmValVec params(6), result(1);
-                vrt.setBytes(0, x->data(), x->size());
-                auto* trap =
-                    ww(&import.at("float_pow"),
-                       params,
-                       result,
-                       0,
-                       floatSize,
-                       2,
-                       2 * floatSize,
-                       floatSize,
-                       0);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 == floatSize);
-                auto const resultBytes = vrt.getBytes(params, 3);
-                BEAST_EXPECT(resultBytes == *y);
-            }
-        }
-    }
-
-    void
-    testFloatSpecialCases()
-    {
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        WasmHostFunctionsImpl const hfs(ac, dummyEscrow);
-
-        testcase("float non-canonical");
-
-        {  // non-canonical mantissa 100000e-4
-            Bytes const y = {
-                0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x86, 0xA0, 0xFF, 0xFF, 0xFF, 0xFC};
-            auto const result = hfs.floatCompare(makeSlice(y), makeSlice(float10));
-            BEAST_EXPECT(result && *result == 0);
-        }
-    }
-
-    void
-    testFloatFromSTAmount()
-    {
-        testcase("floatFromSTAmount");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.floatFromSTAmount(amount, -1);
-            STAmount const amount = XRP(100);
-            Bytes amountBytes = toBytes(amount);
-            vrt.setBytes(0, amountBytes.data(), amountBytes.size());
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_stamount"),
-                   params,
-                   result,
-                   0,
-                   amountBytes.size(),
-                   256,
-                   floatSize,
-                   -1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatFromSTAmount(amount, 4);
-            STAmount const amount = XRP(100);
-            Bytes amountBytes = toBytes(amount);
-            vrt.setBytes(0, amountBytes.data(), amountBytes.size());
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_stamount"),
-                   params,
-                   result,
-                   0,
-                   amountBytes.size(),
-                   256,
-                   floatSize,
-                   4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatFromSTAmount(amount, 0);
-            STAmount const amount = XRP(0);
-            Bytes amountBytes = toBytes(amount);
-            vrt.setBytes(0, amountBytes.data(), amountBytes.size());
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_stamount"),
-                   params,
-                   result,
-                   0,
-                   amountBytes.size(),
-                   256,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatIntZero);
-        }
-
-        {
-            // hfs.floatFromSTAmount(amount, 0);
-            STAmount const amount = XRP(-1);
-            auto const y = hfs.floatFromMantExp(-1 * 1'000'000, 0, 0);
-            if (BEAST_EXPECT(y))
-            {
-                Bytes amountBytes = toBytes(amount);
-                vrt.setBytes(0, amountBytes.data(), amountBytes.size());
-                WasmValVec params(5), result(1);
-                auto* trap =
-                    ww(&import.at("float_from_stamount"),
-                       params,
-                       result,
-                       0,
-                       amountBytes.size(),
-                       256,
-                       floatSize,
-                       0);
-
-                BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                    BEAST_EXPECT(result[0].of.i32 == floatSize);
-                auto const resultBytes = vrt.getBytes(params, 2);
-                BEAST_EXPECT(resultBytes == *y);
-            }
-        }
-
-        {
-            // hfs.floatFromSTAmount(amount, 0);
-            auto const y = hfs.floatFromMantExp(9223372036854776, 3, 0);
-            STAmount const amount(noIssue(), std::numeric_limits::max());
-            Bytes amountBytes = toBytes(amount);
-            vrt.setBytes(0, amountBytes.data(), amountBytes.size());
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_stamount"),
-                   params,
-                   result,
-                   0,
-                   amountBytes.size(),
-                   256,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == *y);
-        }
-
-        {
-            bool ex = false;
-            try
-            {
-                STAmount const amount(noIssue(), -1, Number::kMaxExponent + normalExp);
-                [[maybe_unused]] Bytes const amountBytes = toBytes(amount);
-            }
-            catch (...)
-            {
-                ex = true;
-            }
-
-            BEAST_EXPECT(ex);
-        }
-
-        auto const usd = env.master["USD"];
-        {
-            // hfs.floatFromSTAmount(amount, 0);
-            STAmount const amount(
-                IOUAmount(STAmount::kMinValue, STAmount::kMinOffset), usd.issue());
-            Bytes amountBytes = toBytes(amount);
-            vrt.setBytes(0, amountBytes.data(), amountBytes.size());
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_stamount"),
-                   params,
-                   result,
-                   0,
-                   amountBytes.size(),
-                   256,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatMinIOU);
-        }
-
-        {
-            // hfs.floatFromSTAmount(amount, 0);
-            STAmount const amount(
-                IOUAmount(STAmount::kMaxValue, STAmount::kMaxOffset), usd.issue());
-            Bytes amountBytes = toBytes(amount);
-            vrt.setBytes(0, amountBytes.data(), amountBytes.size());
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_stamount"),
-                   params,
-                   result,
-                   0,
-                   amountBytes.size(),
-                   256,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatMaxIOU);
-        }
-    }
-
-    void
-    testFloatFromSTNumber()
-    {
-        testcase("floatFromSTNumber");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // Test with invalid rounding mode
-        {
-            // hfs.floatFromSTNumber(num, -1);
-            STNumber const num(sfNumber, Number(123, 0));
-            Bytes numBytes = toBytes(num);
-            vrt.setBytes(0, numBytes.data(), numBytes.size());
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_stnumber"),
-                   params,
-                   result,
-                   0,
-                   numBytes.size(),
-                   256,
-                   floatSize,
-                   -1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatFromSTNumber(num, 4);
-            STNumber const num(sfNumber, Number(123, 0));
-            Bytes numBytes = toBytes(num);
-            vrt.setBytes(0, numBytes.data(), numBytes.size());
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_stnumber"),
-                   params,
-                   result,
-                   0,
-                   numBytes.size(),
-                   256,
-                   floatSize,
-                   4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatFromSTNumber(num, 0);
-            STNumber const num(
-                sfNumber, Number(std::numeric_limits::max(), 0, Number::Normalized{}));
-            Bytes numBytes = toBytes(num);
-            vrt.setBytes(0, numBytes.data(), numBytes.size());
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_stnumber"),
-                   params,
-                   result,
-                   0,
-                   numBytes.size(),
-                   256,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatUIntMax);
-        }
-
-        {
-            // hfs.floatFromSTNumber(num, 0);
-            STNumber const num(sfNumber, Number(-1, Number::kMaxExponent + normalExp));
-            Bytes numBytes = toBytes(num);
-            vrt.setBytes(0, numBytes.data(), numBytes.size());
-            WasmValVec params(5), result(1);
-            auto* trap =
-                ww(&import.at("float_from_stnumber"),
-                   params,
-                   result,
-                   0,
-                   numBytes.size(),
-                   256,
-                   floatSize,
-                   0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const resultBytes = vrt.getBytes(params, 2);
-            BEAST_EXPECT(resultBytes == floatMinusMaxExp);
-        }
-    }
-
-    void
-    testFloatToInt()
-    {
-        testcase("floatToInt");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.floatToInt(makeSlice(float1), -1);
-            vrt.setBytes(0, float1.data(), float1.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, -1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatToInt(makeSlice(float1), 4);
-            vrt.setBytes(0, float1.data(), float1.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatToInt(Slice(), 0);
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, 0, 256, 8, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatToInt(makeSlice(invalid), 0);
-            vrt.setBytes(0, invalid.data(), invalid.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatToInt(makeSlice(floatIntZero), 0);
-            vrt.setBytes(0, floatIntZero.data(), floatIntZero.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 8);
-            auto const resultVal = vrt.getInt64(params, 2);
-            BEAST_EXPECT(resultVal == 0);
-
-            // roundtrip
-            auto const result2 = hfs.floatFromInt(resultVal, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == floatIntZero);
-        }
-
-        {
-            // hfs.floatToInt(makeSlice(float1), 0);
-            vrt.setBytes(0, float1.data(), float1.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 8);
-            auto const resultVal = vrt.getInt64(params, 2);
-            BEAST_EXPECT(resultVal == 1);
-
-            // roundtrip
-            auto const result2 = hfs.floatFromInt(resultVal, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == float1);
-        }
-
-        {
-            // hfs.floatToInt(makeSlice(floatMinus1), 0);
-            vrt.setBytes(0, floatMinus1.data(), floatMinus1.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 8);
-            auto const resultVal = vrt.getInt64(params, 2);
-            BEAST_EXPECT(resultVal == -1);
-
-            // roundtrip
-            auto const result2 = hfs.floatFromInt(resultVal, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == floatMinus1);
-        }
-
-        {
-            // hfs.floatToInt(makeSlice(floatIntMax), 0);
-            vrt.setBytes(0, floatIntMax.data(), floatIntMax.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 8);
-            auto const resultVal = vrt.getInt64(params, 2);
-            BEAST_EXPECT(resultVal == std::numeric_limits::max());
-
-            // roundtrip
-            auto const result2 = hfs.floatFromInt(resultVal, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == floatIntMax);
-        }
-
-        {
-            // int64.min is rounded to nearest: -(2^63-1), which fits into int64
-            // hfs.floatToInt(makeSlice(floatIntMin), 0);
-            vrt.setBytes(0, floatIntMin.data(), floatIntMin.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 8);
-            auto const resultVal = vrt.getInt64(params, 2);
-            BEAST_EXPECT(resultVal == -std::numeric_limits::max());
-
-            // roundtrip
-            auto const result2 = hfs.floatFromInt(resultVal, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == floatIntMin);
-        }
-
-        {
-            // hfs.floatToInt(makeSlice(floatUIntMax), 0);
-            vrt.setBytes(0, floatUIntMax.data(), floatUIntMax.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatComputationError));
-        }
-
-        // Test rounding modes with pi (3.141592653589793)
-        {
-            // to_nearest (mode 0): should round to 3
-            // hfs.floatToInt(makeSlice(floatPi), 0);
-            vrt.setBytes(0, floatPi.data(), floatPi.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, 0);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 8);
-            auto const resultVal = vrt.getInt64(params, 2);
-            BEAST_EXPECT(resultVal == 3);
-        }
-
-        {
-            // towards_zero (mode 1): should truncate to 3
-            // hfs.floatToInt(makeSlice(floatPi), 1);
-            vrt.setBytes(0, floatPi.data(), floatPi.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, 1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 8);
-            auto const resultVal = vrt.getInt64(params, 2);
-            BEAST_EXPECT(resultVal == 3);
-        }
-
-        {
-            // downward (mode 2): should round down to 3
-            // hfs.floatToInt(makeSlice(floatPi), 2);
-            vrt.setBytes(0, floatPi.data(), floatPi.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, 2);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 8);
-            auto const resultVal = vrt.getInt64(params, 2);
-            BEAST_EXPECT(resultVal == 3);
-        }
-
-        {
-            // upward (mode 3): should round up to 4
-            // hfs.floatToInt(makeSlice(floatPi), 3);
-            vrt.setBytes(0, floatPi.data(), floatPi.size());
-            WasmValVec params(5), result(1);
-            auto* trap = ww(&import.at("float_to_int"), params, result, 0, floatSize, 256, 8, 3);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == 8);
-            auto const resultVal = vrt.getInt64(params, 2);
-            BEAST_EXPECT(resultVal == 4);
-        }
-    }
-
-    void
-    testFloatToMantExp()
-    {
-        testcase("floatToMantExp");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        {
-            // hfs.floatToMantExp(makeSlice(invalid));
-            vrt.setBytes(0, invalid.data(), invalid.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("float_to_mant_exp"), params, result, 0, floatSize, 256, 8, 512, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 ==
-                    static_cast(HostFunctionError::FloatInputMalformed));
-        }
-
-        {
-            // hfs.floatToMantExp(makeSlice(floatIntZero));
-            vrt.setBytes(0, floatIntZero.data(), floatIntZero.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("float_to_mant_exp"), params, result, 0, floatSize, 256, 8, 512, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const mantissa = vrt.getInt64(params, 2);
-            auto const exponent = vrt.getInt32(params, 4);
-            BEAST_EXPECT(mantissa == 0) &&
-                BEAST_EXPECT(exponent == std::numeric_limits::min());
-
-            // roundtrip
-            auto const result2 = hfs.floatFromMantExp(mantissa, exponent, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == floatIntZero);
-        }
-
-        {
-            // hfs.floatToMantExp(makeSlice(float1));
-            vrt.setBytes(0, float1.data(), float1.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("float_to_mant_exp"), params, result, 0, floatSize, 256, 8, 512, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const mantissa = vrt.getInt64(params, 2);
-            auto const exponent = vrt.getInt32(params, 4);
-            BEAST_EXPECT(mantissa == 1000000000000000000) && BEAST_EXPECT(exponent == -normalExp);
-
-            // roundtrip
-            auto const result2 = hfs.floatFromMantExp(mantissa, exponent, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == float1);
-        }
-
-        {
-            // hfs.floatToMantExp(makeSlice(floatMinus1));
-            vrt.setBytes(0, floatMinus1.data(), floatMinus1.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("float_to_mant_exp"), params, result, 0, floatSize, 256, 8, 512, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const mantissa = vrt.getInt64(params, 2);
-            auto const exponent = vrt.getInt32(params, 4);
-            BEAST_EXPECT(mantissa == -1000000000000000000) && BEAST_EXPECT(exponent == -normalExp);
-
-            // roundtrip
-            auto const result2 = hfs.floatFromMantExp(mantissa, exponent, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == floatMinus1);
-        }
-
-        {
-            // hfs.floatToMantExp(makeSlice(float10));
-            vrt.setBytes(0, float10.data(), float10.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("float_to_mant_exp"), params, result, 0, floatSize, 256, 8, 512, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const mantissa = vrt.getInt64(params, 2);
-            auto const exponent = vrt.getInt32(params, 4);
-            BEAST_EXPECT(mantissa == 1000000000000000000) &&
-                BEAST_EXPECT(exponent == -normalExp + 1);
-
-            // roundtrip
-            auto const result2 = hfs.floatFromMantExp(mantissa, exponent, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == float10);
-        }
-
-        {
-            // hfs.floatToMantExp(makeSlice(floatPi));
-            vrt.setBytes(0, floatPi.data(), floatPi.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("float_to_mant_exp"), params, result, 0, floatSize, 256, 8, 512, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const mantissa = vrt.getInt64(params, 2);
-            auto const exponent = vrt.getInt32(params, 4);
-            BEAST_EXPECT(mantissa == 3141592653589793000) && BEAST_EXPECT(exponent == -normalExp);
-
-            // roundtrip
-            auto const result2 = hfs.floatFromMantExp(mantissa, exponent, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == floatPi);
-        }
-
-        {
-            // hfs.floatToMantExp(makeSlice(floatIntMax));
-            vrt.setBytes(0, floatIntMax.data(), floatIntMax.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("float_to_mant_exp"), params, result, 0, floatSize, 256, 8, 512, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const mantissa = vrt.getInt64(params, 2);
-            auto const exponent = vrt.getInt32(params, 4);
-            BEAST_EXPECT(mantissa == std::numeric_limits::max()) &&
-                BEAST_EXPECT(exponent == 0);
-
-            // roundtrip
-            auto const result2 = hfs.floatFromMantExp(mantissa, exponent, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == floatIntMax);
-        }
-
-        {
-            // hfs.floatToMantExp(makeSlice(floatIntMin));
-            vrt.setBytes(0, floatIntMin.data(), floatIntMin.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("float_to_mant_exp"), params, result, 0, floatSize, 256, 8, 512, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const mantissa = vrt.getInt64(params, 2);
-            auto const exponent = vrt.getInt32(params, 4);
-            BEAST_EXPECT(mantissa == -std::numeric_limits::max()) &&
-                BEAST_EXPECT(exponent == 0);
-
-            // roundtrip
-            auto const result2 = hfs.floatFromMantExp(mantissa, exponent, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == floatIntMin);
-        }
-
-        {
-            // hfs.floatToMantExp(makeSlice(floatMax));
-            vrt.setBytes(0, floatMax.data(), floatMax.size());
-            WasmValVec params(6), result(1);
-            auto* trap =
-                ww(&import.at("float_to_mant_exp"), params, result, 0, floatSize, 256, 8, 512, 4);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == floatSize);
-            auto const mantissa = vrt.getInt64(params, 2);
-            auto const exponent = vrt.getInt32(params, 4);
-            BEAST_EXPECT(mantissa == Number::kMaxRep) &&
-                BEAST_EXPECT(exponent == Number::kMaxExponent);
-
-            // roundtrip
-            auto const result2 = hfs.floatFromMantExp(mantissa, exponent, 0);
-            BEAST_EXPECT(result2) && BEAST_EXPECT(*result2 == floatMax);
-        }
-    }
-
-    void
-    testFloats()
-    {
-        // for checking binary formats manually
-        // printNumbersBin();
-
-        testTraceFloat();
-        testFloatFromInt();
-        testFloatFromUint();
-        testFloatFromSTAmount();
-        testFloatFromSTNumber();
-        testFloatToInt();
-        testFloatToMantExp();
-        testfloatFromMantExp();
-        testFloatCompare();
-        testFloatAdd();
-        testFloatSubtract();
-        testFloatMultiply();
-        testFloatDivide();
-        testFloatRoot();
-        testFloatPower();
-        testFloatSpecialCases();
-    }
-
-    void
-    testVectorIndexes()
-    {
-        testcase("WasmValVec indicies");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        bool ex = false;
-        try
-        {
-            // hfs.getLedgerSqn();
-            WasmValVec params(2), result(1);
-            // 3 parameters instead of 2
-            auto* trap = ww(&import.at("ldgr_index"), params, result, 0, sizeof(std::uint32_t), 1);
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == sizeof(std::uint32_t)) &&
-                BEAST_EXPECT(vrt.getUint32(params, 0) == env.current()->header().seq);
-        }
-        catch (std::exception const& e)
-        {
-            BEAST_EXPECTS(e.what() == std::string("Out of bound"), e.what());
-            ex = true;
-        }
-
-        // const version
-        ex = false;
-        try
-        {
-            WasmValVec params(2);
-            [[maybe_unused]] auto const x = params[2];
-        }
-        catch (std::exception const& e)
-        {
-            BEAST_EXPECTS(e.what() == std::string("Out of bound"), e.what());
-            ex = true;
-        }
-
-        BEAST_EXPECT(ex);
-    }
-
-    void
-    testTransferLimit()
-    {
-        testcase("transferLimit");
-        using namespace test::jtx;
-
-        Env env{*this};
-        OpenView ov{*env.current()};
-        ApplyContext ac = createApplyContext(env, ov);
-        auto const dummyEscrow =
-            keylet::escrow(env.master, SeqProxy::rawSequence(env.seq(env.master)));
-        VirtualRuntime vrt;
-        WasmHostFunctionsImpl hfs(ac, dummyEscrow);
-
-        auto import = xrpl::createWasmImport(hfs);
-        hfs.setRT(vrt);
-
-        // Test 1: Test setData() - copying FROM host TO wasm
-        // Multiple calls to getLedgerSqn() which uses setData() to write result to WASM memory
-        vrt.setTransferLimit(kWasmTransferLimit + 1024);
-
-        // hfs.getLedgerSqn();
-        for (int i = 0; i < (kWasmTransferLimit / vrt.transferDiff) - 3; ++i)
-        {
-            WasmValVec params(2), result(1);
-
-            auto* trap = ww(&import.at("ldgr_index"), params, result, 0, sizeof(std::uint32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(result[0].of.i32 == sizeof(std::uint32_t)) &&
-                BEAST_EXPECT(vrt.getUint32(params, 0) == env.current()->header().seq);
-        }
-
-        BEAST_EXPECT((vrt.getTestTransferLimit() >= 0) && (vrt.getTestTransferLimit() < 1024));
-
-        // Next call should hit OutOfTransferLimit
-        {
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("ldgr_index"), params, result, 0, sizeof(std::uint32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == hfErrorToInt(HostFunctionError::OutOfTransferLimit));
-        }
-
-        // After limit exhausted, all next call return OutOfTransferLimit
-        {
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("ldgr_index"), params, result, 0, sizeof(std::uint32_t));
-
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == hfErrorToInt(HostFunctionError::OutOfTransferLimit));
-        }
-
-        // Reset transfer limit to a small value that can accommodate overhead but not AccountID
-        // copy
-        vrt.setTransferLimit(vrt.transferDiff + 10);
-
-        Account const alice("alice");
-        auto const aliceID = env.master.id();
-        vrt.setBytes(0, aliceID.data(), AccountID::size());
-
-        // This should fail because getDataAccountID() needs to copy AccountID (20 bytes)
-        // After getTransferLimit() overhead (1024), we only have 10 bytes left, not enough for 20
-        {
-            WasmValVec params(4), result(1);
-            auto* trap =
-                ww(&import.at("accountroot_id"), params, result, 0, AccountID::size(), 100, 32);
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == hfErrorToInt(HostFunctionError::OutOfTransferLimit));
-        }
-
-        // Verify that reading slices (without copying) does NOT consume transfer limit
-        vrt.setTransferLimit(vrt.transferDiff + 10);
-
-        // trace() uses getDataString() -> getDataSlice() which does NOT check transfer limit
-        std::string testMsg = "This message is longer than 10 bytes to prove slices don't count";
-        vrt.setBytes(0, testMsg.data(), testMsg.size());
-        vrt.setBytes(
-            100,
-            reinterpret_cast("dummy"),
-            5);  // Empty data slice for trace
-        {
-            WasmValVec params(5), result(0);
-            // trace(msg_ptr, msg_len, data_type, data_ptr, data_len) -- returns nothing
-            auto* trap =
-                ww(&import.at("trace"),
-                   params,
-                   result,
-                   0,
-                   testMsg.size(),
-                   traceDataTypeToInt(TraceDataType::AsText),
-                   100,
-                   5);
-
-            // Should not trap even though the message is >10 bytes, because trace only reads
-            // slices (no transfer limit check in getDataSlice) and never charges the limit.
-            BEAST_EXPECT(!trap);
-        }
-
-        // setData should return OutOfTransferLimit when the transfer limit is exhausted.
-        // trace left the limit untouched, so the next getTransferLimit() overhead (1024)
-        // takes 1034 down to 10 -- not enough for the 32-byte hash copy.
-        {
-            WasmValVec params(2), result(1);
-            auto* trap = ww(&import.at("parent_ldgr_hash"), params, result, 500, 32);
-
-            //  the transfer limit went negative
-            BEAST_EXPECT(!trap) && BEAST_EXPECT(result[0].kind == WASM_I32) &&
-                BEAST_EXPECT(
-                    result[0].of.i32 == hfErrorToInt(HostFunctionError::OutOfTransferLimit));
-        }
-    }
-
-    void
-    run() override
-    {
-        testGetLedgerSqn();
-        testGetParentLedgerTime();
-        testGetParentLedgerHash();
-        testGetBaseFee();
-        testIsAmendmentEnabled();
-        testCacheLedgerObj();
-        testGetTxField();
-        testGetCurrentLedgerObjField();
-        testGetLedgerObjField();
-        testGetTxNestedField();
-        testGetCurrentLedgerObjNestedField();
-        testGetLedgerObjNestedField();
-        testGetTxArrayLen();
-        testGetCurrentLedgerObjArrayLen();
-        testGetLedgerObjArrayLen();
-        testGetTxNestedArrayLen();
-        testGetCurrentLedgerObjNestedArrayLen();
-        testGetLedgerObjNestedArrayLen();
-        testUpdateData();
-        testCheckSignature();
-        testComputeSha512HalfHash();
-        testKeyletFunctions();
-        testGetNFT();
-        testGetNFTIssuer();
-        testGetNFTTaxon();
-        testGetNFTFlags();
-        testGetNFTTransferFee();
-        testGetNFTSerial();
-        testTrace();
-        testTraceNum();
-        testTraceAccount();
-        testTraceAmount();
-        testFloats();
-
-        testVectorIndexes();
-
-        testTransferLimit();
-    }
-};
-
-BEAST_DEFINE_TESTSUITE(HostFuncImpl, app, xrpl);
-
-}  // namespace xrpl::test
diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp
deleted file mode 100644
index ffdfe6bc83..0000000000
--- a/src/test/app/Invariants_test.cpp
+++ /dev/null
@@ -1,6260 +0,0 @@
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-namespace xrpl {
-
-// Test-only factory — not part of the public API.
-// The returned Transactor holds a raw reference to ctx; the caller must ensure
-// the ApplyContext outlives the Transactor. Implemented in applySteps.cpp
-std::unique_ptr
-makeTransactor(ApplyContext& ctx);
-
-}  // namespace xrpl
-
-namespace xrpl::test {
-
-class Invariants_test : public beast::unit_test::Suite
-{
-    // The optional Preclose function is used to process additional transactions
-    // on the ledger after creating two accounts, but before closing it, and
-    // before the Precheck function. These should only be valid functions, and
-    // not direct manipulations. Preclose is not commonly used.
-    using Preclose = std::function<
-        bool(test::jtx::Account const& a, test::jtx::Account const& b, test::jtx::Env& env)>;
-
-    // this is common setup/method for running a failing invariant check. The
-    // precheck function is used to manipulate the ApplyContext with view
-    // changes that will cause the check to fail.
-    using Precheck = std::function<
-        bool(test::jtx::Account const& a, test::jtx::Account const& b, ApplyContext& ac)>;
-
-    static FeatureBitset
-    defaultAmendments()
-    {
-        return xrpl::test::jtx::testableAmendments() | fixCleanup3_1_3 | fixCleanup3_2_0;
-    }
-
-    test::jtx::Env
-    makeEnv(FeatureBitset features)
-    {
-        return {*this, test::jtx::envconfig(), features, nullptr, beast::Severity::Disabled};
-    }
-
-    /**
-     * Run a specific test case to put the ledger into a state that will be
-     * detected by an invariant. Simulates the actions of a transaction that
-     * would violate an invariant.
-     *
-     * @param expect_logs One or more messages related to the failing invariant
-     *  that should be in the log output
-     * @precheck See "Precheck" above
-     * @fee If provided, the fee amount paid by the simulated transaction.
-     * @tx A mock transaction that took the actions to trigger the invariant. In
-     *  most cases, only the type matters.
-     * @ters The TER results expected on the two passes of the invariant
-     *  checker.
-     * @preclose See "Preclose" above. Note that @preclose runs *before*
-     * @precheck, but is the last parameter for historical reasons
-     * @setTxAccount optionally set to add sfAccount to tx (either A1 or A2)
-     */
-    enum class TxAccount : int { None = 0, A1, A2 };
-    void
-    doInvariantCheck(
-        std::vector const& expectLogs,
-        Precheck const& precheck,
-        XRPAmount fee = XRPAmount{},
-        STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}},
-        std::initializer_list ters = {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-        Preclose const& preclose = {},
-        TxAccount setTxAccount = TxAccount::None)
-    {
-        doInvariantCheck(
-            makeEnv(defaultAmendments()),
-            expectLogs,
-            precheck,
-            fee,
-            tx,
-            ters,
-            preclose,
-            setTxAccount);
-    }
-
-    void
-    doInvariantCheck(
-        test::jtx::Env&& env,
-        std::vector const& expectLogs,
-        Precheck const& precheck,
-        XRPAmount fee = XRPAmount{},
-        STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}},
-        std::initializer_list ters = {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-        Preclose const& preclose = {},
-        TxAccount setTxAccount = TxAccount::None)
-    {
-        using namespace test::jtx;
-
-        Account const a1{"A1"};
-        Account const a2{"A2"};
-        env.fund(XRP(1000), a1, a2);
-        if (preclose)
-            BEAST_EXPECT(preclose(a1, a2, env));
-        env.close();
-
-        if (setTxAccount != TxAccount::None)
-            tx.setAccountID(sfAccount, setTxAccount == TxAccount::A1 ? a1.id() : a2.id());
-
-        doInvariantCheck(std::move(env), a1, a2, expectLogs, precheck, fee, tx, ters);
-    }
-
-    void
-    doInvariantCheck(
-        // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
-        test::jtx::Env&& env,
-        test::jtx::Account const& a1,
-        test::jtx::Account const& a2,
-        std::vector const& expectLogs,
-        Precheck const& precheck,
-        XRPAmount fee = XRPAmount{},
-        STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}},
-        std::initializer_list ters = {tecINVARIANT_FAILED, tefINVARIANT_FAILED})
-    {
-        using namespace test::jtx;
-
-        OpenView ov{*env.current()};
-        test::StreamSink sink{beast::Severity::Warning};
-        beast::Journal const jlog{sink};
-        ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
-
-        // Invariants normally run in the Transaction's "apply" (operator()) context, and can always
-        // access global Rules.
-        CurrentTransactionRulesGuard const rulesGuard(ov.rules());
-
-        BEAST_EXPECT(precheck(a1, a2, ac));
-
-        auto transactor = makeTransactor(ac);
-        if (!BEAST_EXPECT(transactor))
-            return;
-
-        // invoke check twice to cover tec and tef cases
-        if (!BEAST_EXPECT(ters.size() == 2))
-            return;
-
-        TER terActual = tesSUCCESS;
-        for (TER const& terExpect : ters)
-        {
-            terActual = transactor->checkInvariants(terActual, fee);
-            BEAST_EXPECTS(
-                terExpect == terActual,
-                "expected: " + transToken(terExpect) + " got: " + transToken(terActual));
-            auto const messages = sink.messages().str();
-
-            if (!isTesSuccess(terActual))
-            {
-                BEAST_EXPECTS(
-                    messages.starts_with("Invariant failed:") ||
-                        messages.starts_with("Transaction caused an exception"),
-                    messages);
-            }
-
-            // std::cerr << messages << '\n';
-            for (auto const& m : expectLogs)
-            {
-                BEAST_EXPECTS(messages.contains(m), m);
-            }
-        }
-    }
-
-    void
-    testXRPNotCreated()
-    {
-        using namespace test::jtx;
-        testcase << "XRP created";
-        doInvariantCheck(
-            {{"XRP net change was positive: 500"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // put a single account in the view and "manufacture" some XRP
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                auto amt = sle->getFieldAmount(sfBalance);
-                sle->setFieldAmount(sfBalance, amt + STAmount{500});
-                ac.view().update(sle);
-                return true;
-            });
-    }
-
-    void
-    testAccountRootsNotRemoved()
-    {
-        using namespace test::jtx;
-        testcase << "account root removed";
-
-        // An account was deleted, but not by an AccountDelete transaction.
-        doInvariantCheck(
-            {{"an account root was deleted"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // remove an account from the view
-                auto sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                // Clear the balance so the "account deletion left behind a
-                // non-zero balance" check doesn't trip earlier than the desired
-                // check.
-                sle->at(sfBalance) = beast::kZero;
-                ac.view().erase(sle);
-                return true;
-            });
-
-        // Successful AccountDelete transaction that didn't delete an account.
-        //
-        // Note that this is a case where a second invocation of the invariant
-        // checker returns a tecINVARIANT_FAILED, not a tefINVARIANT_FAILED.
-        // After a discussion with the team, we believe that's okay.
-        doInvariantCheck(
-            {{"account deletion succeeded without deleting an account"}},
-            [](Account const&, Account const&, ApplyContext& ac) { return true; },
-            XRPAmount{},
-            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
-
-        // Successful AccountDelete that deleted more than one account.
-        doInvariantCheck(
-            {{"account deletion succeeded but deleted multiple accounts"}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                // remove two accounts from the view
-                auto sleA1 = ac.view().peek(keylet::account(a1.id()));
-                auto sleA2 = ac.view().peek(keylet::account(a2.id()));
-                if (!sleA1 || !sleA2)
-                    return false;
-                // Clear the balance so the "account deletion left behind a
-                // non-zero balance" check doesn't trip earlier than the desired
-                // check.
-                sleA1->at(sfBalance) = beast::kZero;
-                sleA2->at(sfBalance) = beast::kZero;
-                ac.view().erase(sleA1);
-                ac.view().erase(sleA2);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
-    }
-
-    void
-    testAccountRootsDeletedClean()
-    {
-        using namespace test::jtx;
-        testcase << "account root deletion left artifact";
-
-        doInvariantCheck(
-            {{"account deletion left behind a non-zero balance"}},
-            // NOLINTNEXTLINE(readability-identifier-naming)
-            [&](Account const& A1, Account const& A2, ApplyContext& ac) {
-                // A1 has a balance. Delete A1
-                auto const a1 = A1.id();
-                auto const sleA1 = ac.view().peek(keylet::account(a1));
-                if (!sleA1)
-                    return false;
-                if (!BEAST_EXPECT(*sleA1->at(sfBalance) != beast::kZero))
-                    return false;
-
-                ac.view().erase(sleA1);
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
-
-        doInvariantCheck(
-            {{"account deletion left behind a non-zero owner count"}},
-            // NOLINTNEXTLINE(readability-identifier-naming)
-            [&](Account const& A1, Account const& A2, ApplyContext& ac) {
-                // Increment A1's owner count, then delete A1
-                auto const a1 = A1.id();
-                auto const sleA1 = ac.view().peek(keylet::account(a1));
-                if (!sleA1)
-                    return false;
-                // Clear the balance so the "account deletion left behind a
-                // non-zero balance" check doesn't trip earlier than the desired
-                // check.
-                sleA1->at(sfBalance) = beast::kZero;
-                BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0);
-                increaseOwnerCount(ac.view(), sleA1, {}, 1, ac.journal);
-
-                ac.view().erase(sleA1);
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
-
-        doInvariantCheck(
-            {{"account deletion left behind a sponsorship field"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
-                if (!sleA1)
-                    return false;
-                sleA1->at(sfBalance) = beast::kZero;
-                sleA1->setFieldU32(sfSponsoredOwnerCount, 1);
-
-                ac.view().erase(sleA1);
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
-
-        doInvariantCheck(
-            {{"account deletion left behind a sponsorship field"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
-                if (!sleA1)
-                    return false;
-                sleA1->at(sfBalance) = beast::kZero;
-                sleA1->setFieldU32(sfSponsoringOwnerCount, 1);
-
-                ac.view().erase(sleA1);
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
-
-        doInvariantCheck(
-            {{"account deletion left behind a sponsorship field"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const a1Id = a1.id();
-                auto const sleA1 = ac.view().peek(keylet::account(a1Id));
-                if (!sleA1)
-                    return false;
-                sleA1->at(sfBalance) = beast::kZero;
-                sleA1->setFieldU32(sfSponsoringAccountCount, 1);
-
-                ac.view().erase(sleA1);
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
-
-        doInvariantCheck(
-            {{"account deletion left behind a sponsorship field"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
-                if (!sleA1)
-                    return false;
-                sleA1->at(sfBalance) = beast::kZero;
-                sleA1->setAccountID(sfSponsor, a2.id());
-
-                ac.view().erase(sleA1);
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
-
-        doInvariantCheck(
-            Env{*this, FeatureBitset{featureSponsor}},
-            {{"account deletion left behind a sponsorship field"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
-                if (!sleA1)
-                    return false;
-                sleA1->at(sfBalance) = beast::kZero;
-                sleA1->setAccountID(sfSponsor, a2.id());
-
-                ac.view().erase(sleA1);
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
-
-        for (auto const& keyletInfo : kDirectAccountKeylets)
-        {
-            // TODO: Use structured binding once LLVM 16 is the minimum
-            // supported version. See also:
-            // https://github.com/llvm/llvm-project/issues/48582
-            // https://github.com/llvm/llvm-project/commit/127bf44385424891eb04cff8e52d3f157fc2cb7c
-            if (!keyletInfo.includeInTests)
-                continue;
-            auto const& keyletfunc = keyletInfo.function;
-            auto const& type = keyletInfo.expectedLEName;
-
-            using namespace std::string_literals;
-
-            doInvariantCheck(
-                {{"account deletion left behind a "s + type.cStr() + " object"}},
-                // NOLINTNEXTLINE(readability-identifier-naming)
-                [&](Account const& A1, Account const& A2, ApplyContext& ac) {
-                    // Add an object to the ledger for account A1, then delete
-                    // A1
-                    auto const a1 = A1.id();
-                    auto sleA1 = ac.view().peek(keylet::account(a1));
-                    if (!sleA1)
-                        return false;
-
-                    auto const key = std::invoke(keyletfunc, a1);
-                    auto const newSLE = std::make_shared(key);
-                    ac.view().insert(newSLE);
-                    // Clear the balance so the "account deletion left behind a
-                    // non-zero balance" check doesn't trip earlier than the
-                    // desired check.
-                    sleA1->at(sfBalance) = beast::kZero;
-                    ac.view().erase(sleA1);
-
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
-        }
-
-        // NFT special case
-        doInvariantCheck(
-            {{"account deletion left behind a NFTokenPage object"}},
-            [&](Account const& a1, Account const&, ApplyContext& ac) {
-                // remove an account from the view
-                auto sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                // Clear the balance so the "account deletion left behind a
-                // non-zero balance" check doesn't trip earlier than the desired
-                // check.
-                sle->at(sfBalance) = beast::kZero;
-                sle->at(sfOwnerCount) = 0;
-                ac.view().erase(sle);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            [&](Account const& a1, Account const&, Env& env) {
-                // Preclose callback to mint the NFT which will be deleted in
-                // the Precheck callback above.
-                env(token::mint(a1));
-
-                return true;
-            });
-
-        // AMM special cases
-        AccountID ammAcctID;
-        uint256 ammKey;
-        Issue ammIssue;
-        doInvariantCheck(
-            {{"account deletion left behind a DirectoryNode object"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                // Delete the AMM account without cleaning up the directory or
-                // deleting the AMM object
-                auto sle = ac.view().peek(keylet::account(ammAcctID));
-                if (!sle)
-                    return false;
-
-                BEAST_EXPECT(sle->at(~sfAMMID));
-                BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
-
-                // Clear the balance so the "account deletion left behind a
-                // non-zero balance" check doesn't trip earlier than the desired
-                // check.
-                sle->at(sfBalance) = beast::kZero;
-                sle->at(sfOwnerCount) = 0;
-                ac.view().erase(sle);
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttAMM_WITHDRAW, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                // Preclose callback to create the AMM which will be partially
-                // deleted in the Precheck callback above.
-                AMM const amm(env, a1, XRP(100), a1["USD"](50));
-                ammAcctID = amm.ammAccount();
-                ammKey = amm.ammID();
-                ammIssue = amm.lptIssue();
-                return true;
-            });
-        doInvariantCheck(
-            {{"account deletion left behind a AMM object"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                // Delete all the AMM's trust lines, remove the AMM from the AMM
-                // account's directory (this deletes the directory), and delete
-                // the AMM account. Do not delete the AMM object.
-                auto sle = ac.view().peek(keylet::account(ammAcctID));
-                if (!sle)
-                    return false;
-
-                BEAST_EXPECT(sle->at(~sfAMMID));
-                BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
-
-                for (auto const& trustKeylet :
-                     {keylet::trustLine(ammAcctID, a1["USD"]), keylet::trustLine(a1, ammIssue)})
-                {
-                    auto const line = ac.view().peek(trustKeylet);
-                    if (!line)
-                    {
-                        return false;
-                    }
-
-                    STAmount const lowLimit = line->at(sfLowLimit);
-                    STAmount const highLimit = line->at(sfHighLimit);
-                    BEAST_EXPECT(
-                        trustDelete(
-                            ac.view(),
-                            line,
-                            lowLimit.getIssuer(),
-                            highLimit.getIssuer(),
-                            ac.journal) == tesSUCCESS);
-                }
-
-                auto const ammSle = ac.view().peek(keylet::amm(ammKey));
-                if (!BEAST_EXPECT(ammSle))
-                    return false;
-                auto const ownerDirKeylet = keylet::ownerDir(ammAcctID);
-
-                BEAST_EXPECT(
-                    ac.view().dirRemove(ownerDirKeylet, ammSle->at(sfOwnerNode), ammKey, false));
-                BEAST_EXPECT(
-                    !ac.view().exists(ownerDirKeylet) || ac.view().emptyDirDelete(ownerDirKeylet));
-
-                // Clear the balance so the "account deletion left behind a
-                // non-zero balance" check doesn't trip earlier than the desired
-                // check.
-                sle->at(sfBalance) = beast::kZero;
-                sle->at(sfOwnerCount) = 0;
-                ac.view().erase(sle);
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttAMM_WITHDRAW, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                // Preclose callback to create the AMM which will be partially
-                // deleted in the Precheck callback above.
-                AMM const amm(env, a1, XRP(100), a1["USD"](50));
-                ammAcctID = amm.ammAccount();
-                ammKey = amm.ammID();
-                ammIssue = amm.lptIssue();
-                return true;
-            });
-    }
-
-    void
-    testTypesMatch()
-    {
-        using namespace test::jtx;
-        testcase << "ledger entry types don't match";
-        doInvariantCheck(
-            {{"ledger entry type mismatch"}, {"XRP net change of -1000000000 doesn't match fee 0"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // replace an entry in the table with an SLE of a different type
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                auto const sleNew = std::make_shared(ltTICKET, sle->key());
-                ac.rawView().rawReplace(sleNew);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"invalid ledger entry type added"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // add an entry in the table with an SLE of an invalid type
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-
-                // make a dummy escrow ledger entry, then change the type to an
-                // unsupported value so that the valid type invariant check
-                // will fail.
-                auto const sleNew = std::make_shared(
-                    keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
-
-                // We don't use ltNICKNAME directly since it's marked deprecated
-                // to prevent accidental use elsewhere.
-                sleNew->type_ = static_cast('n');
-                ac.view().insert(sleNew);
-                return true;
-            });
-    }
-
-    void
-    testNoXRPTrustLine()
-    {
-        using namespace test::jtx;
-        testcase << "trust lines with XRP not allowed";
-        doInvariantCheck(
-            {{"an XRP trust line was created"}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                // create simple trust SLE with xrp currency
-                auto const sleNew =
-                    std::make_shared(keylet::trustLine(a1, a2, xrpIssue().currency));
-                ac.view().insert(sleNew);
-                return true;
-            });
-    }
-
-    void
-    testNoDeepFreezeTrustLinesWithoutFreeze()
-    {
-        using namespace test::jtx;
-        testcase << "trust lines with deep freeze flag without freeze "
-                    "not allowed";
-        doInvariantCheck(
-            {{"a trust line with deep freeze flag without normal freeze was "
-              "created"}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleNew =
-                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
-                sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
-                sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
-
-                std::uint32_t uFlags = 0u;
-                uFlags |= lsfLowDeepFreeze;
-                sleNew->setFieldU32(sfFlags, uFlags);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"a trust line with deep freeze flag without normal freeze was "
-              "created"}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleNew =
-                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
-                sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
-                sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
-                std::uint32_t uFlags = 0u;
-                uFlags |= lsfHighDeepFreeze;
-                sleNew->setFieldU32(sfFlags, uFlags);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"a trust line with deep freeze flag without normal freeze was "
-              "created"}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleNew =
-                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
-                sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
-                sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
-                std::uint32_t uFlags = 0u;
-                uFlags |= lsfLowDeepFreeze | lsfHighDeepFreeze;
-                sleNew->setFieldU32(sfFlags, uFlags);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"a trust line with deep freeze flag without normal freeze was "
-              "created"}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleNew =
-                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
-                sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
-                sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
-                std::uint32_t uFlags = 0u;
-                uFlags |= lsfLowDeepFreeze | lsfHighFreeze;
-                sleNew->setFieldU32(sfFlags, uFlags);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"a trust line with deep freeze flag without normal freeze was "
-              "created"}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sleNew =
-                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
-                sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
-                sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
-                std::uint32_t uFlags = 0u;
-                uFlags |= lsfLowFreeze | lsfHighDeepFreeze;
-                sleNew->setFieldU32(sfFlags, uFlags);
-                ac.view().insert(sleNew);
-                return true;
-            });
-    }
-
-    void
-    testTransfersNotFrozen()
-    {
-        using namespace test::jtx;
-        testcase << "transfers when frozen";
-
-        Account const g1{"G1"};
-        // Helper function to establish the trustlines
-        auto const createTrustlines = [&](Account const& a1, Account const& a2, Env& env) {
-            // Preclose callback to establish trust lines with gateway
-            env.fund(XRP(1000), g1);
-
-            env.trust(g1["USD"](10000), a1);
-            env.trust(g1["USD"](10000), a2);
-            env.close();
-
-            env(pay(g1, a1, g1["USD"](1000)));
-            env(pay(g1, a2, g1["USD"](1000)));
-            env.close();
-
-            return true;
-        };
-
-        auto const a1FrozenByIssuer = [&](Account const& a1, Account const& a2, Env& env) {
-            createTrustlines(a1, a2, env);
-            env(trust(g1, a1["USD"](10000), tfSetFreeze));
-            env.close();
-
-            return true;
-        };
-
-        auto const a1DeepFrozenByIssuer = [&](Account const& a1, Account const& a2, Env& env) {
-            a1FrozenByIssuer(a1, a2, env);
-            env(trust(g1, a1["USD"](10000), tfSetDeepFreeze));
-            env.close();
-
-            return true;
-        };
-
-        auto const changeBalances = [&](Account const& a1,
-                                        Account const& a2,
-                                        ApplyContext& ac,
-                                        int a1Balance,
-                                        int a2Balance) {
-            auto const sleA1 = ac.view().peek(keylet::trustLine(a1, g1["USD"]));
-            auto const sleA2 = ac.view().peek(keylet::trustLine(a2, g1["USD"]));
-
-            sleA1->setFieldAmount(sfBalance, g1["USD"](a1Balance));
-            sleA2->setFieldAmount(sfBalance, g1["USD"](a2Balance));
-
-            ac.view().update(sleA1);
-            ac.view().update(sleA2);
-        };
-
-        // test: imitating frozen A1 making a payment to A2.
-        doInvariantCheck(
-            {{"Attempting to move frozen funds"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                changeBalances(a1, a2, ac, -900, -1100);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPAYMENT, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            a1FrozenByIssuer);
-
-        // test: imitating deep frozen A1 making a payment to A2.
-        doInvariantCheck(
-            {{"Attempting to move frozen funds"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                changeBalances(a1, a2, ac, -900, -1100);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPAYMENT, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            a1DeepFrozenByIssuer);
-
-        // test: imitating A2 making a payment to deep frozen A1.
-        doInvariantCheck(
-            {{"Attempting to move frozen funds"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                changeBalances(a1, a2, ac, -1100, -900);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPAYMENT, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            a1DeepFrozenByIssuer);
-    }
-
-    void
-    testXRPBalanceCheck()
-    {
-        using namespace test::jtx;
-        testcase << "XRP balance checks";
-
-        doInvariantCheck(
-            {{"Cannot return non-native STAmount as XRPAmount"}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                // non-native balance
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                STAmount const nonNative(a2["USD"](51));
-                sle->setFieldAmount(sfBalance, nonNative);
-                ac.view().update(sle);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"incorrect account XRP balance"}, {"XRP net change was positive: 99999999000000001"}},
-            [this](Account const& a1, Account const&, ApplyContext& ac) {
-                // balance exceeds genesis amount
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                // Use `drops(1)` to bypass a call to STAmount::canonicalize
-                // with an invalid value
-                sle->setFieldAmount(sfBalance, kInitialXrp + drops(1));
-                BEAST_EXPECT(!sle->getFieldAmount(sfBalance).negative());
-                ac.view().update(sle);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"incorrect account XRP balance"},
-             {"XRP net change of -1000000001 doesn't match fee 0"}},
-            [this](Account const& a1, Account const&, ApplyContext& ac) {
-                // balance is negative
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                sle->setFieldAmount(sfBalance, STAmount{1, true});
-                BEAST_EXPECT(sle->getFieldAmount(sfBalance).negative());
-                ac.view().update(sle);
-                return true;
-            });
-    }
-
-    void
-    testTransactionFeeCheck()
-    {
-        using namespace test::jtx;
-        using namespace std::string_literals;
-        testcase << "Transaction fee checks";
-
-        doInvariantCheck(
-            {{"fee paid was negative: -1"}, {"XRP net change of 0 doesn't match fee -1"}},
-            [](Account const&, Account const&, ApplyContext&) { return true; },
-            XRPAmount{-1});
-
-        doInvariantCheck(
-            {{"fee paid exceeds system limit: "s + to_string(kInitialXrp)},
-             {"XRP net change of 0 doesn't match fee "s + to_string(kInitialXrp)}},
-            [](Account const&, Account const&, ApplyContext&) { return true; },
-            XRPAmount{kInitialXrp});
-
-        doInvariantCheck(
-            {{"fee paid is 20 exceeds fee specified in transaction."},
-             {"XRP net change of 0 doesn't match fee 20"}},
-            [](Account const&, Account const&, ApplyContext&) { return true; },
-            XRPAmount{20},
-            STTx{ttACCOUNT_SET, [](STObject& tx) { tx.setFieldAmount(sfFee, XRPAmount{10}); }});
-    }
-
-    void
-    testNoBadOffers()
-    {
-        using namespace test::jtx;
-        testcase << "no bad offers";
-
-        doInvariantCheck(
-            {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
-                // offer with negative takerpays
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                auto sleNew = std::make_shared(
-                    keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
-                sleNew->setAccountID(sfAccount, a1.id());
-                sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
-                sleNew->setFieldAmount(sfTakerPays, XRP(-1));
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
-                // offer with negative takergets
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                auto sleNew = std::make_shared(
-                    keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
-                sleNew->setAccountID(sfAccount, a1.id());
-                sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
-                sleNew->setFieldAmount(sfTakerPays, a1["USD"](10));
-                sleNew->setFieldAmount(sfTakerGets, XRP(-1));
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
-                // offer XRP to XRP
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                auto sleNew = std::make_shared(
-                    keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
-                sleNew->setAccountID(sfAccount, a1.id());
-                sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
-                sleNew->setFieldAmount(sfTakerPays, XRP(10));
-                sleNew->setFieldAmount(sfTakerGets, XRP(11));
-                ac.view().insert(sleNew);
-                return true;
-            });
-    }
-
-    void
-    testNoZeroEscrow()
-    {
-        using namespace test::jtx;
-        testcase << "no zero escrow";
-
-        doInvariantCheck(
-            {{"XRP net change of -1000000 doesn't match fee 0"},
-             {"escrow specifies invalid amount"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // escrow with negative amount
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                auto sleNew = std::make_shared(
-                    keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
-                sleNew->setFieldAmount(sfAmount, XRP(-1));
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"XRP net change was positive: 100000000000000001"},
-             {"escrow specifies invalid amount"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // escrow with too-large amount
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                auto sleNew = std::make_shared(
-                    keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
-                // Use `drops(1)` to bypass a call to STAmount::canonicalize
-                // with an invalid value
-                sleNew->setFieldAmount(sfAmount, kInitialXrp + drops(1));
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        // IOU < 0
-        doInvariantCheck(
-            {{"escrow specifies invalid amount"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // escrow with too-little iou
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                auto sleNew = std::make_shared(
-                    keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
-
-                Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
-                STAmount const amt(usd, -1);
-                sleNew->setFieldAmount(sfAmount, amt);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        // IOU bad currency
-        doInvariantCheck(
-            {{"escrow specifies invalid amount"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // escrow with bad iou currency
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                auto sleNew = std::make_shared(
-                    keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
-
-                Issue const bad{badCurrency(), AccountID(0x4985601)};
-                STAmount const amt(bad, 1);
-                sleNew->setFieldAmount(sfAmount, amt);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        // MPT < 0
-        doInvariantCheck(
-            {{"escrow specifies invalid amount"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // escrow with too-little mpt
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                auto sleNew = std::make_shared(
-                    keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
-
-                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
-                STAmount const amt(mpt, -1);
-                sleNew->setFieldAmount(sfAmount, amt);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        // MPT OutstandingAmount < 0
-        doInvariantCheck(
-            {{"escrow specifies invalid amount"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // mptissuance outstanding is negative
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-
-                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
-                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
-                sleNew->setFieldU64(sfOutstandingAmount, -1);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        // MPT LockedAmount < 0
-        doInvariantCheck(
-            {{"escrow specifies invalid amount"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // mptissuance locked is less than locked
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-
-                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
-                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
-                sleNew->setFieldU64(sfLockedAmount, -1);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        // MPT OutstandingAmount < LockedAmount
-        doInvariantCheck(
-            {{"escrow specifies invalid amount"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // mptissuance outstanding is less than locked
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-
-                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
-                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
-                sleNew->setFieldU64(sfOutstandingAmount, 1);
-                sleNew->setFieldU64(sfLockedAmount, 10);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        // MPT MPTAmount < 0
-        doInvariantCheck(
-            {{"escrow specifies invalid amount"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // mptoken amount is negative
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-
-                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
-                auto sleNew = std::make_shared(keylet::mptoken(mpt.getMptID(), a1));
-                sleNew->setFieldU64(sfMPTAmount, -1);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        // MPT LockedAmount < 0
-        doInvariantCheck(
-            {{"escrow specifies invalid amount"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // mptoken locked amount is negative
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-
-                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
-                auto sleNew = std::make_shared(keylet::mptoken(mpt.getMptID(), a1));
-                sleNew->setFieldU64(sfLockedAmount, -1);
-                ac.view().insert(sleNew);
-                return true;
-            });
-    }
-
-    void
-    testValidNewAccountRoot()
-    {
-        using namespace test::jtx;
-        testcase << "valid new account root";
-
-        doInvariantCheck(
-            {{"account root created illegally"}},
-            [](Account const&, Account const&, ApplyContext& ac) {
-                // Insert a new account root created by a non-payment into
-                // the view.
-                Account const a3{"A3"};
-                Keylet const acctKeylet = keylet::account(a3);
-                auto const sleNew = std::make_shared(acctKeylet);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"multiple accounts created in a single transaction"}},
-            [](Account const&, Account const&, ApplyContext& ac) {
-                // Insert two new account roots into the view.
-                {
-                    Account const a3{"A3"};
-                    Keylet const acctKeylet = keylet::account(a3);
-                    auto const sleA3 = std::make_shared(acctKeylet);
-                    ac.view().insert(sleA3);
-                }
-                {
-                    Account const a4{"A4"};
-                    Keylet const acctKeylet = keylet::account(a4);
-                    auto const sleA4 = std::make_shared(acctKeylet);
-                    ac.view().insert(sleA4);
-                }
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"account created with wrong starting sequence number"}},
-            [](Account const&, Account const&, ApplyContext& ac) {
-                // Insert a new account root with the wrong starting sequence.
-                Account const a3{"A3"};
-                Keylet const acctKeylet = keylet::account(a3);
-                auto const sleNew = std::make_shared(acctKeylet);
-                sleNew->setFieldU32(sfSequence, ac.view().seq() + 1);
-                ac.view().insert(sleNew);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPAYMENT, [](STObject& tx) {}});
-
-        doInvariantCheck(
-            {{"pseudo-account created by a wrong transaction type"}},
-            [](Account const&, Account const&, ApplyContext& ac) {
-                Account const a3{"A3"};
-                Keylet const acctKeylet = keylet::account(a3);
-                auto const sleNew = std::make_shared(acctKeylet);
-                sleNew->setFieldU32(sfSequence, 0);
-                sleNew->setFieldH256(sfAMMID, uint256(1));
-                sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple);
-                ac.view().insert(sleNew);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPAYMENT, [](STObject& tx) {}});
-
-        doInvariantCheck(
-            {{"account created with wrong starting sequence number"}},
-            [](Account const&, Account const&, ApplyContext& ac) {
-                Account const a3{"A3"};
-                Keylet const acctKeylet = keylet::account(a3);
-                auto const sleNew = std::make_shared(acctKeylet);
-                sleNew->setFieldU32(sfSequence, ac.view().seq());
-                sleNew->setFieldH256(sfAMMID, uint256(1));
-                sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
-                ac.view().insert(sleNew);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttAMM_CREATE, [](STObject& tx) {}});
-
-        doInvariantCheck(
-            {{"pseudo-account created with wrong flags"}},
-            [](Account const&, Account const&, ApplyContext& ac) {
-                Account const a3{"A3"};
-                Keylet const acctKeylet = keylet::account(a3);
-                auto const sleNew = std::make_shared(acctKeylet);
-                sleNew->setFieldU32(sfSequence, 0);
-                sleNew->setFieldH256(sfAMMID, uint256(1));
-                sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple);
-                ac.view().insert(sleNew);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CREATE, [](STObject& tx) {}});
-
-        doInvariantCheck(
-            {{"pseudo-account created with wrong flags"}},
-            [](Account const&, Account const&, ApplyContext& ac) {
-                Account const a3{"A3"};
-                Keylet const acctKeylet = keylet::account(a3);
-                auto const sleNew = std::make_shared(acctKeylet);
-                sleNew->setFieldU32(sfSequence, 0);
-                sleNew->setFieldH256(sfAMMID, uint256(1));
-                sleNew->setFieldU32(
-                    sfFlags,
-                    lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth | lsfRequireDestTag);
-                ac.view().insert(sleNew);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttAMM_CREATE, [](STObject& tx) {}});
-    }
-
-    void
-    testNFTokenPageInvariants()
-    {
-        using namespace test::jtx;
-        testcase << "NFTokenPage";
-
-        // lambda that returns an STArray of NFTokenIDs.
-        uint256 const firstNFTID(
-            "0000000000000000000000000000000000000001FFFFFFFFFFFFFFFF00000000");
-        auto makeNFTokenIDs = [&firstNFTID](unsigned int nftCount) {
-            SOTemplate const* nfTokenTemplate =
-                InnerObjectFormats::getInstance().findSOTemplateBySField(sfNFToken);
-
-            uint256 nftID(firstNFTID);
-            STArray ret;
-            for (int i = 0; i < nftCount; ++i)
-            {
-                STObject newNFToken(*nfTokenTemplate, sfNFToken, [&nftID](STObject& object) {
-                    object.setFieldH256(sfNFTokenID, nftID);
-                });
-                ret.pushBack(std::move(newNFToken));
-                ++nftID;
-            }
-            return ret;
-        };
-
-        doInvariantCheck(
-            {{"NFT page has invalid size"}},
-            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
-                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
-                nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(0));
-
-                ac.view().insert(nftPage);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"NFT page has invalid size"}},
-            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
-                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
-                nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(33));
-
-                ac.view().insert(nftPage);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"NFTs on page are not sorted"}},
-            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
-                STArray nfTokens = makeNFTokenIDs(2);
-                std::iter_swap(nfTokens.begin(), nfTokens.begin() + 1);
-
-                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
-                nftPage->setFieldArray(sfNFTokens, nfTokens);
-
-                ac.view().insert(nftPage);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"NFT contains empty URI"}},
-            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
-                STArray nfTokens = makeNFTokenIDs(1);
-                nfTokens[0].setFieldVL(sfURI, Blob{});
-
-                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
-                nftPage->setFieldArray(sfNFTokens, nfTokens);
-
-                ac.view().insert(nftPage);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"NFT page is improperly linked"}},
-            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
-                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
-                nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
-                nftPage->setFieldH256(sfPreviousPageMin, keylet::nftokenPageMax(a1).key);
-
-                ac.view().insert(nftPage);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"NFT page is improperly linked"}},
-            [&makeNFTokenIDs](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
-                nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
-                nftPage->setFieldH256(sfPreviousPageMin, keylet::nftokenPageMin(a2).key);
-
-                ac.view().insert(nftPage);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"NFT page is improperly linked"}},
-            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
-                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
-                nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
-                nftPage->setFieldH256(sfNextPageMin, nftPage->key());
-
-                ac.view().insert(nftPage);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"NFT page is improperly linked"}},
-            [&makeNFTokenIDs](Account const& a1, Account const& a2, ApplyContext& ac) {
-                STArray nfTokens = makeNFTokenIDs(1);
-                auto nftPage = std::make_shared(keylet::nftokenPage(
-                    keylet::nftokenPageMax(a1), ++(nfTokens[0].getFieldH256(sfNFTokenID))));
-                nftPage->setFieldArray(sfNFTokens, nfTokens);
-                nftPage->setFieldH256(sfNextPageMin, keylet::nftokenPageMax(a2).key);
-
-                ac.view().insert(nftPage);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"NFT found in incorrect page"}},
-            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
-                STArray nfTokens = makeNFTokenIDs(2);
-                auto nftPage = std::make_shared(keylet::nftokenPage(
-                    keylet::nftokenPageMax(a1), (nfTokens[1].getFieldH256(sfNFTokenID))));
-                nftPage->setFieldArray(sfNFTokens, nfTokens);
-
-                ac.view().insert(nftPage);
-                return true;
-            });
-    }
-
-    void
-    testAMMDeleteInvariants(FeatureBitset features)
-    {
-        using namespace test::jtx;
-
-        bool const enforceAMMDelete = features[fixCleanup3_3_0];
-        testcase << "AMM delete invariants" + std::string(enforceAMMDelete ? " fix" : "");
-
-        Env env(*this, features);
-        Account const issuer{"issuer"};
-        Issue const lptIssue{Currency(0x4c50540000000000), issuer.id()};
-        STAmount const zeroLP{lptIssue, 0};
-        STAmount const nonZeroLP{lptIssue, 1};
-
-        auto const makeAMM = [](STAmount const& lptBalance) {
-            auto sleAMM = std::make_shared(keylet::amm(uint256(1)));
-            sleAMM->setFieldAmount(sfLPTokenBalance, lptBalance);
-            return sleAMM;
-        };
-
-        auto const checkInvariant = [&](TxType txType,
-                                        TER result,
-                                        std::optional const& deletedLPBalance,
-                                        bool expected,
-                                        std::string const& expectedLog) {
-            test::StreamSink sink{beast::Severity::Warning};
-            beast::Journal const jlog{sink};
-            ValidAMM invariant;
-
-            if (deletedLPBalance)
-                invariant.visitEntry(true, makeAMM(*deletedLPBalance), nullptr);
-
-            bool const actual = invariant.finalize(
-                STTx{txType, [](STObject&) {}}, result, XRPAmount{}, *env.current(), jlog);
-
-            BEAST_EXPECTS(actual == expected, "unexpected AMM delete invariant result");
-            auto const messages = sink.messages().str();
-            auto const expectedLogWhenEnforced = enforceAMMDelete ? expectedLog : "";
-            if (!expectedLogWhenEnforced.empty())
-            {
-                BEAST_EXPECTS(messages.contains(expectedLogWhenEnforced), expectedLogWhenEnforced);
-            }
-            else
-            {
-                BEAST_EXPECTS(messages.empty(), messages);
-            }
-        };
-
-        checkInvariant(
-            ttPAYMENT,
-            tesSUCCESS,
-            nonZeroLP,
-            !enforceAMMDelete,
-            "Invariant failed: AMM failed, unexpected AMM deletion by");
-        checkInvariant(
-            ttAMM_DELETE,
-            tesSUCCESS,
-            std::nullopt,
-            !enforceAMMDelete,
-            "Invariant failed: AMMDelete failed, AMM object remained on tesSUCCESS");
-        checkInvariant(
-            ttAMM_DELETE,
-            tesSUCCESS,
-            nonZeroLP,
-            !enforceAMMDelete,
-            "Invariant failed: AMMDelete failed, AMM object deleted with non-zero LP balance");
-        checkInvariant(
-            ttAMM_DELETE,
-            tecINCOMPLETE,
-            zeroLP,
-            !enforceAMMDelete,
-            "Invariant failed: AMMDelete failed, AMM object deleted when result is not tesSUCCESS");
-
-        checkInvariant(ttAMM_WITHDRAW, tesSUCCESS, nonZeroLP, true, "");
-        checkInvariant(ttAMM_CLAWBACK, tesSUCCESS, nonZeroLP, true, "");
-
-        checkInvariant(ttAMM_DELETE, tesSUCCESS, zeroLP, true, "");
-        checkInvariant(ttAMM_WITHDRAW, tesSUCCESS, zeroLP, true, "");
-        checkInvariant(ttAMM_CLAWBACK, tesSUCCESS, zeroLP, true, "");
-    }
-
-    static SLE::pointer
-    createPermissionedDomain(
-        ApplyContext& ac,
-        test::jtx::Account const& a1,
-        test::jtx::Account const& a2,
-        std::uint32_t numCreds = 2,
-        std::uint32_t seq = 10)
-    {
-        Keylet const pdKeylet = keylet::permissionedDomain(a1.id(), SeqProxy::rawSequence(seq));
-        auto sle = std::make_shared(pdKeylet);
-
-        sle->setAccountID(sfOwner, a1);
-        sle->setFieldU32(sfSequence, seq);
-
-        if (numCreds != 0u)
-        {
-            // This array is sorted naturally, but if you are going to change
-            // this behavior, don't forget to use credentials::makeSorted
-            STArray credentials(sfAcceptedCredentials, numCreds);
-            for (std::size_t n = 0; n < numCreds; ++n)
-            {
-                auto cred = STObject::makeInnerObject(sfCredential);
-                cred.setAccountID(sfIssuer, a2);
-                auto credType = "cred_type" + std::to_string(n);
-                cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
-                credentials.pushBack(std::move(cred));
-            }
-            sle->setFieldArray(sfAcceptedCredentials, credentials);
-        }
-
-        ac.view().insert(sle);
-        return sle;
-    };
-
-    void
-    testPermissionedDomainInvariants(FeatureBitset features)
-    {
-        using namespace test::jtx;
-
-        bool const fixEnabled = features[fixCleanup3_1_3];
-        std::initializer_list const badTers = {tecINVARIANT_FAILED, tecINVARIANT_FAILED};
-        std::initializer_list const failTers = {tecINVARIANT_FAILED, tefINVARIANT_FAILED};
-
-        testcase << "PermissionedDomain" + std::string(fixEnabled ? " fix" : "");
-
-        doInvariantCheck(
-            makeEnv(features),
-            {{"permissioned domain with no rules."}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                return createPermissionedDomain(ac, a1, a2, 0).get();
-            },
-            XRPAmount{},
-            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
-            fixEnabled ? failTers : badTers);
-
-        testcase << "PermissionedDomain 2";
-
-        static constexpr auto kTooBig = kMaxPermissionedDomainCredentialsArraySize + 1;
-        doInvariantCheck(
-            makeEnv(features),
-            {{"permissioned domain bad credentials size " + std::to_string(kTooBig)}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                return !!createPermissionedDomain(ac, a1, a2, kTooBig);
-            },
-            XRPAmount{},
-            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
-            fixEnabled ? failTers : badTers);
-
-        testcase << "PermissionedDomain 3";
-        doInvariantCheck(
-            makeEnv(features),
-            {{"permissioned domain credentials aren't sorted"}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto slePd = createPermissionedDomain(ac, a1, a2, 0);
-
-                STArray credentials(sfAcceptedCredentials, 2);
-                for (std::size_t n = 0; n < 2; ++n)
-                {
-                    auto cred = STObject::makeInnerObject(sfCredential);
-                    cred.setAccountID(sfIssuer, a2);
-                    auto credType = std::string("cred_type") + std::to_string(9 - n);
-                    cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
-                    credentials.pushBack(std::move(cred));
-                }
-                slePd->setFieldArray(sfAcceptedCredentials, credentials);
-                ac.view().update(slePd);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
-            fixEnabled ? failTers : badTers);
-
-        testcase << "PermissionedDomain 4";
-        doInvariantCheck(
-            makeEnv(features),
-            {{"permissioned domain credentials aren't unique"}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto slePd = createPermissionedDomain(ac, a1, a2, 0);
-
-                STArray credentials(sfAcceptedCredentials, 2);
-                for (std::size_t n = 0; n < 2; ++n)
-                {
-                    auto cred = STObject::makeInnerObject(sfCredential);
-                    cred.setAccountID(sfIssuer, a2);
-                    cred.setFieldVL(sfCredentialType, Slice("cred_type", 9));
-                    credentials.pushBack(std::move(cred));
-                }
-                slePd->setFieldArray(sfAcceptedCredentials, credentials);
-                ac.view().update(slePd);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
-            fixEnabled ? failTers : badTers);
-
-        testcase << "PermissionedDomain Set 1";
-        doInvariantCheck(
-            makeEnv(features),
-            {{"permissioned domain with no rules."}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                // create PD
-                auto slePd = createPermissionedDomain(ac, a1, a2);
-
-                // update PD with empty rules
-                {
-                    STArray const credentials(sfAcceptedCredentials, 2);
-                    slePd->setFieldArray(sfAcceptedCredentials, credentials);
-                    ac.view().update(slePd);
-                }
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
-            fixEnabled ? failTers : badTers);
-
-        testcase << "PermissionedDomain Set 2";
-        doInvariantCheck(
-            makeEnv(features),
-            {{"permissioned domain bad credentials size " + std::to_string(kTooBig)}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                // create PD
-                auto slePd = createPermissionedDomain(ac, a1, a2);
-
-                // update PD
-                {
-                    STArray credentials(sfAcceptedCredentials, kTooBig);
-
-                    for (std::size_t n = 0; n < kTooBig; ++n)
-                    {
-                        auto cred = STObject::makeInnerObject(sfCredential);
-                        cred.setAccountID(sfIssuer, a2);
-                        auto credType = "cred_type2" + std::to_string(n);
-                        cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
-                        credentials.pushBack(std::move(cred));
-                    }
-
-                    slePd->setFieldArray(sfAcceptedCredentials, credentials);
-                    ac.view().update(slePd);
-                }
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
-            fixEnabled ? failTers : badTers);
-
-        testcase << "PermissionedDomain Set 3";
-        doInvariantCheck(
-            makeEnv(features),
-            {{"permissioned domain credentials aren't sorted"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                // create PD
-                auto slePd = createPermissionedDomain(ac, a1, a2);
-
-                // update PD
-                {
-                    STArray credentials(sfAcceptedCredentials, 2);
-                    for (std::size_t n = 0; n < 2; ++n)
-                    {
-                        auto cred = STObject::makeInnerObject(sfCredential);
-                        cred.setAccountID(sfIssuer, a2);
-                        auto credType = std::string("cred_type2") + std::to_string(9 - n);
-                        cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
-                        credentials.pushBack(std::move(cred));
-                    }
-
-                    slePd->setFieldArray(sfAcceptedCredentials, credentials);
-                    ac.view().update(slePd);
-                }
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
-            fixEnabled ? failTers : badTers);
-
-        testcase << "PermissionedDomain Set 4";
-        doInvariantCheck(
-            makeEnv(features),
-            {{"permissioned domain credentials aren't unique"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                // create PD
-                auto slePd = createPermissionedDomain(ac, a1, a2);
-
-                // update PD
-                {
-                    STArray credentials(sfAcceptedCredentials, 2);
-                    for (std::size_t n = 0; n < 2; ++n)
-                    {
-                        auto cred = STObject::makeInnerObject(sfCredential);
-                        cred.setAccountID(sfIssuer, a2);
-                        cred.setFieldVL(sfCredentialType, Slice("cred_type", 9));
-                        credentials.pushBack(std::move(cred));
-                    }
-                    slePd->setFieldArray(sfAcceptedCredentials, credentials);
-                    ac.view().update(slePd);
-                }
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
-            fixEnabled ? failTers : badTers);
-
-        std::initializer_list const goodTers = {tesSUCCESS, tesSUCCESS};
-
-        std::vector const badMoreThan1{
-            {"transaction affected more than 1 permissioned domain entry."}};
-        std::vector const emptyV;
-        std::vector const badNoDomains{{"no domain objects affected by"}};
-        std::vector const badNotDeleted{
-            {"domain object modified, but not deleted by "}};
-        std::vector const badDeleted{{"domain object deleted by"}};
-        std::vector const badTx{
-            {"domain object(s) affected by an unauthorized transaction."}};
-
-        {
-            testcase << "PermissionedDomain set 2 domains ";
-            doInvariantCheck(
-                makeEnv(features),
-                fixEnabled ? badMoreThan1 : emptyV,
-                [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    createPermissionedDomain(ac, a1, a2);
-                    createPermissionedDomain(ac, a1, a2, 2, 11);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
-                fixEnabled ? failTers : goodTers);
-        }
-
-        {
-            testcase << "PermissionedDomain del 2 domains";
-
-            Env env1(*this, features);
-
-            Account const a1{"A1"};
-            Account const a2{"A2"};
-            env1.fund(XRP(1000), a1, a2);
-            env1.close();
-
-            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
-            [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
-            env1.close();
-
-            doInvariantCheck(
-                std::move(env1),
-                a1,
-                a2,
-                fixEnabled ? badMoreThan1 : emptyV,
-                [&pd1, &pd2](Account const&, Account const&, ApplyContext& ac) {
-                    auto sle1 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd1});
-                    auto sle2 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd2});
-                    ac.view().erase(sle1);
-                    ac.view().erase(sle2);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
-                fixEnabled ? failTers : goodTers);
-        }
-
-        {
-            testcase << "PermissionedDomain set 0 domains ";
-            doInvariantCheck(
-                makeEnv(features),
-                fixEnabled ? badNoDomains : emptyV,
-                [](Account const&, Account const&, ApplyContext&) { return true; },
-                XRPAmount{},
-                STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
-                fixEnabled ? badTers : goodTers);
-        }
-
-        {
-            testcase << "PermissionedDomain del 0 domains";
-
-            Env env1(*this, features);
-
-            Account const a1{"A1"};
-            Account const a2{"A2"};
-            env1.fund(XRP(1000), a1, a2);
-            env1.close();
-
-            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
-            [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
-            env1.close();
-
-            doInvariantCheck(
-                makeEnv(features),
-                a1,
-                a2,
-                fixEnabled ? badNoDomains : emptyV,
-                [](Account const&, Account const&, ApplyContext&) { return true; },
-                XRPAmount{},
-                STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
-                fixEnabled ? badTers : goodTers);
-        }
-
-        {
-            testcase << "PermissionedDomain set, delete domain";
-
-            Env env1(*this, features);
-
-            Account const a1{"A1"};
-            Account const a2{"A2"};
-            env1.fund(XRP(1000), a1, a2);
-            env1.close();
-
-            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
-            env1.close();
-
-            doInvariantCheck(
-                std::move(env1),
-                a1,
-                a2,
-                fixEnabled ? badDeleted : emptyV,
-                [&pd1](Account const&, Account const&, ApplyContext& ac) {
-                    auto sle1 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd1});
-                    ac.view().erase(sle1);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
-                fixEnabled ? failTers : goodTers);
-        }
-
-        {
-            testcase << "PermissionedDomain del, create domain ";
-            doInvariantCheck(
-                makeEnv(features),
-                fixEnabled ? badNotDeleted : emptyV,
-                [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    createPermissionedDomain(ac, a1, a2);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
-                fixEnabled ? failTers : goodTers);
-        }
-
-        {
-            testcase << "PermissionedDomain invalid tx";
-
-            doInvariantCheck(
-                fixEnabled ? badTx : emptyV,
-                [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    createPermissionedDomain(ac, a1, a2);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttPAYMENT, [](STObject&) {}},
-                failTers);
-        }
-    }
-
-    void
-    testValidPseudoAccounts()
-    {
-        testcase << "valid pseudo accounts";
-
-        using namespace jtx;
-
-        AccountID pseudoAccountID;
-        Preclose const createPseudo = [&, this](Account const& a, Account const& b, Env& env) {
-            PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
-
-            // Create vault
-            Vault const vault{env};
-            auto [tx, vKeylet] = vault.create({.owner = a, .asset = xrpAsset});
-            env(tx);
-            env.close();
-            if (auto const vSle = env.le(vKeylet); BEAST_EXPECT(vSle))
-            {
-                pseudoAccountID = vSle->at(sfAccount);
-            }
-
-            return BEAST_EXPECT(env.le(keylet::account(pseudoAccountID)));
-        };
-
-        /* Cases to check
-            "pseudo-account has 0 pseudo-account fields set"
-            "pseudo-account has 2 pseudo-account fields set"
-            "pseudo-account sequence changed"
-            "pseudo-account flags are not set"
-            "pseudo-account has a regular key"
-            "pseudo-account has a sponsorship field"
-        */
-        struct Mod
-        {
-            std::string expectedFailure;
-            std::function func;
-        };
-        auto const mods = std::to_array({
-            {
-                .expectedFailure = "pseudo-account has 0 pseudo-account fields set",
-                .func =
-                    [this](SLE::pointer& sle) {
-                        BEAST_EXPECT(sle->at(~sfVaultID));
-                        sle->at(~sfVaultID) = std::nullopt;
-                    },
-            },
-            {
-                .expectedFailure = "pseudo-account sequence changed",
-                .func = [](SLE::pointer& sle) { sle->at(sfSequence) = 12345; },
-            },
-            {
-                .expectedFailure = "pseudo-account flags are not set",
-                .func = [](SLE::pointer& sle) { sle->at(sfFlags) = lsfNoFreeze; },
-            },
-            {
-                .expectedFailure = "pseudo-account has a regular key",
-                .func = [](SLE::pointer& sle) { sle->at(sfRegularKey) = Account("regular").id(); },
-            },
-            {
-                .expectedFailure = "pseudo-account has a sponsorship field",
-                .func = [](SLE::pointer& sle) { sle->at(sfSponsoredOwnerCount) = 1; },
-            },
-            {
-                .expectedFailure = "pseudo-account has a sponsorship field",
-                .func = [](SLE::pointer& sle) { sle->at(sfSponsoringOwnerCount) = 1; },
-            },
-            {
-                .expectedFailure = "pseudo-account has a sponsorship field",
-                .func = [](SLE::pointer& sle) { sle->at(sfSponsoringAccountCount) = 1; },
-            },
-            {
-                .expectedFailure = "pseudo-account has a sponsorship field",
-                .func = [](SLE::pointer& sle) { sle->at(sfSponsor) = Account("sponsor").id(); },
-            },
-        });
-
-        for (auto const& mod : mods)
-        {
-            doInvariantCheck(
-                {{mod.expectedFailure}},
-                [&](Account const& a1, Account const&, ApplyContext& ac) {
-                    auto sle = ac.view().peek(keylet::account(pseudoAccountID));
-                    if (!sle)
-                        return false;
-                    mod.func(sle);
-                    ac.view().update(sle);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttACCOUNT_SET, [](STObject& tx) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                createPseudo);
-        }
-        for (auto const pField : getPseudoAccountFields())
-        {
-            // createPseudo creates a vault, so sfVaultID will be set, and
-            // setting it again will not cause an error
-            if (pField == &sfVaultID)
-                continue;
-            doInvariantCheck(
-                {{"pseudo-account has 2 pseudo-account fields set"}},
-                [&](Account const& a1, Account const&, ApplyContext& ac) {
-                    auto sle = ac.view().peek(keylet::account(pseudoAccountID));
-                    if (!sle)
-                        return false;
-
-                    auto const vaultID = ~sle->at(~sfVaultID);
-                    BEAST_EXPECT(vaultID && !sle->isFieldPresent(*pField));
-                    sle->setFieldH256(*pField, *vaultID);
-
-                    ac.view().update(sle);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttACCOUNT_SET, [](STObject& tx) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                createPseudo);
-        }
-
-        // Take one of the regular accounts and set the sequence to 0, which
-        // will make it look like a pseudo-account
-        doInvariantCheck(
-            {{"pseudo-account has 0 pseudo-account fields set"},
-             {"pseudo-account sequence changed"},
-             {"pseudo-account flags are not set"}},
-            [&](Account const& a1, Account const&, ApplyContext& ac) {
-                auto sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-                sle->at(sfSequence) = 0;
-                ac.view().update(sle);
-                return true;
-            });
-    }
-
-    static std::pair
-    createPermissionedDomainEnv(
-        test::jtx::Env& env,
-        test::jtx::Account const& a1,
-        test::jtx::Account const& a2,
-        std::uint32_t numCreds = 2)
-    {
-        using namespace test::jtx;
-
-        pdomain::Credentials credentials;
-
-        for (std::size_t n = 0; n < numCreds; ++n)
-        {
-            auto credType = "cred_type" + std::to_string(n);
-            credentials.push_back({.issuer = a2, .credType = credType});
-        }
-
-        std::uint32_t const seq = env.seq(a1);
-        env(pdomain::setTx(a1, credentials));
-        uint256 const key = pdomain::getNewDomain(env.meta());
-
-        // std::cout << "PD, acc: " << A1.id() << ", seq: " << seq << ", k: " <<
-        // key << std::endl;
-        return {seq, key};
-    }
-
-    void
-    testPermissionedDEX(FeatureBitset features)
-    {
-        using namespace test::jtx;
-
-        bool const fixEnabled = features[fixCleanup3_1_3];
-
-        testcase << "PermissionedDEX" + std::string(fixEnabled ? " fix" : "");
-
-        doInvariantCheck(
-            makeEnv(features),
-            {{"domain doesn't exist"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                Keylet const offerKey = keylet::offer(a1.id(), SeqProxy::rawSequence(10));
-                auto sleOffer = std::make_shared(offerKey);
-                sleOffer->setAccountID(sfAccount, a1);
-                sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
-                sleOffer->setFieldAmount(sfTakerGets, XRP(1));
-                ac.view().insert(sleOffer);
-                return true;
-            },
-            XRPAmount{},
-            STTx{
-                ttOFFER_CREATE,
-                [](STObject& tx) {
-                    tx.setFieldH256(
-                        sfDomainID,
-                        uint256{"F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E33"
-                                "70F3649CE134E5"});
-                    Account const a1{"A1"};
-                    tx.setFieldAmount(sfTakerPays, a1["USD"](10));
-                    tx.setFieldAmount(sfTakerGets, XRP(1));
-                }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
-
-        // missing domain ID in offer object
-        doInvariantCheck(
-            makeEnv(features),
-            {{"hybrid offer is malformed"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
-                auto sleOffer = std::make_shared(offerKey);
-                sleOffer->setAccountID(sfAccount, a2);
-                sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
-                sleOffer->setFieldAmount(sfTakerGets, XRP(1));
-                sleOffer->setFlag(lsfHybrid);
-
-                STArray bookArr;
-                bookArr.pushBack(STObject::makeInnerObject(sfBook));
-                sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
-                ac.view().insert(sleOffer);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttOFFER_CREATE, [&](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
-
-        // more than one entry in sfAdditionalBooks
-        {
-            Env env1(*this, features);
-
-            Account const a1{"A1"};
-            Account const a2{"A2"};
-            env1.fund(XRP(1000), a1, a2);
-            env1.close();
-
-            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
-            env1.close();
-
-            doInvariantCheck(
-                std::move(env1),
-                a1,
-                a2,
-                {{"hybrid offer is malformed"}},
-                [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
-                    auto sleOffer = std::make_shared(offerKey);
-                    sleOffer->setAccountID(sfAccount, a2);
-                    sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
-                    sleOffer->setFieldAmount(sfTakerGets, XRP(1));
-                    sleOffer->setFlag(lsfHybrid);
-                    sleOffer->setFieldH256(sfDomainID, pd1);
-
-                    STArray bookArr;
-                    bookArr.pushBack(STObject::makeInnerObject(sfBook));
-                    bookArr.pushBack(STObject::makeInnerObject(sfBook));
-                    sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
-                    ac.view().insert(sleOffer);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttOFFER_CREATE, [&](STObject&) {}},
-                {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
-        }
-
-        // empty sfAdditionalBooks (size 0)
-        {
-            Env env1(*this, features);
-
-            Account const a1{"A1"};
-            Account const a2{"A2"};
-            env1.fund(XRP(1000), a1, a2);
-            env1.close();
-
-            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
-            env1.close();
-
-            doInvariantCheck(
-                std::move(env1),
-                a1,
-                a2,
-                fixEnabled ? std::vector{{"hybrid offer is malformed"}}
-                           : std::vector{},
-                [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
-                    auto sleOffer = std::make_shared(offerKey);
-                    sleOffer->setAccountID(sfAccount, a2);
-                    sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
-                    sleOffer->setFieldAmount(sfTakerGets, XRP(1));
-                    sleOffer->setFlag(lsfHybrid);
-                    sleOffer->setFieldH256(sfDomainID, pd1);
-
-                    STArray const bookArr;  // empty array, size 0
-                    sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
-                    ac.view().insert(sleOffer);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttOFFER_CREATE, [&](STObject&) {}},
-                fixEnabled ? std::initializer_list{tecINVARIANT_FAILED, tecINVARIANT_FAILED}
-                           : std::initializer_list{tesSUCCESS, tesSUCCESS});
-        }
-
-        // hybrid offer missing sfAdditionalBooks
-        {
-            Env env1(*this, features);
-
-            Account const a1{"A1"};
-            Account const a2{"A2"};
-            env1.fund(XRP(1000), a1, a2);
-            env1.close();
-
-            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
-            env1.close();
-
-            doInvariantCheck(
-                std::move(env1),
-                a1,
-                a2,
-                {{"hybrid offer is malformed"}},
-                [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
-                    auto sleOffer = std::make_shared(offerKey);
-                    sleOffer->setAccountID(sfAccount, a2);
-                    sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
-                    sleOffer->setFieldAmount(sfTakerGets, XRP(1));
-                    sleOffer->setFlag(lsfHybrid);
-                    sleOffer->setFieldH256(sfDomainID, pd1);
-                    ac.view().insert(sleOffer);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttOFFER_CREATE, [&](STObject&) {}},
-                {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
-        }
-
-        {
-            Env env1(*this, features);
-
-            Account const a1{"A1"};
-            Account const a2{"A2"};
-            env1.fund(XRP(1000), a1, a2);
-            env1.close();
-
-            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
-            [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
-            env1.close();
-
-            doInvariantCheck(
-                std::move(env1),
-                a1,
-                a2,
-                {{"transaction consumed wrong domains"}},
-                [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
-                    auto sleOffer = std::make_shared(offerKey);
-                    sleOffer->setAccountID(sfAccount, a2);
-                    sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
-                    sleOffer->setFieldAmount(sfTakerGets, XRP(1));
-                    sleOffer->setFieldH256(sfDomainID, pd1);
-                    ac.view().insert(sleOffer);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttOFFER_CREATE,
-                    [&pd2, &a1](STObject& tx) {
-                        tx.setFieldH256(sfDomainID, pd2);
-                        tx.setFieldAmount(sfTakerPays, a1["USD"](10));
-                        tx.setFieldAmount(sfTakerGets, XRP(1));
-                    }},
-                {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
-        }
-
-        {
-            Env env1(*this, features);
-
-            Account const a1{"A1"};
-            Account const a2{"A2"};
-            env1.fund(XRP(1000), a1, a2);
-            env1.close();
-
-            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
-            env1.close();
-
-            doInvariantCheck(
-                std::move(env1),
-                a1,
-                a2,
-                {{"domain transaction affected regular offers"}},
-                [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
-                    auto sleOffer = std::make_shared(offerKey);
-                    sleOffer->setAccountID(sfAccount, a2);
-                    sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
-                    sleOffer->setFieldAmount(sfTakerGets, XRP(1));
-                    ac.view().insert(sleOffer);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttOFFER_CREATE,
-                    [&](STObject& tx) {
-                        Account const a1{"A1"};
-                        tx.setFieldH256(sfDomainID, pd1);
-                        tx.setFieldAmount(sfTakerPays, a1["USD"](10));
-                        tx.setFieldAmount(sfTakerGets, XRP(1));
-                    }},
-                {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
-        }
-    }
-
-    void
-    testBookDirectoryExchangeRate()
-    {
-        using namespace test::jtx;
-        testcase << "book directory exchange rate";
-
-        auto const getBookRootKey = [](Account const& account, std::uint64_t quality) {
-            Book const book{xrpIssue(), account["USD"], std::nullopt};
-            return keylet::quality(keylet::book(book), quality);
-        };
-
-        // Root book-directory pages carry exchange-rate metadata that must
-        // match the quality encoded in the directory key.
-        auto const makeRootPage = [](Keylet const& dir, std::uint64_t exchangeRate) {
-            auto sleDir = std::make_shared(dir);
-            sleDir->setFieldH256(sfRootIndex, dir.key);
-            STVector256 indexes;
-            indexes.pushBack(uint256{1});
-            sleDir->setFieldV256(sfIndexes, indexes);
-            sleDir->setFieldU64(sfExchangeRate, exchangeRate);
-            return sleDir;
-        };
-
-        // Child pages do not carry quality metadata; they only point back to
-        // the root directory.
-        auto const makeChildPage = [](Keylet const& rootDir) {
-            auto sleDir = std::make_shared(keylet::page(rootDir, 1));
-            sleDir->setFieldH256(sfRootIndex, rootDir.key);
-            STVector256 indexes;
-            indexes.pushBack(uint256{2});
-            sleDir->setFieldV256(sfIndexes, indexes);
-            return sleDir;
-        };
-
-        auto const makeOfferCreateTx = [] {
-            return STTx{ttOFFER_CREATE, [](STObject& tx) {
-                            Account const account{"A1"};
-                            tx.setFieldAmount(sfTakerPays, XRP(1));
-                            tx.setFieldAmount(sfTakerGets, account["USD"](1));
-                        }};
-        };
-        std::initializer_list const failTers = {tecINVARIANT_FAILED, tefINVARIANT_FAILED};
-
-        // Creating a root book directory with mismatched exchange-rate
-        // metadata violates the invariant.
-        doInvariantCheck(
-            {{"book directory exchange rate does not match directory quality"}},
-            [&](Account const& a1, Account const&, ApplyContext& ac) {
-                auto const directoryQuality = STAmount::kURateOne;
-                auto const dir = getBookRootKey(a1, directoryQuality);
-                ac.view().insert(makeRootPage(dir, directoryQuality + 1));
-                return true;
-            },
-            XRPAmount{},
-            makeOfferCreateTx(),
-            failTers);
-
-        // A new child page must point to an existing root page.
-        doInvariantCheck(
-            {{"book directory root missing"}},
-            [&](Account const& a1, Account const&, ApplyContext& ac) {
-                auto const directoryQuality = STAmount::kURateOne;
-                auto const rootDir = getBookRootKey(a1, directoryQuality);
-                // Insert only the child page.  It points at rootDir, but the
-                // corresponding root page is intentionally missing.
-                ac.view().insert(makeChildPage(rootDir));
-                return true;
-            },
-            XRPAmount{},
-            makeOfferCreateTx(),
-            failTers);
-
-        // Legacy bad-root tolerance:
-        // - The view contains a pre-existing root page with bad sfExchangeRate
-        //   metadata.
-        // - The simulated transaction only creates a child page pointing to
-        //   that root.
-        // - The invariant must pass because this transaction did not create
-        //   the bad root, only adding a child page.
-        {
-            Env env{*this, defaultAmendments()};
-            Account const a1{"A1"};
-            env.fund(XRP(1000), a1);
-            env.close();
-
-            OpenView view{*env.current()};
-            auto const directoryQuality = STAmount::kURateOne;
-            auto const rootDir = getBookRootKey(a1, directoryQuality);
-            view.rawInsert(makeRootPage(rootDir, directoryQuality + 1));
-
-            ValidBookDirectory invariant;
-            invariant.visitEntry(false, nullptr, makeChildPage(rootDir));
-
-            test::StreamSink sink{beast::Severity::Warning};
-            beast::Journal const jlog{sink};
-            BEAST_EXPECT(
-                invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
-        }
-
-        // A bad root is rejected when added, ignored when a legacy bad root is
-        // modified without changing sfRootIndex or deleted, and checked when a
-        // modified directory changes sfRootIndex.
-        {
-            Env env{*this, defaultAmendments()};
-            Account const a1{"A1"};
-            env.fund(XRP(1000), a1);
-            env.close();
-
-            OpenView view{*env.current()};
-            auto const directoryQuality = STAmount::kURateOne;
-            auto const rootDir = getBookRootKey(a1, directoryQuality);
-            auto const missingRootDir = getBookRootKey(a1, directoryQuality + 1);
-            auto const badRoot = makeRootPage(rootDir, directoryQuality + 1);
-            view.rawInsert(badRoot);
-
-            test::StreamSink sink{beast::Severity::Warning};
-            beast::Journal const jlog{sink};
-
-            {
-                // add
-                ValidBookDirectory invariant;
-                invariant.visitEntry(false, nullptr, badRoot);
-
-                BEAST_EXPECT(
-                    !invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
-            }
-            {
-                // modify (without changing the sfRootIndex)
-                ValidBookDirectory invariant;
-                invariant.visitEntry(false, badRoot, badRoot);
-
-                BEAST_EXPECT(
-                    invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
-            }
-            {
-                // modify (changing sfRootIndex to a missing root)
-                auto const childBefore = makeChildPage(rootDir);
-                auto const childAfter = std::make_shared(*childBefore, childBefore->key());
-                childAfter->setFieldH256(sfRootIndex, missingRootDir.key);
-
-                ValidBookDirectory invariant;
-                invariant.visitEntry(false, childBefore, childAfter);
-
-                test::StreamSink missingRootSink{beast::Severity::Warning};
-                beast::Journal const missingRootJlog{missingRootSink};
-                BEAST_EXPECT(!invariant.finalize(
-                    makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, missingRootJlog));
-                BEAST_EXPECT(
-                    missingRootSink.messages().str().contains("book directory root missing"));
-            }
-            {
-                // delete
-                view.rawErase(badRoot);
-                BEAST_EXPECT(!view.exists(rootDir));
-
-                ValidBookDirectory invariant;
-                invariant.visitEntry(true, badRoot, badRoot);
-                BEAST_EXPECT(
-                    invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
-            }
-        }
-    }
-
-    Keylet
-    createLoanBroker(jtx::Account const& a, jtx::Env& env, jtx::PrettyAsset const& asset)
-    {
-        using namespace jtx;
-
-        // Create vault
-        uint256 vaultID;
-        Vault const vault{env};
-        auto [tx, vKeylet] = vault.create({.owner = a, .asset = asset});
-        env(tx);
-        BEAST_EXPECT(env.le(vKeylet));
-
-        vaultID = vKeylet.key;
-
-        // Create Loan Broker
-        using namespace loan_broker;
-
-        auto const loanBrokerKeylet = keylet::loanBroker(a.id(), SeqProxy::rawSequence(env.seq(a)));
-        // Create a Loan Broker with all default values.
-        env(set(a, vaultID), Fee(kIncrement));
-
-        return loanBrokerKeylet;
-    };
-
-    void
-    testNoModifiedUnmodifiableFields()
-    {
-        testcase("no modified unmodifiable fields");
-        using namespace jtx;
-
-        // Initialize with a placeholder value because there's no default ctor
-        Keylet loanBrokerKeylet = keylet::amendments();
-        Preclose const createLoanBroker = [&, this](Account const& a, Account const& b, Env& env) {
-            PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
-
-            loanBrokerKeylet = this->createLoanBroker(a, env, xrpAsset);
-            return BEAST_EXPECT(env.le(loanBrokerKeylet));
-        };
-
-        {
-            auto const mods = std::to_array>({
-                [](SLE::pointer& sle) { sle->at(sfSequence) += 1; },
-                [](SLE::pointer& sle) { sle->at(sfOwnerNode) += 1; },
-                [](SLE::pointer& sle) { sle->at(sfVaultNode) += 1; },
-                [](SLE::pointer& sle) { sle->at(sfVaultID) = uint256(1u); },
-                [](SLE::pointer& sle) { sle->at(sfAccount) = sle->at(sfOwner); },
-                [](SLE::pointer& sle) { sle->at(sfOwner) = sle->at(sfAccount); },
-                [](SLE::pointer& sle) { sle->at(sfManagementFeeRate) += 1; },
-                [](SLE::pointer& sle) { sle->at(sfCoverRateMinimum) += 1; },
-                [](SLE::pointer& sle) { sle->at(sfCoverRateLiquidation) += 1; },
-                [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
-                [](SLE::pointer& sle) { sle->at(sfLedgerIndex) = sle->at(sfVaultID).value(); },
-            });
-
-            for (auto const& mod : mods)
-            {
-                doInvariantCheck(
-                    {{"changed an unchangeable field"}},
-                    [&](Account const& a1, Account const&, ApplyContext& ac) {
-                        auto sle = ac.view().peek(loanBrokerKeylet);
-                        if (!sle)
-                            return false;
-                        mod(sle);
-                        ac.view().update(sle);
-                        return true;
-                    },
-                    XRPAmount{},
-                    STTx{ttACCOUNT_SET, [](STObject& tx) {}},
-                    {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                    createLoanBroker);
-            }
-        }
-
-        // TODO: Loan Object
-
-        {
-            auto const mods = std::to_array>({
-                [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
-                [](SLE::pointer& sle) { sle->at(sfLedgerIndex) = uint256(1u); },
-            });
-
-            for (auto const& mod : mods)
-            {
-                doInvariantCheck(
-                    {{"changed an unchangeable field"}},
-                    [&](Account const& a1, Account const&, ApplyContext& ac) {
-                        auto sle = ac.view().peek(keylet::account(a1.id()));
-                        if (!sle)
-                            return false;
-                        mod(sle);
-                        ac.view().update(sle);
-                        return true;
-                    });
-            }
-        }
-    }
-
-    void
-    testValidLoanBroker()
-    {
-        testcase << "valid loan broker";
-
-        using namespace jtx;
-
-        enum class Asset { XRP, IOU, MPT };
-        auto const assetTypes = std::to_array({Asset::XRP, Asset::IOU, Asset::MPT});
-
-        for (auto const assetType : assetTypes)
-        {
-            // Initialize with a placeholder value because there's no default
-            // ctor
-            auto const setupAsset =
-                [&](Account const& alice, Account const& issuer, Env& env) -> PrettyAsset {
-                switch (assetType)
-                {
-                    case Asset::IOU: {
-                        PrettyAsset const iouAsset = issuer["IOU"];
-                        env(trust(alice, iouAsset(1000)));
-                        env(pay(issuer, alice, iouAsset(1000)));
-                        env.close();
-                        return iouAsset;
-                    }
-                    case Asset::MPT: {
-                        MPTTester mptt{env, issuer, kMptInitNoFund};
-                        mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
-                        PrettyAsset const mptAsset = mptt.issuanceID();
-                        mptt.authorize({.account = alice});
-                        env(pay(issuer, alice, mptAsset(1000)));
-                        env.close();
-                        return mptAsset;
-                    }
-                    case Asset::XRP:
-                    default:
-                        return PrettyAsset{xrpIssue(), 1'000'000};
-                }
-            };
-
-            Keylet loanBrokerKeylet = keylet::amendments();
-            Preclose const createLoanBroker =
-                [&, this](Account const& alice, Account const& issuer, Env& env) {
-                    auto const asset = setupAsset(alice, issuer, env);
-                    loanBrokerKeylet = this->createLoanBroker(alice, env, asset);
-                    return BEAST_EXPECT(env.le(loanBrokerKeylet));
-                };
-
-            // Ensure the test scenarios are set up completely. The test cases
-            // will need to recompute any of these values it needs for itself
-            // rather than trying to return a bunch of items
-            auto setupTest = [&, this](Account const& a1, Account const&, ApplyContext& ac)
-                -> std::optional> {
-                if (loanBrokerKeylet.type != ltLOAN_BROKER)
-                    return {};
-                auto sleBroker = ac.view().peek(loanBrokerKeylet);
-                if (!sleBroker)
-                    return {};
-                if (!BEAST_EXPECT(sleBroker->at(sfOwnerCount) == 0))
-                    return {};
-                // Need to touch sleBroker so that it is included in the
-                // modified entries for the invariant to find
-                ac.view().update(sleBroker);
-
-                // The pseudo-account holds the directory, so get it
-                auto const pseudoAccountID = sleBroker->at(sfAccount);
-                auto const pseudoAccountKeylet = keylet::account(pseudoAccountID);
-                // Strictly speaking, we don't need to load the
-                // ACCOUNT_ROOT, but check anyway
-                auto slePseudo = ac.view().peek(pseudoAccountKeylet);
-                if (!BEAST_EXPECT(slePseudo))
-                    return {};
-                // Make sure the directory doesn't already exist
-                auto const dirKeylet = keylet::ownerDir(pseudoAccountID);
-                auto sleDir = ac.view().peek(dirKeylet);
-                auto const describe = describeOwnerDir(pseudoAccountID);
-                if (!sleDir)
-                {
-                    // Create the directory
-                    BEAST_EXPECT(
-                        ::xrpl::directory::createRoot(
-                            ac.view(), dirKeylet, loanBrokerKeylet.key, describe) == 0);
-
-                    sleDir = ac.view().peek(dirKeylet);
-                }
-
-                return std::make_pair(slePseudo, sleDir);
-            };
-
-            doInvariantCheck(
-                {{"Loan Broker with zero OwnerCount has multiple directory "
-                  "pages"}},
-                [&setupTest, this](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    auto test = setupTest(a1, a2, ac);
-                    if (!test || !test->first || !test->second)
-                        return false;
-
-                    auto slePseudo = test->first;
-                    auto sleDir = test->second;
-                    auto const describe = describeOwnerDir(slePseudo->at(sfAccount));
-
-                    BEAST_EXPECT(
-                        ::xrpl::directory::insertPage(
-                            ac.view(),
-                            0,
-                            sleDir,
-                            0,
-                            sleDir,
-                            slePseudo->key(),
-                            keylet::page(sleDir->key(), 0),
-                            describe) == 1);
-
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                createLoanBroker);
-
-            doInvariantCheck(
-                {{"Loan Broker with zero OwnerCount has multiple indexes in "
-                  "the Directory root"}},
-                [&setupTest](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    auto test = setupTest(a1, a2, ac);
-                    if (!test || !test->first || !test->second)
-                        return false;
-
-                    auto slePseudo = test->first;
-                    auto sleDir = test->second;
-                    auto indexes = sleDir->getFieldV256(sfIndexes);
-
-                    // Put some extra garbage into the directory
-                    for (auto const& key : {slePseudo->key(), sleDir->key()})
-                    {
-                        ::xrpl::directory::insertKey(ac.view(), sleDir, 0, false, indexes, key);
-                    }
-
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                createLoanBroker);
-
-            doInvariantCheck(
-                {{"Loan Broker directory corrupt"}},
-                [&setupTest](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    auto test = setupTest(a1, a2, ac);
-                    if (!test || !test->first || !test->second)
-                        return false;
-
-                    auto slePseudo = test->first;
-                    auto sleDir = test->second;
-                    auto const describe = describeOwnerDir(slePseudo->at(sfAccount));
-                    // Empty vector will overwrite the existing entry for the
-                    // holding, if any, avoiding the "has multiple indexes"
-                    // failure.
-                    STVector256 indexes;
-
-                    // Put one meaningless key into the directory
-                    auto const key = keylet::account(Account("random").id()).key;
-                    ::xrpl::directory::insertKey(ac.view(), sleDir, 0, false, indexes, key);
-
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                createLoanBroker);
-
-            doInvariantCheck(
-                {{"Loan Broker with zero OwnerCount has an unexpected entry in "
-                  "the directory"}},
-                [&setupTest](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    auto test = setupTest(a1, a2, ac);
-                    if (!test || !test->first || !test->second)
-                        return false;
-
-                    auto slePseudo = test->first;
-                    auto sleDir = test->second;
-                    // Empty vector will overwrite the existing entry for the
-                    // holding, if any, avoiding the "has multiple indexes"
-                    // failure.
-                    STVector256 indexes;
-
-                    ::xrpl::directory::insertKey(
-                        ac.view(), sleDir, 0, false, indexes, slePseudo->key());
-
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                createLoanBroker);
-
-            doInvariantCheck(
-                {{"Loan Broker sequence number decreased"}},
-                [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    if (loanBrokerKeylet.type != ltLOAN_BROKER)
-                        return false;
-                    auto sleBroker = ac.view().peek(loanBrokerKeylet);
-                    if (!sleBroker)
-                        return false;
-                    if (!BEAST_EXPECT(sleBroker->at(sfLoanSequence) > 0))
-                        return false;
-                    // Need to touch sleBroker so that it is included in the
-                    // modified entries for the invariant to find
-                    ac.view().update(sleBroker);
-
-                    sleBroker->at(sfLoanSequence) -= 1;
-
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                createLoanBroker);
-
-            // Test: cover available less than pseudo-account asset balance
-            {
-                Keylet brokerKeylet = keylet::amendments();
-                Preclose const createBrokerWithCover =
-                    [&, this](Account const& alice, Account const& issuer, Env& env) {
-                        auto const asset = setupAsset(alice, issuer, env);
-                        brokerKeylet = this->createLoanBroker(alice, env, asset);
-                        if (!BEAST_EXPECT(env.le(brokerKeylet)))
-                            return false;
-                        env(loan_broker::coverDeposit(alice, brokerKeylet.key, asset(10)));
-                        env.close();
-                        return BEAST_EXPECT(env.le(brokerKeylet));
-                    };
-
-                doInvariantCheck(
-                    {{"Loan Broker cover available is less than pseudo-account asset balance"}},
-                    [&](Account const&, Account const&, ApplyContext& ac) {
-                        auto sle = ac.view().peek(brokerKeylet);
-                        if (!BEAST_EXPECT(sle))
-                            return false;
-                        // Pseudo-account holds 10 units, set cover to 5
-                        sle->at(sfCoverAvailable) = Number(5);
-                        ac.view().update(sle);
-                        return true;
-                    },
-                    XRPAmount{},
-                    STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
-                    {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                    createBrokerWithCover);
-            }
-
-            // Test: cover available greater than pseudo-account asset balance
-            // (requires fixCleanup3_1_3)
-            doInvariantCheck(
-                {{"Loan Broker cover available is greater than pseudo-account asset balance"}},
-                [&](Account const&, Account const&, ApplyContext& ac) {
-                    auto sle = ac.view().peek(loanBrokerKeylet);
-                    if (!BEAST_EXPECT(sle))
-                        return false;
-                    // Pseudo-account has no cover deposited; set cover
-                    // higher than any incidental balance
-                    sle->at(sfCoverAvailable) = Number(1'000'000);
-                    ac.view().update(sle);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                createLoanBroker);
-        }
-    }
-
-    void
-    testVault()  // NOLINT(readability-function-size)
-    {
-        using namespace test::jtx;
-
-        struct AccountAmount
-        {
-            AccountID account;
-            int amount;
-        };
-        struct Adjustments
-        {
-            // NOLINTBEGIN(readability-redundant-member-init)
-            std::optional assetsTotal = std::nullopt;
-            std::optional assetsAvailable = std::nullopt;
-            std::optional lossUnrealized = std::nullopt;
-            std::optional assetsMaximum = std::nullopt;
-            std::optional sharesTotal = std::nullopt;
-            std::optional vaultAssets = std::nullopt;
-            std::optional accountAssets = std::nullopt;
-            std::optional accountShares = std::nullopt;
-            // NOLINTEND(readability-redundant-member-init)
-        };
-        constexpr auto kAdjust = [&](ApplyView& ac, xrpl::Keylet keylet, Adjustments args) {
-            auto sleVault = ac.peek(keylet);
-            if (!sleVault)
-                return false;
-
-            auto const mptIssuanceID = (*sleVault)[sfShareMPTID];
-            auto sleShares = ac.peek(keylet::mptokenIssuance(mptIssuanceID));
-            if (!sleShares)
-                return false;
-
-            // These two fields are adjusted in absolute terms
-            if (args.lossUnrealized)
-                (*sleVault)[sfLossUnrealized] = *args.lossUnrealized;
-            if (args.assetsMaximum)
-                (*sleVault)[sfAssetsMaximum] = *args.assetsMaximum;
-
-            // Remaining fields are adjusted in terms of difference
-            if (args.assetsTotal)
-                (*sleVault)[sfAssetsTotal] = *(*sleVault)[sfAssetsTotal] + *args.assetsTotal;
-            if (args.assetsAvailable)
-            {
-                (*sleVault)[sfAssetsAvailable] =
-                    *(*sleVault)[sfAssetsAvailable] + *args.assetsAvailable;
-            }
-            ac.update(sleVault);
-
-            if (args.sharesTotal)
-            {
-                (*sleShares)[sfOutstandingAmount] =
-                    *(*sleShares)[sfOutstandingAmount] + *args.sharesTotal;
-                ac.update(sleShares);
-            }
-
-            auto const assets = *(*sleVault)[sfAsset];
-            auto const pseudoId = *(*sleVault)[sfAccount];
-            if (args.vaultAssets)
-            {
-                if (assets.native())
-                {
-                    auto slePseudoAccount = ac.peek(keylet::account(pseudoId));
-                    if (!slePseudoAccount)
-                        return false;
-                    (*slePseudoAccount)[sfBalance] =
-                        *(*slePseudoAccount)[sfBalance] + *args.vaultAssets;
-                    ac.update(slePseudoAccount);
-                }
-                else if (assets.holds())
-                {
-                    auto const mptId = assets.get().getMptID();
-                    auto sleMPToken = ac.peek(keylet::mptoken(mptId, pseudoId));
-                    if (!sleMPToken)
-                        return false;
-                    (*sleMPToken)[sfMPTAmount] = *(*sleMPToken)[sfMPTAmount] + *args.vaultAssets;
-                    ac.update(sleMPToken);
-                }
-                else
-                {
-                    return false;  // Not supporting testing with IOU
-                }
-            }
-
-            if (args.accountAssets)
-            {
-                auto const& pair = *args.accountAssets;
-                if (assets.native())
-                {
-                    auto sleAccount = ac.peek(keylet::account(pair.account));
-                    if (!sleAccount)
-                        return false;
-                    (*sleAccount)[sfBalance] = *(*sleAccount)[sfBalance] + pair.amount;
-                    ac.update(sleAccount);
-                }
-                else if (assets.holds())
-                {
-                    auto const mptID = assets.get().getMptID();
-                    auto sleMPToken = ac.peek(keylet::mptoken(mptID, pair.account));
-                    if (!sleMPToken)
-                        return false;
-                    (*sleMPToken)[sfMPTAmount] = *(*sleMPToken)[sfMPTAmount] + pair.amount;
-                    ac.update(sleMPToken);
-                }
-                else
-                {
-                    return false;  // Not supporting testing with IOU
-                }
-            }
-
-            if (args.accountShares)
-            {
-                auto const& pair = *args.accountShares;
-                auto sleMPToken = ac.peek(keylet::mptoken(mptIssuanceID, pair.account));
-                if (!sleMPToken)
-                    return false;
-                (*sleMPToken)[sfMPTAmount] = *(*sleMPToken)[sfMPTAmount] + pair.amount;
-                ac.update(sleMPToken);
-            }
-            return true;
-        };
-
-        static constexpr auto kArgs = [](AccountID id, int adjustment, auto fn) -> Adjustments {
-            Adjustments sample = {
-                .assetsTotal = adjustment,
-                .assetsAvailable = adjustment,
-                .lossUnrealized = 0,
-                .sharesTotal = adjustment,
-                .vaultAssets = adjustment,
-                .accountAssets =  //
-                AccountAmount{.account = id, .amount = -adjustment},
-                .accountShares =  //
-                AccountAmount{.account = id, .amount = adjustment}};
-            fn(sample);
-            return sample;
-        };
-
-        Account const a3{"A3"};
-        Account const a4{"A4"};
-        auto const precloseXrp = [&](Account const& a1, Account const& a2, Env& env) -> bool {
-            env.fund(XRP(1000), a3, a4);
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
-            env(tx);
-            env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
-            env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = XRP(10)}));
-            env(vault.deposit({.depositor = a3, .id = keylet.key, .amount = XRP(10)}));
-            return true;
-        };
-
-        testcase << "Vault general checks";
-        doInvariantCheck(
-            {"vault deletion succeeded without deleting a vault"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                ac.view().update(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DELETE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"vault updated by a wrong transaction type",
-             "deleted Vault without deleting its pseudo-account"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                ac.view().erase(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPAYMENT, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"vault updated by a wrong transaction type"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                ac.view().update(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPAYMENT, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"vault updated by a wrong transaction type"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sequence = ac.view().seq();
-                auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
-                auto sleVault = std::make_shared(vaultKeylet);
-                auto const vaultPage = ac.view().dirInsert(
-                    keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
-                sleVault->setFieldU64(sfOwnerNode, *vaultPage);
-                sleVault->setAccountID(sfAccount, a1.id());
-                ac.view().insert(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttPAYMENT, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
-
-        doInvariantCheck(
-            {"vault deleted by a wrong transaction type",
-             "deleted Vault without deleting its pseudo-account"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                ac.view().erase(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_SET, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"vault operation updated more than single vault",
-             "deleted Vault without deleting its pseudo-account"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                {
-                    auto const keylet =
-                        keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                    auto sleVault = ac.view().peek(keylet);
-                    if (!sleVault)
-                        return false;
-                    ac.view().erase(sleVault);
-                }
-                {
-                    auto const keylet =
-                        keylet::vault(a2.id(), SeqProxy::rawSequence(ac.view().seq()));
-                    auto sleVault = ac.view().peek(keylet);
-                    if (!sleVault)
-                        return false;
-                    ac.view().erase(sleVault);
-                }
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DELETE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                {
-                    auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
-                    env(tx);
-                }
-                {
-                    auto [tx, _] = vault.create({.owner = a2, .asset = xrpIssue()});
-                    env(tx);
-                }
-                return true;
-            });
-
-        doInvariantCheck(
-            {"vault operation updated more than single vault"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sequence = ac.view().seq();
-                auto const insertVault = [&](Account const a) {
-                    auto const vaultKeylet = keylet::vault(a.id(), SeqProxy::rawSequence(sequence));
-                    auto sleVault = std::make_shared(vaultKeylet);
-                    auto const vaultPage = ac.view().dirInsert(
-                        keylet::ownerDir(a.id()), sleVault->key(), describeOwnerDir(a.id()));
-                    sleVault->setFieldU64(sfOwnerNode, *vaultPage);
-                    sleVault->setAccountID(sfAccount, a.id());
-                    ac.view().insert(sleVault);
-                };
-                insertVault(a1);
-                insertVault(a2);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CREATE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
-
-        doInvariantCheck(
-            {"deleted vault must also delete shares",
-             "deleted Vault without deleting its pseudo-account"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                ac.view().erase(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DELETE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"deleted vault must have no shares outstanding",
-             "deleted vault must have no assets outstanding",
-             "deleted vault must have no assets available"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
-                if (!sleShares)
-                    return false;
-                ac.view().erase(sleVault);
-                ac.view().erase(sleShares);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DELETE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
-                return true;
-            });
-
-        doInvariantCheck(
-            {"vault operation succeeded without modifying a vault"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
-                if (!sleShares)
-                    return false;
-                // Note, such an "orphaned" update of MPT issuance attached to a
-                // vault is invalid; ttVAULT_SET must also update Vault object.
-                sleShares->setFieldH256(sfDomainID, uint256(13));
-                ac.view().update(sleShares);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_SET, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"vault operation succeeded without modifying a vault"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
-            XRPAmount{},
-            STTx{ttVAULT_CREATE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"vault operation succeeded without modifying a vault"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
-            XRPAmount{},
-            STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"vault operation succeeded without modifying a vault"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
-            XRPAmount{},
-            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"vault operation succeeded without modifying a vault"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
-            XRPAmount{},
-            STTx{ttVAULT_CLAWBACK, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"vault operation succeeded without modifying a vault"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
-            XRPAmount{},
-            STTx{ttVAULT_DELETE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"updated vault must have shares"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                (*sleVault)[sfAssetsMaximum] = 200;
-                ac.view().update(sleVault);
-
-                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
-                if (!sleShares)
-                    return false;
-                ac.view().erase(sleShares);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_SET, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"vault operation succeeded without updating shares",
-             "assets available must not be greater than assets outstanding"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                (*sleVault)[sfAssetsTotal] = 9;
-                ac.view().update(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
-                return true;
-            });
-
-        doInvariantCheck(
-            {"set must not change assets outstanding",
-             "set must not change assets available",
-             "set must not change shares outstanding",
-             "set must not change vault balance",
-             "assets available must not be negative",
-             "assets available must not be greater than assets outstanding",
-             "assets outstanding must not be negative"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                auto slePseudoAccount = ac.view().peek(keylet::account(*(*sleVault)[sfAccount]));
-                if (!slePseudoAccount)
-                    return false;
-                (*slePseudoAccount)[sfBalance] = *(*slePseudoAccount)[sfBalance] - 10;
-                ac.view().update(slePseudoAccount);
-
-                // Move 10 drops to A4 to enforce total XRP balance
-                auto sleA4 = ac.view().peek(keylet::account(a4.id()));
-                if (!sleA4)
-                    return false;
-                (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
-                ac.view().update(sleA4);
-
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
-                                   sample.assetsAvailable = (kDropsPerXrp * -100).value();
-                                   sample.assetsTotal = (kDropsPerXrp * -200).value();
-                                   sample.sharesTotal = -1;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_SET, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"violation of vault immutable data"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                sleVault->setFieldIssue(sfAsset, STIssue{sfAsset, MPTIssue(MPTID(42))});
-                ac.view().update(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_SET, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp);
-
-        doInvariantCheck(
-            {"violation of vault immutable data"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                sleVault->setAccountID(sfAccount, a2.id());
-                ac.view().update(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_SET, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp);
-
-        doInvariantCheck(
-            {"violation of vault immutable data"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                (*sleVault)[sfShareMPTID] = MPTID(42);
-                ac.view().update(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_SET, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp);
-
-        doInvariantCheck(
-            {"vault transaction must not change loss unrealized",
-             "set must not change assets outstanding"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
-                                   sample.lossUnrealized = 13;
-                                   sample.assetsTotal = 20;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_SET, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"loss unrealized must not exceed the difference "
-             "between assets outstanding and available",
-             "vault transaction must not change loss unrealized"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 100, [&](Adjustments& sample) {
-                                   sample.lossUnrealized = 13;
-                               }));
-            },
-            XRPAmount{},
-            STTx{
-                ttVAULT_DEPOSIT, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        // A negative loss unrealized must trip the invariant. ttLOAN_MANAGE is
-        // allowed to change loss unrealized, so it isolates this check from the
-        // "must not change loss unrealized" invariant. Gated behind
-        // fixCleanup3_4_0 (see below).
-        doInvariantCheck(
-            {"loss unrealized must not be negative"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
-                                   sample.lossUnrealized = -1;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttLOAN_MANAGE, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        // Without fixCleanup3_4_0 the same state must NOT trip the invariant,
-        // preserving pre-amendment behavior (no fork risk).
-        doInvariantCheck(
-            makeEnv(defaultAmendments() - fixCleanup3_4_0),
-            {},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
-                                   sample.lossUnrealized = -1;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttLOAN_MANAGE, [](STObject& tx) {}},
-            {tesSUCCESS, tesSUCCESS},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"set assets outstanding must not exceed assets maximum"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
-                                   sample.assetsMaximum = 1;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_SET, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"assets maximum must not be negative"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
-                                   sample.assetsMaximum = -1;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_SET, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"set must not change shares outstanding",
-             "updated zero sized vault must have no assets outstanding",
-             "updated zero sized vault must have no assets available"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                ac.view().update(sleVault);
-                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
-                if (!sleShares)
-                    return false;
-                (*sleShares)[sfOutstandingAmount] = 0;
-                ac.view().update(sleShares);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_SET, [](STObject& tx) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"updated shares must not exceed maximum"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
-                if (!sleShares)
-                    return false;
-                (*sleShares)[sfMaximumAmount] = 10;
-                ac.view().update(sleShares);
-
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"updated shares must not exceed maximum"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
-
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
-                if (!sleShares)
-                    return false;
-                (*sleShares)[sfOutstandingAmount] = kMaxMpTokenAmount + 1;
-                ac.view().update(sleShares);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        testcase << "Vault create";
-        doInvariantCheck(
-            {
-                "created vault must be empty",
-                "updated zero sized vault must have no assets outstanding",
-                "create operation must not have updated a vault",
-            },
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                (*sleVault)[sfAssetsTotal] = 9;
-                ac.view().update(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CREATE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {
-                "created vault must be empty",
-                "updated zero sized vault must have no assets available",
-                "assets available must not be greater than assets outstanding",
-                "create operation must not have updated a vault",
-            },
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                (*sleVault)[sfAssetsAvailable] = 9;
-                ac.view().update(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CREATE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {
-                "created vault must be empty",
-                "loss unrealized must not exceed the difference between assets "
-                "outstanding and available",
-                "vault transaction must not change loss unrealized",
-                "create operation must not have updated a vault",
-            },
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                (*sleVault)[sfLossUnrealized] = 1;
-                ac.view().update(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CREATE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {
-                "created vault must be empty",
-                "create operation must not have updated a vault",
-            },
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
-                if (!sleShares)
-                    return false;
-                ac.view().update(sleVault);
-                (*sleShares)[sfOutstandingAmount] = 9;
-                ac.view().update(sleShares);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CREATE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {
-                "assets maximum must not be negative",
-                "create operation must not have updated a vault",
-            },
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                (*sleVault)[sfAssetsMaximum] = Number(-1);
-                ac.view().update(sleVault);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CREATE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"create operation must not have updated a vault",
-             "shares issuer and vault pseudo-account must be the same",
-             "shares issuer must be a pseudo-account",
-             "shares issuer pseudo-account must point back to the vault"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                auto sleVault = ac.view().peek(keylet);
-                if (!sleVault)
-                    return false;
-                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
-                if (!sleShares)
-                    return false;
-                ac.view().update(sleVault);
-                (*sleShares)[sfIssuer] = a1.id();
-                ac.view().update(sleShares);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CREATE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            [&](Account const& a1, Account const& a2, Env& env) {
-                Vault const vault{env};
-                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
-                env(tx);
-                return true;
-            });
-
-        doInvariantCheck(
-            {"vault created by a wrong transaction type", "account root created illegally"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                // The code below will create a valid vault with (almost) all
-                // the invariants holding. Except one: it is created by the
-                // wrong transaction type.
-                auto const sequence = ac.view().seq();
-                auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
-                auto sleVault = std::make_shared(vaultKeylet);
-                auto const vaultPage = ac.view().dirInsert(
-                    keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
-                sleVault->setFieldU64(sfOwnerNode, *vaultPage);
-
-                auto pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
-                // Create pseudo-account.
-                auto sleAccount = std::make_shared(keylet::account(pseudoId));
-                sleAccount->setAccountID(sfAccount, pseudoId);
-                sleAccount->setFieldAmount(sfBalance, STAmount{});
-                std::uint32_t const seqno =                             //
-                    ac.view().rules().enabled(featureSingleAssetVault)  //
-                    ? 0                                                 //
-                    : sequence;
-                sleAccount->setFieldU32(sfSequence, seqno);
-                sleAccount->setFieldU32(
-                    sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
-                sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
-                ac.view().insert(sleAccount);
-
-                auto const sharesMptId = makeMptID(sequence, pseudoId);
-                auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
-                auto sleShares = std::make_shared(sharesKeylet);
-                auto const sharesPage = ac.view().dirInsert(
-                    keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
-                sleShares->setFieldU64(sfOwnerNode, *sharesPage);
-
-                sleShares->at(sfFlags) = 0;
-                sleShares->at(sfIssuer) = pseudoId;
-                sleShares->at(sfOutstandingAmount) = 0;
-                sleShares->at(sfSequence) = sequence;
-
-                sleVault->at(sfAccount) = pseudoId;
-                sleVault->at(sfFlags) = 0;
-                sleVault->at(sfSequence) = sequence;
-                sleVault->at(sfOwner) = a1.id();
-                sleVault->at(sfAssetsTotal) = Number(0);
-                sleVault->at(sfAssetsAvailable) = Number(0);
-                sleVault->at(sfLossUnrealized) = Number(0);
-                sleVault->at(sfShareMPTID) = sharesMptId;
-                sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
-
-                ac.view().insert(sleVault);
-                ac.view().insert(sleShares);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_SET, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-
-        doInvariantCheck(
-            {"shares issuer and vault pseudo-account must be the same",
-             "shares issuer pseudo-account must point back to the vault"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sequence = ac.view().seq();
-                auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
-                auto sleVault = std::make_shared(vaultKeylet);
-                auto const vaultPage = ac.view().dirInsert(
-                    keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
-                sleVault->setFieldU64(sfOwnerNode, *vaultPage);
-
-                auto pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
-                // Create pseudo-account.
-                auto sleAccount = std::make_shared(keylet::account(pseudoId));
-                sleAccount->setAccountID(sfAccount, pseudoId);
-                sleAccount->setFieldAmount(sfBalance, STAmount{});
-                std::uint32_t const seqno =                             //
-                    ac.view().rules().enabled(featureSingleAssetVault)  //
-                    ? 0                                                 //
-                    : sequence;
-                sleAccount->setFieldU32(sfSequence, seqno);
-                sleAccount->setFieldU32(
-                    sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
-                // sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
-                // Setting wrong vault key
-                sleAccount->setFieldH256(sfVaultID, uint256(42));
-                ac.view().insert(sleAccount);
-
-                auto const sharesMptId = makeMptID(sequence, pseudoId);
-                auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
-                auto sleShares = std::make_shared(sharesKeylet);
-                auto const sharesPage = ac.view().dirInsert(
-                    keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
-                sleShares->setFieldU64(sfOwnerNode, *sharesPage);
-
-                sleShares->at(sfFlags) = 0;
-                sleShares->at(sfIssuer) = pseudoId;
-                sleShares->at(sfOutstandingAmount) = 0;
-                sleShares->at(sfSequence) = sequence;
-
-                // sleVault->at(sfAccount) = pseudoId;
-                // Setting wrong pseudo account ID
-                sleVault->at(sfAccount) = a2.id();
-                sleVault->at(sfFlags) = 0;
-                sleVault->at(sfSequence) = sequence;
-                sleVault->at(sfOwner) = a1.id();
-                sleVault->at(sfAssetsTotal) = Number(0);
-                sleVault->at(sfAssetsAvailable) = Number(0);
-                sleVault->at(sfLossUnrealized) = Number(0);
-                sleVault->at(sfShareMPTID) = sharesMptId;
-                sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
-
-                ac.view().insert(sleVault);
-                ac.view().insert(sleShares);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CREATE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-
-        doInvariantCheck(
-            {"shares issuer and vault pseudo-account must be the same", "shares issuer must exist"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sequence = ac.view().seq();
-                auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
-                auto sleVault = std::make_shared(vaultKeylet);
-                auto const vaultPage = ac.view().dirInsert(
-                    keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
-                sleVault->setFieldU64(sfOwnerNode, *vaultPage);
-
-                auto const sharesMptId = makeMptID(sequence, a2.id());
-                auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
-                auto sleShares = std::make_shared(sharesKeylet);
-                auto const sharesPage = ac.view().dirInsert(
-                    keylet::ownerDir(a2.id()), sharesKeylet, describeOwnerDir(a2.id()));
-                sleShares->setFieldU64(sfOwnerNode, *sharesPage);
-
-                sleShares->at(sfFlags) = 0;
-                // Setting wrong pseudo account ID
-                sleShares->at(sfIssuer) = AccountID(42);
-                sleShares->at(sfOutstandingAmount) = 0;
-                sleShares->at(sfSequence) = sequence;
-
-                sleVault->at(sfAccount) = a2.id();
-                sleVault->at(sfFlags) = 0;
-                sleVault->at(sfSequence) = sequence;
-                sleVault->at(sfOwner) = a1.id();
-                sleVault->at(sfAssetsTotal) = Number(0);
-                sleVault->at(sfAssetsAvailable) = Number(0);
-                sleVault->at(sfLossUnrealized) = Number(0);
-                sleVault->at(sfShareMPTID) = sharesMptId;
-                sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
-
-                ac.view().insert(sleVault);
-                ac.view().insert(sleShares);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CREATE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-
-        testcase << "Vault deposit";
-        doInvariantCheck(
-            {"deposit must change vault balance"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [](Adjustments& sample) {
-                                   sample.vaultAssets.reset();
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp);
-
-        doInvariantCheck(
-            {"deposit assets outstanding must not exceed assets maximum"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 200, [&](Adjustments& sample) {
-                                   sample.assetsMaximum = 1;
-                               }));
-            },
-            XRPAmount{},
-            STTx{
-                ttVAULT_DEPOSIT, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        // This really convoluted unit tests makes the zero balance on the
-        // depositor, by sending them the same amount as the transaction fee.
-        // The operation makes no sense, but the defensive check in
-        // ValidVault::finalize is otherwise impossible to trigger.
-        doInvariantCheck(
-            {"deposit must increase vault balance", "deposit must change depositor balance"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-
-                // Move 10 drops to A4 to enforce total XRP balance
-                auto sleA4 = ac.view().peek(keylet::account(a4.id()));
-                if (!sleA4)
-                    return false;
-                (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
-                ac.view().update(sleA4);
-
-                return kAdjust(ac.view(), keylet, kArgs(a3.id(), -10, [&](Adjustments& sample) {
-                                   sample.accountAssets->amount = -100;
-                               }));
-            },
-            XRPAmount{100},
-            STTx{
-                ttVAULT_DEPOSIT,
-                [&](STObject& tx) {
-                    tx[sfFee] = XRPAmount(100);
-                    tx[sfAccount] = a3.id();
-                }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp);
-
-        doInvariantCheck(
-            {"deposit must increase vault balance",
-             "deposit must decrease depositor balance",
-             "deposit must change vault and depositor balance by equal amount",
-             "deposit and assets outstanding must add up",
-             "deposit and assets available must add up"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-
-                // Move 10 drops from A2 to A3 to enforce total XRP balance
-                auto sleA3 = ac.view().peek(keylet::account(a3.id()));
-                if (!sleA3)
-                    return false;
-                (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10;
-                ac.view().update(sleA3);
-
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
-                                   sample.vaultAssets = -20;
-                                   sample.accountAssets->amount = 10;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"deposit must change depositor balance"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-
-                // Move 10 drops from A3 to vault to enforce total XRP balance
-                auto sleA3 = ac.view().peek(keylet::account(a3.id()));
-                if (!sleA3)
-                    return false;
-                (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 10;
-                ac.view().update(sleA3);
-
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
-                                   sample.accountAssets->amount = 0;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"deposit must change depositor shares"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
-                                   sample.accountShares.reset();
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"deposit must change vault shares"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments& sample) {
-                                   sample.sharesTotal = 0;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"deposit must increase depositor shares",
-             "deposit must change depositor and vault shares by equal amount",
-             "deposit must not change vault balance by more than deposited "
-             "amount"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
-                                   sample.accountShares->amount = -5;
-                                   sample.sharesTotal = -10;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(5); }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"deposit and assets outstanding must add up"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto sleA3 = ac.view().peek(keylet::account(a3.id()));
-                (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
-                ac.view().update(sleA3);
-
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
-                                   sample.assetsTotal = 11;
-                               }));
-            },
-            XRPAmount{2000},
-            STTx{
-                ttVAULT_DEPOSIT,
-                [&](STObject& tx) {
-                    tx[sfAmount] = XRPAmount(10);
-                    tx[sfDelegate] = a3.id();
-                    tx[sfFee] = XRPAmount(2000);
-                }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"deposit and assets outstanding must add up",
-             "deposit and assets available must add up"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
-                                   sample.assetsTotal = 7;
-                                   sample.assetsAvailable = 7;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        testcase << "Vault withdrawal";
-        doInvariantCheck(
-            {"withdrawal must change vault balance"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [](Adjustments& sample) {
-                                   sample.vaultAssets.reset();
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp);
-
-        // Almost identical to the really convoluted test for deposit, where the
-        // depositor spends only the transaction fee. In case of withdrawal,
-        // this test is almost the same as normal withdrawal where the
-        // sfDestination would have been A4, but has been omitted.
-        doInvariantCheck(
-            {"withdrawal must change one destination balance"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-
-                // Move 10 drops to A4 to enforce total XRP balance
-                auto sleA4 = ac.view().peek(keylet::account(a4.id()));
-                if (!sleA4)
-                    return false;
-                (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
-                ac.view().update(sleA4);
-
-                return kAdjust(ac.view(), keylet, kArgs(a3.id(), -10, [&](Adjustments& sample) {
-                                   sample.accountAssets->amount = -100;
-                               }));
-            },
-            XRPAmount{100},
-            STTx{
-                ttVAULT_WITHDRAW,
-                [&](STObject& tx) {
-                    tx[sfFee] = XRPAmount(100);
-                    tx[sfAccount] = a3.id();
-                    // This commented out line causes the invariant violation.
-                    // tx[sfDestination] = A4.id();
-                }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp);
-
-        doInvariantCheck(
-            {
-                "withdrawal must change vault and destination balance by equal amount",
-                "withdrawal must decrease vault balance",
-                "withdrawal must increase destination balance",
-                "withdrawal and assets outstanding must add up",
-                "withdrawal and assets available must add up",
-            },
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-
-                // Move 10 drops from A2 to A3 to enforce total XRP balance
-                auto sleA3 = ac.view().peek(keylet::account(a3.id()));
-                if (!sleA3)
-                    return false;
-                (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10;
-                ac.view().update(sleA3);
-
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
-                                   sample.vaultAssets = 10;
-                                   sample.accountAssets->amount = -20;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"withdrawal must change one destination balance"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                if (!kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
-                                 *sample.vaultAssets -= 5;
-                             })))
-                    return false;
-                auto sleA3 = ac.view().peek(keylet::account(a3.id()));
-                if (!sleA3)
-                    return false;
-                (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 5;
-                ac.view().update(sleA3);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttVAULT_WITHDRAW, [&](STObject& tx) { tx.setAccountID(sfDestination, a3.id()); }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"withdrawal must change depositor shares"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
-                                   sample.accountShares.reset();
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"withdrawal must change vault shares"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [](Adjustments& sample) {
-                                   sample.sharesTotal = 0;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"withdrawal must decrease depositor shares",
-             "withdrawal must change depositor and vault shares by equal "
-             "amount"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
-                                   sample.accountShares->amount = 5;
-                                   sample.sharesTotal = 10;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"withdrawal and assets outstanding must add up",
-             "withdrawal and assets available must add up"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
-                                   sample.assetsTotal = -15;
-                                   sample.assetsAvailable = -15;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        doInvariantCheck(
-            {"withdrawal and assets outstanding must add up"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto sleA3 = ac.view().peek(keylet::account(a3.id()));
-                (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
-                ac.view().update(sleA3);
-
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
-                                   sample.assetsTotal = -7;
-                               }));
-            },
-            XRPAmount{2000},
-            STTx{
-                ttVAULT_WITHDRAW,
-                [&](STObject& tx) {
-                    tx[sfAmount] = XRPAmount(10);
-                    tx[sfDelegate] = a3.id();
-                    tx[sfFee] = XRPAmount(2000);
-                }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp,
-            TxAccount::A2);
-
-        auto const precloseMpt = [&](Account const& a1, Account const& a2, Env& env) -> bool {
-            env.fund(XRP(1000), a3, a4);
-
-            // Create MPT asset
-            {
-                json::Value jv;
-                jv[sfAccount] = a3.human();
-                jv[sfTransactionType] = jss::MPTokenIssuanceCreate;
-                jv[sfFlags] = tfMPTCanTransfer;
-                env(jv);
-                env.close();
-            }
-
-            auto const mptID = makeMptID(env.seq(a3) - 1, a3);
-            Asset const asset = MPTIssue(mptID);
-            // Authorize A1 A2 A4
-            {
-                json::Value jv;
-                jv[sfAccount] = a1.human();
-                jv[sfTransactionType] = jss::MPTokenAuthorize;
-                jv[sfMPTokenIssuanceID] = to_string(mptID);
-                env(jv);
-                jv[sfAccount] = a2.human();
-                env(jv);
-                jv[sfAccount] = a4.human();
-                env(jv);
-
-                env.close();
-            }
-            // Send tokens to A1 A2 A4
-            {
-                env(pay(a3, a1, asset(1000)));
-                env(pay(a3, a2, asset(1000)));
-                env(pay(a3, a4, asset(1000)));
-                env.close();
-            }
-
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
-            env(tx);
-            env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = asset(10)}));
-            env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = asset(10)}));
-            env(vault.deposit({.depositor = a4, .id = keylet.key, .amount = asset(10)}));
-            return true;
-        };
-
-        doInvariantCheck(
-            {"withdrawal must decrease depositor shares",
-             "withdrawal must change depositor and vault shares by equal "
-             "amount"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet =
-                    keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
-                                   sample.accountShares->amount = 5;
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_WITHDRAW, [&](STObject& tx) { tx[sfAccount] = a3.id(); }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseMpt,
-            TxAccount::A2);
-
-        testcase << "Vault clawback";
-        doInvariantCheck(
-            {"clawback must change vault balance"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet =
-                    keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -1, [&](Adjustments& sample) {
-                                   sample.vaultAssets.reset();
-                               }));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CLAWBACK, [&](STObject& tx) { tx[sfAccount] = a3.id(); }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseMpt);
-
-        // Not the same as below check: attempt to clawback XRP
-        doInvariantCheck(
-            {"clawback may only be performed by the asset issuer"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {}));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CLAWBACK, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseXrp);
-
-        // Not the same as above check: attempt to clawback MPT by bad account
-        doInvariantCheck(
-            {"clawback may only be performed by the asset issuer"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet =
-                    keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
-                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {}));
-            },
-            XRPAmount{},
-            STTx{ttVAULT_CLAWBACK, [&](STObject& tx) { tx[sfAccount] = a4.id(); }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseMpt);
-
-        doInvariantCheck(
-            {"clawback must decrease vault balance",
-             "clawback must decrease holder shares",
-             "clawback must change vault shares"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet =
-                    keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
-                return kAdjust(ac.view(), keylet, kArgs(a4.id(), 10, [&](Adjustments& sample) {
-                                   sample.sharesTotal = 0;
-                               }));
-            },
-            XRPAmount{},
-            STTx{
-                ttVAULT_CLAWBACK,
-                [&](STObject& tx) {
-                    tx[sfAccount] = a3.id();
-                    tx[sfHolder] = a4.id();
-                }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseMpt);
-
-        doInvariantCheck(
-            {"clawback must change holder shares"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet =
-                    keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
-                return kAdjust(ac.view(), keylet, kArgs(a4.id(), -10, [&](Adjustments& sample) {
-                                   sample.accountShares.reset();
-                               }));
-            },
-            XRPAmount{},
-            STTx{
-                ttVAULT_CLAWBACK,
-                [&](STObject& tx) {
-                    tx[sfAccount] = a3.id();
-                    tx[sfHolder] = a4.id();
-                }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseMpt);
-
-        doInvariantCheck(
-            {"clawback must change holder and vault shares by equal amount",
-             "clawback and assets outstanding must add up",
-             "clawback and assets available must add up"},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const keylet =
-                    keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
-                return kAdjust(ac.view(), keylet, kArgs(a4.id(), -10, [&](Adjustments& sample) {
-                                   sample.accountShares->amount = -8;
-                                   sample.assetsTotal = -7;
-                                   sample.assetsAvailable = -7;
-                               }));
-            },
-            XRPAmount{},
-            STTx{
-                ttVAULT_CLAWBACK,
-                [&](STObject& tx) {
-                    tx[sfAccount] = a3.id();
-                    tx[sfHolder] = a4.id();
-                }},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseMpt);
-    }
-
-    void
-    testMPT()
-    {
-        using namespace test::jtx;
-        testcase << "MPT";
-
-        MPTIssue const nonCanonicalMPTIssue{makeMptID(1, AccountID(0x4985601))};
-        auto const nonCanonicalMPTAmount = [&](SField const& field) {
-            return STAmount{
-                field,
-                nonCanonicalMPTIssue,
-                kMaxMpTokenAmount + std::uint64_t{1},
-                0,
-                false,
-                STAmount::Unchecked{}};
-        };
-        auto const negativeMPTAmount = [&](SField const& field) {
-            return STAmount{field, nonCanonicalMPTIssue, 2, 0, true, STAmount::Unchecked{}};
-        };
-        auto const nonCanonicalMPTPayment = [&]() {
-            return STTx{ttPAYMENT, [&](STObject& tx) {
-                            tx.setFieldAmount(sfAmount, nonCanonicalMPTAmount(sfAmount));
-                        }};
-        };
-
-        doInvariantCheck(
-            makeEnv(defaultAmendments() - fixCleanup3_2_0),
-            {},
-            [](Account const&, Account const&, ApplyContext&) { return true; },
-            XRPAmount{},
-            nonCanonicalMPTPayment(),
-            {tesSUCCESS, tesSUCCESS});
-
-        doInvariantCheck(
-            {{"ledger entry contains non-canonical MPT or XRP amount"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-
-                auto sleNew = std::make_shared(
-                    keylet::check(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
-                sleNew->setAccountID(sfAccount, a1.id());
-                sleNew->setAccountID(sfDestination, a2.id());
-                sleNew->setFieldAmount(sfSendMax, nonCanonicalMPTAmount(sfSendMax));
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        doInvariantCheck(
-            {{"ledger entry contains non-canonical MPT or XRP amount"}},
-            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-
-                auto sleNew = std::make_shared(
-                    keylet::check(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
-                sleNew->setAccountID(sfAccount, a1.id());
-                sleNew->setAccountID(sfDestination, a2.id());
-                sleNew->setFieldAmount(sfSendMax, negativeMPTAmount(sfSendMax));
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        // MPT OutstandingAmount > MaximumAmount
-        doInvariantCheck(
-            {{"OutstandingAmount overflow"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                // mptissuance outstanding is negative
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-
-                MPTIssue const mpt{makeMptID(sle->getFieldU32(sfSequence), a1)};
-                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
-                sleNew->setFieldU64(sfOutstandingAmount, 110);
-                sleNew->setFieldU64(sfMaximumAmount, 100);
-                ac.view().insert(sleNew);
-                return true;
-            });
-
-        // MPTToken amount doesn't add up to OutstandingAmount
-        doInvariantCheck(
-            {{"invalid OutstandingAmount balance"}},
-            [](Account const& a1, Account const& a2, ApplyContext& ac) {
-                // mptissuance outstanding is negative
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-
-                MPTIssue const mpt{makeMptID(sle->getFieldU32(sfSequence), a1)};
-                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
-                sleNew->setFieldU64(sfOutstandingAmount, 100);
-                sleNew->setFieldU64(sfMaximumAmount, 100);
-                ac.view().insert(sleNew);
-
-                sleNew = std::make_shared(keylet::mptoken(mpt.getMptID(), a2));
-                sleNew->setFieldU64(sfMPTAmount, 90);
-                ac.view().insert(sleNew);
-
-                return true;
-            });
-
-        // Overflow/Invalid balance on payment
-        auto testPayment = [&](std::string const& log, auto&& update) {
-            MPTID id;
-            doInvariantCheck(
-                {{log}},
-                [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    return update(id, ac, a1);
-                },
-                XRPAmount{},
-                STTx{ttPAYMENT, [](STObject& tx) {}},
-                {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-                [&](Account const& a1, Account const& a2, Env& env) {
-                    Account const gw("gw");
-                    env.fund(XRP(1'000), gw);
-                    MPTTester const mpt(
-                        {.env = env, .issuer = gw, .holders = {a1}, .pay = 100, .maxAmt = 100});
-                    id = mpt.issuanceID();
-                    return true;
-                });
-        };
-        testPayment(
-            "invalid OutstandingAmount balance",
-            [&](MPTID const& id, ApplyContext& ac, Account const& a1) {
-                auto sle = ac.view().peek(keylet::mptoken(id, a1));
-                if (!sle)
-                    return false;
-                sle->setFieldU64(sfMPTAmount, 101);
-                ac.view().update(sle);
-                return true;
-            });
-        testPayment(
-            "OutstandingAmount overflow", [&](MPTID const& id, ApplyContext& ac, Account const&) {
-                auto sle = ac.view().peek(keylet::mptokenIssuance(id));
-                if (!sle)
-                    return false;
-                sle->setFieldU64(sfOutstandingAmount, 101);
-                ac.view().update(sle);
-                return true;
-            });
-
-        // Invalid IOU clawback delta must fail once MPTokensV2 enforces before/after validation.
-        {
-            Env env(*this, defaultAmendments());
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            auto const usd = issuer["USD"];
-            env.trust(usd(100), holder);
-            env(pay(issuer, holder, usd(100)));
-            env.close();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {{"Invariant failed: trustline clawback balance change is invalid"}},
-                [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
-                    auto sle =
-                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
-                    if (!sle)
-                        return false;
-
-                    STAmount balance{Issue{usd.currency, issuer.id()}, 80};
-                    if (holder.id() > issuer.id())
-                        balance.negate();
-                    sle->setFieldAmount(sfBalance, balance);
-                    ac.view().update(sle);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
-                    }},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-        }
-
-        // Full IOU clawback may delete the trustline; missing after-SLE represents zero balance.
-        {
-            Env env(*this, defaultAmendments());
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            auto const usd = issuer["USD"];
-            env.trust(usd(100), holder);
-            env(pay(issuer, holder, usd(100)));
-            env.close();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {},
-                [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
-                    auto const sle =
-                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
-                    if (!sle)
-                        return false;
-
-                    ac.view().erase(sle);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 100};
-                    }},
-                {tesSUCCESS, tesSUCCESS});
-        }
-
-        // Pre-MPTokensV2 invalid IOU clawback delta logs but remains non-enforcing.
-        {
-            Env env(*this, defaultAmendments() - featureMPTokensV2);
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            auto const usd = issuer["USD"];
-            env.trust(usd(100), holder);
-            env(pay(issuer, holder, usd(100)));
-            env.close();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {{"Invariant failed: trustline clawback balance change is invalid"}},
-                [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
-                    auto sle =
-                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
-                    if (!sle)
-                        return false;
-
-                    STAmount balance{Issue{usd.currency, issuer.id()}, 80};
-                    if (holder.id() > issuer.id())
-                        balance.negate();
-                    sle->setFieldAmount(sfBalance, balance);
-                    ac.view().update(sle);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
-                    }},
-                {tesSUCCESS, tesSUCCESS});
-        }
-
-        // Invalid MPT clawback delta must fail when raw MPToken debit mismatches sfAmount.
-        {
-            Env env(*this, defaultAmendments());
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            MPTTester const mpt(
-                {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
-            auto const id = mpt.issuanceID();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {{"Invariant failed: MPT clawback balance change is invalid"}},
-                [id](Account const& holder, Account const&, ApplyContext& ac) {
-                    auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
-                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
-                    if (!sleToken || !sleIssuance)
-                        return false;
-
-                    sleToken->setFieldU64(sfMPTAmount, 80);
-                    sleIssuance->setFieldU64(sfOutstandingAmount, 80);
-                    ac.view().update(sleToken);
-                    ac.view().update(sleIssuance);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfHolder] = holder.id();
-                        tx[sfAmount] = STAmount{MPTIssue{id}, 10};
-                    }},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-        }
-
-        // A clawback that mutates both IOU and MPT entries must fail under MPTokensV2.
-        {
-            Env env(*this, defaultAmendments());
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            auto const usd = issuer["USD"];
-            env.trust(usd(100), holder);
-            env(pay(issuer, holder, usd(100)));
-            MPTTester const mpt(
-                {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
-            auto const id = mpt.issuanceID();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {{"Invariant failed: trustline and MPToken both changed"}},
-                [issuer, usd, id](Account const& holder, Account const&, ApplyContext& ac) {
-                    auto const sleLine =
-                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
-                    auto const sleToken = ac.view().peek(keylet::mptoken(id, holder.id()));
-                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
-                    if (!sleLine || !sleToken || !sleIssuance)
-                        return false;
-
-                    STAmount balance{Issue{usd.currency, issuer.id()}, 90};
-                    if (holder.id() > issuer.id())
-                        balance.negate();
-                    sleLine->setFieldAmount(sfBalance, balance);
-                    sleToken->setFieldU64(sfMPTAmount, 90);
-                    sleIssuance->setFieldU64(sfOutstandingAmount, 90);
-                    ac.view().update(sleLine);
-                    ac.view().update(sleToken);
-                    ac.view().update(sleIssuance);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfHolder] = holder.id();
-                        tx[sfAmount] = STAmount{MPTIssue{id}, 10};
-                    }},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-        }
-
-        // Clawback that modifies a trustline other than the one implied by the
-        // tx amount: clawbackTrustLineBalanceInHolderTerms returns nullopt for
-        // the mismatched line.
-        {
-            Env env(*this, defaultAmendments());
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            auto const usd = issuer["USD"];
-            auto const eur = issuer["EUR"];
-            env.trust(eur(100), holder);
-            env(pay(issuer, holder, eur(100)));
-            env.close();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {{"Invariant failed: trustline clawback changed the wrong line"}},
-                [issuer, eur](Account const& holder, Account const&, ApplyContext& ac) {
-                    auto sle =
-                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), eur.currency));
-                    if (!sle)
-                        return false;
-                    STAmount balance{Issue{eur.currency, issuer.id()}, 90};
-                    if (holder.id() > issuer.id())
-                        balance.negate();
-                    sle->setFieldAmount(sfBalance, balance);
-                    ac.view().update(sle);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
-                    }},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-        }
-
-        // Clawback leaving the holder's balance negative.
-        {
-            Env env(*this, defaultAmendments());
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            auto const usd = issuer["USD"];
-            env.trust(usd(100), holder);
-            env(pay(issuer, holder, usd(100)));
-            env.close();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {{"Invariant failed: trustline or MPT balance is negative"}},
-                [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
-                    auto sle =
-                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
-                    if (!sle)
-                        return false;
-                    // Make the holder's balance negative from their perspective.
-                    STAmount balance{Issue{usd.currency, issuer.id()}, 80};
-                    if (holder.id() < issuer.id())
-                        balance.negate();
-                    sle->setFieldAmount(sfBalance, balance);
-                    ac.view().update(sle);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
-                    }},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-        }
-
-        // IOU-amount clawback while only an MPToken changed: no trustline was
-        // recorded, so iou_.before is empty.
-        {
-            Env env(*this, defaultAmendments());
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            auto const usd = issuer["USD"];
-            MPTTester const mpt(
-                {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
-            auto const id = mpt.issuanceID();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {{"Invariant failed: trustline clawback changed the wrong line"}},
-                [id](Account const& holder, Account const&, ApplyContext& ac) {
-                    auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
-                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
-                    if (!sleToken || !sleIssuance)
-                        return false;
-                    sleToken->setFieldU64(sfMPTAmount, 90);
-                    sleIssuance->setFieldU64(sfOutstandingAmount, 90);
-                    ac.view().update(sleToken);
-                    ac.view().update(sleIssuance);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
-                    }},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-        }
-
-        // Valid trustline change but a zero clawback amount.
-        {
-            Env env(*this, defaultAmendments());
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            auto const usd = issuer["USD"];
-            env.trust(usd(100), holder);
-            env(pay(issuer, holder, usd(100)));
-            env.close();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {{"Invariant failed: trustline clawback amount is invalid"}},
-                [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
-                    auto sle =
-                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
-                    if (!sle)
-                        return false;
-                    STAmount balance{Issue{usd.currency, issuer.id()}, 90};
-                    if (holder.id() > issuer.id())
-                        balance.negate();
-                    sle->setFieldAmount(sfBalance, balance);
-                    ac.view().update(sle);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 0};
-                    }},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-        }
-
-        // MPT clawback tx missing the Holder field.
-        {
-            Env env(*this, defaultAmendments());
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            MPTTester const mpt(
-                {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
-            auto const id = mpt.issuanceID();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {{"Invariant failed: MPT clawback missing holder"}},
-                [id](Account const& holder, Account const&, ApplyContext& ac) {
-                    auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
-                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
-                    if (!sleToken || !sleIssuance)
-                        return false;
-                    sleToken->setFieldU64(sfMPTAmount, 90);
-                    sleIssuance->setFieldU64(sfOutstandingAmount, 90);
-                    ac.view().update(sleToken);
-                    ac.view().update(sleIssuance);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfAmount] = STAmount{MPTIssue{id}, 10};
-                    }},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-        }
-
-        // MPT clawback where the holder's MPToken was deleted (after is empty).
-        {
-            Env env(*this, defaultAmendments());
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            MPTTester const mpt(
-                {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
-            auto const id = mpt.issuanceID();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {{"Invariant failed: MPT clawback token is missing"}},
-                [id](Account const& holder, Account const&, ApplyContext& ac) {
-                    auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
-                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
-                    if (!sleToken || !sleIssuance)
-                        return false;
-                    // Keep the issuance consistent after removing the token.
-                    sleIssuance->setFieldU64(sfOutstandingAmount, 0);
-                    ac.view().update(sleIssuance);
-                    ac.view().erase(sleToken);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfHolder] = holder.id();
-                        tx[sfAmount] = STAmount{MPTIssue{id}, 10};
-                    }},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-        }
-
-        // MPT clawback that changed a different holder's MPToken.
-        {
-            Env env(*this, defaultAmendments());
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            MPTTester const mpt(
-                {.env = env,
-                 .issuer = issuer,
-                 .holders = {holder, other},
-                 .pay = 100,
-                 .maxAmt = 200});
-            auto const id = mpt.issuanceID();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {{"Invariant failed: MPT clawback changed the wrong token"}},
-                [id](Account const&, Account const& other, ApplyContext& ac) {
-                    auto const sleToken = ac.view().peek(keylet::mptoken(id, other));
-                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
-                    if (!sleToken || !sleIssuance)
-                        return false;
-                    sleToken->setFieldU64(sfMPTAmount, 90);
-                    sleIssuance->setFieldU64(sfOutstandingAmount, 190);
-                    ac.view().update(sleToken);
-                    ac.view().update(sleIssuance);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfHolder] = holder.id();
-                        tx[sfAmount] = STAmount{MPTIssue{id}, 10};
-                    }},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-        }
-
-        // Valid MPToken change but a zero MPT clawback amount.
-        {
-            Env env(*this, defaultAmendments());
-            Account const issuer{"issuer"};
-            Account const holder{"holder"};
-            Account const other{"other"};
-            env.fund(XRP(1'000), issuer, holder, other);
-            MPTTester const mpt(
-                {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
-            auto const id = mpt.issuanceID();
-
-            doInvariantCheck(
-                std::move(env),
-                holder,
-                other,
-                {{"Invariant failed: MPT clawback amount is invalid"}},
-                [id](Account const& holder, Account const&, ApplyContext& ac) {
-                    auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
-                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
-                    if (!sleToken || !sleIssuance)
-                        return false;
-                    sleToken->setFieldU64(sfMPTAmount, 90);
-                    sleIssuance->setFieldU64(sfOutstandingAmount, 90);
-                    ac.view().update(sleToken);
-                    ac.view().update(sleIssuance);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{
-                    ttCLAWBACK,
-                    [&](STObject& tx) {
-                        tx[sfAccount] = issuer.id();
-                        tx[sfHolder] = holder.id();
-                        tx[sfAmount] = STAmount{MPTIssue{id}, 0};
-                    }},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-        }
-
-        // More MPTokens created than expected
-        std::array, 4> const tests = {
-            std::make_pair(ttAMM_WITHDRAW, 2),
-            std::make_pair(ttAMM_CLAWBACK, 2),
-            std::make_pair(ttAMM_CREATE, 3),
-            std::make_pair(ttCHECK_CASH, 2)};
-        for (auto const& [tx, nTokens] : tests)
-        {
-            doInvariantCheck(
-                {{std::string("MPToken created for the MPT issuer")}},
-                [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    auto const sle = ac.view().peek(keylet::account(a1.id()));
-                    if (!sle)
-                        return false;
-
-                    auto seq = sle->getFieldU32(sfSequence);
-                    for (int i = 0; i < nTokens; ++i)
-                    {
-                        MPTIssue const mpt{makeMptID(seq + i, a1)};
-                        auto sleNew =
-                            std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
-                        ac.view().insert(sleNew);
-
-                        sleNew = std::make_shared(keylet::mptoken(mpt.getMptID(), a2));
-                        ac.view().insert(sleNew);
-                    }
-
-                    return true;
-                },
-                XRPAmount{},
-                STTx{tx, [](STObject& tx) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
-        }
-
-        // More MPTokens deleted than expected
-        for (auto const& tx : {ttAMM_WITHDRAW, ttAMM_CLAWBACK})
-        {
-            MPTID id;
-            Account const a3("A3");
-            doInvariantCheck(
-                {{"MPT authorize  succeeded but created/deleted bad number of mptokens"}},
-                [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    for (auto const& a : {a1, a2, a3})
-                    {
-                        auto sle = ac.view().peek(keylet::mptoken(id, a));
-                        if (!sle)
-                            return false;
-                        ac.view().erase(sle);
-                    }
-                    return true;
-                },
-                XRPAmount{},
-                STTx{tx, [](STObject& tx) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                [&](Account const& a1, Account const& a2, Env& env) {
-                    Account const gw("gw");
-                    env.fund(XRP(1'000), gw, a3);
-                    MPTTester const mpt({.env = env, .issuer = gw, .holders = {a1, a2, a3}});
-                    id = mpt.issuanceID();
-                    return true;
-                });
-        }
-
-        // sfReferenceHolding can only be set on creation by VaultCreate. A
-        // non-VaultCreate transaction that creates an MPTokenIssuance with
-        // sfReferenceHolding present must trip the invariant.
-        doInvariantCheck(
-            {{"sfReferenceHolding set on a new MPTokenIssuance by a "
-              "non-VaultCreate transaction"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                auto const sleAcct = ac.view().peek(keylet::account(a1.id()));
-                if (!sleAcct)
-                    return false;
-                MPTIssue const mpt{makeMptID(sleAcct->getFieldU32(sfSequence), a1)};
-                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
-                sleNew->setFieldH256(sfReferenceHolding, uint256{1});
-                ac.view().insert(sleNew);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_SET, [](STObject&) {}});
-
-        // sfReferenceHolding is immutable: changing the field on an
-        // existing MPTokenIssuance must trip the invariant. Set up a real
-        // vault via preclose (so the share issuance carries
-        // sfReferenceHolding), then mutate it in precheck to produce a
-        // before/after pair.
-        {
-            uint256 vaultKey;
-            doInvariantCheck(
-                {{"sfReferenceHolding was modified on an existing "
-                  "MPTokenIssuance"}},
-                [&](Account const&, Account const&, ApplyContext& ac) {
-                    auto const sleVault = ac.view().peek(keylet::vault(vaultKey));
-                    if (!sleVault)
-                        return false;
-                    auto sleIssuance =
-                        ac.view().peek(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
-                    if (!sleIssuance)
-                        return false;
-                    sleIssuance->setFieldH256(sfReferenceHolding, uint256{2});
-                    ac.view().update(sleIssuance);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttACCOUNT_SET, [](STObject&) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                [&](Account const& a1, Account const&, Env& env) {
-                    Account const issuer{"issuer"};
-                    env.fund(XRP(10'000), issuer);
-                    env.close();
-                    MPTTester mptt{env, issuer, kMptInitNoFund};
-                    mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
-                    PrettyAsset const asset = mptt.issuanceID();
-                    mptt.authorize({.account = a1});
-                    env.close();
-
-                    Vault const vault{env};
-                    auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
-                    env(tx);
-                    env.close();
-                    vaultKey = keylet.key;
-                    return true;
-                });
-        }
-
-        // A vault pseudo-account's MPToken cannot be deleted by anything
-        // other than a VaultDelete transaction. Set up a vault, then have
-        // an arbitrary tx erase the pseudo's MPToken in precheck.
-        {
-            uint256 vaultKey;
-            doInvariantCheck(
-                {{"vault pseudo-account holding deleted by a "
-                  "non-VaultDelete transaction"}},
-                [&](Account const&, Account const&, ApplyContext& ac) {
-                    auto const sleVault = ac.view().peek(keylet::vault(vaultKey));
-                    if (!sleVault)
-                        return false;
-                    auto const sleIssuance =
-                        ac.view().peek(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
-                    if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
-                        return false;
-                    auto sleHolding = ac.view().peek(
-                        keylet::unchecked(sleIssuance->getFieldH256(sfReferenceHolding)));
-                    if (!sleHolding)
-                        return false;
-                    ac.view().erase(sleHolding);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttACCOUNT_SET, [](STObject&) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                [&](Account const& a1, Account const&, Env& env) {
-                    Account const issuer{"issuer"};
-                    env.fund(XRP(10'000), issuer);
-                    env.close();
-                    MPTTester mptt{env, issuer, kMptInitNoFund};
-                    mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
-                    PrettyAsset const asset = mptt.issuanceID();
-                    mptt.authorize({.account = a1});
-                    env.close();
-
-                    Vault const vault{env};
-                    auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
-                    env(tx);
-                    env.close();
-                    vaultKey = keylet.key;
-                    return true;
-                });
-        }
-
-        // Invalid transfer
-        std::array, 3> const invalidTransferTests = {
-            std::make_pair(ttAMM_WITHDRAW, false),
-            std::make_pair(ttPAYMENT, false),
-            std::make_pair(ttPAYMENT, true)};
-        for (auto const enabled : {true, false})
-        {
-            for (auto const& [tx, crossCurrencyPayment] : invalidTransferTests)
-            {
-                for (auto const flag :
-                     {static_cast(lsfMPTLocked),
-                      ~lsfMPTCanTransfer,
-                      ~lsfMPTCanTrade,
-                      0u})
-                {
-                    MPTID id{};
-                    auto const isSuccess = !enabled || flag == 0 ||
-                        (tx == ttPAYMENT && !crossCurrencyPayment && (flag == ~lsfMPTCanTrade)) ||
-                        (tx == ttAMM_WITHDRAW &&
-                         (flag == ~lsfMPTCanTrade || flag == ~lsfMPTCanTransfer));
-                    std::pair const error = isSuccess
-                        ? std::make_pair(TER(tesSUCCESS), TER(tesSUCCESS))
-                        : std::make_pair(TER(tecINVARIANT_FAILED), TER(tefINVARIANT_FAILED));
-                    doInvariantCheck(
-                        {{isSuccess ? "" : "invalid MPToken transfer between holders"}},
-                        [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                            auto update = [&](AccountID const& a, std::uint64_t v) {
-                                auto sle = ac.view().peek(keylet::mptoken(id, a));
-                                if (!sle)
-                                    return false;
-                                sle->at(sfMPTAmount) = v;
-                                ac.view().update(sle);
-                                return true;
-                            };
-                            auto issuanceSle = ac.view().peek(keylet::mptokenIssuance(id));
-                            if (!issuanceSle)
-                                return false;
-                            auto const flags = issuanceSle->at(sfFlags);
-                            if (flag == lsfMPTLocked)
-                            {
-                                issuanceSle->at(sfFlags) = flags | lsfMPTLocked;
-                            }
-                            else if (flag != 0u)
-                            {
-                                issuanceSle->at(sfFlags) = flags & flag;
-                            }
-                            issuanceSle->at(sfOutstandingAmount) = 200;
-                            ac.view().update(issuanceSle);
-                            return update(a1, 101) && update(a2, 99);
-                        },
-                        XRPAmount{},
-                        STTx{
-                            tx,
-                            [&](STObject& tx) {
-                                if (crossCurrencyPayment)
-                                {
-                                    tx.setFieldAmount(
-                                        sfSendMax, STAmount(MPTAmount{100}, MPTIssue{id}));
-                                }
-                            }},
-                        {error.first, error.second},
-                        [&](Account const& a1, Account const& a2, Env& env) {
-                            Account const gw("gw");
-                            env.fund(XRP(1'000), gw);
-                            MPTTester const usd(
-                                {.env = env, .issuer = gw, .holders = {a1, a2}, .pay = 100});
-                            id = usd.issuanceID();
-                            if (!enabled)
-                            {
-                                env.disableFeature(featureMPTokensV2);
-                            }
-                            return true;
-                        });
-                }
-            }
-        }
-
-        // Vault-share freeze invariant: isVaultPseudoAccountFrozen descends
-        // through sfReferenceHolding to test the vault's underlying asset for
-        // each changed holder.
-        {
-            Account const gw{"gw"};
-            MPTID shareID{};
-
-            // Vault setup: a1 and a2 both deposit IOU and hold vault shares.
-            auto const setupVault = [&](Account const& a1,
-                                        Account const& a2,
-                                        Env& env) -> std::tuple {
-                env.fund(XRP(1'000), gw);
-                env.trust(gw["IOU"](10'000), a1);
-                env.trust(gw["IOU"](10'000), a2);
-                env.close();
-                env(pay(gw, a1, gw["IOU"](500)));
-                env(pay(gw, a2, gw["IOU"](500)));
-                env.close();
-
-                Vault const vault{env};
-                auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw["IOU"]});
-                env(createTx);
-                env.close();
-                env(vault.deposit(
-                    {.depositor = a1, .id = vaultKeylet.key, .amount = gw["IOU"](100)}));
-                env(vault.deposit(
-                    {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](100)}));
-                env.close();
-
-                return {env.le(vaultKeylet)->at(sfShareMPTID), env.le(vaultKeylet)->at(sfAccount)};
-            };
-
-            // Simulate a vault-share transfer: a1 sends 10 shares to a2.
-            auto const precheck =
-                [&](Account const& a1, Account const& a2, ApplyContext& ac) -> bool {
-                auto sle1 = ac.view().peek(keylet::mptoken(shareID, a1.id()));
-                auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id()));
-                if (!sle1 || !sle2)
-                    return false;
-                (*sle1)[sfMPTAmount] -= 10;
-                (*sle2)[sfMPTAmount] += 10;
-                ac.view().update(sle1);
-                ac.view().update(sle2);
-                return true;
-            };
-
-            // Case: vault pseudo-account's IOU trustline is frozen.
-            {
-                auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
-                    auto [sid, vid] = setupVault(a1, a2, env);
-                    shareID = sid;
-                    env(trust(gw, gw["IOU"](0), Account{"vaultPseudo", vid}, tfSetFreeze));
-                    env.close();
-                    return true;
-                };
-
-                doInvariantCheck(
-                    Env{*this, defaultAmendments()},
-                    {{"invalid MPToken transfer between holders"}},
-                    precheck,
-                    XRPAmount{},
-                    STTx{ttPAYMENT, [](STObject&) {}},
-                    {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                    preclose);
-            }
-
-            // Case: receiver's (a2's) IOU trustline is frozen.
-            {
-                auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
-                    auto [sid, vid] = setupVault(a1, a2, env);
-                    shareID = sid;
-                    env(trust(gw, gw["IOU"](0), a2, tfSetFreeze));
-                    env.close();
-                    return true;
-                };
-
-                doInvariantCheck(
-                    Env{*this, defaultAmendments()},
-                    {{"invalid MPToken transfer between holders"}},
-                    precheck,
-                    XRPAmount{},
-                    STTx{ttPAYMENT, [](STObject&) {}},
-                    {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                    preclose);
-            }
-        }
-    }
-
-    void
-    testAMM()
-    {
-        testcase << "AMM";
-        using namespace jtx;
-
-        MPTID mptID{};
-        uint256 ammID{};
-        AccountID ammAccountID{};
-        Account const gw{"gw"};
-        Issue lptIssue{};
-        PrettyAsset poolAsset{xrpIssue()};
-
-        auto deleteAMMAccount = [&](ApplyContext& ac, bool) {
-            auto sle = ac.view().peek(keylet::account(ammAccountID));
-            if (!sle)
-                return false;
-            ac.view().erase(sle);
-            return true;
-        };
-
-        auto updateLPTokensBalance = [&](ApplyContext& ac, std::int64_t amount) {
-            auto sle = ac.view().peek(keylet::amm(ammID));
-            if (!sle)
-                return false;
-            sle->setFieldAmount(sfLPTokenBalance, STAmount{lptIssue, amount});
-            ac.view().update(sle);
-            return true;
-        };
-        auto updateLPTokensBadAmount = [&](ApplyContext& ac, bool) {
-            return updateLPTokensBalance(ac, -1);
-        };
-        auto updateLPTokensBadBalance = [&](ApplyContext& ac, bool) {
-            return updateLPTokensBalance(ac, 200'000'000);
-        };
-        auto updateAMM = [&](ApplyContext& ac, bool) { return updateLPTokensBalance(ac, 10); };
-
-        auto updateAMMPool = [&](ApplyContext& ac, bool isMPT) {
-            if (isMPT)
-            {
-                auto sle = ac.view().peek(keylet::mptoken(mptID, ammAccountID));
-                if (!sle)
-                    return false;
-                sle->setFieldU64(sfMPTAmount, 1);
-                ac.view().update(sle);
-                return true;
-            }
-            auto sle = ac.view().peek(keylet::account(ammAccountID));
-            if (!sle)
-                return false;
-            sle->setFieldAmount(sfBalance, XRP(1));
-            ac.view().update(sle);
-            return true;
-        };
-
-        auto test = [&](auto const txType,
-                        auto&& update,
-                        bool isMPT,
-                        TER error = tecINVARIANT_FAILED) {
-            doInvariantCheck(
-                {{"AMM"}},
-                [&](Account const&, Account const&, ApplyContext& ac) { return update(ac, isMPT); },
-                XRPAmount{},
-                STTx{txType, [&](STObject& tx) {}},
-                {tecINVARIANT_FAILED, error},
-                [&](Account const&, Account const&, Env& env) {
-                    env.fund(XRP(1'000), gw);
-                    poolAsset = [&]() -> PrettyAsset {
-                        if (isMPT)
-                        {
-                            MPT const mpt = MPTTester({.env = env, .issuer = gw});
-                            mptID = mpt.issuanceID;
-                            return mpt;
-                        }
-                        return gw["USD"];
-                    }();
-                    AMM const amm(env, gw, XRP(100), poolAsset(100));
-                    ammAccountID = amm.ammAccount();
-                    ammID = amm.ammID();
-                    lptIssue = amm.lptIssue();
-                    return true;
-                });
-        };
-
-        for (bool const isMPT : {false, true})
-        {
-            auto const error = isMPT ? TER(tecINVARIANT_FAILED) : TER(tefINVARIANT_FAILED);
-            for (auto txType : {ttAMM_CREATE, ttAMM_DEPOSIT, ttAMM_CLAWBACK, ttAMM_WITHDRAW})
-            {
-                test(txType, deleteAMMAccount, isMPT, tefINVARIANT_FAILED);
-                test(txType, updateLPTokensBadAmount, isMPT);
-                test(txType, updateLPTokensBadBalance, isMPT);
-            }
-            for (auto txType : {ttAMM_BID, ttAMM_VOTE})
-            {
-                test(txType, updateAMMPool, isMPT, error);
-                test(txType, updateLPTokensBadAmount, isMPT);
-                test(txType, updateLPTokensBadBalance, isMPT);
-            }
-            for (auto txType : {ttAMM_DELETE, ttCHECK_CASH, ttOFFER_CREATE, ttPAYMENT})
-            {
-                test(txType, updateAMM, isMPT);
-            }
-        }
-    }
-
-    // Test the invariant overwrite fix for both pre- and post-amendment
-    // behavior. With the fix enabled, |= accumulates violations across
-    // entries so a later valid entry cannot clear an earlier violation.
-    // Without the fix, = assignment means the last-visited entry wins.
-    void
-    testInvariantOverwrite(FeatureBitset features)
-    {
-        using namespace test::jtx;
-        bool const fixEnabled = features[fixCleanup3_1_3];
-        std::initializer_list const failTers = {tecINVARIANT_FAILED, tefINVARIANT_FAILED};
-        std::initializer_list const passTers = {tesSUCCESS, tesSUCCESS};
-
-        // Insert two trust line SLEs in hash-sorted order, with the "bad"
-        // entry at the lower-sorting key so it is visited first by
-        // ApplyStateTable::visit(). The configurer callables receive the
-        // SLE and the Issue corresponding to that side's keylet currency.
-        auto const insertOrderedTrustLinePair = [](ApplyContext& ac,
-                                                   Account const& a1,
-                                                   Account const& a2,
-                                                   Account const& a3,
-                                                   auto const& badConfig,
-                                                   auto const& goodConfig) {
-            char const* const c1 = "USD";
-            char const* const c2 = "EUR";
-            auto const k1 = keylet::trustLine(a1, a2, a1[c1].currency);
-            auto const k2 = keylet::trustLine(a1, a3, a1[c2].currency);
-
-            bool const k1First = k1.key < k2.key;
-            auto const& badKey = k1First ? k1 : k2;
-            auto const& goodKey = k1First ? k2 : k1;
-            Issue const badIss{k1First ? a1[c1].currency : a1[c2].currency, a1.id()};
-            Issue const goodIss{k1First ? a1[c2].currency : a1[c1].currency, a1.id()};
-
-            auto const sleBad = std::make_shared(badKey);
-            badConfig(*sleBad, badIss);
-            ac.view().insert(sleBad);
-
-            auto const sleGood = std::make_shared(goodKey);
-            goodConfig(*sleGood, goodIss);
-            ac.view().insert(sleGood);
-        };
-
-        // Regression: bad XRP trust line followed by a valid trust line.
-        // With the fix, the invariant catches the violation. Without it,
-        // the valid entry overwrites the flag to false. The keylet
-        // currencies are non-XRP (the invariant inspects sfLowLimit /
-        // sfHighLimit issue, not the keylet currency).
-        testcase << "overwrite: NoXRPTrustLines" + std::string(fixEnabled ? " fix" : "");
-        doInvariantCheck(
-            makeEnv(features),
-            fixEnabled ? std::vector{{"an XRP trust line was created"}}
-                       : std::vector{},
-            [&insertOrderedTrustLinePair](Account const& a1, Account const& a2, ApplyContext& ac) {
-                Account const a3{"A3"};
-                insertOrderedTrustLinePair(
-                    ac,
-                    a1,
-                    a2,
-                    a3,
-                    [](SLE& sle, Issue const& iss) {
-                        // sfLowLimit has xrpIssue, making isXrp = true
-                        sle.setFieldAmount(sfLowLimit, STAmount{xrpIssue(), 0});
-                        sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
-                    },
-                    [](SLE& sle, Issue const& iss) {
-                        sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
-                        sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
-                    });
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_SET, [](STObject&) {}},
-            fixEnabled ? failTers : passTers);
-
-        // Regression: bad deep-freeze trust line followed by a valid one.
-        testcase << "overwrite: NoDeepFreeze" + std::string(fixEnabled ? " fix" : "");
-        doInvariantCheck(
-            makeEnv(features),
-            fixEnabled ? std::vector{{"a trust line with deep freeze flag without "
-                                                   "normal freeze was created"}}
-                       : std::vector{},
-            [&insertOrderedTrustLinePair](Account const& a1, Account const& a2, ApplyContext& ac) {
-                Account const a3{"A3"};
-                insertOrderedTrustLinePair(
-                    ac,
-                    a1,
-                    a2,
-                    a3,
-                    [](SLE& sle, Issue const& iss) {
-                        sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
-                        sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
-                        sle.setFieldU32(sfFlags, lsfLowDeepFreeze);
-                    },
-                    [](SLE& sle, Issue const& iss) {
-                        sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
-                        sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
-                        sle.setFieldU32(sfFlags, 0u);
-                    });
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_SET, [](STObject&) {}},
-            fixEnabled ? failTers : passTers);
-
-        // Regression: MPT OutstandingAmount exceeds max, but locked <=
-        // outstanding. Plain assignment would overwrite bad_ = true.
-        // With the fix, NoZeroEscrow catches it.
-        // Without the fix, NoZeroEscrow passes but ValidMPTIssuance
-        // still fires ("a MPT issuance was created").
-        testcase << "overwrite: NoZeroEscrow MPT" + std::string(fixEnabled ? " fix" : "");
-        doInvariantCheck(
-            makeEnv(features),
-            fixEnabled ? std::vector{{"escrow specifies invalid amount"}}
-                       : std::vector{{"a MPT issuance was created"}},
-            [](Account const& a1, Account const&, ApplyContext& ac) {
-                auto const sle = ac.view().peek(keylet::account(a1.id()));
-                if (!sle)
-                    return false;
-
-                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
-                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
-                // outstanding exceeds kMaxMpTokenAmount -> checkAmount sets bad_
-                sleNew->setFieldU64(sfOutstandingAmount, kMaxMpTokenAmount + 1);
-                // locked is valid and <= outstanding -> must NOT clear bad_
-                sleNew->setFieldU64(sfLockedAmount, 10);
-                ac.view().insert(sleNew);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttACCOUNT_SET, [](STObject&) {}},
-            failTers);
-    }
-
-    void
-    testVaultComputeCoarsestScale()
-    {
-        using namespace jtx;
-
-        Account const issuer{"issuer"};
-        PrettyAsset const vaultAsset = issuer["IOU"];
-
-        struct TestCase
-        {
-            std::string name;
-            std::int32_t expectedMinScale;
-            std::vector values;
-        };
-
-        for (auto const mantissaScale : MantissaRange::getAllScales())
-        {
-            if (mantissaScale == MantissaRange::MantissaScale::Small)
-                continue;
-            NumberMantissaScaleGuard const g{mantissaScale};
-
-            auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
-                return {.delta = n, .scale = scale(n, vaultAsset.raw())};
-            };
-
-            auto const testCases = std::vector{
-                {
-                    .name = "No values",
-                    .expectedMinScale = 0,
-                    .values = {},
-                },
-                {
-                    .name = "Mixed integer and Number values",
-                    .expectedMinScale = -15,
-                    .values = {makeDelta(1), makeDelta(-1), makeDelta(Number{10, -1})},
-                },
-                {
-                    .name = "Mixed scales",
-                    .expectedMinScale = -17,
-                    .values =
-                        {makeDelta(Number{1, -2}),
-                         makeDelta(Number{5, -3}),
-                         makeDelta(Number{3, -2})},
-                },
-                {
-                    .name = "Equal scales",
-                    .expectedMinScale = -16,
-                    .values =
-                        {makeDelta(Number{1, -1}),
-                         makeDelta(Number{5, -1}),
-                         makeDelta(Number{1, -1})},
-                },
-                {
-                    .name = "Mixed mantissa sizes",
-                    .expectedMinScale = -12,
-                    .values =
-                        {makeDelta(Number{1}),
-                         makeDelta(Number{1234, -3}),
-                         makeDelta(Number{12345, -6}),
-                         makeDelta(Number{123, 1})},
-                },
-            };
-
-            for (auto const& tc : testCases)
-            {
-                testcase("vault computeCoarsestScale: " + tc.name);
-
-                auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
-
-                BEAST_EXPECTS(
-                    actualScale == tc.expectedMinScale,
-                    "expected: " + std::to_string(tc.expectedMinScale) +
-                        ", actual: " + std::to_string(actualScale));
-                for (auto const& num : tc.values)
-                {
-                    // None of these scales are far enough apart that rounding the
-                    // values would lose information, so check that the rounded
-                    // value matches the original.
-                    auto const actualRounded = roundToAsset(vaultAsset, num.delta, actualScale);
-                    BEAST_EXPECTS(
-                        actualRounded == num.delta,
-                        "number " + to_string(num.delta) + " rounded to scale " +
-                            std::to_string(actualScale) + " is " + to_string(actualRounded));
-                }
-            }
-
-            auto const testCases2 = std::vector{
-                {
-                    .name = "False equivalence",
-                    .expectedMinScale = -15,
-                    .values =
-                        {
-                            makeDelta(Number{1234567890123456789, -18}),
-                            makeDelta(Number{12345, -4}),
-                            makeDelta(Number{1}),
-                        },
-                },
-            };
-
-            // Unlike the first set of test cases, the values in these test could
-            // look equivalent if using the wrong scale.
-            for (auto const& tc : testCases2)
-            {
-                testcase("vault computeCoarsestScale: " + tc.name);
-
-                auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
-
-                BEAST_EXPECTS(
-                    actualScale == tc.expectedMinScale,
-                    "expected: " + std::to_string(tc.expectedMinScale) +
-                        ", actual: " + std::to_string(actualScale));
-                std::optional first;
-                Number firstRounded;
-                for (auto const& num : tc.values)
-                {
-                    if (!first)
-                    {
-                        first = num.delta;
-                        firstRounded = roundToAsset(vaultAsset, num.delta, actualScale);
-                        continue;
-                    }
-                    auto const numRounded = roundToAsset(vaultAsset, num.delta, actualScale);
-                    BEAST_EXPECTS(
-                        numRounded != firstRounded,
-                        "at a scale of " + std::to_string(actualScale) + " " +
-                            to_string(num.delta) + " == " + to_string(*first));
-                }
-            }
-        }
-    }
-
-    void
-    testSponsorship()
-    {
-        using namespace test::jtx;
-        using namespace std::string_literals;
-        testcase("Sponsorship");
-        {
-            auto const expectMessage =
-                "SponsoredOwnerCount does not equal SponsoringOwnerCount delta.";
-
-            doInvariantCheck(
-                {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    auto const sle = ac.view().peek(keylet::account(a1.id()));
-                    if (!sle)
-                        return false;
-                    sle->setFieldU32(sfSponsoredOwnerCount, 1);
-                    ac.view().update(sle);
-                    return true;
-                });
-
-            doInvariantCheck(
-                {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    auto const sle = ac.view().peek(keylet::account(a1.id()));
-                    if (!sle)
-                        return false;
-                    sle->setFieldU32(sfSponsoringOwnerCount, 1);
-                    ac.view().update(sle);
-                    return true;
-                });
-        }
-
-        {
-            auto const expectMessage =
-                "OwnerCount must be greater than or equal to SponsoredOwnerCount.";
-
-            doInvariantCheck(
-                {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    auto const sle = ac.view().peek(keylet::account(a1.id()));
-                    if (!sle)
-                        return false;
-                    sle->setFieldU32(sfOwnerCount, 0);
-                    sle->setFieldU32(sfSponsoredOwnerCount, 1);
-                    ac.view().update(sle);
-
-                    auto const sle2 = ac.view().peek(keylet::account(a2.id()));
-                    if (!sle2)
-                        return false;
-                    sle2->setFieldU32(sfSponsoringOwnerCount, 1);
-                    ac.view().update(sle2);
-                    return true;
-                });
-        }
-
-        {
-            auto const expectMessage =
-                "SponsoredObjectOwnerCount does not equal SponsoredOwnerCount delta.";
-            uint256 checkID;
-
-            doInvariantCheck(
-                {{expectMessage}},
-                [&](Account const&, Account const& a2, ApplyContext& ac) {
-                    auto const check = ac.view().peek(keylet::check(checkID));
-                    if (!check)
-                        return false;
-                    check->setAccountID(sfSponsor, a2.id());
-                    ac.view().update(check);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttACCOUNT_SET, [](STObject&) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                [&checkID](Account const& a1, Account const& a2, Env& env) {
-                    checkID = keylet::check(a1.id(), SeqProxy::rawSequence(env.seq(a1))).key;
-                    env(check::create(a1, a2, XRP(1)));
-                    return true;
-                });
-        }
-
-        {
-            auto const expectMessage =
-                "Invariant failed: Net delta of SponsoringAccountCount does "
-                "not match net delta of sfSponsor presence.";
-
-            doInvariantCheck(
-                {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    auto const sle = ac.view().peek(keylet::account(a1.id()));
-                    if (!sle)
-                        return false;
-                    sle->setFieldU32(sfSponsoringAccountCount, 1);
-                    ac.view().update(sle);
-                    return true;
-                });
-
-            doInvariantCheck(
-                {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
-                    auto const sle = ac.view().peek(keylet::account(a1.id()));
-                    if (!sle)
-                        return false;
-                    sle->setAccountID(sfSponsor, a2.id());
-                    ac.view().update(sle);
-                    return true;
-                });
-        }
-    }
-
-    void
-    testObjectHasPseudoAccount()
-    {
-        testcase << "object has pseudo-account";
-        using namespace jtx;
-
-        auto const amendments = defaultAmendments() | fixCleanup3_3_0;
-
-        // Vault: object deleted without its pseudo-account
-        {
-            Keylet vaultKeylet = keylet::amendments();
-            doInvariantCheck(
-                Env{*this, amendments},
-                {{"deleted Vault without deleting its pseudo-account"}},
-                [&vaultKeylet](Account const&, Account const&, ApplyContext& ac) {
-                    auto sle = ac.view().peek(vaultKeylet);
-                    if (!sle)
-                        return false;
-                    ac.view().erase(sle);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttVAULT_DELETE, [](STObject&) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                [&vaultKeylet](Account const& a1, Account const&, Env& env) {
-                    Vault const vault{env};
-                    auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
-                    env(tx);
-                    vaultKeylet = keylet;
-                    return true;
-                });
-        }
-
-        // AMM: object deleted without its pseudo-account
-        {
-            uint256 ammID{};
-            Account const gw{"gw"};
-            doInvariantCheck(
-                Env{*this, amendments},
-                {{"deleted AMM without deleting its pseudo-account"}},
-                [&ammID](Account const&, Account const&, ApplyContext& ac) {
-                    auto sle = ac.view().peek(keylet::amm(ammID));
-                    if (!sle)
-                        return false;
-                    ac.view().erase(sle);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttAMM_DELETE, [](STObject&) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                [&ammID, &gw](Account const&, Account const&, Env& env) {
-                    env.fund(XRP(1'000), gw);
-                    AMM const amm(env, gw, XRP(100), gw["USD"](100));
-                    ammID = amm.ammID();
-                    return true;
-                });
-        }
-
-        // LoanBroker: object deleted without its pseudo-account
-        {
-            Keylet loanBrokerKeylet = keylet::amendments();
-            doInvariantCheck(
-                Env{*this, amendments},
-                {{"deleted LoanBroker without deleting its pseudo-account"}},
-                [&loanBrokerKeylet](Account const&, Account const&, ApplyContext& ac) {
-                    auto sle = ac.view().peek(loanBrokerKeylet);
-                    if (!sle)
-                        return false;
-                    ac.view().erase(sle);
-                    return true;
-                },
-                XRPAmount{},
-                STTx{ttLOAN_BROKER_DELETE, [](STObject&) {}},
-                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-                [&loanBrokerKeylet, this](Account const& a1, Account const&, Env& env) {
-                    PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
-                    loanBrokerKeylet = this->createLoanBroker(a1, env, xrpAsset);
-                    return BEAST_EXPECT(env.le(loanBrokerKeylet));
-                });
-        }
-
-        // Deleted object missing sfAccount field (defensive check).
-        // Manually construct the view to place a vault SLE without
-        // sfAccount into the base ledger, then erase it.
-        {
-            Env env{*this, amendments};
-            Account const a1{"A1"};
-            Account const a2{"A2"};
-            env.fund(XRP(1000), a1, a2);
-            env.close();
-
-            OpenView ov{*env.current()};
-
-            auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ov.seq()));
-            auto sleVault = std::make_shared(vaultKeylet);
-            sleVault->makeFieldAbsent(sfAccount);
-            ov.rawInsert(sleVault);
-
-            STTx const tx{ttVAULT_DELETE, [](STObject&) {}};
-            test::StreamSink sink{beast::Severity::Warning};
-            beast::Journal const jlog{sink};
-            ApplyContext ac{
-                env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
-            CurrentTransactionRulesGuard const rulesGuard(ov.rules());
-
-            auto sle = ac.view().peek(vaultKeylet);
-            if (!BEAST_EXPECT(sle))
-                return;
-            ac.view().erase(sle);
-
-            auto transactor = makeTransactor(ac);
-            if (!BEAST_EXPECT(transactor))
-                return;
-            TER const result = transactor->checkInvariants(tesSUCCESS, XRPAmount{});
-            BEAST_EXPECT(result == tecINVARIANT_FAILED);
-            BEAST_EXPECT(sink.messages().str().contains("is missing pseudo-account field"));
-        }
-    }
-
-    void
-    testConfidentialMPTTransfer()
-    {
-        using namespace test::jtx;
-        testcase << "ValidConfidentialMPToken";
-
-        MPTID mptID;
-
-        // Generate an MPT with privacy, issue 100 tokens to A2.
-        // Perform a confidential conversion to populate encrypted state.
-        auto const precloseConfidential =
-            [&mptID](Account const& a1, Account const& a2, Env& env) -> bool {
-            MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
-            mpt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
-            mptID = mpt.issuanceID();
-
-            mpt.authorize({.account = a2});
-            mpt.pay(a1, a2, 100);
-
-            mpt.generateKeyPair(a1);
-            mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)});
-
-            mpt.generateKeyPair(a2);
-            mpt.convert({
-                .account = a2,
-                .amt = 100,
-                .holderPubKey = mpt.getPubKey(a2),
-            });
-            return true;
-        };
-
-        // badDelete
-        doInvariantCheck(
-            {"MPToken deleted with encrypted fields while COA > 0"},
-            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
-                if (!sleToken)
-                    return false;
-                // Force an erase of the object while the COA remains 100
-                ac.view().erase(sleToken);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
-            precloseConfidential);
-
-        // badConsistency
-        doInvariantCheck(
-            {"MPToken encrypted field existence inconsistency"},
-            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
-                if (!sleToken)
-                    return false;
-                // Remove one of the required encrypted fields to create a mismatch
-                sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
-                ac.view().update(sleToken);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseConfidential);
-
-        doInvariantCheck(
-            {"MPToken encrypted field existence inconsistency"},
-            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
-                if (!sleToken)
-                    return false;
-                sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
-                sleToken->makeFieldAbsent(sfConfidentialBalanceInbox);
-                sleToken->makeFieldAbsent(sfConfidentialBalanceSpending);
-                sleToken->setFieldVL(sfAuditorEncryptedBalance, Blob{0x00});
-                ac.view().update(sleToken);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseConfidential);
-
-        // requiresPrivacyFlag
-        auto const precloseNoPrivacy = [&mptID](
-                                           Account const& a1, Account const& a2, Env& env) -> bool {
-            MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
-            // completely omitted the tfMPTCanHoldConfidentialBalance flag here.
-            mpt.create({.flags = tfMPTCanTransfer});
-            mptID = mpt.issuanceID();
-            mpt.authorize({.account = a2});
-            mpt.pay(a1, a2, 100);
-            return true;
-        };
-
-        doInvariantCheck(
-            {"MPToken has encrypted fields but Issuance does not have "
-             "lsfMPTCanHoldConfidentialBalance "
-             "set"},
-            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
-                if (!sleToken)
-                    return false;
-                // Inject all three encrypted fields consistently (inbox+spending+issuer must be
-                // in sync or badConsistency fires first and masks requiresPrivacyFlag).
-                sleToken->setFieldVL(sfConfidentialBalanceInbox, Blob{0x00});
-                sleToken->setFieldVL(sfConfidentialBalanceSpending, Blob{0x00});
-                sleToken->setFieldVL(sfIssuerEncryptedBalance, Blob{0x00});
-                ac.view().update(sleToken);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseNoPrivacy);
-
-        // badCOA
-        doInvariantCheck(
-            {"Confidential outstanding amount exceeds total outstanding amount"},
-            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
-                if (!sleIssuance)
-                    return false;
-                // Total outstanding is natively 100; bloat the COA over 100
-                sleIssuance->setFieldU64(sfConfidentialOutstandingAmount, 200);
-                ac.view().update(sleIssuance);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttMPTOKEN_ISSUANCE_SET, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseConfidential);
-
-        // Conservation Violation
-        doInvariantCheck(
-            {"Token conservation violation for MPT"},
-            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
-                if (!sleIssuance)
-                    return false;
-
-                sleIssuance->setFieldU64(
-                    sfConfidentialOutstandingAmount,
-                    sleIssuance->getFieldU64(sfConfidentialOutstandingAmount) - 10);
-                ac.view().update(sleIssuance);
-
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseConfidential);
-
-        // Send/MergeInbox must not change OutstandingAmount (coaDelta == 0)
-        doInvariantCheck(
-            {"Invariant failed: OutstandingAmount changed "
-             "by confidential transaction that should not "
-             "modify it for MPT"},
-            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
-                if (!sleIssuance)
-                    return false;
-                sleIssuance->setFieldU64(
-                    sfOutstandingAmount, sleIssuance->getFieldU64(sfOutstandingAmount) + 1);
-                ac.view().update(sleIssuance);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseConfidential);
-
-        // Send/MergeInbox and zero-COA-delta confidential transactions must not
-        // change public holder MPTAmount.
-        doInvariantCheck(
-            {"Invariant failed: MPTAmount changed by confidential "
-             "transaction that should not modify this field."},
-            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
-                if (!sleToken)
-                    return false;
-                sleToken->setFieldU64(sfMPTAmount, sleToken->getFieldU64(sfMPTAmount) + 1);
-                ac.view().update(sleToken);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseConfidential);
-
-        // badVersion
-        doInvariantCheck(
-            {"MPToken sfConfidentialBalanceVersion not updated when sfConfidentialBalanceSpending "
-             "changed"},
-            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
-                Blob const kChangedConfidentialSpending = {0xBA, 0xDD};
-                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
-                if (!sleToken)
-                    return false;
-                sleToken->setFieldVL(sfConfidentialBalanceSpending, kChangedConfidentialSpending);
-
-                // DO NOT update sfConfidentialBalanceVersion
-                ac.view().update(sleToken);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
-            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
-            precloseConfidential);
-
-        // Skipping Deleted MPTs (Issuance deleted)
-        auto const precloseOrphan = [&mptID](
-                                        Account const& a1, Account const& a2, Env& env) -> bool {
-            MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
-            mpt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
-            mptID = mpt.issuanceID();
-            mpt.authorize({.account = a2});
-
-            // Generate privacy keys and convert 0 amount so Bob has the encrypted fields
-            mpt.generateKeyPair(a1);
-            mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)});
-            mpt.generateKeyPair(a2);
-            mpt.convert({
-                .account = a2,
-                .amt = 0,
-                .holderPubKey = mpt.getPubKey(a2),
-            });
-
-            // Immediately destroy the issuance. A2's empty, encrypted token object lives on.
-            mpt.destroy();
-            return true;
-        };
-
-        doInvariantCheck(
-            {},
-            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
-                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
-                if (!sleToken)
-                    return false;
-                // Safely able to erase the deleted token.
-                ac.view().erase(sleToken);
-                return true;
-            },
-            XRPAmount{},
-            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
-            {tesSUCCESS, tesSUCCESS},
-            precloseOrphan);
-    }
-
-public:
-    void
-    run() override
-    {
-        testXRPNotCreated();
-        testAccountRootsNotRemoved();
-        testAccountRootsDeletedClean();
-        testTypesMatch();
-        testNoXRPTrustLine();
-        testNoDeepFreezeTrustLinesWithoutFreeze();
-        testTransfersNotFrozen();
-        testXRPBalanceCheck();
-        testTransactionFeeCheck();
-        testNoBadOffers();
-        testNoZeroEscrow();
-        testValidNewAccountRoot();
-        testNFTokenPageInvariants();
-        testAMMDeleteInvariants(defaultAmendments());
-        testAMMDeleteInvariants(defaultAmendments() - fixCleanup3_3_0);
-        testPermissionedDomainInvariants(defaultAmendments() | fixCleanup3_1_3);
-        testPermissionedDomainInvariants(defaultAmendments() - fixCleanup3_1_3);
-        testPermissionedDEX(defaultAmendments() | fixCleanup3_1_3);
-        testPermissionedDEX(defaultAmendments() - fixCleanup3_1_3);
-        testBookDirectoryExchangeRate();
-        testNoModifiedUnmodifiableFields();
-        testValidPseudoAccounts();
-        testValidLoanBroker();
-        testVault();
-        testConfidentialMPTTransfer();
-        testMPT();
-        testInvariantOverwrite(defaultAmendments());
-        testInvariantOverwrite(defaultAmendments() - fixCleanup3_1_3);
-        testVaultComputeCoarsestScale();
-        testAMM();
-        testObjectHasPseudoAccount();
-        testSponsorship();
-    }
-};
-
-BEAST_DEFINE_TESTSUITE(Invariants, app, xrpl);
-
-}  // namespace xrpl::test
diff --git a/src/test/app/LPTokenTransfer_test.cpp b/src/test/app/LPTokenTransfer_test.cpp
index e30e37ed98..3e72094eb3 100644
--- a/src/test/app/LPTokenTransfer_test.cpp
+++ b/src/test/app/LPTokenTransfer_test.cpp
@@ -4,6 +4,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include   // IWYU pragma: keep
 #include 
@@ -21,6 +22,8 @@
 #include 
 #include 
 
+#include 
+
 namespace xrpl::test {
 
 class LPTokenTransfer_test : public jtx::AMMTest
@@ -433,6 +436,136 @@ class LPTokenTransfer_test : public jtx::AMMTest
         }
     }
 
+    void
+    testMPTCanTransferDirectStep(FeatureBitset features)
+    {
+        testcase("MPT CanTransfer DirectStep");
+
+        using namespace jtx;
+
+        // An MPT can only be an AMM pool asset once featureMPTokensV2 is
+        // enabled, so this behavior is only meaningful when V2 is present, and
+        // is independent of fixFrozenLPTokenTransfer.
+        if (!features[featureMPTokensV2])
+            return;
+
+        // gw issues an MPT used as one of the AMM pool assets. gw (the MPT
+        // issuer) seeds the pool and hands LP tokens to alice. Transferring LP
+        // tokens between two non-issuer holders is only permitted when the
+        // pool MPT allows transfers (lsfMPTCanTransfer); issuer-involving
+        // transfers are always permitted. The check fires on the redeem step
+        // against the AMM account via canTransferLPToken().
+        auto testLPTokenTransfer = [&](std::uint32_t mptFlags, bool poolXrpToBtc) {
+            Env env{*this, features};
+            env.fund(XRP(30'000), gw_, alice_, bob_);
+            env.close();
+
+            // gw is the MPT issuer, so it may seed the pool regardless of
+            // whether the MPT permits third-party transfers.
+            MPT const btc = MPTTester(
+                {.env = env, .issuer = gw_, .holders = {alice_}, .pay = 1'000, .flags = mptFlags});
+
+            auto const asset1 = poolXrpToBtc ? XRP(10'000) : btc(10'000);
+            auto const asset2 = poolXrpToBtc ? btc(10'000) : XRP(10'000);
+            AMM const amm(env, gw_, asset1, asset2);
+            auto const lpIssue = amm.lptIssue();
+
+            env.trust(STAmount{lpIssue, 100'000}, alice_);
+            env.trust(STAmount{lpIssue, 100'000}, bob_);
+            env.close();
+
+            // Issuer-involving LP token transfer is always allowed (gw is the
+            // pool MPT's issuer), even when the MPT lacks CanTransfer.
+            env(pay(gw_, alice_, STAmount{lpIssue, 1'000}));
+            env.close();
+
+            // Transfer between two non-issuer holders is allowed only if the
+            // pool MPT has CanTransfer set; otherwise the redeem step against
+            // the AMM account blocks it with tecNO_AUTH.
+            if ((mptFlags & tfMPTCanTransfer) != 0u)
+            {
+                env(pay(alice_, bob_, STAmount{lpIssue, 100}));
+            }
+            else
+            {
+                env(pay(alice_, bob_, STAmount{lpIssue, 100}), Ter(tecNO_AUTH));
+            }
+            env.close();
+        };
+
+        // Pool MPT without CanTransfer blocks third-party LP token transfers.
+        testLPTokenTransfer(tfMPTCanTrade, true);
+        testLPTokenTransfer(tfMPTCanTrade, false);
+
+        // Pool MPT with CanTransfer allows them.
+        testLPTokenTransfer(tfMPTCanTrade | tfMPTCanTransfer, true);
+        testLPTokenTransfer(tfMPTCanTrade | tfMPTCanTransfer, false);
+    }
+
+    void
+    testMPTCanTransferOffer(FeatureBitset features)
+    {
+        testcase("MPT CanTransfer Offer");
+
+        using namespace jtx;
+
+        if (!features[featureMPTokensV2])
+            return;
+
+        // Parity with frozen LP tokens for the order book: a non-transferable
+        // pool MPT makes the LP token un-spendable (canTransferLPToken zeroes
+        // the spendable balance in accountHolds, just as isLPTokenFrozen does),
+        // so an offer to sell it cannot be funded - the same tecUNFUNDED_OFFER
+        // outcome as freezing a pool asset (see testOfferCreation).
+        auto testLPTokenTransfer = [&](std::uint32_t mptFlags, bool poolXrpToBtc) {
+            Env env{*this, features};
+            env.fund(XRP(30'000), gw_, carol_);
+            env.close();
+
+            MPT const btc = MPTTester(
+                {.env = env, .issuer = gw_, .holders = {carol_}, .pay = 1'000, .flags = mptFlags});
+
+            auto const asset1 = poolXrpToBtc ? XRP(10'000) : btc(10'000);
+            auto const asset2 = poolXrpToBtc ? btc(10'000) : XRP(10'000);
+            AMM const amm(env, gw_, asset1, asset2);
+            auto const lpIssue = amm.lptIssue();
+
+            env.trust(STAmount{lpIssue, 100'000}, carol_);
+            env.close();
+
+            // gw (the pool MPT issuer) seeds carol_ with LP tokens; issuer
+            // involving transfers are always allowed.
+            env(pay(gw_, carol_, STAmount{lpIssue, 1'000}));
+            env.close();
+
+            // carol_ tries to create an offer to sell the LP token.
+            if ((mptFlags & tfMPTCanTransfer) != 0u)
+            {
+                env(offer(carol_, XRP(10), STAmount{lpIssue, 10}), Txflags(tfPassive));
+                env.close();
+                BEAST_EXPECT(expectOffers(env, carol_, 1));
+            }
+            else
+            {
+                // Non-transferable pool MPT => LP token un-spendable => the
+                // sell offer is unfunded, just as if a pool asset were frozen.
+                env(offer(carol_, XRP(10), STAmount{lpIssue, 10}),
+                    Txflags(tfPassive),
+                    Ter(tecUNFUNDED_OFFER));
+                env.close();
+                BEAST_EXPECT(expectOffers(env, carol_, 0));
+            }
+        };
+
+        // Pool MPT without CanTransfer: LP token sell offer is unfunded.
+        testLPTokenTransfer(tfMPTCanTrade, true);
+        testLPTokenTransfer(tfMPTCanTrade, false);
+
+        // Pool MPT with CanTransfer: LP token sell offer is created.
+        testLPTokenTransfer(tfMPTCanTrade | tfMPTCanTransfer, true);
+        testLPTokenTransfer(tfMPTCanTrade | tfMPTCanTransfer, false);
+    }
+
 public:
     void
     run() override
@@ -447,6 +580,8 @@ public:
             testOfferCrossing(features);
             testCheck(features);
             testNFTOffers(features);
+            testMPTCanTransferDirectStep(features);
+            testMPTCanTransferOffer(features);
         }
     }
 };
diff --git a/src/test/app/LedgerLoad_test.cpp b/src/test/app/LedgerLoad_test.cpp
index ee3bfe5192..8fb10c1088 100644
--- a/src/test/app/LedgerLoad_test.cpp
+++ b/src/test/app/LedgerLoad_test.cpp
@@ -7,10 +7,10 @@
 
 #include 
 
+#include 
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -18,16 +18,16 @@
 #include 
 
 #include 
-#include 
-#include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
@@ -61,7 +61,7 @@ class LedgerLoad_test : public beast::unit_test::Suite
     };
 
     SetupData
-    setupLedger(beast::TempDir const& td)
+    setupLedger(TempDir const& td)
     {
         using namespace test::jtx;
         SetupData retval = {.dbPath = td.path()};
@@ -139,7 +139,7 @@ class LedgerLoad_test : public beast::unit_test::Suite
     {
         testcase("Load ledger: Bad Files");
         using namespace test::jtx;
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
 
         // empty path
         except([&] {
@@ -161,8 +161,8 @@ class LedgerLoad_test : public beast::unit_test::Suite
         });
 
         // make a corrupted version of the ledger file (last 10 bytes removed).
-        boost::system::error_code ec;
-        auto ledgerFileCorrupt = boost::filesystem::path{sd.dbPath} / "ledgerdata_bad.json";
+        std::error_code ec;
+        auto ledgerFileCorrupt = std::filesystem::path{sd.dbPath} / "ledgerdata_bad.json";
         copy_file(sd.ledgerFile, ledgerFileCorrupt, copy_options::overwrite_existing, ec);
         if (!BEAST_EXPECTS(!ec, ec.message()))
             return;
@@ -330,7 +330,7 @@ public:
     void
     run() override
     {
-        beast::TempDir const td;
+        TempDir const td;
         auto sd = setupLedger(td);
 
         // test cases
diff --git a/src/test/app/LedgerMaster_test.cpp b/src/test/app/LedgerMaster_test.cpp
index 3cf9b3a9d9..2f2f81bb8a 100644
--- a/src/test/app/LedgerMaster_test.cpp
+++ b/src/test/app/LedgerMaster_test.cpp
@@ -5,17 +5,25 @@
 #include 
 
 #include 
+#include 
 #include 
 
+#include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 
+#include 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
 
 namespace xrpl::test {
@@ -111,6 +119,200 @@ class LedgerMaster_test : public beast::unit_test::Suite
         }
     }
 
+    // Wait until the SHAMapStore has finished processing the ledger that the
+    // preceding env.close() produced.
+    //
+    // env.close() returns as soon as the ledger_accept RPC returns, but the
+    // validated ledger path -- LedgerMaster::setValidLedger() ->
+    // SHAMapStore::onLedgerClosed() -- runs on a job queue thread. Without
+    // draining the job queue first, the store may not have been handed the
+    // ledger at all, in which case rendezvous() observes working_ == false and
+    // returns immediately, before any work has been done.
+    [[nodiscard]] static bool
+    syncStore(jtx::Env& env)
+    {
+        // Drain the job queue first, so that onLedgerClosed() has run and
+        // working_ is set. Then use the store's timeout overload, so a store
+        // that never finishes fails this test instead of blocking on it.
+        //
+        // Only the second wait is bounded: JobQueue::rendezvous() has no
+        // timeout overload, so a job that never completes hangs here. That is
+        // pre-existing -- ~AppBundle waits on it the same way for every jtx
+        // test -- but it does mean this helper is not hang-proof end to end.
+        env.app().getJobQueue().rendezvous();
+        return env.app().getSHAMapStore().rendezvous(std::chrono::seconds{60});
+    }
+
+    // Bring the SHAMapStore to the point where it has been handed a validated
+    // ledger and initialized lastRotated, and report how many extra ledgers had
+    // to be closed to get it there (normally none). Returns std::nullopt if
+    // syncStore() itself failed.
+    //
+    // syncStore() alone does not guarantee that, because
+    // SHAMapStoreImp::run()'s loop does not use the notification and the
+    // working_ flag safely:
+    //
+    //   * onLedgerClosed() notifies cond_ whether or not run()'s thread is
+    //     parked on it, and run() waits on cond_ without a predicate, so a
+    //     notification that lands while the thread is still starting up --
+    //     before it first reaches that wait -- is lost.
+    //   * run() clears working_ at the top of its loop without checking
+    //     whether newLedger_ is still set, so rendezvous() can report the
+    //     store idle with a validated ledger queued.
+    //
+    // Either way the store ends up parked with work pending, and only another
+    // notification gets it moving again. In a standalone test nothing else
+    // closes ledgers, so that has to come from here: this closes a ledger
+    // rather than polling getLastRotated(), because polling would just time
+    // out. onLedgerClosed() keeps only the most recent ledger in newLedger_,
+    // so the ledger the store picks up -- and therefore lastRotated -- is a
+    // timing detail, which is why the caller derives its expectations from the
+    // value it observes instead of assuming one.
+    //
+    // run() is deliberately left as it is. In production the only effect is
+    // latency: the trigger is validatedSeq >= lastRotated + deleteInterval, so
+    // a lost notification delays rotation to the next validated ledger and
+    // nothing is skipped or accumulated -- starting at 513 instead of 512 does
+    // not matter. Two consequences do follow from leaving it in place, and both
+    // hold today: nothing in production decides anything from working_ or
+    // rendezvous() (rendezvous() has no production callers at all), and a node
+    // whose ledgers only advance on demand -- standalone, driven by
+    // ledger_accept -- can sit on a queued ledger until something closes the
+    // next one, which is exactly the situation this helper is working around.
+    //
+    // So this helper is permanent rather than a stopgap. Working around the
+    // race must not make it invisible, so every extra close is logged. That
+    // keeps how often it is actually hit observable in the unit test output --
+    // which is the only signal left once this testcase stops flaking on it.
+    [[nodiscard]] std::optional
+    initializeStore(jtx::Env& env, int const maxExtraCloses = 3)
+    {
+        auto& store = env.app().getSHAMapStore();
+
+        for (int extraCloses = 0;; ++extraCloses)
+        {
+            if (!syncStore(env))
+                return std::nullopt;
+            if (store.getLastRotated() != 0 || extraCloses == maxExtraCloses)
+            {
+                if (extraCloses != 0)
+                {
+                    log << "initializeStore: the store needed " << extraCloses
+                        << " extra ledger close(s) to pick up a validated ledger. "
+                           "SHAMapStoreImp::run() dropped the notification for the "
+                           "first one; see the comment on initializeStore()."
+                        << std::endl;
+                }
+                return extraCloses;
+            }
+            env.close();
+        }
+    }
+
+    void
+    testCompleteLedgerRange(FeatureBitset features)
+    {
+        // Note that this test is intentionally very similar to
+        // SHAMapStore_test::testLedgerGaps, but has a different
+        // focus.
+
+        testcase("Complete Ledger operations");
+
+        using namespace test::jtx;
+
+        auto const deleteInterval = 8;
+
+        Env env{*this, envconfig(onlineDelete, deleteInterval)};
+
+        auto const alice = Account("alice");
+        env.fund(XRP(1000), alice);
+        env.close();
+
+        auto& lm = env.app().getLedgerMaster();
+        LedgerIndex minSeq = 2;
+        auto& store = env.app().getSHAMapStore();
+        // Which of the existing complete ledgers the store initializes
+        // lastRotated from is a timing detail; all this test needs is that it is
+        // one of them. Everything below derives from the observed value rather
+        // than assuming a particular one.
+        //
+        // The range check and the initializeStore() one both end the testcase
+        // rather than merely reporting, because lastRotated is the only value
+        // from the store that enters minSeq. A lastRotated of 0 -- the value
+        // getLastRotated() reports until the store has been handed a
+        // validated ledger -- makes minSeq 0 below, and the minSeq - 1 and
+        // minSeq - 2 ranges then underflow to first > last, which aborts a
+        // Debug build inside missingFromCompleteLedgerRange().
+        auto const extraCloses = initializeStore(env);
+        if (!BEAST_EXPECT(extraCloses.has_value()))
+            return;
+        LedgerIndex maxSeq = env.closed()->header().seq;
+        LedgerIndex lastRotated = store.getLastRotated();
+        if (!BEAST_EXPECTS(lastRotated >= minSeq && lastRotated <= maxSeq, to_string(lastRotated)))
+            return;
+        // The BEAST_EXPECT above already returned if this is nullopt, but that
+        // is invisible to clang-tidy's optional model.
+        // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+        BEAST_EXPECTS(maxSeq == 3 + *extraCloses, to_string(maxSeq));
+        std::stringstream initialRange;
+        initialRange << minSeq << "-" << maxSeq;
+        BEAST_EXPECTS(lm.getCompleteLedgers() == initialRange.str(), lm.getCompleteLedgers());
+        BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
+        // The inner range is empty unless initializeStore() had to close extra
+        // ledgers, and missingFromCompleteLedgerRange() treats first > last as a
+        // precondition violation that aborts a Debug build via UNREACHABLE, so
+        // only check it when it is well formed.
+        if (minSeq + 1 <= maxSeq - 1)
+        {
+            BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
+        }
+        BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
+        BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
+        BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
+
+        // Close enough ledgers to rotate a few times
+        for (int i = 0; i < 24; ++i)
+        {
+            for (int t = 0; t < 3; ++t)
+            {
+                env(noop(alice));
+            }
+            env.close();
+            BEAST_EXPECT(syncStore(env));
+
+            ++maxSeq;
+
+            if (maxSeq == lastRotated + deleteInterval)
+            {
+                minSeq = lastRotated;
+                lastRotated = maxSeq;
+            }
+            BEAST_EXPECTS(
+                env.closed()->header().seq == maxSeq, to_string(env.closed()->header().seq));
+            BEAST_EXPECTS(store.getLastRotated() == lastRotated, to_string(store.getLastRotated()));
+            std::stringstream expectedRange;
+            expectedRange << minSeq << "-" << maxSeq;
+            BEAST_EXPECTS(lm.getCompleteLedgers() == expectedRange.str(), lm.getCompleteLedgers());
+            BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
+            // missingFromCompleteLedgerRange() treats first > last as a
+            // precondition violation and aborts a Debug build via UNREACHABLE.
+            // The range can only collapse if this test's model of minSeq /
+            // maxSeq has desynced from the store, so report that as a failure
+            // instead of taking down the whole unit test job.
+            if (minSeq + 1 <= maxSeq - 1)
+            {
+                BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
+            }
+            else
+            {
+                BEAST_EXPECTS(false, to_string(minSeq) + "-" + to_string(maxSeq));
+            }
+            BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
+            BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
+            BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
+        }
+    }
+
 public:
     void
     run() override
@@ -124,6 +326,7 @@ public:
     testWithFeats(FeatureBitset features)
     {
         testTxnIdFromIndex(features);
+        testCompleteLedgerRange(features);
     }
 };
 
diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp
index 2e2c80d6f8..0853affab7 100644
--- a/src/test/app/LedgerReplay_test.cpp
+++ b/src/test/app/LedgerReplay_test.cpp
@@ -28,7 +28,6 @@
 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -53,6 +52,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -402,7 +402,7 @@ public:
 
 enum class PeerSetBehavior {
     Good,
-    Drop50,
+    DropAlternate,
     DropAll,
     DropSkipListReply,
     DropLedgerDeltaReply,
@@ -445,17 +445,13 @@ struct TestPeerSet : public PeerSet
         protocol::MessageType type,
         std::shared_ptr const& peer) override
     {
-        int dropRate = 0;
-        if (behavior == PeerSetBehavior::Drop50)
-        {
-            dropRate = 50;
-        }
-        else if (behavior == PeerSetBehavior::DropAll)
-        {
-            dropRate = 100;
-        }
+        if (behavior == PeerSetBehavior::DropAll)
+            return;
 
-        if (randInt(1, 100) <= dropRate)
+        // Drop every other message deterministically. Alternating drops
+        // still exercise the timeout/retry path while guaranteeing every
+        // subtask eventually gets a reply.
+        if (behavior == PeerSetBehavior::DropAlternate && sendCount++ % 2 == 0)
             return;
 
         switch (type)
@@ -500,6 +496,7 @@ struct TestPeerSet : public PeerSet
     LedgerReplayMsgHandler& remote;
     std::shared_ptr dummyPeer;
     PeerSetBehavior behavior;
+    std::atomic sendCount{0};
 };
 
 /**
@@ -1397,7 +1394,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
             case PeerSetBehavior::Good:
                 testcase("good network");
                 break;
-            case PeerSetBehavior::Drop50:
+            case PeerSetBehavior::DropAlternate:
                 testcase("network drops 50% messages");
                 break;
             case PeerSetBehavior::Repeat:
@@ -1613,7 +1610,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite
         testAllInboundLedgers(4);
         testPeerSetBehavior(PeerSetBehavior::Good, 1);
         testPeerSetBehavior(PeerSetBehavior::Good);
-        testPeerSetBehavior(PeerSetBehavior::Drop50);
+        testPeerSetBehavior(PeerSetBehavior::DropAlternate);
         testPeerSetBehavior(PeerSetBehavior::Repeat);
         testStop();
         testSkipListBadReply();
diff --git a/src/test/app/MPToken_test.cpp b/src/test/app/MPToken_test.cpp
index b392dca758..7086adf743 100644
--- a/src/test/app/MPToken_test.cpp
+++ b/src/test/app/MPToken_test.cpp
@@ -789,7 +789,7 @@ class MPToken_test : public beast::unit_test::Suite
 
             // locks up bob's mptoken again
             mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
-            if (!features[featureSingleAssetVault])
+            if (!features[featureSingleAssetVault] && !features[fixCleanup3_4_0])
             {
                 // Delete bob's mptoken even though it is locked
                 mptAlice.authorize({.account = bob, .flags = tfMPTUnauthorize});
@@ -7657,6 +7657,56 @@ class MPToken_test : public beast::unit_test::Suite
             0, tecNO_PERMISSION, tecNO_PERMISSION, tecNO_PERMISSION, tecNO_PERMISSION);
     }
 
+    void
+    testLockedMPTokenDestroyedIssuance(FeatureBitset features)
+    {
+        testcase("Locked MPToken with destroyed issuance");
+
+        using namespace test::jtx;
+        Account const alice("alice");  // issuer
+        Account const bob("bob");      // holder
+
+        Env env{*this, features};
+        env.fund(XRP(1'000), alice, bob);
+        env.close();
+        MPTTester mptAlice(
+            {.env = env, .issuer = alice, .holders = {bob}, .flags = kMptDexFlags | tfMPTCanLock});
+
+        // alice locks bob's mptoken individually
+        mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
+
+        // alice destroys her issuance. This succeeds: MPTokenIssuanceDestroy
+        // only requires that the issuance has no outstanding balance; it does
+        // not require that all holder MPTokens have been deleted first.
+        mptAlice.destroy({.ownerCount = 0});
+
+        if (!features[featureSingleAssetVault] || features[fixCleanup3_4_0])
+        {
+            // pre SAV or post Cleanup340 amendment: bob deletes the dangling locked MPToken
+            mptAlice.authorize({.account = bob, .holderCount = 0, .flags = tfMPTUnauthorize});
+            BEAST_EXPECT(ownerCount(env, bob) == 0);
+        }
+        else
+        {
+            // bob cannot delete his locked MPToken, even though the issuance
+            // no longer exists.
+            mptAlice.authorize(
+                {.account = bob, .flags = tfMPTUnauthorize, .err = tecNO_PERMISSION});
+
+            // and the lock can never be cleared, because unlocking
+            // requires the (destroyed) issuance
+            mptAlice.set(
+                {.account = alice,
+                 .holder = bob,
+                 .flags = tfMPTUnlock,
+                 .err = tecOBJECT_NOT_FOUND});
+
+            // the dangling locked MPToken survives
+            BEAST_EXPECT(env.current()->exists(keylet::mptoken(mptAlice.issuanceID(), bob.id())));
+            BEAST_EXPECT(ownerCount(env, bob) == 1);
+        }
+    }
+
 public:
     void
     run() override
@@ -7703,7 +7753,9 @@ public:
         testSetValidation(all - featurePermissionedDomains);
         testSetValidation(all);
 
+        testSetEnabled(all - featureSingleAssetVault - fixCleanup3_4_0);
         testSetEnabled(all - featureSingleAssetVault);
+        testSetEnabled(all - fixCleanup3_4_0);
         testSetEnabled(all);
 
         // MPT clawback
@@ -7770,6 +7822,10 @@ public:
 
         // Fixes
         testFixDoubleOwnerCount(all);
+        testLockedMPTokenDestroyedIssuance(all);
+        testLockedMPTokenDestroyedIssuance(all - fixCleanup3_4_0);
+        testLockedMPTokenDestroyedIssuance(all - featureSingleAssetVault);
+        testLockedMPTokenDestroyedIssuance(all - featureSingleAssetVault - fixCleanup3_4_0);
     }
 };
 
diff --git a/src/test/app/Manifest_test.cpp b/src/test/app/Manifest_test.cpp
index ef2043a22c..14d176b45f 100644
--- a/src/test/app/Manifest_test.cpp
+++ b/src/test/app/Manifest_test.cpp
@@ -22,14 +22,12 @@
 #include 
 #include 
 
-#include 
-#include 
-
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -56,18 +54,18 @@ private:
     }
 
     static void
-    cleanupDatabaseDir(boost::filesystem::path const& dbPath)
+    cleanupDatabaseDir(std::filesystem::path const& dbPath)
     {
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
         if (!exists(dbPath) || !is_directory(dbPath) || !is_empty(dbPath))
             return;
         remove(dbPath);
     }
 
     static void
-    setupDatabaseDir(boost::filesystem::path const& dbPath)
+    setupDatabaseDir(std::filesystem::path const& dbPath)
     {
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
         if (!exists(dbPath))
         {
             create_directory(dbPath);
@@ -80,10 +78,10 @@ private:
             Throw("Cannot create directory: " + dbPath.string());
         }
     }
-    static boost::filesystem::path
+    static std::filesystem::path
     getDatabasePath()
     {
-        return boost::filesystem::current_path() / "manifest_test_databases";
+        return std::filesystem::current_path() / "manifest_test_databases";
     }
 
 public:
@@ -351,7 +349,7 @@ public:
                 BEAST_EXPECT(loaded.revoked(pk));
             }
         }
-        boost::filesystem::remove(getDatabasePath() / boost::filesystem::path(dbName));
+        std::filesystem::remove(getDatabasePath() / std::filesystem::path(dbName));
     }
 
     void
diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp
index 16c4e6e000..f17054e2d0 100644
--- a/src/test/app/NFTokenBurn_test.cpp
+++ b/src/test/app/NFTokenBurn_test.cpp
@@ -33,6 +33,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
@@ -117,33 +118,30 @@ class NFTokenBurn_test : public beast::unit_test::Suite
                 std::cout << "Ledger state is not array!" << std::endl;
                 return;
             }
-            for (json::UInt i = 0; i < state.size(); ++i)
+            for (auto& i : state)
             {
-                if (state[i].isMember(sfNFTokens.jsonName) &&
-                    state[i][sfNFTokens.jsonName].isArray())
+                if (i.isMember(sfNFTokens.jsonName) && i[sfNFTokens.jsonName].isArray())
                 {
-                    std::uint32_t const tokenCount = state[i][sfNFTokens.jsonName].size();
-                    std::cout << tokenCount << " NFtokens in page "
-                              << state[i][jss::index].asString() << std::endl;
+                    std::uint32_t const tokenCount = i[sfNFTokens.jsonName].size();
+                    std::cout << tokenCount << " NFtokens in page " << i[jss::index].asString()
+                              << std::endl;
 
                     if (vol == Volume::Noisy)
                     {
-                        std::cout << state[i].toStyledString() << std::endl;
+                        std::cout << i.toStyledString() << std::endl;
                     }
                     else
                     {
                         if (tokenCount > 0)
                         {
-                            std::cout
-                                << "first: " << state[i][sfNFTokens.jsonName][0u].toStyledString()
-                                << std::endl;
+                            std::cout << "first: " << i[sfNFTokens.jsonName][0u].toStyledString()
+                                      << std::endl;
                         }
                         if (tokenCount > 1)
                         {
-                            std::cout
-                                << "last: "
-                                << state[i][sfNFTokens.jsonName][tokenCount - 1].toStyledString()
-                                << std::endl;
+                            std::cout << "last: "
+                                      << i[sfNFTokens.jsonName][tokenCount - 1].toStyledString()
+                                      << std::endl;
                         }
                     }
                 }
@@ -419,12 +417,11 @@ class NFTokenBurn_test : public beast::unit_test::Suite
                 json::Value& state = jrr[jss::result][jss::state];
 
                 int pageCount = 0;
-                for (json::UInt i = 0; i < state.size(); ++i)
+                for (auto& i : state)
                 {
-                    if (state[i].isMember(sfNFTokens.jsonName) &&
-                        state[i][sfNFTokens.jsonName].isArray())
+                    if (i.isMember(sfNFTokens.jsonName) && i[sfNFTokens.jsonName].isArray())
                     {
-                        BEAST_EXPECT(state[i][sfNFTokens.jsonName].size() == 32);
+                        BEAST_EXPECT(i[sfNFTokens.jsonName].size() == 32);
                         ++pageCount;
                     }
                 }
@@ -459,11 +456,11 @@ class NFTokenBurn_test : public beast::unit_test::Suite
             {
                 json::Value jrr = env.rpc("json", "ledger_data", to_string(jvParams));
 
-                json::Value& state = jrr[jss::result][jss::state];
+                json::Value const& state = jrr[jss::result][jss::state];
 
-                for (json::UInt i = 0; i < state.size(); ++i)
+                for (auto const& i : state)
                 {
-                    BEAST_EXPECT(!state[i].isMember(sfNFTokens.jsonName));
+                    BEAST_EXPECT(!i.isMember(sfNFTokens.jsonName));
                 }
             }
         };
@@ -757,8 +754,8 @@ class NFTokenBurn_test : public beast::unit_test::Suite
             // We're going to fire an Invariant failure that is difficult to
             // cause.  We do it here because the tools are here.
             //
-            // See Invariants_test.cpp for examples of other invariant tests
-            // that this one is modeled after.
+            // See InvariantsMisc_test.cpp for examples of other invariant
+            // tests that this one is modeled after.
 
             // Generate three closely packed NFTokenPages.
             std::vector nfts = genPackedTokens();
@@ -795,7 +792,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
                 TER terActual = tesSUCCESS;
                 for (TER const& terExpect : {TER(tecINVARIANT_FAILED), TER(tefINVARIANT_FAILED)})
                 {
-                    terActual = ac.checkInvariants(terActual, XRPAmount{});
+                    terActual = xrpl::checkInvariants(ac, terActual, XRPAmount{});
                     BEAST_EXPECT(terExpect == terActual);
                     BEAST_EXPECT(sink.messages().str().starts_with("Invariant failed:"));
                     // uncomment to log the invariant failure message
@@ -831,7 +828,7 @@ class NFTokenBurn_test : public beast::unit_test::Suite
                 TER terActual = tesSUCCESS;
                 for (TER const& terExpect : {TER(tecINVARIANT_FAILED), TER(tefINVARIANT_FAILED)})
                 {
-                    terActual = ac.checkInvariants(terActual, XRPAmount{});
+                    terActual = xrpl::checkInvariants(ac, terActual, XRPAmount{});
                     BEAST_EXPECT(terExpect == terActual);
                     BEAST_EXPECT(sink.messages().str().starts_with("Invariant failed:"));
                     // uncomment to log the invariant failure message
@@ -1076,12 +1073,11 @@ class NFTokenBurn_test : public beast::unit_test::Suite
                 json::Value& state = jrr[jss::result][jss::state];
 
                 int pageCount = 0;
-                for (json::UInt i = 0; i < state.size(); ++i)
+                for (auto& i : state)
                 {
-                    if (state[i].isMember(sfNFTokens.jsonName) &&
-                        state[i][sfNFTokens.jsonName].isArray())
+                    if (i.isMember(sfNFTokens.jsonName) && i[sfNFTokens.jsonName].isArray())
                     {
-                        BEAST_EXPECT(state[i][sfNFTokens.jsonName].size() == 32);
+                        BEAST_EXPECT(i[sfNFTokens.jsonName].size() == 32);
                         ++pageCount;
                     }
                 }
diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp
index c6d2283775..d7caa2d514 100644
--- a/src/test/app/NFToken_test.cpp
+++ b/src/test/app/NFToken_test.cpp
@@ -30,6 +30,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -37,6 +38,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -4791,6 +4793,87 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         checkOffers("nft_buy_offers", 501, 2, __LINE__);
     }
 
+    void
+    testNftXxxOffersMarkerWrongSide(FeatureBitset features)
+    {
+        // A pagination marker passed to nft_buy_offers / nft_sell_offers must
+        // reference an offer on the same side (buy vs. sell) as the directory
+        // being enumerated.  A wrong-side marker is rejected with invalidParams.
+        //
+        // Note: the pre-fix code also returned invalidParams for a wrong-side
+        // marker, but only after scanning the entire target directory (an
+        // O(directory size) walk usable to burn CPU).  The fix short-circuits
+        // that scan.  The scan-avoidance is not observable from the RPC
+        // response, so this test locks the rejection contract (wrong-side ->
+        // error, same-side -> success) rather than the performance property.
+        testcase("nft_buy_offers and nft_sell_offers wrong-side marker");
+
+        using namespace test::jtx;
+
+        Env env{*this, features};
+
+        Account const issuer{"issuer"};
+        Account const buyer{"buyer"};
+
+        env.fund(XRP(10000), issuer, buyer);
+        env.close();
+
+        // Mint a transferable NFT.
+        uint256 const nftID{token::getNextID(env, issuer, 0u, tfTransferable)};
+        env(token::mint(issuer, 0), Txflags(tfTransferable));
+        env.close();
+
+        // Create one sell offer (from the issuer, who owns the NFT) and one
+        // buy offer (from the buyer) for the same NFT.
+        env(token::createOffer(issuer, nftID, XRP(100)), Txflags(tfSellNFToken));
+        env(token::createOffer(buyer, nftID, XRP(50)), token::Owner(issuer));
+        env.close();
+
+        // Grab the index of the single offer on each side from the RPC
+        // response so we can use it as a marker.
+        auto firstOfferIndex = [this, &env, &nftID](char const* request) {
+            json::Value params;
+            params[jss::nft_id] = to_string(nftID);
+            json::Value const result = env.rpc("json", request, to_string(params))[jss::result];
+            BEAST_EXPECT(result.isMember(jss::offers) && result[jss::offers].size() == 1);
+            return result[jss::offers][0u][jss::nft_offer_index].asString();
+        };
+
+        std::string const sellOfferIndex = firstOfferIndex("nft_sell_offers");
+        std::string const buyOfferIndex = firstOfferIndex("nft_buy_offers");
+
+        auto queryWithMarker = [&env, &nftID](char const* request, std::string const& marker) {
+            json::Value params;
+            params[jss::nft_id] = to_string(nftID);
+            params[jss::marker] = marker;
+            return env.rpc("json", request, to_string(params))[jss::result];
+        };
+
+        // A marker referencing an offer on the wrong side is rejected with
+        // invalidParams.
+        {
+            // Sell-side marker passed to nft_buy_offers.
+            json::Value const result = queryWithMarker("nft_buy_offers", sellOfferIndex);
+            BEAST_EXPECT(result[jss::error].asString() == "invalidParams");
+        }
+        {
+            // Buy-side marker passed to nft_sell_offers.
+            json::Value const result = queryWithMarker("nft_sell_offers", buyOfferIndex);
+            BEAST_EXPECT(result[jss::error].asString() == "invalidParams");
+        }
+
+        // A same-side marker is still accepted.  With a single offer on each
+        // side, resuming after it simply yields no further offers.
+        {
+            json::Value const result = queryWithMarker("nft_buy_offers", buyOfferIndex);
+            BEAST_EXPECT(!result.isMember(jss::error));
+        }
+        {
+            json::Value const result = queryWithMarker("nft_sell_offers", sellOfferIndex);
+            BEAST_EXPECT(!result.isMember(jss::error));
+        }
+    }
+
     void
     testNFTokenNegOffer(FeatureBitset features)
     {
@@ -6245,84 +6328,162 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         env.fund(XRP(10000), alice, bob, broker);
         env.close();
 
-        // Verify `nftoken_id` value equals to the NFTokenID that was
-        // changed in the most recent NFTokenMint or NFTokenAcceptOffer
-        // transaction
-        auto verifyNFTokenID = [&](uint256 const& actualNftID) {
+        // Transaction metadata is not always reported under the same field
+        // name: the `ledger` RPC uses `metaData`, the others use `meta`.
+        auto const getMeta = [](json::Value const& tx) -> json::Value const* {
+            if (tx.isMember(jss::meta))
+                return &tx[jss::meta];
+            if (tx.isMember(jss::metaData))
+                return &tx[jss::metaData];
+            return nullptr;
+        };
+
+        // Neither is the transaction hash: api_version 1 nests the
+        // transaction under `tx`, later versions use `tx_json`, and some
+        // responses put the hash on the entry itself.
+        auto const getHash = [](json::Value const& entry) -> std::string {
+            if (entry.isMember(jss::tx) && entry[jss::tx].isMember(jss::hash))
+                return entry[jss::tx][jss::hash].asString();
+            if (entry.isMember(jss::tx_json) && entry[jss::tx_json].isMember(jss::hash))
+                return entry[jss::tx_json][jss::hash].asString();
+            return entry[jss::hash].asString();
+        };
+
+        // Run `verifyMeta` against the metadata of the most recent
+        // transaction as reported by the `tx`, `ledger` and `account_tx`
+        // RPCs, so that the synthetic fields are checked in every response
+        // that carries them. Runs under both api_version 1 (`tx`/`meta`)
+        // and the latest api_version (`tx_json`/synthetic fields alongside
+        // it), since the two versions place fields differently.
+        auto verifyMetaInAllResponses = [&](auto verifyMeta) {
             // Get the hash for the most recent transaction.
             std::string const txHash{
                 env.tx()->getJson(JsonOptions::Values::None)[jss::hash].asString()};
 
             env.close();
-            json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta];
 
-            // Expect nftokens_id field
-            if (!BEAST_EXPECT(meta.isMember(jss::nftoken_id)))
-                return;
+            for (unsigned const apiVersion :
+                 {unsigned{rpc::kApiMinimumSupportedVersion},
+                  unsigned{rpc::kApiMaximumSupportedVersion}})
+            {
+                // Test 1: Check tx RPC response
+                json::Value const txResult = env.rpc(apiVersion, "tx", txHash)[jss::result];
+                verifyMeta(txResult[jss::meta]);
 
-            // Check the value of NFT ID in the meta with the
-            // actual value
-            uint256 nftID;
-            BEAST_EXPECT(nftID.parseHex(meta[jss::nftoken_id].asString()));
-            BEAST_EXPECT(nftID == actualNftID);
+                // Test 2: Check ledger RPC response with expanded
+                // transactions
+                json::Value ledgerParams;
+                ledgerParams[jss::ledger_index] = txResult[jss::ledger_index].asUInt();
+                ledgerParams[jss::transactions] = true;
+                ledgerParams[jss::expand] = true;
+
+                auto const ledgerResult =
+                    env.rpc(apiVersion, "json", "ledger", to_string(ledgerParams));
+                auto const& ledgerTx =
+                    ledgerResult[jss::result][jss::ledger][jss::transactions][0u];
+
+                // Verify transaction hash matches
+                BEAST_EXPECT(getHash(ledgerTx) == txHash);
+
+                if (auto const* meta = getMeta(ledgerTx); BEAST_EXPECT(meta != nullptr))
+                    verifyMeta(*meta);
+
+                // Test 3: Check account_tx RPC response
+                // The transaction is not necessarily alice's, so query
+                // account_tx for the account that actually submitted it.
+                json::Value accountTxParams;
+                accountTxParams[jss::account] = txResult.isMember(jss::tx_json)
+                    ? txResult[jss::tx_json][jss::Account].asString()
+                    : txResult[jss::Account].asString();
+
+                auto const accountTxResult =
+                    env.rpc(apiVersion, "json", "account_tx", to_string(accountTxParams));
+
+                // account_tx ordering is not guaranteed, so find our
+                // transaction by hash rather than assuming it is the most
+                // recent one.
+                json::Value const* accountTx = nullptr;
+                for (auto const& entry : accountTxResult[jss::result][jss::transactions])
+                {
+                    if (getHash(entry) == txHash)
+                    {
+                        accountTx = &entry;
+                        break;
+                    }
+                }
+
+                if (!BEAST_EXPECT(accountTx != nullptr))
+                    continue;
+
+                if (auto const* meta = getMeta(*accountTx); BEAST_EXPECT(meta != nullptr))
+                    verifyMeta(*meta);
+            }
+        };
+
+        // Verify `nftoken_id` value equals to the NFTokenID that was
+        // changed in the most recent NFTokenMint or NFTokenAcceptOffer
+        // transaction
+        auto verifyNFTokenID = [&](uint256 const& actualNftID) {
+            verifyMetaInAllResponses([&](json::Value const& meta) {
+                // Expect nftoken_id field
+                if (!BEAST_EXPECT(meta.isMember(jss::nftoken_id)))
+                    return;
+
+                // Check the value of NFT ID matches
+                uint256 nftID;
+                BEAST_EXPECT(nftID.parseHex(meta[jss::nftoken_id].asString()));
+                BEAST_EXPECT(nftID == actualNftID);
+            });
         };
 
         // Verify `nftoken_ids` value equals to the NFTokenIDs that were
         // changed in the most recent NFTokenCancelOffer transaction
         auto verifyNFTokenIDsInCancelOffer = [&](std::vector actualNftIDs) {
-            // Get the hash for the most recent transaction.
-            std::string const txHash{
-                env.tx()->getJson(JsonOptions::Values::None)[jss::hash].asString()};
-
-            env.close();
-            json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta];
-
-            // Expect nftokens_ids field and verify the values
-            if (!BEAST_EXPECT(meta.isMember(jss::nftoken_ids)))
-                return;
-
-            // Convert NFT IDs from json::Value to uint256
-            std::vector metaIDs;
-            std::transform(
-                meta[jss::nftoken_ids].begin(),
-                meta[jss::nftoken_ids].end(),
-                std::back_inserter(metaIDs),
-                [this](json::Value id) {
-                    uint256 nftID;
-                    BEAST_EXPECT(nftID.parseHex(id.asString()));
-                    return nftID;
-                });
-
-            // Sort both array to prepare for comparison
-            std::ranges::sort(metaIDs);
+            // Sort to prepare for comparison
             std::ranges::sort(actualNftIDs);
 
-            // Make sure the expect number of NFTs is correct
-            BEAST_EXPECT(metaIDs.size() == actualNftIDs.size());
+            verifyMetaInAllResponses([&](json::Value const& meta) {
+                // Expect nftoken_ids field and verify the values
+                if (!BEAST_EXPECT(meta.isMember(jss::nftoken_ids)))
+                    return;
 
-            // Check the value of NFT ID in the meta with the
-            // actual values
-            for (size_t i = 0; i < metaIDs.size(); ++i)
-                BEAST_EXPECT(metaIDs[i] == actualNftIDs[i]);
+                // Convert NFT IDs from json::Value to uint256
+                std::vector metaIDs;
+                std::transform(
+                    meta[jss::nftoken_ids].begin(),
+                    meta[jss::nftoken_ids].end(),
+                    std::back_inserter(metaIDs),
+                    [this](json::Value id) {
+                        uint256 nftID;
+                        BEAST_EXPECT(nftID.parseHex(id.asString()));
+                        return nftID;
+                    });
+
+                std::ranges::sort(metaIDs);
+
+                // Make sure the expect number of NFTs is correct
+                if (!BEAST_EXPECT(metaIDs.size() == actualNftIDs.size()))
+                    return;
+
+                // Check the value of NFT ID in the meta with the
+                // actual values
+                for (size_t i = 0; i < metaIDs.size(); ++i)
+                    BEAST_EXPECT(metaIDs[i] == actualNftIDs[i]);
+            });
         };
 
         // Verify `offer_id` value equals to the offerID that was
         // changed in the most recent NFTokenCreateOffer tx
         auto verifyNFTokenOfferID = [&](uint256 const& offerID) {
-            // Get the hash for the most recent transaction.
-            std::string const txHash{
-                env.tx()->getJson(JsonOptions::Values::None)[jss::hash].asString()};
+            verifyMetaInAllResponses([&](json::Value const& meta) {
+                // Expect offer_id field and verify the value
+                if (!BEAST_EXPECT(meta.isMember(jss::offer_id)))
+                    return;
 
-            env.close();
-            json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta];
-
-            // Expect offer_id field and verify the value
-            if (!BEAST_EXPECT(meta.isMember(jss::offer_id)))
-                return;
-
-            uint256 metaOfferID;
-            BEAST_EXPECT(metaOfferID.parseHex(meta[jss::offer_id].asString()));
-            BEAST_EXPECT(metaOfferID == offerID);
+                uint256 metaOfferID;
+                BEAST_EXPECT(metaOfferID.parseHex(meta[jss::offer_id].asString()));
+                BEAST_EXPECT(metaOfferID == offerID);
+            });
         };
 
         // Check new fields in tx meta when for all NFTtransactions
@@ -7275,6 +7436,127 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testCreateOfferInvalidAmount(FeatureBitset features)
+    {
+        testcase("Invalid NFT offer create amount");
+
+        using namespace test::jtx;
+
+        // Before fixCleanup3_4_0, a fake-XRP offer amount (an IOU using the
+        // "XRP" currency code) is not rejected in preflight. With the amendment
+        // enabled, preflight rejects it with temBAD_CURRENCY.
+        for (bool const withFix : {false, true})
+        {
+            Env env{*this, withFix ? features | fixCleanup3_4_0 : features - fixCleanup3_4_0};
+
+            Account const alice{"alice"};
+            Account const gw{"gw"};
+
+            env.fund(XRP(1000), alice, gw);
+            env.close();
+
+            uint256 const nftID = token::getNextID(env, alice, 0, tfTransferable);
+            env(token::mint(alice, 0u), Txflags(tfTransferable));
+            env.close();
+
+            // Fake XRP (an IOU using the "XRP" currency code) sell offer
+            // amount.
+            auto const bad = IOU(gw, badCurrency());
+            env(token::createOffer(alice, nftID, bad(1)),
+                Txflags(tfSellNFToken),
+                Ter(withFix ? TER{temBAD_CURRENCY} : TER{tesSUCCESS}));
+            env.close();
+        }
+    }
+
+    void
+    testAcceptOfferInvalidBrokerFee(FeatureBitset features)
+    {
+        testcase("Invalid NFT offer accept broker fee");
+
+        using namespace test::jtx;
+
+        // Before fixCleanup3_4_0, a fake-XRP broker fee (an IOU using the "XRP"
+        // currency code) is not rejected in preflight and reaches later offer
+        // validation instead. With the amendment enabled, preflight rejects it
+        // with temBAD_CURRENCY.
+        for (bool const withFix : {false, true})
+        {
+            Env env{*this, withFix ? features | fixCleanup3_4_0 : features - fixCleanup3_4_0};
+
+            Account const alice{"alice"};
+            Account const buyer{"buyer"};
+            Account const broker{"broker"};
+            Account const gw{"gw"};
+
+            env.fund(XRP(1000), alice, buyer, broker, gw);
+            env.close();
+
+            uint256 const nftID = token::getNextID(env, alice, 0, tfTransferable);
+            env(token::mint(alice, 0u), Txflags(tfTransferable));
+            env.close();
+
+            uint256 const sellOfferIndex =
+                keylet::nftokenOffer(alice, SeqProxy::rawSequence(env.seq(alice))).key;
+            env(token::createOffer(alice, nftID, XRP(10)), Txflags(tfSellNFToken));
+            env.close();
+
+            uint256 const buyOfferIndex =
+                keylet::nftokenOffer(buyer, SeqProxy::rawSequence(env.seq(buyer))).key;
+            env(token::createOffer(buyer, nftID, XRP(40)), token::Owner(alice));
+            env.close();
+
+            // Fake XRP (an IOU using the "XRP" currency code) broker fee.
+            auto const bad = IOU(gw, badCurrency());
+            env(token::brokerOffers(broker, buyOfferIndex, sellOfferIndex),
+                token::BrokerFee(bad(1)),
+                Ter(withFix ? TER{temBAD_CURRENCY} : TER{tecNFTOKEN_BUY_SELL_MISMATCH}));
+            env.close();
+        }
+    }
+
+    void
+    testCreateOfferIouIssuerGlobalFreeze(FeatureBitset features)
+    {
+        testcase("Create NFT offer by IOU issuer under global freeze");
+
+        using namespace test::jtx;
+
+        // Before fixCleanup3_4_0, an IOU issuer that has set a global freeze on
+        // their own currency cannot create an NFToken offer denominated in that
+        // currency; the offer is rejected with tecFROZEN.  With the amendment
+        // enabled, the issuer is not subject to their own global freeze when the
+        // offer is denominated in their own IOU (e.g. to receive their own
+        // transfer fees), so the offer succeeds.
+        for (bool const withFix : {false, true})
+        {
+            Env env{*this, withFix ? features | fixCleanup3_4_0 : features - fixCleanup3_4_0};
+
+            Account const issuer{"issuer"};
+            IOU const isISU(issuer["ISU"]);
+
+            env.fund(XRP(1000), issuer);
+            env.close();
+
+            // issuer mints a transferable NFToken.
+            uint256 const nftID = token::getNextID(env, issuer, 0, tfTransferable);
+            env(token::mint(issuer, 0u), Txflags(tfTransferable));
+            env.close();
+
+            // issuer sets a global freeze on their own IOU.
+            env(fset(issuer, asfGlobalFreeze));
+            env.close();
+
+            // issuer creates a sell offer for the NFToken denominated in their
+            // own (globally frozen) IOU.
+            env(token::createOffer(issuer, nftID, isISU(100)),
+                Txflags(tfSellNFToken),
+                Ter(withFix ? TER{tesSUCCESS} : TER{tecFROZEN}));
+            env.close();
+        }
+    }
+
 protected:
     FeatureBitset const allFeatures_{test::jtx::testableAmendments()};
 
@@ -7306,6 +7588,7 @@ protected:
         testNFTokenWithTickets(features);
         testNFTokenDeleteAccount(features);
         testNftXxxOffers(features);
+        testNftXxxOffersMarkerWrongSide(features);
         testNFTokenNegOffer(features);
         testIOUWithTransferFee(features);
         testBrokeredSaleToSelf(features);
@@ -7316,6 +7599,9 @@ protected:
         testUnaskedForAutoTrustline(features);
         testNFTIssuerIsIOUIssuer(features);
         testNFTokenModify(features);
+        testCreateOfferInvalidAmount(features);
+        testAcceptOfferInvalidBrokerFee(features);
+        testCreateOfferIouIssuerGlobalFreeze(features);
     }
 
 public:
diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp
index d03b1b8e93..80541480e8 100644
--- a/src/test/app/OfferMPT_test.cpp
+++ b/src/test/app/OfferMPT_test.cpp
@@ -1,3 +1,5 @@
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -5,6 +7,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -21,11 +24,14 @@
 #include 
 #include 
 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -35,6 +41,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -46,6 +53,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -609,6 +617,267 @@ public:
         testHelper2TokensMix(test);
     }
 
+    void
+    testMPTIssuerOfferUsesRemainingCapacity(FeatureBitset features)
+    {
+        testcase("MPT issuer offer dust removal uses remaining issuance capacity");
+
+        using namespace jtx;
+
+        Account const issuer{"issuer"};
+        Account const carol{"carol"};
+        Account const bob{"bob"};
+
+        Env env{*this, features};
+        env.fund(XRP(10'000), issuer, carol, bob);
+        env.close();
+
+        MPTTester const musd(
+            {.env = env, .issuer = issuer, .holders = {carol, bob}, .maxAmt = 101});
+
+        // The issuer offer is fully fundable when placed. Later issuance leaves
+        // only one MPT of remaining capacity, so this issuer-owned MPT offer
+        // must be clipped by owner funds just like a holder-funded offer.
+        auto const issuerOfferSeq = env.seq(issuer);
+        env(offer(issuer, drops(1), musd(100)));
+        env.close();
+
+        env(pay(issuer, carol, musd(100)));
+        env.close();
+        BEAST_EXPECT(env.balance(issuer, musd) == musd(-100));
+        BEAST_EXPECT(env.balance(carol, musd) == musd(100));
+
+        // Carol's same-quality offer provides the legitimately funded side of
+        // the crossing. Without the issuer-cap dust-removal check, Bob would
+        // receive Carol's 100 MPT plus one free self-issued MPT from issuer's
+        // stale offer while paying only Carol's one drop.
+        auto const carolOfferSeq = env.seq(carol);
+        env(offer(carol, drops(1), musd(100)));
+        env.close();
+
+        auto const issuerOffer = keylet::offer(issuer.id(), SeqProxy::rawSequence(issuerOfferSeq));
+        auto const carolOffer = keylet::offer(carol.id(), SeqProxy::rawSequence(carolOfferSeq));
+        BEAST_EXPECT(env.le(issuerOffer) != nullptr);
+        BEAST_EXPECT(env.le(carolOffer) != nullptr);
+
+        env(offer(bob, musd(101), drops(2), tfImmediateOrCancel));
+        env.close();
+
+        BEAST_EXPECT(env.le(issuerOffer) == nullptr);
+        BEAST_EXPECT(env.le(carolOffer) == nullptr);
+        env.require(offers(issuer, 0), offers(carol, 0), offers(bob, 0));
+        BEAST_EXPECT(env.balance(issuer, musd) == musd(-100));
+        BEAST_EXPECT(env.balance(carol, musd) == musd(0));
+        BEAST_EXPECT(env.balance(bob, musd) == musd(100));
+    }
+
+    void
+    testPartiallyFundedMPTInputOfferZeroInput(FeatureBitset features)
+    {
+        using namespace jtx;
+        auto const alice = Account{"alice"};
+        auto const bob = Account{"bob"};
+
+        {
+            testcase("Partially funded MPT/XRP input offer cannot be consumed for free");
+
+            Env env{*this, features};
+            auto const gw = Account{"gw"};
+
+            env.fund(XRP(10'000), gw, alice, bob);
+            env.close();
+
+            MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}});
+
+            auto const aliceOfferSeq = env.seq(alice);
+            env(offer(alice, usd(1), drops(1'000'000)));
+            env.close();
+
+            auto const targetBalance = reserve(env, 2) + drops(999'999);
+            auto const drain = env.balance(alice).value().xrp() - targetBalance.value().xrp() -
+                env.current()->fees().base;
+            env(pay(alice, gw, drops(drain)));
+            env.close();
+
+            auto const aliceXRPBefore = env.balance(alice);
+            auto const bobXRPBefore = env.balance(bob);
+
+            env(pay(gw, bob, drops(1'000'000)),
+                Sendmax(usd(1)),
+                Path(~XRP),
+                Txflags(tfNoRippleDirect | tfPartialPayment),
+                Ter(tecPATH_DRY));
+            env.close();
+
+            // alice's offer sells 1,000,000 drops for usd(1) but she can fund
+            // only 999,999. Filling the clipped remainder would require a
+            // fractional usd (MPT) input that rounds down to zero, so without
+            // the fix the taker could take the funded drops for free.
+            // shouldRmSmallIncreasedQOffer() now treats the MPT input as
+            // integral (like XRP) and removes the degraded offer, so the
+            // payment goes dry. The removal happens only inside the crossing:
+            // tecPATH_DRY discards everything but the fee, so the offer itself
+            // stays in the ledger, unconsumed.
+            BEAST_EXPECT(
+                env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(aliceOfferSeq))) != nullptr);
+            BEAST_EXPECT(env.balance(alice) == aliceXRPBefore);
+            BEAST_EXPECT(env.balance(bob) == bobXRPBefore);
+        }
+
+        {
+            testcase("Partially funded MPT/IOU input offer cannot be consumed for free");
+
+            Env env{*this, features};
+            auto const mptIssuer = Account{"mptIssuer"};
+            auto const iouIssuer = Account{"iouIssuer"};
+
+            env.fund(XRP(10'000), mptIssuer, iouIssuer, alice, bob);
+            env.close();
+
+            auto const eur = iouIssuer["EUR"];
+            env.trust(eur(100), alice, bob);
+            env(pay(iouIssuer, alice, eur(0.5)));
+            env.close();
+
+            MPTTester const usd({.env = env, .issuer = mptIssuer, .holders = {alice}});
+
+            auto const aliceOfferSeq = env.seq(alice);
+            env(offer(alice, usd(1), eur(1)));
+            env.close();
+
+            auto const aliceEURBefore = env.balance(alice, eur);
+            auto const bobEURBefore = env.balance(bob, eur);
+
+            env(pay(mptIssuer, bob, eur(1)),
+                Sendmax(usd(1)),
+                Path(~eur),
+                Txflags(tfNoRippleDirect | tfPartialPayment),
+                Ter(tecPATH_DRY));
+            env.close();
+
+            // Same zero-input regression as the MPT/XRP case above, but with
+            // an IOU (eur) output leg: the fractional usd (MPT) input rounds
+            // to zero. The degraded offer is removed during crossing, the
+            // payment goes dry, and tecPATH_DRY leaves the offer in the ledger.
+            BEAST_EXPECT(
+                env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(aliceOfferSeq))) != nullptr);
+            BEAST_EXPECT(env.balance(alice, eur) == aliceEURBefore);
+            BEAST_EXPECT(env.balance(bob, eur) == bobEURBefore);
+        }
+
+        {
+            testcase("Partially funded MPT/MPT input offer cannot be consumed for free");
+
+            Env env{*this, features};
+            auto const issuerA = Account{"issuerA"};
+            auto const issuerB = Account{"issuerB"};
+
+            env.fund(XRP(10'000), issuerA, issuerB, alice, bob);
+            env.close();
+
+            MPTTester const usd({.env = env, .issuer = issuerA, .holders = {alice}});
+            MPTTester const eur({.env = env, .issuer = issuerB, .holders = {alice, bob}});
+
+            env(pay(issuerB, alice, eur(999'999)));
+            env.close();
+
+            auto const aliceOfferSeq = env.seq(alice);
+            env(offer(alice, usd(1), eur(1'000'000)));
+            env.close();
+
+            auto const aliceEURBefore = eur.getBalance(alice);
+            auto const bobEURBefore = eur.getBalance(bob);
+
+            env(pay(issuerA, bob, eur(1'000'000)),
+                Sendmax(usd(1)),
+                Path(~eur),
+                Txflags(tfNoRippleDirect | tfPartialPayment),
+                Ter(tecPATH_DRY));
+            env.close();
+
+            // Same zero-input regression as above, but with both legs MPT: the
+            // fractional usd (MPT) input rounds to zero. The degraded offer is
+            // removed during crossing, the payment goes dry, and tecPATH_DRY
+            // leaves the offer in the ledger.
+            BEAST_EXPECT(
+                env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(aliceOfferSeq))) != nullptr);
+            BEAST_EXPECT(env.balance(alice, eur) == eur(aliceEURBefore));
+            BEAST_EXPECT(env.balance(bob, eur) == eur(bobEURBefore));
+        }
+
+        {
+            // The dry cases above never observe the degraded offer actually
+            // being removed, because tecPATH_DRY rolls the removal back. Here a
+            // second, fully funded offer lets the crossing succeed, so the
+            // removal persists: alice's degraded offer is deleted from the
+            // book (not taken for free) while carol's good offer fills.
+            testcase(
+                "Partially funded MPT input offer is removed, not consumed, "
+                "when a funded offer crosses");
+
+            Env env{*this, features};
+            auto const gw = Account{"gw"};
+            auto const carol = Account{"carol"};
+
+            env.fund(XRP(10'000), gw, alice, carol, bob);
+            env.close();
+
+            MPTTester const usd({.env = env, .issuer = gw, .holders = {alice, carol, bob}});
+
+            // alice's offer sells 1,000,000 drops for usd(1) but, as in the
+            // dry cases above, she can fund only 999,999 drops, so filling the
+            // clipped remainder would require a fractional usd (MPT) input that
+            // rounds down to zero.
+            auto const aliceOfferSeq = env.seq(alice);
+            env(offer(alice, usd(1), drops(1'000'000)));
+            env.close();
+
+            auto const targetBalance = reserve(env, 2) + drops(999'999);
+            auto const drain = env.balance(alice).value().xrp() - targetBalance.value().xrp() -
+                env.current()->fees().base;
+            env(pay(alice, gw, drops(drain)));
+            env.close();
+
+            // carol's same-quality offer is fully funded and provides the
+            // legitimate side of the crossing.
+            auto const carolOfferSeq = env.seq(carol);
+            env(offer(carol, usd(1), drops(1'000'000)));
+            env.close();
+
+            // bob needs usd to buy drops.
+            env(pay(gw, bob, usd(2)));
+            env.close();
+
+            auto const aliceOffer = keylet::offer(alice.id(), SeqProxy::rawSequence(aliceOfferSeq));
+            auto const carolOffer = keylet::offer(carol.id(), SeqProxy::rawSequence(carolOfferSeq));
+            BEAST_EXPECT(env.le(aliceOffer) != nullptr);
+            BEAST_EXPECT(env.le(carolOffer) != nullptr);
+
+            auto const aliceXRPBefore = env.balance(alice);
+            auto const bobXRPBefore = env.balance(bob);
+
+            // bob buys drops with usd, wanting more than carol alone supplies so
+            // the crossing also reaches alice's offer. carol's offer fills;
+            // alice's degraded offer is removed rather than taken for free, so
+            // bob receives only carol's 1,000,000 drops and pays only usd(1).
+            env(offer(bob, drops(2'000'000), usd(2), tfImmediateOrCancel));
+            env.close();
+
+            BEAST_EXPECT(env.le(aliceOffer) == nullptr);
+            BEAST_EXPECT(env.le(carolOffer) == nullptr);
+            env.require(offers(alice, 0), offers(carol, 0), offers(bob, 0));
+
+            // alice's offer was removed, not consumed: her balances are
+            // unchanged and none of her funded 999'999 drops leaked to bob.
+            BEAST_EXPECT(env.balance(alice) == aliceXRPBefore);
+            BEAST_EXPECT(env.balance(alice, usd) == usd(0));
+            BEAST_EXPECT(env.balance(carol, usd) == usd(1));
+            BEAST_EXPECT(env.balance(bob, usd) == usd(1));
+            BEAST_EXPECT(
+                env.balance(bob) == bobXRPBefore + drops(1'000'000) - env.current()->fees().base);
+        }
+    }
+
     void
     testInsufficientReserve(FeatureBitset features)
     {
@@ -947,6 +1216,161 @@ public:
         }
     }
 
+    void
+    testMPTAMMLimitQualityRounding(FeatureBitset features)
+    {
+        testcase("MPT AMM limitQuality checks rounded integral output");
+
+        using namespace jtx;
+
+        Account const gw{"gateway"};
+        Account const alice{"alice"};
+        Account const bob{"bob"};
+
+        // IOC used to reject the AMM strand with tecKILLED.  The continuous
+        // limitQuality target is about 32.88 MPT; rounding to nearest requested
+        // 33 MPT and made the realized AMM quality miss Bob's limit.  The
+        // discrete fallback takes the largest satisfying integer output: 32.
+        {
+            Env env{*this, features};
+
+            env.fund(XRP(10'000), gw, alice, bob);
+            env.close();
+
+            MPTTester const btc(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {alice, bob},
+                 .pay = 100'000,
+                 .flags = kMptDexFlags});
+            AMM const amm(env, alice, XRP(100), btc(1'000));
+
+            auto const bobBTCBefore = btc.getBalance(bob);
+            auto const [xrpBefore, btcBefore, lpBefore] = amm.balances();
+
+            env(offer(bob, btc(100), drops(10'340'000)), Txflags(tfImmediateOrCancel));
+            env.close();
+
+            auto const [xrpAfter, btcAfter, lpAfter] = amm.balances();
+            BEAST_EXPECT(btc.getBalance(bob) == bobBTCBefore + 32);
+            BEAST_EXPECT(xrpAfter > xrpBefore);
+            BEAST_EXPECT(btcAfter < btcBefore);
+            BEAST_EXPECT(lpAfter == lpBefore);
+            BEAST_EXPECT(expectOffers(env, bob, 0));
+        }
+
+        // A standard OfferCreate at the same limit used to bypass the AMM and
+        // rest unchanged on the book.  It should now take the largest
+        // satisfying 32-MPT AMM fill first, then leave only the remainder on
+        // the book.
+        {
+            Env env{*this, features};
+
+            env.fund(XRP(10'000), gw, alice, bob);
+            env.close();
+
+            MPTTester const btc(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {alice, bob},
+                 .pay = 100'000,
+                 .flags = kMptDexFlags});
+            AMM const amm(env, alice, XRP(100), btc(1'000));
+
+            auto const bobBTCBefore = btc.getBalance(bob);
+            auto const [xrpBefore, btcBefore, lpBefore] = amm.balances();
+
+            env(offer(bob, btc(100), drops(10'340'000)));
+            env.close();
+
+            auto const [xrpAfter, btcAfter, lpAfter] = amm.balances();
+            BEAST_EXPECT(btc.getBalance(bob) == bobBTCBefore + 32);
+            BEAST_EXPECT(xrpAfter > xrpBefore);
+            BEAST_EXPECT(btcAfter < btcBefore);
+            BEAST_EXPECT(lpAfter == lpBefore);
+            BEAST_EXPECT(expectOffers(env, bob, 1));
+
+            auto const bobOffers = offersOnAccount(env, bob);
+            if (BEAST_EXPECT(bobOffers.size() == 1))
+            {
+                BEAST_EXPECT((*bobOffers[0])[sfTakerPays] != btc(100));
+                BEAST_EXPECT((*bobOffers[0])[sfTakerGets] != drops(10'340'000));
+            }
+        }
+
+        // Mirror the IOC case with the integral output flipped from MPT units
+        // to XRP drops.  The same continuous target (~32.88) used to round up
+        // to 33 drops and miss limitQuality; the discrete fallback allows the
+        // largest satisfying 32-drop AMM fill.
+        {
+            Env env{*this, features};
+
+            env.fund(XRP(10'000), gw, alice, bob);
+            env.close();
+
+            MPTTester const btc(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {alice, bob},
+                 .pay = 200'000'000,
+                 .flags = kMptDexFlags});
+            AMM const amm(env, alice, drops(1'000), btc(100'000'000));
+
+            auto const bobXRPBefore = env.balance(bob, XRP);
+            auto const baseFee = env.current()->fees().base;
+            auto const [xrpBefore, btcBefore, lpBefore] = amm.balances();
+
+            env(offer(bob, drops(100), btc(10'340'000)), Txflags(tfImmediateOrCancel));
+            env.close();
+
+            auto const [xrpAfter, btcAfter, lpAfter] = amm.balances();
+            env.require(Balance(bob, bobXRPBefore + drops(32) - baseFee));
+            BEAST_EXPECT(xrpAfter < xrpBefore);
+            BEAST_EXPECT(btcAfter > btcBefore);
+            BEAST_EXPECT(lpAfter == lpBefore);
+            BEAST_EXPECT(expectOffers(env, bob, 0));
+        }
+
+        // Mirror the standard OfferCreate case as well.  It should consume the
+        // largest satisfying 32-drop AMM fill before leaving only the remainder
+        // on the book.
+        {
+            Env env{*this, features};
+
+            env.fund(XRP(10'000), gw, alice, bob);
+            env.close();
+
+            MPTTester const btc(
+                {.env = env,
+                 .issuer = gw,
+                 .holders = {alice, bob},
+                 .pay = 200'000'000,
+                 .flags = kMptDexFlags});
+            AMM const amm(env, alice, drops(1'000), btc(100'000'000));
+
+            auto const bobXRPBefore = env.balance(bob, XRP);
+            auto const baseFee = env.current()->fees().base;
+            auto const [xrpBefore, btcBefore, lpBefore] = amm.balances();
+
+            env(offer(bob, drops(100), btc(10'340'000)));
+            env.close();
+
+            auto const [xrpAfter, btcAfter, lpAfter] = amm.balances();
+            env.require(Balance(bob, bobXRPBefore + drops(32) - baseFee));
+            BEAST_EXPECT(xrpAfter < xrpBefore);
+            BEAST_EXPECT(btcAfter > btcBefore);
+            BEAST_EXPECT(lpAfter == lpBefore);
+            BEAST_EXPECT(expectOffers(env, bob, 1));
+
+            auto const bobOffers = offersOnAccount(env, bob);
+            if (BEAST_EXPECT(bobOffers.size() == 1))
+            {
+                BEAST_EXPECT((*bobOffers[0])[sfTakerPays] != drops(100));
+                BEAST_EXPECT((*bobOffers[0])[sfTakerGets] != btc(10'340'000));
+            }
+        }
+    }
+
     void
     testMalformed(FeatureBitset features)
     {
@@ -2727,6 +3151,50 @@ public:
         using namespace jtx;
         auto const gw1 = Account("gateway1");
 
+        {
+            auto const issuer = Account("issuer");
+            auto const sender = Account("sender");
+            auto const receiver = Account("receiver");
+            auto const seller = Account("seller");
+            auto const buyer = Account("buyer");
+
+            Env env{*this, features};
+            env.fund(XRP(10'000), issuer, sender, receiver, seller, buyer);
+            env.close();
+
+            MPTTester mpt{
+                {.env = env,
+                 .issuer = issuer,
+                 .holders = {sender, receiver, seller, buyer},
+                 .transferFee = 100}};
+            MPT const token = mpt;
+
+            mpt.pay(issuer, sender, 2'000);
+            mpt.pay(issuer, seller, 2'000);
+
+            // A direct holder-to-holder payment of 999 MPT at a 0.1% fee
+            // requires 1000 from the sender and burns one MPT.
+            env(pay(sender, receiver, token(999)), Ter(tecPATH_PARTIAL));
+            env.close();
+            env(pay(sender, receiver, token(999)), Sendmax(token(1'000)));
+            env.close();
+
+            BEAST_EXPECT(mpt.getBalance(sender) == 1'000);
+            BEAST_EXPECT(mpt.getBalance(receiver) == 999);
+            BEAST_EXPECT(mpt.getBalance(issuer) == 3'999);
+
+            // CLOB crossing should apply the same fee quantum.  The offer
+            // owner pays ceil(999 * 1.001) = 1000, not floor(...) = 999.
+            env(offer(seller, XRP(999), token(999)));
+            env.close();
+            env(offer(buyer, token(999), XRP(999)));
+            env.close();
+
+            BEAST_EXPECT(mpt.getBalance(seller) == 1'000);
+            BEAST_EXPECT(mpt.getBalance(buyer) == 999);
+            BEAST_EXPECT(mpt.getBalance(issuer) == 3'998);
+        }
+
         auto test = [&](auto&& issue1, auto&& issue2) {
             Env env{*this, features};
 
@@ -3102,6 +3570,260 @@ public:
         }
     }
 
+    void
+    testTransferRateOverflowOffer(FeatureBitset features)
+    {
+        testcase("Transfer Rate Overflow Offer");
+
+        using namespace jtx;
+
+        auto const issuer = Account("issuer");
+        auto const taker = Account("taker");
+
+        {
+            Env env{*this, features};
+            env.fund(XRP(10'000), issuer, taker);
+            env.close();
+
+            auto constexpr takerFunds = 2'000'000'000'000'000'000LL;
+            MPTTester const token{
+                {.env = env,
+                 .issuer = issuer,
+                 .holders = {taker},
+                 .transferFee = 50'000,
+                 .pay = takerFunds,
+                 .maxAmt = kMaxMpTokenAmount}};
+
+            // Covers OfferCreate::flowCross() sendMax calculation. A large
+            // non-issuer MPT offer with a transfer fee used to overflow in
+            // multiplyRound() before the offer could be placed.
+            auto constexpr offerAmount = 1'230'000'000'000'000'000LL;
+            auto const takerSeq = env.seq(taker);
+            env(offer(taker, XRP(1), token(offerAmount)));
+            env.close();
+
+            BEAST_EXPECT(
+                env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr);
+            BEAST_EXPECT(env.balance(taker, token) == token(takerFunds));
+        }
+
+        // Each scenario below targets a BookStep/OfferStream overflow path.
+        // The expected behavior is the same in all cases: remove the unusable
+        // book tip offer and let the taker's crossing offer remain rather than
+        // returning tecINTERNAL with the poison offer still on-ledger.
+        {
+            Env env{*this, features};
+            env.fund(XRP(10'000), issuer, taker);
+            env.close();
+
+            MPTTester const token{
+                {.env = env, .issuer = issuer, .holders = {taker}, .transferFee = 10'000}};
+
+            // Covers BookStep::forEachOffer() offer preparation, where
+            // ownerGives = mulRatio(ofrAmt.out, transferRateOut) overflowed
+            // for an oversized MPT output with a transfer fee.
+            std::int64_t const poisonAmount = 8'500'000'000'000'000'000LL;
+            auto const poisonSeq = env.seq(issuer);
+            env(offer(issuer, XRP(1), token(poisonAmount)));
+            env.close();
+
+            auto const poisonKeylet = keylet::offer(issuer.id(), SeqProxy::rawSequence(poisonSeq));
+            BEAST_EXPECT(env.le(poisonKeylet) != nullptr);
+
+            auto const takerSeq = env.seq(taker);
+            env(offer(taker, token(100), XRP(100)));
+            env.close();
+
+            BEAST_EXPECT(env.le(poisonKeylet) == nullptr);
+            BEAST_EXPECT(
+                env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr);
+        }
+
+        {
+            auto const gwA = Account("gatewayA");
+            auto const gwB = Account("gatewayB");
+            auto const alice = Account("alice");
+            auto const mallory = Account("mallory");
+
+            Env env{*this, features};
+            env.fund(XRP(10'000), gwA, gwB, alice, mallory);
+            env.close();
+
+            MPTTester const tokenA{
+                {.env = env, .issuer = gwA, .holders = {alice, mallory}, .transferFee = 50'000}};
+
+            MPTTester const tokenB{{.env = env, .issuer = gwB, .holders = {alice, mallory}}};
+
+            env(pay(gwA, alice, tokenA(1'000)));
+
+            // Covers BookStep::forEachOffer() offer preparation, where
+            // stpAmt.in = mulRatio(ofrAmt.in, transferRateIn) overflowed.
+            // The MPT/MPT amounts keep the offer quality reachable while
+            // applying tokenA's transfer rate overflows the input side.
+            std::int64_t const poisonPays = 6'148'914'691'236'517'205LL;
+            std::int64_t const poisonGets = 34'000'000'000'000'000LL;
+            env(pay(gwB, mallory, tokenB(poisonGets)));
+
+            auto const poisonSeq = env.seq(mallory);
+            env(offer(mallory, tokenA(poisonPays), tokenB(poisonGets)));
+            env.close();
+
+            auto const poisonKeylet = keylet::offer(mallory.id(), SeqProxy::rawSequence(poisonSeq));
+            BEAST_EXPECT(env.le(poisonKeylet) != nullptr);
+
+            auto const aliceSeq = env.seq(alice);
+            env(offer(alice, tokenB(1), tokenA(100)));
+            env.close();
+
+            BEAST_EXPECT(env.le(poisonKeylet) == nullptr);
+            BEAST_EXPECT(
+                env.le(keylet::offer(alice.id(), SeqProxy::rawSequence(aliceSeq))) != nullptr);
+        }
+
+        {
+            // Companion to the transfer-rate overflow cases above. The taker
+            // sells TakerPays=MPT(~1.84e18) for TakerGets=XRP(1) against a
+            // same-magnitude poison offer, forcing BookStep::revImp()'s
+            // limitStepOut() to strictly reduce and overflow.
+            //
+            // The taker's own quality is also unrepresentable here
+            // (getRate(TakerGets, TakerPays) == 0: a large MPT numerator over
+            // a small XRP denominator overflows the rate mantissa), but that
+            // no longer short-circuits the transaction -- crossing is
+            // attempted, and only a residual that would REST is stopped. So
+            // this exercises the deeper safety net:
+            // BookStep::forEachOffer's catch(std::overflow_error), which under
+            // featureMPTokensV2 removes the offending offer rather than
+            // propagating.
+            //
+            // Net effect: the poison offer is consumed off the book instead of
+            // being left to poison the next taker, nothing crosses, and the
+            // taker's own offer is not placed because its rate is
+            // unrepresentable -- so tecKILLED, which charges a fee and
+            // advances the sequence.
+            Env env{*this, features};
+            env.fund(XRP(10'000), issuer, taker);
+            env.close();
+
+            MPTTester const token{
+                {.env = env, .issuer = issuer, .holders = {taker}, .maxAmt = kMaxMpTokenAmount}};
+
+            env(pay(issuer, taker, token(1)));
+            env.close();
+
+            auto const funded = 1'844'674'407'370'955'162LL;
+            auto const offerOut = funded + 1;
+
+            auto const poisonSeq = env.seq(issuer);
+            env(offer(issuer, XRP(1), token(offerOut)));
+            env.close();
+
+            auto const poisonKeylet = keylet::offer(issuer.id(), SeqProxy::rawSequence(poisonSeq));
+            BEAST_EXPECT(env.le(poisonKeylet) != nullptr);
+
+            auto const issuerXRPBefore = env.balance(issuer, XRP);
+            auto const takerXRPBefore = env.balance(taker, XRP);
+            auto const takerMPTBefore = env.balance(taker, token);
+            auto const takerSeqBefore = env.seq(taker);
+
+            auto const takerSeq = takerSeqBefore;
+            auto const fee = env.current()->fees().base;
+            env(offer(taker, token(funded), XRP(1)), Ter(tecKILLED));
+            env.close();
+
+            // The overflowing poison offer is removed by BookStep. Nothing
+            // crossed, so no asset changes hands and the taker's offer is not
+            // placed; the fee is burned and the sequence advances.
+            BEAST_EXPECT(env.le(poisonKeylet) == nullptr);
+            BEAST_EXPECT(
+                env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) == nullptr);
+            BEAST_EXPECT(env.balance(issuer, XRP) == issuerXRPBefore);
+            BEAST_EXPECT(env.balance(taker, XRP) == takerXRPBefore - fee);
+            BEAST_EXPECT(env.balance(taker, token) == takerMPTBefore);
+            BEAST_EXPECT(env.seq(taker) == takerSeqBefore + 1);
+        }
+
+        {
+            auto const poisonMaker = Account("poisonMaker");
+
+            Env env{*this, features};
+            env.fund(XRP(10'000), issuer, poisonMaker, taker);
+            env.close();
+
+            MPTTester const token{
+                {.env = env,
+                 .issuer = issuer,
+                 .holders = {poisonMaker, taker},
+                 .maxAmt = kMaxMpTokenAmount}};
+
+            // Covers OfferStream::step() filtering. The offer is mostly
+            // funded, but reducing it to the actual owner funds inside
+            // shouldRmSmallIncreasedQOffer() used to overflow before BookStep
+            // saw the offer.
+            auto const funded = 1'844'674'407'370'955'162LL;
+            auto const offerOut = funded + 1;
+            env(pay(issuer, poisonMaker, token(funded)));
+
+            auto const poisonSeq = env.seq(poisonMaker);
+            env(offer(poisonMaker, XRP(1), token(offerOut)));
+            env.close();
+
+            auto const poisonKeylet =
+                keylet::offer(poisonMaker.id(), SeqProxy::rawSequence(poisonSeq));
+            BEAST_EXPECT(env.le(poisonKeylet) != nullptr);
+
+            auto const takerSeq = env.seq(taker);
+            env(offer(taker, token(1), XRP(1)));
+            env.close();
+
+            BEAST_EXPECT(env.le(poisonKeylet) == nullptr);
+            BEAST_EXPECT(
+                env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr);
+            BEAST_EXPECT(env.balance(poisonMaker, token) == token(funded));
+            BEAST_EXPECT(env.balance(taker, token) == token(0));
+        }
+
+        {
+            // Same overflow scenario as the ownerGives case above, but run with
+            // trace-level logging so BookStep::forEachOffer's removeOffer()
+            // emits its "Removing offer with overflowing amount calculation"
+            // trace line. This exercises the JLOG body inside removeOffer,
+            // which is skipped when logging is above trace severity.
+            std::string logs;
+            {
+                Env env{
+                    *this,
+                    envconfig(),
+                    features,
+                    std::make_unique(&logs),
+                    beast::Severity::Trace};
+                env.fund(XRP(10'000), issuer, taker);
+                env.close();
+
+                MPTTester const token{
+                    {.env = env, .issuer = issuer, .holders = {taker}, .transferFee = 10'000}};
+
+                std::int64_t const poisonAmount = 8'500'000'000'000'000'000LL;
+                auto const poisonSeq = env.seq(issuer);
+                env(offer(issuer, XRP(1), token(poisonAmount)));
+                env.close();
+
+                auto const poisonKeylet =
+                    keylet::offer(issuer.id(), SeqProxy::rawSequence(poisonSeq));
+                BEAST_EXPECT(env.le(poisonKeylet) != nullptr);
+
+                auto const takerSeq = env.seq(taker);
+                env(offer(taker, token(100), XRP(100)));
+                env.close();
+
+                BEAST_EXPECT(env.le(poisonKeylet) == nullptr);
+                BEAST_EXPECT(
+                    env.le(keylet::offer(taker.id(), SeqProxy::rawSequence(takerSeq))) != nullptr);
+            }
+            BEAST_EXPECT(logs.contains("Removing offer with overflowing amount calculation"));
+        }
+    }
+
     void
     testSelfCrossOffer1(FeatureBitset features)
     {
@@ -4787,6 +5509,215 @@ public:
         }
     }
 
+    void
+    testMPTOfferZeroRate(FeatureBitset features)
+    {
+        // An MPT offer whose quality is not representable must not REST -- on
+        // both the buy and sell sides, with or without a TickSize on the IOU
+        // issuer. Here nothing crosses it, so the whole offer is the remainder
+        // and the result is tecKILLED with nothing placed.
+        //
+        // getRate(TakerGets, TakerPays) returns 0 when the rate overflows: a
+        // large MPT TakerPays (XLS-0082 allows up to 2^63-1) over a small IOU
+        // TakerGets. Such an offer would otherwise (a) rest in the quality-0
+        // book directory, whose index equals getBookBase(), which BookTip's
+        // strict successor scan never returns -- so it can never be crossed yet
+        // still consumes the owner's reserve; and (b) on a TickSize market,
+        // drive the tick-rounding path in applyGuts to divide by a zero rate,
+        // throwing and surfacing as tefEXCEPTION. A normally-priced offer in the
+        // same market is unaffected.
+        //
+        // See testMPTOfferZeroRateCrossable for the other half of the
+        // behavior: an unrepresentable quality that CROSSES is not rejected.
+        testcase("MPT Offer Zero Rate");
+
+        using namespace jtx;
+
+        // Mantissa well above the ~1.84e17 overflow threshold (with an IOU
+        // denominator mantissa of 1e15); still within the XLS-0082 range.
+        auto const kBigMpt = 5'000'000'000'000'000'000LL;
+
+        auto runScenario = [&](bool withTickSize) {
+            Env env{*this, features};
+            auto const gw = Account{"gateway"};
+            auto const alice = Account{"alice"};
+            env.fund(XRP(10'000), gw, alice);
+            env.close();
+
+            auto const usd = gw["USD"];
+            env(trust(alice, usd(1'000)));
+            env(pay(gw, alice, usd(100)));
+            env.close();
+
+            if (withTickSize)
+            {
+                auto txn = noop(gw);
+                txn[sfTickSize.fieldName] = 5;
+                env(txn);
+                env.close();
+                BEAST_EXPECT((*env.le(gw))[sfTickSize] == 5);
+            }
+
+            // gw issues a DEX-tradable MPT (CanTrade | CanTransfer by default)
+            // and authorizes alice to hold it.
+            MPT const mpt = MPTTester(
+                {.env = env, .issuer = gw, .holders = {alice}, .maxAmt = kMaxMpTokenAmount});
+
+            // Buy side: TakerPays = large MPT, TakerGets = small IOU.
+            // getRate() overflows to 0 and nothing crosses -> killed, no
+            // offer placed and no reserve consumed.
+            BEAST_EXPECT(getRate(usd(1), mpt(kBigMpt)) == 0);
+            env(offer(alice, mpt(kBigMpt), usd(1)), Ter(tecKILLED));
+            env.close();
+            BEAST_EXPECT(offersOnAccount(env, alice).empty());
+
+            // Sell side (tfSell): killed regardless of the flag, since the
+            // rate is computed from the raw amounts either way.
+            BEAST_EXPECT(getRate(usd(1), mpt(kBigMpt)) == 0);
+            env(offer(alice, mpt(kBigMpt), usd(1), tfSell), Ter(tecKILLED));
+            env.close();
+            BEAST_EXPECT(offersOnAccount(env, alice).empty());
+
+            // Control: a normally-priced offer in the same market still
+            // places (and the tick-size rounding path still works when
+            // withTickSize is set).
+            env(offer(alice, mpt(10'000'000), usd(30)), Ter(tesSUCCESS));
+            env.close();
+            BEAST_EXPECT(offersOnAccount(env, alice).size() == 1);
+        };
+
+        // Without a TickSize: previously placed as a dead, never-crossable
+        // quality-0 entry that still consumed reserve.
+        runScenario(/*withTickSize=*/false);
+        // With a TickSize: previously threw and surfaced as tefEXCEPTION. The
+        // rounding is now skipped when the rate is unrepresentable.
+        runScenario(/*withTickSize=*/true);
+    }
+
+    void
+    testZeroRateXrpIouOffer(FeatureBitset features)
+    {
+        // A rate-0 offer is reachable without MPT: for XRP/IOU the "too
+        // good" underflow path makes getRate() return 0 when a tiny IOU
+        // TakerPays is divided by an XRP TakerGets.
+        //
+        // Without featureMPTokensV2 the offer is accepted and placed, but
+        // rests in the quality-0 book directory (whose index == getBookBase),
+        // which BookTip's strict successor scan never returns -- so it can
+        // never be crossed, even by a willing, better-priced counterparty.
+        // With featureMPTokensV2 the same offer crosses nothing and is not
+        // placed, so it is killed.
+        testcase("Zero Rate XRP/IOU Offer");
+
+        using namespace jtx;
+
+        auto const gw = Account{"gateway"};
+        auto const alice = Account{"alice"};
+        auto const bob = Account{"bob"};
+        auto const usd = gw["USD"];
+
+        // Smallest-magnitude IOU: mantissa kMinValue, exponent kMinOffset
+        // (= 1e-81). divide(tinyUsd, XRP(1000)) underflows below kMinOffset
+        // and canonicalizes to 0, so getRate() returns 0.
+        auto const tinyUsd = STAmount{usd, UINT64_C(1'000'000'000'000'000), -96};
+
+        auto setup = [&](Env& env) {
+            env.fund(XRP(100'000), gw, alice, bob);
+            env.close();
+            env(trust(alice, usd(1'000)));
+            env(trust(bob, usd(1'000)));
+            env(pay(gw, bob, usd(100)));
+            env.close();
+        };
+
+        // featureMPTokensV2 disabled: legacy behavior -- placed but inert.
+        {
+            Env env{*this, features - featureMPTokensV2};
+            setup(env);
+
+            // TakerPays = tiny IOU, TakerGets = XRP -> rate 0.
+            BEAST_EXPECT(getRate(XRP(1'000), tinyUsd) == 0);
+            env(offer(alice, tinyUsd, XRP(1'000)), Ter(tesSUCCESS));
+            env.close();
+
+            auto const aliceOffers = offersOnAccount(env, alice);
+            BEAST_EXPECT(aliceOffers.size() == 1);
+            // Placed in the quality-0 book directory.
+            BEAST_EXPECT(getQuality((*aliceOffers.front())[sfBookDirectory]) == 0);
+
+            // A complementary offer that would cross a usable offer at this
+            // (astronomically good) price does NOT cross it, because the
+            // quality-0 directory is never visited: both offers rest.
+            env(offer(bob, XRP(1'000), usd(10)), Ter(tesSUCCESS));
+            env.close();
+            BEAST_EXPECT(offersOnAccount(env, alice).size() == 1);
+            BEAST_EXPECT(offersOnAccount(env, bob).size() == 1);
+        }
+
+        // featureMPTokensV2 disabled, with a TickSize on the IOU issuer: the
+        // tick-rounding path divides by the zero rate and throws, surfacing as
+        // tefEXCEPTION. Legacy behavior, and it must stay that way -- the
+        // guard that skips the rounding is gated on the amendment, since
+        // changing this without a gate would fork a pre-amendment ledger.
+        {
+            Env env{*this, features - featureMPTokensV2};
+            setup(env);
+
+            auto txn = noop(gw);
+            txn[sfTickSize.fieldName] = 5;
+            env(txn);
+            env.close();
+            BEAST_EXPECT((*env.le(gw))[sfTickSize] == 5);
+
+            BEAST_EXPECT(getRate(XRP(1'000), tinyUsd) == 0);
+            env(offer(alice, tinyUsd, XRP(1'000)), Ter(tefEXCEPTION));
+            env.close();
+            BEAST_EXPECT(offersOnAccount(env, alice).empty());
+        }
+
+        // featureMPTokensV2 enabled: nothing crosses, so the remainder is the
+        // whole offer and it is killed rather than placed.
+        {
+            Env env{*this, features};
+            setup(env);
+
+            BEAST_EXPECT(getRate(XRP(1000), tinyUsd) == 0);
+            env(offer(alice, tinyUsd, XRP(1000)), Ter(tecKILLED));
+            env.close();
+            BEAST_EXPECT(offersOnAccount(env, alice).empty());
+        }
+
+        // featureMPTokensV2 enabled, with a counterparty already on the book:
+        // the same unrepresentable quality now CROSSES. This is the reviewer's
+        // objection with no MPT anywhere in it -- the old preflight check
+        // rejected this outright even though it fills completely and rests
+        // nothing.
+        {
+            Env env{*this, features};
+            setup(env);
+
+            // Bob rests first: he gives usd(10) to receive XRP(1'000).
+            auto const bobSeq = env.seq(bob);
+            env(offer(bob, XRP(1'000), usd(10)), Ter(tesSUCCESS));
+            env.close();
+            BEAST_EXPECT(env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobSeq))) != nullptr);
+
+            // Alice offers up to XRP(1'000) for a dust amount of USD -- rate
+            // 0, at a price bob's offer improves on enormously.
+            BEAST_EXPECT(getRate(XRP(1'000), tinyUsd) == 0);
+            env(offer(alice, tinyUsd, XRP(1'000)), Ter(tesSUCCESS));
+            env.close();
+
+            // Alice asked for dust and got exactly that, so her offer is
+            // fully satisfied and never reaches the book. Bob's offer is
+            // barely touched and stays. The old preflight check rejected this
+            // transaction outright, with no MPT involved anywhere.
+            BEAST_EXPECT(env.balance(alice, usd).value() == tinyUsd);
+            BEAST_EXPECT(env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobSeq))) != nullptr);
+            BEAST_EXPECT(offersOnAccount(env, alice).empty());
+        }
+    }
+
     void
     testAutoCreateReserve(FeatureBitset features)
     {
@@ -4882,6 +5813,642 @@ public:
         }
     }
 
+    void
+    testBookOffersMPTFunding(FeatureBitset features)
+    {
+        testcase("book_offers uses MPT issuer capacity, transfer fees, and locks");
+
+        using namespace jtx;
+
+        Account const issuer{"issuer"};
+        Account const maker{"maker"};
+        Account const buyer{"buyer"};
+
+        // Issuer-owned MPT offers are funded only by remaining issuance
+        // capacity. Once ordinary issuance consumes the cap, book_offers must
+        // report the stale issuer offer as zero-funded.
+        {
+            Env env{*this, features};
+
+            env.fund(XRP(10'000), issuer, maker, buyer);
+            env.close();
+
+            MPTTester musd(
+                {.env = env, .issuer = issuer, .holders = {maker, buyer}, .maxAmt = 100});
+            MPT const usd = musd;
+
+            auto const issuerOfferSeq = env.seq(issuer);
+            env(offer(issuer, XRP(100), usd(100)));
+
+            musd.pay(issuer, maker, 100);
+
+            auto const issuance = env.le(keylet::mptokenIssuance(usd.mpt()));
+            if (!BEAST_EXPECT(issuance))
+                return;
+            BEAST_EXPECT(issuance->getFieldU64(sfOutstandingAmount) == 100);
+            BEAST_EXPECT(issuance->getFieldU64(sfMaximumAmount) == 100);
+
+            env(offer(maker, XRP(200), usd(100)));
+
+            json::Value const jrr = getBookOffers(env, XRP, usd);
+            json::Value const& bookOffers = jrr[jss::offers];
+            BEAST_EXPECT(bookOffers.isArray());
+            if (!BEAST_EXPECT(bookOffers.size() >= 2))
+                return;
+
+            json::Value const& issuerOffer = bookOffers[0u];
+            BEAST_EXPECT(issuerOffer[sfAccount.jsonName] == issuer.human());
+            BEAST_EXPECT(issuerOffer[sfSequence.jsonName] == issuerOfferSeq);
+            BEAST_EXPECT(issuerOffer[jss::owner_funds] == "0");
+            BEAST_EXPECT(issuerOffer.isMember(jss::taker_gets_funded));
+            BEAST_EXPECT(issuerOffer[jss::taker_gets_funded][jss::value] == "0");
+            BEAST_EXPECT(issuerOffer.isMember(jss::taker_pays_funded));
+            BEAST_EXPECT(issuerOffer[jss::taker_pays_funded] == "0");
+        }
+
+        // Multiple issuer-owned MPT offers share the same bounded self-issue
+        // capacity. The second offer exercises the cached running balance path
+        // after the first offer has consumed part of the issuer's capacity.
+        {
+            Env env{*this, features};
+
+            env.fund(XRP(10'000), issuer, buyer);
+            env.close();
+
+            MPTTester const musd({.env = env, .issuer = issuer, .holders = {buyer}, .maxAmt = 150});
+            MPT const usd = musd;
+
+            auto const firstIssuerOfferSeq = env.seq(issuer);
+            env(offer(issuer, XRP(100), usd(100)));
+            auto const secondIssuerOfferSeq = env.seq(issuer);
+            env(offer(issuer, XRP(100), usd(100)));
+
+            json::Value const jrr = getBookOffers(env, XRP, usd);
+            json::Value const& bookOffers = jrr[jss::offers];
+            BEAST_EXPECT(bookOffers.isArray());
+            if (!BEAST_EXPECT(bookOffers.size() >= 2))
+                return;
+
+            json::Value const& firstOffer = bookOffers[0u];
+            BEAST_EXPECT(firstOffer[sfAccount.jsonName] == issuer.human());
+            BEAST_EXPECT(firstOffer[sfSequence.jsonName] == firstIssuerOfferSeq);
+            BEAST_EXPECT(firstOffer[jss::owner_funds] == "150");
+            BEAST_EXPECT(!firstOffer.isMember(jss::taker_gets_funded));
+            BEAST_EXPECT(!firstOffer.isMember(jss::taker_pays_funded));
+
+            json::Value const& secondOffer = bookOffers[1u];
+            BEAST_EXPECT(secondOffer[sfAccount.jsonName] == issuer.human());
+            BEAST_EXPECT(secondOffer[sfSequence.jsonName] == secondIssuerOfferSeq);
+            BEAST_EXPECT(!secondOffer.isMember(jss::owner_funds));
+            BEAST_EXPECT(secondOffer.isMember(jss::taker_gets_funded));
+            BEAST_EXPECT(secondOffer[jss::taker_gets_funded][jss::value] == "50");
+            BEAST_EXPECT(secondOffer.isMember(jss::taker_pays_funded));
+            BEAST_EXPECT(secondOffer[jss::taker_pays_funded] == "50000000");
+        }
+
+        auto checkTransferFeeBookOffers = [&](std::uint16_t transferFee, auto&& checkOffers) {
+            Env env{*this, features};
+
+            env.fund(XRP(10'000), issuer, maker, buyer);
+            env.close();
+
+            MPTTester const musd(
+                {.env = env,
+                 .issuer = issuer,
+                 .holders = {maker, buyer},
+                 .transferFee = transferFee,
+                 .pay = 3'000});
+            MPT const usd = musd;
+            if (transferFee != 0)
+                BEAST_EXPECT(musd.checkTransferFee(transferFee));
+
+            auto const firstOfferSeq = env.seq(maker);
+            env(offer(maker, XRP(1'500), usd(1'500)));
+            auto const secondOfferSeq = env.seq(maker);
+            env(offer(maker, XRP(1'500), usd(1'500)));
+
+            json::Value const jrr = getBookOffers(env, XRP, usd);
+            json::Value const& bookOffers = jrr[jss::offers];
+            BEAST_EXPECT(bookOffers.isArray());
+            if (!BEAST_EXPECT(bookOffers.size() == 2))
+                return;
+
+            checkOffers(bookOffers, firstOfferSeq, secondOfferSeq);
+        };
+
+        // With no MPT transfer fee, two identical maker offers backed by 3000
+        // owner funds are both fully funded for 1500 MPT.
+        checkTransferFeeBookOffers(
+            0,
+            [&](json::Value const& bookOffers,
+                std::uint32_t firstOfferSeq,
+                std::uint32_t secondOfferSeq) {
+                for (auto const i : {0u, 1u})
+                {
+                    json::Value const& offer = bookOffers[i];
+                    BEAST_EXPECT(offer[sfAccount.jsonName] == maker.human());
+                    BEAST_EXPECT(
+                        offer[sfSequence.jsonName] == (i == 0u ? firstOfferSeq : secondOfferSeq));
+                    BEAST_EXPECT(!offer.isMember(jss::taker_gets_funded));
+                    BEAST_EXPECT(!offer.isMember(jss::taker_pays_funded));
+                }
+                BEAST_EXPECT(bookOffers[0u][jss::owner_funds] == "3000");
+            });
+
+        // With a 50% MPT transfer fee, the first identical maker offer consumes
+        // 2250 owner funds, so the second offer can deliver only 500 MPT.
+        checkTransferFeeBookOffers(
+            50'000,
+            [&](json::Value const& bookOffers,
+                std::uint32_t firstOfferSeq,
+                std::uint32_t secondOfferSeq) {
+                json::Value const& firstOffer = bookOffers[0u];
+                BEAST_EXPECT(firstOffer[sfAccount.jsonName] == maker.human());
+                BEAST_EXPECT(firstOffer[sfSequence.jsonName] == firstOfferSeq);
+                BEAST_EXPECT(firstOffer[jss::owner_funds] == "3000");
+                BEAST_EXPECT(!firstOffer.isMember(jss::taker_gets_funded));
+                BEAST_EXPECT(!firstOffer.isMember(jss::taker_pays_funded));
+
+                json::Value const& secondOffer = bookOffers[1u];
+                BEAST_EXPECT(secondOffer[sfAccount.jsonName] == maker.human());
+                BEAST_EXPECT(secondOffer[sfSequence.jsonName] == secondOfferSeq);
+                // A 50% MPT transfer fee leaves only 750 owner funds after
+                // the first offer. That can fund 500 MPT delivered to the
+                // taker on the same second offer that was fully funded without
+                // the transfer fee.
+                BEAST_EXPECT(secondOffer.isMember(jss::taker_gets_funded));
+                BEAST_EXPECT(secondOffer[jss::taker_gets_funded][jss::value] == "500");
+                BEAST_EXPECT(secondOffer.isMember(jss::taker_pays_funded));
+                BEAST_EXPECT(secondOffer[jss::taker_pays_funded] == "500000000");
+            });
+
+        // A large MPT balance used to overflow the fee adjustment. divide()
+        // assumes an IOU mantissa, always normalized into [1e15, 1e16), and
+        // scales the numerator by 1e17. An MPT mantissa is the raw int64
+        // balance, so past ~1.8e17 the scaled quotient leaves uint64 range and
+        // throws -- failing the whole RPC with "internal", so one offer owner
+        // blanked the entire book for every caller.
+        //
+        // The quotient itself always fits, because the branch only runs when
+        // the rate exceeds parity. The cases below pin that at the edges of
+        // the domain rather than leaving it to inspection.
+        auto checkLargeOwnerFunds =
+            [&](std::uint16_t transferFee, std::int64_t funds, char const* expectedFunded) {
+                Env env{*this, features};
+                env.fund(XRP(10'000), issuer, maker, buyer);
+                env.close();
+
+                MPT const usd = MPTTester(
+                    {.env = env,
+                     .issuer = issuer,
+                     .holders = {maker, buyer},
+                     .transferFee = transferFee,
+                     .maxAmt = kMaxMpTokenAmount});
+                env(pay(issuer, maker, usd(funds)));
+                env.close();
+
+                auto const offerSeq = env.seq(maker);
+                env(offer(maker, XRP(100), usd(funds)));
+                env.close();
+
+                json::Value const jrr = getBookOffers(env, XRP, usd);
+                BEAST_EXPECT(!jrr.isMember(jss::error));
+                json::Value const& bookOffers = jrr[jss::offers];
+                BEAST_EXPECT(bookOffers.isArray());
+                if (!BEAST_EXPECT(bookOffers.size() == 1))
+                    return;
+
+                json::Value const& offer = bookOffers[0u];
+                BEAST_EXPECT(offer[sfAccount.jsonName] == maker.human());
+                BEAST_EXPECT(offer[sfSequence.jsonName] == offerSeq);
+                BEAST_EXPECT(offer[jss::owner_funds] == std::to_string(funds));
+                BEAST_EXPECT(offer[jss::taker_gets_funded][jss::value] == expectedFunded);
+            };
+
+        // Above the ~2.77e17 boundary at the maximum transfer rate of 1.5:
+        // 3e17 of owner funds covers 2e17 delivered.
+        checkLargeOwnerFunds(kMaxTransferFee, 300'000'000'000'000'000LL, "200000000000000000");
+        // Large balance at the maximum rate. Kept at 6e18 so that 6e18 * 1.5
+        // stays representable: offer crossing's rate-preservation path
+        // overflows above that, which is a separate defect from this one.
+        checkLargeOwnerFunds(kMaxTransferFee, 6'000'000'000'000'000'000LL, "4000000000000000000");
+        // Near-maximum balance at the smallest rate above parity. This is the
+        // largest quotient the branch can produce, and the case the old code
+        // failed earliest on -- its overflow boundary is lowest, ~1.8e17, when
+        // the rate is closest to parity.
+        checkLargeOwnerFunds(1, 9'000'000'000'000'000'000LL, "8999910000899991000");
+
+        // An MPT global lock makes book_offers report the locked MPT book
+        // liquidity as zero-funded instead of funded.
+        {
+            Env env{*this, features};
+
+            env.fund(XRP(10'000), issuer, maker, buyer);
+            env.close();
+
+            MPTTester musd(
+                {.env = env,
+                 .issuer = issuer,
+                 .holders = {maker, buyer},
+                 .pay = 100,
+                 .flags = kMptDexFlags | tfMPTCanLock});
+            MPT const usd = musd;
+
+            auto const offerSeq = env.seq(maker);
+            env(offer(maker, XRP(100), usd(100)));
+            env.close();
+
+            {
+                json::Value const jrr = getBookOffers(env, XRP, usd);
+                json::Value const& bookOffers = jrr[jss::offers];
+                BEAST_EXPECT(bookOffers.isArray());
+                if (!BEAST_EXPECT(bookOffers.size() == 1))
+                    return;
+
+                json::Value const& offer = bookOffers[0u];
+                BEAST_EXPECT(offer[sfAccount.jsonName] == maker.human());
+                BEAST_EXPECT(offer[sfSequence.jsonName] == offerSeq);
+                BEAST_EXPECT(offer[jss::owner_funds] == "100");
+                BEAST_EXPECT(!offer.isMember(jss::taker_gets_funded));
+                BEAST_EXPECT(!offer.isMember(jss::taker_pays_funded));
+            }
+
+            musd.set({.flags = tfMPTLock});
+
+            {
+                // The lock does not remove the offer from the ledger;
+                // book_offers must report it as zero-funded liquidity.
+                auto const bookOffers = getBookOffers(env, XRP, usd)[jss::offers];
+                BEAST_EXPECT(bookOffers.isArray() && bookOffers.size() == 1);
+
+                json::Value const& offer = bookOffers[0u];
+                BEAST_EXPECT(offer[sfAccount] == maker.human());
+                BEAST_EXPECT(offer[sfSequence] == offerSeq);
+                BEAST_EXPECT(offer[jss::owner_funds] == "0");
+                BEAST_EXPECT(offer.isMember(jss::taker_gets_funded));
+                BEAST_EXPECT(offer[jss::taker_gets_funded][jss::value] == "0");
+                BEAST_EXPECT(offer.isMember(jss::taker_pays_funded));
+                BEAST_EXPECT(offer[jss::taker_pays_funded] == "0");
+            }
+        }
+    }
+
+    // getBookBase hashes raw concatenations of fixed-width fields, so the
+    // (Issue,MPT) preimage `currency(20)||mptID(24)||account(20)` and the
+    // (MPT,Issue) preimage `mptID(24)||currency(20)||account(20)` are both
+    // 64 bytes and collide when the bytes align. An attacker picks the IOU
+    // currency, reuses an IOU issuer, and grinds an MPT issuer / sequence;
+    // the per-branch discriminator in getBookBase blocks this.
+    void
+    testBookBaseMixedAssetCollision(FeatureBitset /*features*/)
+    {
+        testcase("getBookBase: (Issue,MPT) vs (MPT,Issue) preimage collision");
+
+        // Construction recipe:
+        //   issuerB last 4 bytes == seq_A; mptID_B = BE(5) || issuerB
+        //   currencyA            == mptID_B[0..19] = BE(5) || issuerB[0..15]
+        //   issuerA              == currencyB (both 20-byte all-0xBB)
+        //   sharedIOUIssuer      == acct_A == acct_B
+        AccountID issuerB;
+        AccountID issuerA;
+        Currency currencyB;
+        Currency currencyA;
+        AccountID sharedIOUIssuer;
+        BEAST_EXPECT(issuerB.parseHex("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA00000007"));
+        BEAST_EXPECT(issuerA.parseHex("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"));
+        BEAST_EXPECT(currencyB.parseHex("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"));
+        BEAST_EXPECT(currencyA.parseHex("00000005AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"));
+        BEAST_EXPECT(sharedIOUIssuer.parseHex("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"));
+
+        Book const bookA{
+            Asset{Issue{currencyA, sharedIOUIssuer}},
+            Asset{MPTIssue{0x00000007u, issuerA}},
+            std::nullopt};
+        Book const bookB{
+            Asset{MPTIssue{0x00000005u, issuerB}},
+            Asset{Issue{currencyB, sharedIOUIssuer}},
+            std::nullopt};
+
+        BEAST_EXPECT(bookA != bookB);
+        BEAST_EXPECT(getBookBase(bookA) != getBookBase(bookB));
+    }
+
+    // (MPT,MPT) bodies are 48 bytes and can't length-match the 64-byte
+    // mixed branches, but tag them too for symmetry/future-proofing; this
+    // test also pins the directional asymmetry of an (MPT,MPT) book.
+    void
+    testBookBaseMptMptDistinct(FeatureBitset /*features*/)
+    {
+        testcase("getBookBase: (MPT,MPT) distinguishes from mixed branches");
+
+        AccountID issuerX;
+        AccountID issuerY;
+        Currency currency;
+        AccountID iouIssuer;
+        BEAST_EXPECT(issuerX.parseHex("1111111111111111111111111111111111111111"));
+        BEAST_EXPECT(issuerY.parseHex("2222222222222222222222222222222222222222"));
+        BEAST_EXPECT(currency.parseHex("3333333333333333333333333333333333333333"));
+        BEAST_EXPECT(iouIssuer.parseHex("4444444444444444444444444444444444444444"));
+
+        Asset const mptX{MPTIssue{1u, issuerX}};
+        Asset const mptY{MPTIssue{2u, issuerY}};
+        Book const mptBook{mptX, mptY, std::nullopt};
+        Book const mixedBook{mptX, Asset{Issue{currency, iouIssuer}}, std::nullopt};
+        Book const reversedMptBook{mptY, mptX, std::nullopt};
+
+        BEAST_EXPECT(getBookBase(mptBook) != getBookBase(mixedBook));
+        BEAST_EXPECT(getBookBase(mptBook) != getBookBase(reversedMptBook));
+    }
+
+    void
+    testBookBaseDomainMptDistinct(FeatureBitset /*features*/)
+    {
+        testcase("getBookBase: domain does not reopen MPT preimage collisions");
+
+        // The type tag is a front prefix and the domain is a 32-byte suffix, so a
+        // domain'd book must (a) stay distinct from its public counterpart and
+        // (b) preserve the mixed-branch tag distinction that the public case has.
+        AccountID issuerX, issuerY, iouIssuer;
+        Currency currency;
+        BEAST_EXPECT(issuerX.parseHex("1111111111111111111111111111111111111111"));
+        BEAST_EXPECT(issuerY.parseHex("2222222222222222222222222222222222222222"));
+        BEAST_EXPECT(currency.parseHex("3333333333333333333333333333333333333333"));
+        BEAST_EXPECT(iouIssuer.parseHex("4444444444444444444444444444444444444444"));
+
+        uint256 const domainA = uint256::fromVoid(
+            "\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD"
+            "\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD");
+
+        Asset const mptX{MPTIssue{1u, issuerX}};
+        Asset const iou{Issue{currency, iouIssuer}};
+
+        // (a) same pair, public vs domain'd -> distinct directories.
+        Book const publicBook{mptX, iou, std::nullopt};
+        Book const domainBook{mptX, iou, domainA};
+        BEAST_EXPECT(getBookBase(publicBook) != getBookBase(domainBook));
+
+        // (b) mixed-branch tag distinction still holds *with* a domain set:
+        //     (MPT,Issue) vs (Issue,MPT), both domain'd, must not collide.
+        Book const mi{mptX, iou, domainA};
+        Book const im{iou, mptX, domainA};
+        BEAST_EXPECT(mi != im);
+        BEAST_EXPECT(getBookBase(mi) != getBookBase(im));
+    }
+
+    void
+    testMPTOfferZeroRateCrossable(FeatureBitset features)
+    {
+        // An unrepresentable quality does not imply an offer that cannot
+        // function. "Can never be crossed" describes an offer that RESTS:
+        // crossing happens in applyGuts, before any residual is placed in the
+        // book, so an offer whose rate is unrepresentable can still consume a
+        // resting offer in full and never reach the quality-0 directory.
+        //
+        // The two sides of one trade do not have the same rate
+        // representability: getRate(TakerGets, TakerPays) overflows to 0 for
+        // the side paying a large MPT, but not for the side paying XRP. So a
+        // preflight rejection keyed on getRate() == 0 admits the resting half
+        // of a trade and rejects the crossing half.
+        testcase("MPT Offer Zero Rate - crossable quality");
+
+        using namespace jtx;
+
+        // Above the rate-overflow threshold: divide() scales the XRP
+        // denominator up to a 1e15 mantissa and then evaluates
+        // muldiv(mptMantissa, 1e17, denMantissa), which exceeds 2^64 -- so
+        // getRate() takes its catch-all and returns 0.
+        auto const kBigMpt = 200'000'000'000'000'000LL;
+
+        // Both scenarios are the same trade against the same resting offer,
+        // and both execute identically (at bob's price). They differ only in
+        // the price alice quotes, and therefore only in whether the rate on
+        // HER side of the book is representable.
+        auto runScenario = [&](STAmount const& aliceQuote, bool rateRepresentable) {
+            Env env{*this, features};
+            auto const gw = Account{"gateway"};
+            auto const alice = Account{"alice"};
+            auto const bob = Account{"bob"};
+            env.fund(XRP(10'000), gw, alice, bob);
+            env.close();
+
+            MPT const mpt = MPTTester(
+                {.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = kMaxMpTokenAmount});
+
+            env(pay(gw, bob, mpt(kBigMpt)));
+            env.close();
+
+            // Bob rests the sell side: TakerPays = XRP(1), TakerGets =
+            // kBigMpt. getRate(TakerGets, TakerPays) is representable in this
+            // direction, so preflight admits it and it rests at a normal
+            // quality.
+            BEAST_EXPECT(getRate(mpt(kBigMpt), XRP(1)) != 0);
+            auto const bobSeq = env.seq(bob);
+            env(offer(bob, XRP(1), mpt(kBigMpt)), Ter(tesSUCCESS));
+            env.close();
+            BEAST_EXPECT(env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobSeq))) != nullptr);
+
+            // Alice takes it from the other side: TakerPays = kBigMpt,
+            // TakerGets = her quote.
+            BEAST_EXPECT((getRate(aliceQuote, mpt(kBigMpt)) != 0) == rateRepresentable);
+
+            auto const bobXrpBefore = env.balance(bob).value().xrp();
+            env(offer(alice, mpt(kBigMpt), aliceQuote), Ter(tesSUCCESS));
+            env.close();
+
+            // Alice's offer crosses bob's in full, so it never rests: nothing
+            // ends up in the quality-0 directory, no reserve is stranded, and
+            // the tick-rounding divide is never reached with a zero rate.
+            BEAST_EXPECT(env.balance(alice, mpt) == mpt(kBigMpt));
+            BEAST_EXPECT(env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobSeq))) == nullptr);
+            BEAST_EXPECT(offersOnAccount(env, alice).empty());
+            // And it executes at bob's 1 XRP, whatever alice quoted.
+            BEAST_EXPECT(env.balance(bob).value().xrp() == bobXrpBefore + XRP(1).value().xrp());
+        };
+
+        // Alice quotes bob's exact price. getRate() overflows to 0 on her
+        // side, yet the offer crosses in full and never rests.
+        runScenario(XRP(1), /*rateRepresentable=*/false);
+        // Alice quotes a price worse for herself, which halves the rate into
+        // representable range. Same execution as above: she pays 1 XRP for
+        // kBigMpt. The two cases are therefore numerically, not economically,
+        // different.
+        runScenario(XRP(2), /*rateRepresentable=*/true);
+    }
+
+    void
+    testMPTOfferZeroRatePartialCross(FeatureBitset features)
+    {
+        // The case between the two extremes: an unrepresentable quality that
+        // crosses PARTIALLY. The crossed portion must execute -- it never
+        // touches the book -- while the residual must not be placed, since it
+        // would rest in the quality-0 directory holding a reserve it could
+        // never earn back by being crossed.
+        testcase("MPT Offer Zero Rate - partial cross");
+
+        using namespace jtx;
+
+        auto const kBigMpt = 200'000'000'000'000'000LL;
+
+        Env env{*this, features};
+        auto const gw = Account{"gateway"};
+        auto const alice = Account{"alice"};
+        auto const bob = Account{"bob"};
+        env.fund(XRP(10'000), gw, alice, bob);
+        env.close();
+
+        MPT const mpt = MPTTester(
+            {.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = kMaxMpTokenAmount});
+
+        env(pay(gw, bob, mpt(kBigMpt)));
+        env.close();
+
+        // Bob rests a sell of kBigMpt for XRP(1) -- representable on his side.
+        auto const bobSeq = env.seq(bob);
+        env(offer(bob, XRP(1), mpt(kBigMpt)), Ter(tesSUCCESS));
+        env.close();
+        BEAST_EXPECT(env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobSeq))) != nullptr);
+
+        // Alice asks for twice what bob has, at the same price. Her rate is
+        // unrepresentable: mantissa ratio 4e17 / 2e15 = 200 > ~184.47.
+        BEAST_EXPECT(getRate(XRP(2), mpt(2 * kBigMpt)) == 0);
+
+        auto const aliceXrpBefore = env.balance(alice).value().xrp();
+        auto const fee = env.current()->fees().base;
+
+        env(offer(alice, mpt(2 * kBigMpt), XRP(2)), Ter(tesSUCCESS));
+        env.close();
+
+        // The half that crossed executed at bob's price...
+        BEAST_EXPECT(env.balance(alice, mpt) == mpt(kBigMpt));
+        BEAST_EXPECT(env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobSeq))) == nullptr);
+        BEAST_EXPECT(
+            env.balance(alice).value().xrp() == aliceXrpBefore - XRP(1).value().xrp() - fee);
+        // ...and the half that did not is dropped rather than placed, so no
+        // offer rests and no reserve is consumed.
+        BEAST_EXPECT(offersOnAccount(env, alice).empty());
+        BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 1);  // the MPToken only
+    }
+
+    void
+    testMPTOfferZeroRateTickSizeCross(FeatureBitset features)
+    {
+        // TickSize plus an unrepresentable quality plus a counterparty on the
+        // book. The tick-rounding path is skipped for a zero rate, since it
+        // would divide by that rate and throw, so the offer crosses at its raw
+        // price. Every other TickSize case here faces an empty book, making
+        // this the only coverage that the skip leaves crossing intact --
+        // without it this transaction is tefEXCEPTION.
+        testcase("MPT Offer Zero Rate - tick size with crossing");
+
+        using namespace jtx;
+
+        auto const kBigMpt = 5'000'000'000'000'000'000LL;
+
+        Env env{*this, features};
+        auto const gw = Account{"gateway"};
+        auto const alice = Account{"alice"};
+        auto const bob = Account{"bob"};
+        env.fund(XRP(10'000), gw, alice, bob);
+        env.close();
+
+        auto const usd = gw["USD"];
+        env(trust(alice, usd(1'000)));
+        env(pay(gw, alice, usd(100)));
+        env.close();
+
+        auto txn = noop(gw);
+        txn[sfTickSize.fieldName] = 5;
+        env(txn);
+        env.close();
+        BEAST_EXPECT((*env.le(gw))[sfTickSize] == 5);
+
+        MPT const mpt = MPTTester(
+            {.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = kMaxMpTokenAmount});
+        env(pay(gw, bob, mpt(kBigMpt)));
+        env.close();
+
+        // Bob rests the sell side; representable in that direction.
+        BEAST_EXPECT(getRate(mpt(kBigMpt), usd(1)) != 0);
+        auto const bobSeq = env.seq(bob);
+        env(offer(bob, usd(1), mpt(kBigMpt)), Ter(tesSUCCESS));
+        env.close();
+        BEAST_EXPECT(env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobSeq))) != nullptr);
+
+        // Alice takes it from the unrepresentable side, with the tick size in
+        // force on her TakerGets.
+        BEAST_EXPECT(getRate(usd(1), mpt(kBigMpt)) == 0);
+        env(offer(alice, mpt(kBigMpt), usd(1)), Ter(tesSUCCESS));
+        env.close();
+
+        BEAST_EXPECT(env.balance(alice, mpt) == mpt(kBigMpt));
+        BEAST_EXPECT(env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobSeq))) == nullptr);
+        BEAST_EXPECT(offersOnAccount(env, alice).empty());
+    }
+
+    void
+    testMPTOfferZeroRateFlags(FeatureBitset features)
+    {
+        // A zero rate must not change what tfFillOrKill and tfImmediateOrCancel
+        // do. Both are handled above the unrepresentable-quality guard, but the
+        // ordering is not observable and no test can pin it: the guard returns
+        // the same pair either flag would. Immediate-or-cancel matches it by
+        // construction, and fill-or-kill disables partial payment
+        // (OfferCreate.cpp: flowCross is passed !tfFillOrKill), so a
+        // not-fully-fillable offer leaves crossed == false and both paths give
+        // {tecKILLED, false}. What this does cover is flags combined with an
+        // unrepresentable quality, which nothing else exercises.
+        testcase("MPT Offer Zero Rate - IOC and FoK");
+
+        using namespace jtx;
+
+        auto const kBigMpt = 200'000'000'000'000'000LL;
+
+        auto const runScenario = [&](std::uint32_t flags, TER expected) {
+            Env env{*this, features};
+            auto const gw = Account{"gateway"};
+            auto const alice = Account{"alice"};
+            auto const bob = Account{"bob"};
+            env.fund(XRP(10'000), gw, alice, bob);
+            env.close();
+
+            MPT const mpt = MPTTester(
+                {.env = env, .issuer = gw, .holders = {alice, bob}, .maxAmt = kMaxMpTokenAmount});
+            env(pay(gw, bob, mpt(kBigMpt)));
+            env.close();
+
+            auto const bobSeq = env.seq(bob);
+            env(offer(bob, XRP(1), mpt(kBigMpt)), Ter(tesSUCCESS));
+            env.close();
+
+            // Asking for twice what bob has forces a partial cross, so the
+            // flag handling -- not the fully-crossed early return -- decides.
+            BEAST_EXPECT(getRate(XRP(2), mpt(2 * kBigMpt)) == 0);
+            env(offer(alice, mpt(2 * kBigMpt), XRP(2), flags), Ter(expected));
+            env.close();
+
+            auto const bobOfferLive =
+                env.le(keylet::offer(bob.id(), SeqProxy::rawSequence(bobSeq))) != nullptr;
+            if (isTesSuccess(expected))
+            {
+                // Immediate-or-cancel: the crossed part is kept, the rest is
+                // cancelled -- the same shape the guard would produce.
+                BEAST_EXPECT(env.balance(alice, mpt) == mpt(kBigMpt));
+                BEAST_EXPECT(!bobOfferLive);
+            }
+            else
+            {
+                // Fill-or-kill: the offer is not fully fillable, so nothing
+                // crosses at all and bob's offer survives untouched.
+                BEAST_EXPECT(env.balance(alice, mpt) == mpt(0));
+                BEAST_EXPECT(bobOfferLive);
+            }
+            BEAST_EXPECT(offersOnAccount(env, alice).empty());
+        };
+
+        runScenario(tfImmediateOrCancel, tesSUCCESS);
+        runScenario(tfFillOrKill, tecKILLED);
+    }
+
     void
     testAll(FeatureBitset features)
     {
@@ -4920,6 +6487,7 @@ public:
         testSellOffer(features);
         testSellWithFillOrKill(features);
         testTransferRateOffer(features);
+        testTransferRateOverflowOffer(features);
         testSelfCrossOffer(features);
         testSelfIssueOffer(features);
         testDirectToDirectPath(features);
@@ -4934,11 +6502,24 @@ public:
         testDeletedOfferIssuer(features);
         testTicketOffer(features);
         testTicketCancelOffer(features);
+        testMPTAMMLimitQualityRounding(features);
         testRmSmallIncreasedQOffersXRP(features);
         testRmSmallIncreasedQOffersMPT(features);
+        testMPTIssuerOfferUsesRemainingCapacity(features);
+        testPartiallyFundedMPTInputOfferZeroInput(features);
         testFillOrKill(features);
         testTickSize(features);
+        testMPTOfferZeroRate(features);
+        testMPTOfferZeroRateCrossable(features);
+        testMPTOfferZeroRatePartialCross(features);
+        testMPTOfferZeroRateTickSizeCross(features);
+        testMPTOfferZeroRateFlags(features);
+        testZeroRateXrpIouOffer(features);
+        testBookOffersMPTFunding(features);
         testAutoCreateReserve(features);
+        testBookBaseMixedAssetCollision(features);
+        testBookBaseMptMptDistinct(features);
+        testBookBaseDomainMptDistinct(features);
     }
 
     void
diff --git a/src/test/app/PathMPT_test.cpp b/src/test/app/PathMPT_test.cpp
index ff4a024cb8..87da13087f 100644
--- a/src/test/app/PathMPT_test.cpp
+++ b/src/test/app/PathMPT_test.cpp
@@ -14,6 +14,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 
@@ -25,6 +27,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -33,6 +37,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -231,6 +236,102 @@ public:
         env.require(Balance("bob", usd(24)));
     }
 
+    void
+    sourceCurrencyWithSendMax()
+    {
+        testcase("source currency with send_max");
+        using namespace jtx;
+
+        Env env = pathTestEnv();
+        auto const alice = Account("alice");
+        auto const bob = Account("bob");
+        auto const gw = Account("gateway");
+        env.fund(XRP(10'000), alice, bob, gw);
+
+        MPT const usd = MPTTester({.env = env, .issuer = gw, .holders = {alice, bob}});
+        env(pay(gw, alice, usd(25)));
+        env.close();
+
+        // MPT source_currencies entries do not carry an issuer. A matching
+        // send_max identifies the same issuance, so the request should not run
+        // the IOU issuer reconciliation path.
+        auto const result = findPathsRequest(
+            env,
+            alice,
+            bob,
+            usd(-1),
+            std::optional(usd(10).value()),
+            std::optional(usd.mpt()));
+        BEAST_EXPECTS(!result.isMember(jss::error), result.toStyledString());
+
+        auto const& alternatives = result[jss::alternatives];
+        if (BEAST_EXPECT(alternatives.size() == 1))
+        {
+            auto const sa = amountFromJson(sfGeneric, alternatives[0u][jss::source_amount]);
+            auto const da = amountFromJson(sfGeneric, alternatives[0u][jss::destination_amount]);
+            BEAST_EXPECTS(equal(sa, usd(10)), sa.getFullText());
+            BEAST_EXPECTS(equal(da, usd(10)), da.getFullText());
+        }
+    }
+
+    void
+    maxedOutMPTPathfinding()
+    {
+        testcase("maxed-out MPT pathfinding");
+        using namespace jtx;
+
+        auto hasMPT = [](auto const& assets, MPT const& mpt) {
+            return std::ranges::any_of(assets, [&](auto const& asset) {
+                return asset.template holds() && asset.template get() == mpt.mpt();
+            });
+        };
+
+        Env env = pathTestEnv();
+        auto const gw = Account("gateway");
+        auto const alice = Account("alice");
+        auto const bob = Account("bob");
+        auto const carol = Account("carol");
+
+        env.fund(XRP(10'000), gw, alice, bob, carol);
+        env.close();
+
+        MPT const usd =
+            MPTTester({.env = env, .issuer = gw, .holders = {alice, bob, carol}, .maxAmt = 100});
+        env(pay(gw, alice, usd(90)));
+        env(pay(gw, bob, usd(10)));
+        env.close();
+
+        auto const cache =
+            std::make_shared(env.current(), env.app().getJournal("AssetCache"));
+
+        BEAST_EXPECT(hasMPT(accountSourceAssets(alice.id(), cache, false), usd));
+        BEAST_EXPECT(hasMPT(accountDestAssets(bob.id(), cache, false), usd));
+        BEAST_EXPECT(hasMPT(accountDestAssets(carol.id(), cache, false), usd));
+
+        // A fully minted issuance should not be advertised as issuer-side
+        // mintable source liquidity.
+        BEAST_EXPECT(!hasMPT(accountSourceAssets(gw.id(), cache, false), usd));
+
+        auto [st, sa, da] = findPaths(env, alice, bob, usd(5));
+        BEAST_EXPECT(st.empty());
+        BEAST_EXPECT(equal(sa, usd(5)));
+        BEAST_EXPECT(equal(da, usd(5)));
+
+        env(offer(carol, usd(5), XRP(5)));
+        env.close();
+
+        std::tie(st, sa, da) = findPaths(env, alice, bob, drops(-1), usd(100).value());
+        BEAST_EXPECT(sa == usd(5));
+        BEAST_EXPECT(equal(da, XRP(5)));
+        if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1))
+        {
+            auto const& pathElem = st[0][0];
+            BEAST_EXPECT(
+                pathElem.isOffer() && pathElem.getIssuerID() == xrpAccount() &&
+                pathElem.getCurrency() == xrpCurrency());
+        }
+    }
+
     void
     pathFind(bool const domainEnabled)
     {
@@ -441,6 +542,124 @@ public:
         }
     }
 
+    // Regression test: the Pathfinder constructor must honor the
+    // caller-supplied srcAmount (= the user's send_max from PathRequest)
+    // when ranking candidate paths in convert_all mode.
+    //
+    // Background. The MPT-DEX refactor of `Pathfinder::Pathfinder`
+    // (src/xrpld/rpc/detail/Pathfinder.cpp) replaced the original
+    // `mSrcAmount(srcAmount.value_or(...))` initializer with an
+    // unconditional `amountFromPathAsset(...)` call. The latter always
+    // returns the negative "no limit" STAmount sentinel, so the
+    // `srcAmount` constructor parameter became dead code:
+    // `getPathLiquidity` and `computePathRanks` ran `rippleCalculate`
+    // with `saMaxAmountReq` = sentinel and recorded each path's
+    // saturated capacity instead of the capacity reachable inside
+    // send_max.
+    //
+    // In convert_all_ mode (the only mode that allows send_max),
+    // `Pathfinder::rankPaths` ignores quality and orders purely by
+    // liquidity, then `Pathfinder::getBestPaths` only fills the last
+    // (kMaxPaths-th = 4th) slot when `pathRank.liquidity >= remaining`.
+    // For convert_all_ `remaining = largestAmount(dstAmount_)`, so the
+    // last slot effectively never fills and the cut keeps the top 3
+    // ranked paths. With the wrong (unbounded-budget) ranking, a
+    // low-capacity / high-rate path that would actually deliver the
+    // most under the user's send_max can be excluded entirely.
+    //
+    // Topology. Four candidate paths from alice's XRP to bob's USD-MPT,
+    // each via a distinct IOU intermediary issued by a different market
+    // maker:
+    //
+    //   charlie: XRP(1000) -> AUD(1000) -> USD(500)    cap 1000 XRP, rate 0.5
+    //   dave:    XRP(1000) -> EUR(1000) -> USD(500)    cap 1000 XRP, rate 0.5
+    //   eve:     XRP(1000) -> GBP(1000) -> USD(500)    cap 1000 XRP, rate 0.5
+    //   frank:   XRP(50)   -> JPY(50)   -> USD(75)     cap   50 XRP, rate 1.5
+    //
+    // Alice queries findPaths with destination = USD-MPT(-1) (convert_all)
+    // and send_max = XRP(100).
+    //
+    // Bug-free ranking (post-fix), with srcAmount = XRP(100):
+    //   charlie/dave/eve liquidity = min(100, 1000) * 0.5 = 50 USD each
+    //   frank   liquidity         = min(100, 50)   * 1.5 = 75 USD
+    // -> frank ranks first; the flow uses frank's 50 XRP at 1.5 (=75 USD)
+    //    plus 50 XRP via a 0.5-rate path (=25 USD), delivering USD(100).
+    //
+    // Pre-fix ranking, with srcAmount silently replaced by the sentinel:
+    //   charlie/dave/eve liquidity = 1000 * 0.5 = 500 USD each
+    //   frank   liquidity         =   50 * 1.5 = 75 USD
+    // -> frank ranks 4th; the last-slot rule excludes it from the
+    //    surviving path set, the cut keeps the three 0.5-rate paths,
+    //    and the flow delivers only 100 * 0.5 = USD(50).
+    //
+    // This test asserts the post-fix outcome (USD(100)). On the pre-fix
+    // tree the assertion fails with USD(50).
+    void
+    convertAllSendMaxRanking()
+    {
+        testcase("convert_all + send_max: srcAmount governs path ranking");
+        using namespace jtx;
+
+        Env env = pathTestEnv();
+        auto const alice = Account("alice");
+        auto const bob = Account("bob");
+        auto const gw = Account("gateway");
+        auto const charlie = Account("charlie");
+        auto const dave = Account("dave");
+        auto const eve = Account("eve");
+        auto const frank = Account("frank");
+
+        env.fund(XRP(10'000), alice, bob, gw, charlie, dave, eve, frank);
+        env.close();
+
+        // USD MPT issued by gw; the four market makers and bob are holders.
+        // alice is not a holder because she only pays XRP; USD only ever
+        // flows from gw / market-maker offers to bob.
+        MPT const usd =
+            MPTTester({.env = env, .issuer = gw, .holders = {charlie, dave, eve, frank, bob}});
+
+        // Capitalize each market maker with the USD-MPT they will sell.
+        env(pay(gw, charlie, usd(500)));
+        env(pay(gw, dave, usd(500)));
+        env(pay(gw, eve, usd(500)));
+        env(pay(gw, frank, usd(75)));
+        env.close();
+
+        // Each market maker issues their own intermediate IOU.
+        auto const aud = charlie["AUD"];
+        auto const eur = dave["EUR"];
+        auto const gbp = eve["GBP"];
+        auto const jpy = frank["JPY"];
+
+        // Three high-capacity, low-rate paths (1 XRP -> 0.5 USD-MPT,
+        // capacity 1000 XRP each).
+        env(offer(charlie, XRP(1'000), aud(1'000)));
+        env(offer(charlie, aud(1'000), usd(500)));
+        env(offer(dave, XRP(1'000), eur(1'000)));
+        env(offer(dave, eur(1'000), usd(500)));
+        env(offer(eve, XRP(1'000), gbp(1'000)));
+        env(offer(eve, gbp(1'000), usd(500)));
+
+        // One low-capacity, high-rate path (1 XRP -> 1.5 USD-MPT,
+        // capacity 50 XRP).
+        env(offer(frank, XRP(50), jpy(50)));
+        env(offer(frank, jpy(50), usd(75)));
+        env.close();
+
+        // ripple_path_find with convert_all (USD(-1)) and send_max XRP(100).
+        STPathSet st;
+        STAmount sa;
+        STAmount da;
+        std::tie(st, sa, da) =
+            findPaths(env, alice, bob, usd(-1), std::optional(XRP(100).value()));
+
+        // Post-fix: frank's high-rate path is included in the surviving
+        // path set, so the flow uses 50 XRP at 1.5 plus 50 XRP at 0.5,
+        // delivering exactly USD(100) on alice's 100-XRP budget.
+        BEAST_EXPECT(sa == XRP(100));
+        BEAST_EXPECT(equal(da, usd(100)));
+    }
+
     void
     run() override
     {
@@ -448,6 +667,9 @@ public:
         noDirectPathNoIntermediaryNoAlternatives();
         directPathNoIntermediary();
         paymentAutoPathFind();
+        sourceCurrencyWithSendMax();
+        maxedOutMPTPathfinding();
+        convertAllSendMaxRanking();
         for (auto const domainEnabled : {false, true})
         {
             pathFind(domainEnabled);
diff --git a/src/test/app/Path_test.cpp b/src/test/app/Path_test.cpp
index 29b4a5b048..cd61668b03 100644
--- a/src/test/app/Path_test.cpp
+++ b/src/test/app/Path_test.cpp
@@ -147,7 +147,8 @@ public:
         STAmount const& saDstAmount,
         std::optional const& saSendMax = std::nullopt,
         std::optional const& saSrcCurrency = std::nullopt,
-        std::optional const& domain = std::nullopt)
+        std::optional const& domain = std::nullopt,
+        std::optional const& saSrcIssuer = std::nullopt)
     {
         using namespace jtx;
 
@@ -181,6 +182,10 @@ public:
             auto& sc = params[jss::source_currencies] = json::ValueType::Array;
             json::Value j = json::ValueType::Object;
             j[jss::currency] = to_string(saSrcCurrency.value());
+            // Optional issuer for tests that need to exercise
+            // source_currencies entries more precisely than currency alone.
+            if (saSrcIssuer)
+                j[jss::issuer] = toBase58(*saSrcIssuer);
             sc.append(j);
         }
         if (domain)
@@ -209,10 +214,11 @@ public:
         STAmount const& saDstAmount,
         std::optional const& saSendMax = std::nullopt,
         std::optional const& saSrcCurrency = std::nullopt,
-        std::optional const& domain = std::nullopt)
+        std::optional const& domain = std::nullopt,
+        std::optional const& saSrcIssuer = std::nullopt)
     {
-        json::Value result =
-            findPathsRequest(env, src, dst, saDstAmount, saSendMax, saSrcCurrency, domain);
+        json::Value result = findPathsRequest(
+            env, src, dst, saDstAmount, saSendMax, saSrcCurrency, domain, saSrcIssuer);
         BEAST_EXPECT(!result.isMember(jss::error));
 
         STAmount da;
@@ -325,6 +331,53 @@ public:
         BEAST_EXPECT(result.isMember(jss::error));
     }
 
+    void
+    sourceCurrencyIssuerSelection()
+    {
+        testcase("source currency issuer selection");
+        using namespace jtx;
+
+        Env env = pathTestEnv();
+        auto const alice = Account("alice");
+        auto const bob = Account("bob");
+        auto const gateway = Account("gateway");
+
+        env.fund(XRP(10000), alice, bob, gateway);
+        env.close();
+
+        auto const usd = gateway["USD"];
+        env.trust(usd(600), alice);
+        env.trust(usd(700), bob);
+        env.trust(alice["USD"](700), bob);
+        env(pay(gateway, alice, usd(70)));
+        env(pay(gateway, bob, usd(50)));
+        env.close();
+
+        // Ask for USD from an explicit source issuer while send_max is
+        // Alice-issued USD. The parser should choose gateway-issued USD
+        // because gateway is the issuer in source_currencies.
+        //
+        // The Alice/Bob trust line is intentional: if Alice-issued USD is also
+        // considered as a source asset, pathfinding can produce an additional
+        // alternative. The single expected alternative below verifies that only
+        // the explicit issuer is selected.
+        auto const result = findPathsRequest(
+            env,
+            alice,
+            bob,
+            bob["USD"](-1),
+            alice["USD"](100).value(),
+            usd.currency,
+            std::nullopt,
+            gateway.id());
+        auto const& alternatives = result[jss::alternatives];
+        BEAST_EXPECT(alternatives.size() == 1);
+        auto const sa = amountFromJson(sfGeneric, alternatives[0u][jss::source_amount]);
+        auto const da = amountFromJson(sfGeneric, alternatives[0u][jss::destination_amount]);
+        BEAST_EXPECTS(equal(sa, usd(100)), sa.getFullText());
+        BEAST_EXPECTS(equal(da, bob["USD"](100)), da.getFullText());
+    }
+
     void
     noDirectPathNoIntermediaryNoAlternatives()
     {
@@ -1968,6 +2021,7 @@ public:
     run() override
     {
         sourceCurrenciesLimit();
+        sourceCurrencyIssuerSelection();
         noDirectPathNoIntermediaryNoAlternatives();
         directPathNoIntermediary();
         paymentAutoPathFind();
diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp
index a7e4cd7615..fbe942948d 100644
--- a/src/test/app/PermissionedDEX_test.cpp
+++ b/src/test/app/PermissionedDEX_test.cpp
@@ -21,6 +21,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -179,7 +180,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite
     void
     testOfferCreate(FeatureBitset features)
     {
-        testcase("OfferCreate");
+        bool const fixEnabled = features[fixCleanup3_4_0];
+
+        testcase << "OfferCreate"
+                 << (fixEnabled ? " (Cleanup3_4_0 enabled)" : " (Cleanup3_4_0 disabled)");
 
         // test preflight
         {
@@ -273,8 +277,10 @@ class PermissionedDEX_test : public beast::unit_test::Suite
             // time advance
             env.close(std::chrono::seconds(20));
 
-            // devin cannot create offer with expired cred
-            env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(tecNO_PERMISSION));
+            // Devin cannot create offer with expired cred. After fixCleanup3_4_0,
+            // doApply deletes the expired credential SLE and returns tecEXPIRED.
+            TER const expectedExpiredCredTer = fixEnabled ? tecEXPIRED : tecNO_PERMISSION;
+            env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(expectedExpiredCredTer));
             env.close();
         }
 
@@ -1510,7 +1516,9 @@ class PermissionedDEX_test : public beast::unit_test::Suite
         env.close(std::chrono::seconds(100));
 
         // Confirm devin can no longer create domain offers.
-        env(offer(devin, XRP(1), USD(1)), Domain(domainID), Ter(tecNO_PERMISSION));
+        // After fixCleanup3_4_0, OfferCreate deletes the expired credential and
+        // returns tecEXPIRED (covered in depth by testExpiredCredentialCleanup).
+        env(offer(devin, XRP(1), USD(1)), Domain(domainID), Ter(tecEXPIRED));
         env.close();
 
         // The hybrid offer must still exist in the open book after expiry.
@@ -1635,6 +1643,202 @@ class PermissionedDEX_test : public beast::unit_test::Suite
         BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq));
     }
 
+    void
+    testExpiredCredentialCleanup(FeatureBitset features)
+    {
+        bool const fixEnabled = features[fixCleanup3_4_0];
+
+        testcase << "Expired credential cleanup"
+                 << (fixEnabled ? " (Cleanup3_4_0 enabled)" : " (Cleanup3_4_0 disabled)");
+
+        TER const expectedExpiredCredTer = fixEnabled ? tecEXPIRED : tecNO_PERMISSION;
+
+        auto const fundAccount =
+            [](Env& env, Account const& account, Account const& gw, IOU const& usd) {
+                env.fund(XRP(1000), account);
+                env.close();
+                env.trust(usd(1000), account);
+                env.close();
+                env(pay(gw, account, usd(100)));
+                env.close();
+            };
+
+        auto const fundDevin = [&](Env& env, Account const& gw, IOU const& usd) {
+            Account const devin("devin");
+            fundAccount(env, devin, gw, usd);
+            return devin;
+        };
+
+        auto const createExpiringCredential = [](Env& env,
+                                                 Account const& subject,
+                                                 Account const& issuer,
+                                                 std::string const& credType) {
+            auto jv = credentials::create(subject, issuer, credType);
+            uint32_t const t = env.current()->header().parentCloseTime.time_since_epoch().count();
+            jv[sfExpiration.jsonName] = t + 20;
+            env(jv);
+            env(credentials::accept(subject, issuer, credType));
+            env.close();
+
+            return keylet::credential(subject.id(), issuer.id(), makeSlice(credType));
+        };
+
+        auto const expectExpiredCredentialState = [&](Env const& env, Keylet const& credKey) {
+            if (fixEnabled)
+            {
+                BEAST_EXPECT(!env.le(credKey));
+            }
+            else
+            {
+                BEAST_EXPECT(env.le(credKey));
+            }
+        };
+
+        // A payment referencing a non-existent domain is rejected in preclaim.
+        {
+            Env env(*this, features);
+            auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
+                PermissionedDEX(env);
+
+            uint256 const badDomain{
+                "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134"
+                "E5"};
+
+            env(offer(bob, XRP(10), USD(10)), Domain(domainID));
+            env.close();
+
+            env(pay(alice, bob, USD(10)),
+                Path(~USD),
+                Sendmax(XRP(10)),
+                Domain(badDomain),
+                Ter(tecNO_PERMISSION));
+            env.close();
+        }
+
+        // OfferCreate with an expired credential.
+        {
+            Env env(*this, features);
+            auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
+                PermissionedDEX(env);
+
+            Account const devin = fundDevin(env, gw, USD);
+            auto const credKey = createExpiringCredential(env, devin, domainOwner, credType);
+            BEAST_EXPECT(env.le(credKey));  // credential exists before expiry
+
+            env.close(std::chrono::seconds(20));
+
+            env(offer(devin, XRP(10), USD(10)), Domain(domainID), Ter(expectedExpiredCredTer));
+            env.close();
+
+            expectExpiredCredentialState(env, credKey);
+        }
+
+        // Payment where the sender's credential is expired.
+        {
+            Env env(*this, features);
+            auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
+                PermissionedDEX(env);
+
+            Account const devin = fundDevin(env, gw, USD);
+            auto const credKey = createExpiringCredential(env, devin, domainOwner, credType);
+
+            auto const bobOfferSeq{env.seq(bob)};
+            auto const bobCredKey =
+                keylet::credential(bob.id(), domainOwner.id(), makeSlice(credType));
+            env(offer(bob, XRP(10), USD(10)), Domain(domainID));
+            env.close();
+
+            BEAST_EXPECT(env.le(credKey));
+            BEAST_EXPECT(env.le(bobCredKey));
+            BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
+
+            env.close(std::chrono::seconds(20));
+
+            env(pay(devin, alice, USD(10)),
+                Path(~USD),
+                Sendmax(XRP(10)),
+                Domain(domainID),
+                Ter(expectedExpiredCredTer));
+            env.close();
+
+            expectExpiredCredentialState(env, credKey);
+            BEAST_EXPECT(env.le(bobCredKey));
+            BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
+        }
+
+        // Payment where the destination's credential is expired.
+        {
+            Env env(*this, features);
+            auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
+                PermissionedDEX(env);
+
+            Account const devin = fundDevin(env, gw, USD);
+            auto const credKey = createExpiringCredential(env, devin, domainOwner, credType);
+
+            auto const bobOfferSeq{env.seq(bob)};
+            auto const bobCredKey =
+                keylet::credential(bob.id(), domainOwner.id(), makeSlice(credType));
+            env(offer(bob, XRP(10), USD(10)), Domain(domainID));
+            env.close();
+
+            BEAST_EXPECT(env.le(credKey));
+            BEAST_EXPECT(env.le(bobCredKey));
+            BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
+
+            env.close(std::chrono::seconds(20));
+
+            env(pay(alice, devin, USD(10)),
+                Path(~USD),
+                Sendmax(XRP(10)),
+                Domain(domainID),
+                Ter(expectedExpiredCredTer));
+            env.close();
+
+            expectExpiredCredentialState(env, credKey);
+            BEAST_EXPECT(env.le(bobCredKey));
+            BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
+        }
+
+        // Payment where both sender and destination credentials are expired.
+        {
+            Env env(*this, features);
+            auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
+                PermissionedDEX(env);
+
+            Account const devin = fundDevin(env, gw, USD);
+            Account const erin("erin");
+            fundAccount(env, erin, gw, USD);
+
+            auto const devinCredKey = createExpiringCredential(env, devin, domainOwner, credType);
+            auto const erinCredKey = createExpiringCredential(env, erin, domainOwner, credType);
+
+            auto const bobOfferSeq{env.seq(bob)};
+            auto const bobCredKey =
+                keylet::credential(bob.id(), domainOwner.id(), makeSlice(credType));
+            env(offer(bob, XRP(10), USD(10)), Domain(domainID));
+            env.close();
+
+            BEAST_EXPECT(env.le(devinCredKey));
+            BEAST_EXPECT(env.le(erinCredKey));
+            BEAST_EXPECT(env.le(bobCredKey));
+            BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
+
+            env.close(std::chrono::seconds(20));
+
+            env(pay(devin, erin, USD(10)),
+                Path(~USD),
+                Sendmax(XRP(10)),
+                Domain(domainID),
+                Ter(expectedExpiredCredTer));
+            env.close();
+
+            expectExpiredCredentialState(env, devinCredKey);
+            expectExpiredCredentialState(env, erinCredKey);
+            BEAST_EXPECT(env.le(bobCredKey));
+            BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
+        }
+    }
+
     void
     testHybridMalformedOffer(FeatureBitset features)
     {
@@ -2008,6 +2212,143 @@ class PermissionedDEX_test : public beast::unit_test::Suite
         }
     }
 
+    void
+    testDomainOfferInWrongBook(FeatureBitset features)
+    {
+        bool const fixEnabled = features[fixCleanup3_4_0];
+
+        testcase << "Domain offer indexed in the wrong domain book"
+                 << (fixEnabled ? " (fixCleanup3_4_0 enabled)" : " (fixCleanup3_4_0 disabled)");
+
+        // Bob (a member of domains A and B) places an offer in domain A's
+        // book, which we then corrupt to claim domain B while it stays in
+        // domain A's book. A payment routed through domain A meets this offer.
+        //
+        // - With fixCleanup3_4_0: OfferStream sees the offer's domain (B)
+        //   mismatch the book (A) and errors out -> tecPATH_PARTIAL.
+        // - Without it: OfferStream only checks the offer's own domain (B,
+        //   which Bob is in), so it is used; the invariant then catches the
+        //   mismatch -> tecINVARIANT_FAILED.
+        //
+        // Either way the payment fails and the offer is left untouched.
+
+        Env env(*this, features);
+        auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
+            PermissionedDEX(env);
+
+        // A second domain that Bob also belongs to.
+        Account const bobAcct = bob;
+        auto const domainID2 =
+            setupDomain(env, {bobAcct}, Account("permdex-domainOwner2"), "permdex-cred2");
+
+        // Bob places a domain offer in domain A's book.
+        auto const bobOfferSeq{env.seq(bob)};
+        env(offer(bob, XRP(10), USD(10)), Domain(domainID));
+        env.close();
+        BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
+
+        // Corrupt the offer: point its sfDomainID at domain B while it stays
+        // indexed in domain A's book directory.
+        auto const offerKey = keylet::offer(bob.id(), SeqProxy::rawSequence(bobOfferSeq));
+        env.app().getOpenLedger().modify([&offerKey, &domainID2](OpenView& view, beast::Journal) {
+            auto const sle = view.read(offerKey);
+            if (!sle)
+                return false;
+            auto replacement = std::make_shared(*sle, sle->key());
+            replacement->setFieldH256(sfDomainID, domainID2);
+            view.rawReplace(replacement);
+            return true;
+        });
+
+        if (fixEnabled)
+        {
+            // With the fix: OfferStream rejects the mismatched offer.
+            env(pay(alice, carol, USD(10)),
+                Path(~USD),
+                Sendmax(XRP(10)),
+                Domain(domainID),
+                Ter(tecPATH_PARTIAL));
+            BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
+        }
+        else
+        {
+            // Without the fix: the offer is used, then the invariant
+            // rejects the whole transaction.
+            env(pay(alice, carol, USD(10)),
+                Path(~USD),
+                Sendmax(XRP(10)),
+                Domain(domainID),
+                Ter(tecINVARIANT_FAILED));
+            BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
+        }
+    }
+
+    void
+    testDomainBookOfferMissingDomain(FeatureBitset features)
+    {
+        bool const fixEnabled = features[fixCleanup3_4_0];
+
+        testcase << "Offer without a domain indexed in a domain book"
+                 << (fixEnabled ? " (fixCleanup3_4_0 enabled)" : " (fixCleanup3_4_0 disabled)");
+
+        // Same corruption as testDomainOfferInWrongBook, except the offer
+        // loses sfDomainID entirely instead of pointing at another domain
+        // while it stays indexed in domain A's book.
+        //
+        // - With fixCleanup3_4_0: OfferStream sees an offer that claims no
+        //   domain in a domain book and errors out -> tecPATH_PARTIAL.
+        // - Without it: neither the domain mismatch check nor the domain
+        //   membership check fires (both are gated on sfDomainID being
+        //   present), and the invariant does not catch it either because the
+        //   offer is fully consumed and deleted. The payment succeeds using an
+        //   offer that was never credential checked.
+
+        Env env(*this, features);
+        auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
+            PermissionedDEX(env);
+
+        // Bob places a domain offer in domain A's book.
+        auto const bobOfferSeq{env.seq(bob)};
+        env(offer(bob, XRP(10), USD(10)), Domain(domainID));
+        env.close();
+        BEAST_EXPECT(checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true));
+
+        // Corrupt the offer: drop sfDomainID while it stays indexed in domain
+        // A's book directory.
+        auto const offerKey = keylet::offer(bob.id(), SeqProxy::rawSequence(bobOfferSeq));
+        env.app().getOpenLedger().modify([&offerKey](OpenView& view, beast::Journal) {
+            auto const sle = view.read(offerKey);
+            if (!sle)
+                return false;
+            auto replacement = std::make_shared(*sle, sle->key());
+            replacement->makeFieldAbsent(sfDomainID);
+            view.rawReplace(replacement);
+            return true;
+        });
+
+        auto const carolBefore = env.balance(carol, USD);
+
+        if (fixEnabled)
+        {
+            // With the fix: OfferStream rejects the domainless offer.
+            env(pay(alice, carol, USD(10)),
+                Path(~USD),
+                Sendmax(XRP(10)),
+                Domain(domainID),
+                Ter(tecPATH_PARTIAL));
+            BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
+            BEAST_EXPECT(env.balance(carol, USD) - carolBefore == USD(0));
+        }
+        else
+        {
+            // Without the fix: the offer is silently usable in the domain
+            // book, and the payment goes through.
+            env(pay(alice, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
+            BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq));
+            BEAST_EXPECT(env.balance(carol, USD) - carolBefore == USD(10));
+        }
+    }
+
     void
     testReplaceDomainOfferWithOtherDomainOffer(FeatureBitset features)
     {
@@ -2072,6 +2413,7 @@ public:
         // Test domain offer (w/o hybrid)
         testOfferCreate(all);
         testOfferCreate(all - fixCleanup3_2_0);
+        testOfferCreate(all - fixCleanup3_4_0);
         testPayment(all);
         testPayment(all - fixCleanup3_2_0);
         testBookStep(all);
@@ -2082,6 +2424,8 @@ public:
         testAmmQualityNotLeaked(all);
         testAmmQualityNotLeaked(all - fixCleanup3_3_0);
         testAutoBridge(all);
+        testExpiredCredentialCleanup(all);
+        testExpiredCredentialCleanup(all - fixCleanup3_4_0);
 
         // Test hybrid offers
         testHybridOfferCreate(all);
@@ -2100,6 +2444,14 @@ public:
         // only after fixCleanup3_2_0.
         testCancelRegularOfferWithDomainCreate(all);
         testCancelRegularOfferWithDomainCreate(all - fixCleanup3_2_0);
+
+        // A domain offer indexed in the wrong domain book is caught only
+        // after fixCleanup3_4_0. (Not an existing bug, but defensive testing)
+        testDomainOfferInWrongBook(all);
+        testDomainOfferInWrongBook(all - fixCleanup3_4_0);
+        testDomainBookOfferMissingDomain(all);
+        testDomainBookOfferMissingDomain(all - fixCleanup3_4_0);
+
         testReplaceDomainOfferWithOtherDomainOffer(all);
         testReplaceDomainOfferWithOtherDomainOffer(all - fixCleanup3_4_0);
     }
diff --git a/src/test/app/SHAMapStore_test.cpp b/src/test/app/SHAMapStore_test.cpp
index 6ee7442d23..4a69ac17f9 100644
--- a/src/test/app/SHAMapStore_test.cpp
+++ b/src/test/app/SHAMapStore_test.cpp
@@ -1,7 +1,10 @@
+#include 
 #include 
 #include 
 #include 
+#include 
 
+#include 
 #include 
 #include 
 #include 
@@ -9,10 +12,13 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -22,30 +28,229 @@
 #include 
 #include 
 #include 
+#include 
 
-#include 
-
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
+#include 
 
 namespace xrpl::test {
 
 class SHAMapStore_test : public beast::unit_test::Suite
 {
-    static auto const kDeleteInterval = 8;
+    static constexpr int kDeleteInterval = 8;
+
+    // Mirrors SHAMapStoreImp::kMinimumDeletionIntervalSa, the floor that
+    // online_delete is held to in standalone mode. Note that the
+    // max_waiting_ledgers floor is derived from this minimum rather than from
+    // the configured interval -- SHAMapStoreImp.cpp computes it as
+    // minInterval / 4 -- so both constants below stay put if kDeleteInterval is
+    // ever raised.
+    static constexpr int kMinDeleteInterval = 8;
+    static constexpr int kMinWaitingLedgers = kMinDeleteInterval / 4;
+    static_assert(kDeleteInterval >= kMinDeleteInterval);
+
+    // The two wait durations healthWait() can choose from, spelled as they
+    // appear in its log message. onlineDelete() below sets
+    // recovery_wait_seconds to 1, so the full wait is 1000ms and the shortened
+    // wait is a tenth of that. Note that "Waiting 1000ms" does not contain
+    // "Waiting 100ms", so the two are distinguishable by substring.
+    static constexpr char const* kFullWait = "Waiting 1000ms for node to stabilize";
+    static constexpr char const* kShortWait = "Waiting 100ms for node to stabilize";
+
+    // Distinctive fragments of the other messages the tests below key off. Each
+    // is unique among everything SHAMapStoreImp logs, so a substring match
+    // identifies the message unambiguously.
+    //
+    // kRotating is logged once run() has committed to a rotation, immediately
+    // after the health check that gates it, and kFinished once a rotation has
+    // run to completion. kExpired is logged by healthWait() when the circuit
+    // breaker trips.
+    static constexpr char const* kRotating = "rotating";
+    static constexpr char const* kFinished = "finished rotation";
+    static constexpr char const* kExpired = "unable to make progress";
+
+    // A Logs implementation that records every message the store's own
+    // partition emits, keeping each message's severity alongside its text, and
+    // lets a test block until a given message has appeared.
+    //
+    // Severity is recorded because healthWait() picks the severity and the wait
+    // duration together, so the pair identifies which of its three logging
+    // branches ran: warn at the full wait when the server is unhealthy for a
+    // reason that is not expected to resolve on its own, trace at a tenth of
+    // the wait when the only missing ledger is the one currently being built,
+    // and info at the full wait otherwise. Matching on the pair is what lets
+    // the tests below assert which branch was taken instead of merely that some
+    // wait happened.
+    //
+    // waitFor() exists because run() logs on entry to a rotation, which is the
+    // only signal a test has that the store has passed the health check gating
+    // the rotation and is now inside it. Several of the branches under test are
+    // only reachable from there, and no other handle on the store exposes it.
+    class StoreLogs : public Logs
+    {
+        mutable std::mutex mutex_;
+        std::condition_variable cond_;
+        std::vector> messages_;
+
+        class Sink : public beast::Journal::Sink
+        {
+            StoreLogs& owner_;
+
+        public:
+            Sink(beast::Severity threshold, StoreLogs& owner)
+                : beast::Journal::Sink(threshold, false), owner_(owner)
+            {
+            }
+
+            // Env::AppBundle calls Logs::threshold() after the Application is
+            // built, which would otherwise raise this sink above Trace and
+            // discard the messages the buildingIndex branch logs.
+            void
+            threshold(beast::Severity) override
+            {
+            }
+
+            void
+            write(beast::Severity level, std::string const& text) override
+            {
+                {
+                    std::scoped_lock const lock(owner_.mutex_);
+                    owner_.messages_.emplace_back(level, text);
+                }
+                owner_.cond_.notify_all();
+            }
+
+            void
+            writeAlways(beast::Severity level, std::string const& text) override
+            {
+                write(level, text);
+            }
+        };
+
+        // Caller must hold mutex_. A nullopt severity matches any severity.
+        [[nodiscard]] std::size_t
+        countLocked(std::optional severity, std::string const& text) const
+        {
+            return std::count_if(messages_.begin(), messages_.end(), [&](auto const& message) {
+                return (!severity || message.first == *severity) &&
+                    message.second.find(text) != std::string::npos;
+            });
+        }
+
+    public:
+        StoreLogs() : Logs(beast::Severity::Trace)
+        {
+        }
+
+        // Only the store's own partition is logged at Trace; everything else
+        // is silenced, so that enabling trace for this one branch does not pay
+        // for formatting every trace message in the server.
+        std::unique_ptr
+        makeSink(std::string const& partition, beast::Severity) override
+        {
+            return std::make_unique(
+                partition == "SHAMapStore" ? beast::Severity::Trace : beast::Severity::Disabled,
+                *this);
+        }
+
+        // How many recorded messages were logged at `severity` and contain
+        // `text`.
+        [[nodiscard]] std::size_t
+        count(beast::Severity severity, std::string const& text) const
+        {
+            std::scoped_lock const lock(mutex_);
+            return countLocked(severity, text);
+        }
+
+        // How many recorded messages contain `text`, at any severity.
+        [[nodiscard]] std::size_t
+        count(std::string const& text) const
+        {
+            std::scoped_lock const lock(mutex_);
+            return countLocked(std::nullopt, text);
+        }
+
+        // Blocks until `text` has been logged at least `expected` times at
+        // `severity` -- or at any severity, if that is nullopt -- or until the
+        // timeout expires. Returns whether it got there.
+        //
+        // Waiting rather than sleeping-then-counting matters for the branches
+        // that are only reached after a rotation has started: the store gets
+        // there when it gets there, so a fixed sleep has to be sized for the
+        // slowest plausible machine, whereas this returns as soon as the
+        // message appears.
+        [[nodiscard]] bool
+        waitFor(
+            std::optional severity,
+            std::string const& text,
+            std::chrono::milliseconds timeout,
+            std::size_t expected = 1)
+        {
+            std::unique_lock lock(mutex_);
+            return cond_.wait_for(
+                lock, timeout, [&] { return countLocked(severity, text) >= expected; });
+        }
+
+        // As above, at any severity.
+        [[nodiscard]] bool
+        waitFor(
+            std::string const& text,
+            std::chrono::milliseconds timeout,
+            std::size_t expected = 1)
+        {
+            return waitFor(std::nullopt, text, timeout, expected);
+        }
+    };
 
     static auto
     onlineDelete(std::unique_ptr cfg)
     {
-        cfg->ledgerHistory = kDeleteInterval;
+        cfg = jtx::onlineDelete(std::move(cfg), kDeleteInterval);
+        cfg->section(Sections::kNodeDatabase).set(Keys::kRecoveryWaitSeconds, "1");
+        return cfg;
+    }
+
+    // online delete tuned so that a rotation, once it has started, spends a
+    // long time in clearPrior() before reaching the first health check inside
+    // the rotation body.
+    //
+    // clearSql() sleeps back_off_milliseconds at the top of every iteration and
+    // advances by delete_batch rows per iteration, so a delete_batch of 1 costs
+    // one sleep per ledger removed, for each of the three tables it is called
+    // on. That is what gives parkMidRotation() below a window measured in
+    // seconds rather than in microseconds. delete_batch is therefore the actual
+    // lever; back_off_milliseconds is set to the value SHAMapStoreImp already
+    // defaults to, and is spelled out only so the arithmetic above can be
+    // checked against the config rather than against the implementation.
+    //
+    // max_waiting_ledgers is pinned to its floor so that tripping the circuit
+    // breaker takes the fewest possible ledger closes.
+    static auto
+    slowOnlineDelete(std::unique_ptr cfg)
+    {
+        cfg = onlineDelete(std::move(cfg));
         auto& section = cfg->section(Sections::kNodeDatabase);
-        section.set(Keys::kOnlineDelete, std::to_string(kDeleteInterval));
+        section.set(Keys::kDeleteBatch, "1");
+        section.set(Keys::kBackOffMilliseconds, "100");
+        section.set(Keys::kMaxWaitingLedgers, std::to_string(kMinWaitingLedgers));
         return cfg;
     }
 
@@ -136,6 +341,96 @@ class SHAMapStore_test : public beast::unit_test::Suite
         BEAST_EXPECT(env.app().getRelationalDatabase().getAccountTransactionCount() == rows);
     }
 
+    // Wait until the SHAMapStore has finished processing the ledger that the
+    // preceding env.close() produced.
+    //
+    // env.close() returns as soon as the ledger_accept RPC returns, but the
+    // validated ledger path -- LedgerMaster::setValidLedger() ->
+    // SHAMapStore::onLedgerClosed() -- runs on a job queue thread. Without
+    // draining the job queue first, the store may not have been handed the
+    // ledger at all, in which case rendezvous() observes working_ == false and
+    // returns immediately, before any work has been done.
+    [[nodiscard]] static bool
+    syncStore(jtx::Env& env)
+    {
+        // Drain the job queue first, so that onLedgerClosed() has run and
+        // working_ is set. Then use the store's timeout overload, so a store
+        // that never finishes fails this test instead of blocking on it.
+        //
+        // Only the second wait is bounded: JobQueue::rendezvous() has no
+        // timeout overload, so a job that never completes hangs here. That is
+        // pre-existing -- ~AppBundle waits on it the same way for every jtx
+        // test -- but it does mean this helper is not hang-proof end to end.
+        env.app().getJobQueue().rendezvous();
+        return env.app().getSHAMapStore().rendezvous(std::chrono::seconds{60});
+    }
+
+    // Bring the SHAMapStore to the point where it has been handed a validated
+    // ledger and initialized lastRotated, and report how many extra ledgers had
+    // to be closed to get it there (normally none). Returns std::nullopt if
+    // syncStore() itself failed.
+    //
+    // syncStore() alone does not guarantee that, because
+    // SHAMapStoreImp::run()'s loop does not use the notification and the
+    // working_ flag safely:
+    //
+    //   * onLedgerClosed() notifies cond_ whether or not run()'s thread is
+    //     parked on it, and run() waits on cond_ without a predicate, so a
+    //     notification that lands while the thread is still starting up --
+    //     before it first reaches that wait -- is lost.
+    //   * run() clears working_ at the top of its loop without checking
+    //     whether newLedger_ is still set, so rendezvous() can report the
+    //     store idle with a validated ledger queued.
+    //
+    // Either way the store ends up parked with work pending, and only another
+    // notification gets it moving again. In a standalone test nothing else
+    // closes ledgers, so that has to come from here: this closes a ledger
+    // rather than polling getLastRotated(), because polling would just time
+    // out. onLedgerClosed() keeps only the most recent ledger in newLedger_,
+    // so the ledger the store picks up -- and therefore lastRotated -- is a
+    // timing detail, which is why the callers derive their expectations from
+    // the value they observe instead of assuming one.
+    //
+    // run() is deliberately left as it is. In production the only effect is
+    // latency: the trigger is validatedSeq >= lastRotated + deleteInterval, so
+    // a lost notification delays rotation to the next validated ledger and
+    // nothing is skipped or accumulated -- starting at 513 instead of 512 does
+    // not matter. Two consequences do follow from leaving it in place, and both
+    // hold today: nothing in production decides anything from working_ or
+    // rendezvous() (rendezvous() has no production callers at all), and a node
+    // whose ledgers only advance on demand -- standalone, driven by
+    // ledger_accept -- can sit on a queued ledger until something closes the
+    // next one, which is exactly the situation this helper is working around.
+    //
+    // So this helper is permanent rather than a stopgap. Working around the
+    // race must not make it invisible, so every extra close is logged. That
+    // keeps how often it is actually hit observable in the unit test output --
+    // which is the only signal left once these testcases stop flaking on it.
+    [[nodiscard]] std::optional
+    initializeStore(jtx::Env& env, int const maxExtraCloses = 3)
+    {
+        auto& store = env.app().getSHAMapStore();
+
+        for (int extraCloses = 0;; ++extraCloses)
+        {
+            if (!syncStore(env))
+                return std::nullopt;
+            if (store.getLastRotated() != 0 || extraCloses == maxExtraCloses)
+            {
+                if (extraCloses != 0)
+                {
+                    log << "initializeStore: the store needed " << extraCloses
+                        << " extra ledger close(s) to pick up a validated ledger. "
+                           "SHAMapStoreImp::run() dropped the notification for the "
+                           "first one; see the comment on initializeStore()."
+                        << std::endl;
+                }
+                return extraCloses;
+            }
+            env.close();
+        }
+    }
+
     int
     waitForReady(jtx::Env& env)
     {
@@ -144,11 +439,11 @@ class SHAMapStore_test : public beast::unit_test::Suite
         auto& store = env.app().getSHAMapStore();
 
         int ledgerSeq = 3;
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
         BEAST_EXPECT(!store.getLastRotated());
 
         env.close();
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         auto ledger = env.rpc("ledger", "validated");
         BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++)));
@@ -157,7 +452,417 @@ class SHAMapStore_test : public beast::unit_test::Suite
         return ledgerSeq;
     }
 
+    // Construct an Env whose config has online_delete enabled and is then
+    // mutated by `tweak`, and report how SHAMapStoreImp's constructor judged
+    // it: the message of the exception it threw, or std::nullopt if the Env was
+    // constructed successfully.
+    //
+    // SHAMapStoreImp is built from ApplicationImp's member initializer list, so
+    // a configuration it rejects surfaces as an exception thrown out of the Env
+    // constructor rather than as a failure at some later point.
+    //
+    // Note that ~AppBundle does not run when the Env constructor throws, so the
+    // global debug log sink it installed -- which holds a reference to this
+    // suite -- would outlive the suite. The catch below clears it, so callers
+    // are free to end on a configuration that is rejected.
+    std::optional
+    storeConfigResult(std::function const& tweak)
+    {
+        using namespace test::jtx;
+
+        try
+        {
+            Env const env{
+                *this,
+                envconfig([&tweak](std::unique_ptr cfg) {
+                    cfg = onlineDelete(std::move(cfg));
+                    tweak(*cfg);
+                    return cfg;
+                }),
+                nullptr,
+                beast::Severity::Disabled};
+            return std::nullopt;
+        }
+        // Deliberately broader than the std::runtime_error that
+        // SHAMapStoreImp throws: an unexpected exception type then shows up as
+        // a message mismatch naming the actual failure, rather than escaping
+        // this testcase.
+        catch (std::exception const& e)
+        {
+            // ~AppBundle did not run, so drop the sink it installed by hand
+            // rather than leaving a reference to this suite live in a global.
+            setDebugLogSink(nullptr);
+            return std::string{e.what()};
+        }
+    }
+
+    void
+    expectConfigRejected(std::string const& expected, std::function const& tweak)
+    {
+        auto const result = storeConfigResult(tweak);
+        BEAST_EXPECTS(result == expected, result.value_or(""));
+    }
+
+    void
+    expectConfigAccepted(std::function const& tweak)
+    {
+        auto const result = storeConfigResult(tweak);
+        BEAST_EXPECTS(!result, result.value_or(""));
+    }
+
+    // The state parkInHealthWait() leaves behind.
+    struct Parked
+    {
+        // Value of getLastRotated() before the rotation attempt began. The
+        // store must still report this for as long as it stays parked.
+        LedgerIndex lastRotated = 0;
+        // The validated ledger the store is waiting on, and the sequence
+        // getLastRotated() will report once the rotation finally completes.
+        LedgerIndex validated = 0;
+        // Sequence removed from LedgerMaster to create the gap, or 0 if
+        // `createGap` was false.
+        LedgerIndex gap = 0;
+    };
+
+    // Drive the store to the point where it is parked inside healthWait(),
+    // unable to proceed with a rotation: close ledgers until one more close
+    // would make the store due to rotate, optionally remove the newest ledger
+    // from LedgerMaster so that the attempt sees a gap in the range, close the
+    // triggering ledger, and then set the operating mode to `modeAfterClose`.
+    //
+    // Once parked, the store stays parked indefinitely. Its wait loop reruns
+    // every recovery_wait_seconds and exits only when the server looks healthy,
+    // when it is stopped, or when the validated ledger index reaches the
+    // circuit breaker -- and that index only advances when this test closes
+    // another ledger. So a caller can establish any server state it likes,
+    // hold it, and be sure the store observes it. That is what makes the tests
+    // below state machines rather than races.
+    //
+    // Returns std::nullopt if the setup did not reach a parked store, having
+    // already reported the failure.
+    std::optional
+    parkInHealthWait(jtx::Env& env, bool createGap, OperatingMode modeAfterClose)
+    {
+        using namespace std::chrono_literals;
+        using namespace test::jtx;
+
+        auto& lm = env.app().getLedgerMaster();
+        auto& store = env.app().getSHAMapStore();
+        auto& netOPs = env.app().getOPs();
+
+        env.fund(XRP(1000), Account("alice"));
+        env.close();
+        if (!BEAST_EXPECT(initializeStore(env).has_value()))
+            return std::nullopt;
+
+        Parked parked;
+        // The store adopts the first validated ledger it sees as lastRotated,
+        // and which one that is depends on timing, so read it rather than
+        // assuming a value.
+        parked.lastRotated = store.getLastRotated();
+        if (!BEAST_EXPECT(parked.lastRotated))
+            return std::nullopt;
+
+        // Close ledgers until the next close is the one that makes
+        // validatedSeq reach lastRotated + deleteInterval.
+        LedgerIndex maxSeq = env.closed()->header().seq;
+        while (maxSeq + 1 < parked.lastRotated + kDeleteInterval)
+        {
+            env.close();
+            ++maxSeq;
+            if (!BEAST_EXPECT(syncStore(env)))
+                return std::nullopt;
+            if (!BEAST_EXPECTS(
+                    store.getLastRotated() == parked.lastRotated,
+                    std::to_string(store.getLastRotated())))
+                return std::nullopt;
+        }
+
+        // Drop out of FULL before touching LedgerMaster's internals, matching
+        // testLedgerGaps. This also keeps the store from rotating on the
+        // triggering close before the caller has set the state it wants
+        // observed.
+        netOPs.setMode(OperatingMode::CONNECTED);
+
+        // The gap goes one below the sequence that the close further down makes
+        // validated, never at that sequence itself: healthWait() derives
+        // buildingIndex from numMissing == 1 && !haveLedger(index), so a gap at
+        // the validated index reads as "that ledger is about to be built" and
+        // takes the short trace wait, whereas a gap below it reads as a
+        // genuinely incomplete range and takes the full wait. Nothing refills
+        // the gap, so the wait loop ends only when the circuit breaker trips or
+        // stop() intervenes.
+        if (createGap)
+        {
+            std::size_t iterations = 30;
+            while (!lm.haveLedger(maxSeq) && --iterations > 0)
+            {
+                std::this_thread::sleep_for(10ms);
+            }
+            if (!BEAST_EXPECTS(lm.haveLedger(maxSeq), std::to_string(maxSeq)))
+                return std::nullopt;
+
+            // Give the server a moment to finish any internal work on the
+            // ledger about to be removed, as testLedgerGaps does.
+            std::this_thread::sleep_for(250ms);
+
+            lm.clearLedger(maxSeq);
+            if (!BEAST_EXPECT(!lm.haveLedger(maxSeq)))
+                return std::nullopt;
+            parked.gap = maxSeq;
+        }
+
+        // This close makes the store due to rotate.
+        env.close();
+        ++maxSeq;
+        parked.validated = maxSeq;
+        netOPs.setMode(modeAfterClose);
+
+        // Drain the job queue so that onLedgerClosed() has handed the ledger to
+        // the store. Without this, working_ may still be false from the
+        // previous cycle and rendezvous() would report "done" before the store
+        // has even looked at this ledger.
+        env.app().getJobQueue().rendezvous();
+
+        if (!BEAST_EXPECT(!store.rendezvous(1s)))
+            return std::nullopt;
+        if (!BEAST_EXPECTS(
+                store.getLastRotated() == parked.lastRotated,
+                std::to_string(store.getLastRotated())))
+            return std::nullopt;
+
+        return parked;
+    }
+
+    // Drive the store to the point where it has passed the health check that
+    // gates a rotation and is inside the rotation body, then make the server
+    // unhealthy so that the next health check in there parks it.
+    //
+    // The gap cannot be created up front the way parkInHealthWait() does it,
+    // because the gating check would see it and refuse to start the rotation at
+    // all -- which is what testLedgerGaps() exercises. So this waits for the
+    // message run() logs immediately after that check, which is the store
+    // publishing that it is committed to the rotation, and creates the gap then.
+    //
+    // The margin that makes that safe is clearPrior(), which runs between the
+    // log and the first health check inside the rotation. Under
+    // slowOnlineDelete() it works through three tables one sequence at a time,
+    // sleeping back_off_milliseconds before each, and checks health after every
+    // one of those sleeps. So the store spends on the order of a second per
+    // table repeatedly asking whether it is healthy, against the microseconds
+    // this function needs to clear a ledger once waitFor() has returned.
+    //
+    // The gap has to be the validated ledger itself. healthWait() counts missing
+    // ledgers over the range from lastGoodValidatedLedger_ to the validated
+    // index, and run() sets the former to the latter just before starting the
+    // rotation, so for the duration of the rotation that range begins as a
+    // single sequence and grows only as the caller closes more ledgers.
+    //
+    // Which health check inside the rotation ends up observing the gap is not
+    // pinned down, and does not need to be: whichever one it is returns the same
+    // answer, clearPrior() gives up, and run() reaches its first switch on
+    // healthWait() with the condition still in force. Every assertion below
+    // holds for any of them.
+    //
+    // Returns std::nullopt if the setup did not reach a parked store, having
+    // already reported the failure.
+    std::optional
+    parkMidRotation(jtx::Env& env, StoreLogs& log)
+    {
+        using namespace std::chrono_literals;
+        using namespace test::jtx;
+
+        auto& lm = env.app().getLedgerMaster();
+        auto& store = env.app().getSHAMapStore();
+
+        auto const alice = Account("alice");
+        env.fund(XRP(1000), alice);
+        env.close();
+        if (!BEAST_EXPECT(initializeStore(env).has_value()))
+            return std::nullopt;
+
+        LedgerIndex maxSeq = env.closed()->header().seq;
+        // Close one ledger, carrying a transaction so that the sequence has rows
+        // in all three of the tables clearSql() works through.
+        auto closeOne = [&]() -> bool {
+            env(noop(alice));
+            env.close();
+            ++maxSeq;
+            return BEAST_EXPECT(syncStore(env));
+        };
+
+        // Let one rotation complete before setting up the one to be parked. The
+        // window this helper depends on only exists once the tables hold a full
+        // delete interval of rows: on the very first rotation there is at most
+        // one sequence to remove, so clearSql() sleeps once or not at all.
+        LedgerIndex const firstRotated = store.getLastRotated();
+        if (!BEAST_EXPECT(firstRotated))
+            return std::nullopt;
+        while (store.getLastRotated() == firstRotated)
+        {
+            if (!closeOne())
+                return std::nullopt;
+            // The rotation is due once maxSeq reaches firstRotated +
+            // kDeleteInterval. Allow one close beyond that before giving up,
+            // rather than closing ledgers forever.
+            if (!BEAST_EXPECTS(maxSeq <= firstRotated + kDeleteInterval, std::to_string(maxSeq)))
+                return std::nullopt;
+        }
+
+        Parked parked;
+        parked.lastRotated = store.getLastRotated();
+
+        // Close ledgers until the next close is the one that makes the store due
+        // to rotate again.
+        while (maxSeq + 1 < parked.lastRotated + kDeleteInterval)
+        {
+            if (!closeOne())
+                return std::nullopt;
+            if (!BEAST_EXPECTS(
+                    store.getLastRotated() == parked.lastRotated,
+                    std::to_string(store.getLastRotated())))
+                return std::nullopt;
+        }
+
+        // One rotation has already been logged, so wait for the next one rather
+        // than for the first.
+        auto const rotationsBefore = log.count(kRotating);
+
+        // This close makes the store due to rotate. Deliberately do not drain
+        // the store here: the point is to interrupt it partway through.
+        env(noop(alice));
+        env.close();
+        ++maxSeq;
+        parked.validated = maxSeq;
+
+        // Draining the job queue, on the other hand, is required. In standalone
+        // mode switchLCL() inserts the closed ledger into LedgerMaster's
+        // complete range and then posts an advance job, and publishing the
+        // ledger from that job inserts it a second time. Clearing the ledger
+        // between those two inserts does not leave a lasting gap: publication
+        // puts it straight back, the rotation's health checks see a healthy
+        // node, and the rotation runs to completion. Waiting for the queue to
+        // drain closes that window, because publication is what advances
+        // pubLedger_ -- once it has happened, the ledger is never published, and
+        // so never inserted, again.
+        //
+        // This waits only for the job queue, not for the store, whose thread is
+        // its own and is the thing being interrupted here.
+        env.app().getJobQueue().rendezvous();
+
+        // Publishing the ledger is what advances pubLedger_, so this is the
+        // observable confirmation that the window above has closed. Asserting it
+        // here means that if anything ever reopens it, this setup step says so
+        // directly instead of the tests below failing for reasons that look
+        // nothing like the cause.
+        auto const published = lm.getPublishedLedger();
+        if (!BEAST_EXPECTS(
+                published && published->header().seq >= parked.validated,
+                std::to_string(published ? published->header().seq : 0)))
+            return std::nullopt;
+
+        if (!BEAST_EXPECT(log.waitFor(kRotating, 10s, rotationsBefore + 1)))
+            return std::nullopt;
+
+        // The store is now inside clearPrior(). Remove the validated ledger so
+        // that every health check from here on reports a gap.
+        if (!BEAST_EXPECTS(lm.haveLedger(parked.validated), std::to_string(parked.validated)))
+            return std::nullopt;
+        lm.clearLedger(parked.validated);
+        parked.gap = parked.validated;
+        if (!BEAST_EXPECT(!lm.haveLedger(parked.gap)))
+            return std::nullopt;
+
+        // The rotation must now be stuck. Wait longer than
+        // recovery_wait_seconds, so that this is a settled state rather than a
+        // store that has yet to reach its next health check.
+        if (!BEAST_EXPECT(!store.rendezvous(1500ms)))
+            return std::nullopt;
+        if (!BEAST_EXPECTS(
+                store.getLastRotated() == parked.lastRotated,
+                std::to_string(store.getLastRotated())))
+            return std::nullopt;
+        // The rotation started, has not finished, and has not yet given up.
+        if (!BEAST_EXPECTS(
+                log.count(kRotating) == rotationsBefore + 1, std::to_string(log.count(kRotating))))
+            return std::nullopt;
+        if (!BEAST_EXPECTS(log.count(kFinished) == 1, std::to_string(log.count(kFinished))))
+            return std::nullopt;
+        if (!BEAST_EXPECTS(log.count(kExpired) == 0, std::to_string(log.count(kExpired))))
+            return std::nullopt;
+
+        return parked;
+    }
+
 public:
+    // Cover the [node_db] validation that SHAMapStoreImp performs when it is
+    // constructed. The rejected cases stop inside SHAMapStoreImp's constructor,
+    // so they cost only a partial Application construction; the accepted ones
+    // start a full node and immediately tear it down.
+    void
+    testConfig()
+    {
+        testcase("config validation");
+
+        // online_delete below the standalone minimum. ledger_history is still
+        // kDeleteInterval here, so it is too large for this online_delete as
+        // well; the assertion pins which of the two errors wins.
+        expectConfigRejected(
+            "online_delete must be at least " + std::to_string(kMinDeleteInterval),
+            [](Config& cfg) {
+                cfg.section(Sections::kNodeDatabase)
+                    .set(Keys::kOnlineDelete, std::to_string(kMinDeleteInterval - 1));
+            });
+
+        // ledger_history above online_delete asks the node to retain more
+        // history than online delete is allowed to keep.
+        expectConfigRejected(
+            "online_delete must not be less than ledger_history (currently " +
+                std::to_string(kDeleteInterval + 1) + ")",
+            [](Config& cfg) { cfg.ledgerHistory = kDeleteInterval + 1; });
+
+        // recovery_wait_seconds is the interval at which online delete rechecks
+        // the node's health while it waits for missing ledgers to arrive, so a
+        // zero wait would turn that into a spin.
+        expectConfigRejected("recovery_wait_seconds must be at least 1 second", [](Config& cfg) {
+            cfg.section(Sections::kNodeDatabase).set(Keys::kRecoveryWaitSeconds, "0");
+        });
+
+        // max_waiting_ledgers is the circuit breaker that eventually lets
+        // online delete stop waiting, so it has a floor rather than being
+        // free-form.
+        auto const tooFewWaiting =
+            "max_waiting_ledgers must be at least " + std::to_string(kMinWaitingLedgers);
+        expectConfigRejected(tooFewWaiting, [](Config& cfg) {
+            cfg.section(Sections::kNodeDatabase)
+                .set(Keys::kMaxWaitingLedgers, std::to_string(kMinWaitingLedgers - 1));
+        });
+        // 0 is not a magic "never give up" value, just a value below the floor.
+        expectConfigRejected(tooFewWaiting, [](Config& cfg) {
+            cfg.section(Sections::kNodeDatabase).set(Keys::kMaxWaitingLedgers, "0");
+        });
+
+        // The floor itself is accepted, and so is a value far above
+        // online_delete: there is no upper bound.
+        expectConfigAccepted([](Config& cfg) {
+            cfg.section(Sections::kNodeDatabase)
+                .set(Keys::kMaxWaitingLedgers, std::to_string(kMinWaitingLedgers));
+        });
+        expectConfigAccepted([](Config& cfg) {
+            cfg.section(Sections::kNodeDatabase)
+                .set(Keys::kMaxWaitingLedgers, std::to_string(kDeleteInterval * 100));
+        });
+
+        // All of the above is gated on online_delete being enabled. With it
+        // turned off, the same values are ignored rather than rejected.
+        expectConfigAccepted([](Config& cfg) {
+            auto& section = cfg.section(Sections::kNodeDatabase);
+            section.set(Keys::kOnlineDelete, "0");
+            section.set(Keys::kMaxWaitingLedgers, "0");
+            section.set(Keys::kRecoveryWaitSeconds, "0");
+        });
+    }
+
     void
     testClear()
     {
@@ -228,7 +933,7 @@ public:
             BEAST_EXPECT(goodLedger(env, ledger, std::to_string(kDeleteInterval + 4)));
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         BEAST_EXPECT(store.getLastRotated() == kDeleteInterval + 3);
         lastRotated = store.getLastRotated();
@@ -255,7 +960,7 @@ public:
                 !getHash(ledgers[i]).empty());
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         BEAST_EXPECT(store.getLastRotated() == kDeleteInterval + lastRotated);
 
@@ -293,7 +998,7 @@ public:
             BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         // The database will always have back to ledger 2,
         // regardless of lastRotated.
@@ -308,7 +1013,7 @@ public:
             BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         ledgerCheck(env, ledgerSeq - lastRotated, lastRotated);
         BEAST_EXPECT(lastRotated != store.getLastRotated());
@@ -324,7 +1029,7 @@ public:
             BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         ledgerCheck(env, kDeleteInterval + 1, lastRotated);
         BEAST_EXPECT(lastRotated != store.getLastRotated());
@@ -363,7 +1068,7 @@ public:
             BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         ledgerCheck(env, ledgerSeq - 2, 2);
         BEAST_EXPECT(lastRotated == store.getLastRotated());
@@ -373,7 +1078,7 @@ public:
         BEAST_EXPECT(!rpc::containsError(canDelete[jss::result]));
         BEAST_EXPECT(canDelete[jss::result][jss::can_delete] == ledgerSeq + (kDeleteInterval / 2));
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         ledgerCheck(env, ledgerSeq - 2, 2);
         BEAST_EXPECT(store.getLastRotated() == lastRotated);
@@ -386,7 +1091,7 @@ public:
             BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         ledgerCheck(env, ledgerSeq - lastRotated, lastRotated);
 
@@ -402,7 +1107,7 @@ public:
             BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         BEAST_EXPECT(store.getLastRotated() == lastRotated);
 
@@ -414,7 +1119,7 @@ public:
             BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         ledgerCheck(env, ledgerSeq - firstBatch, firstBatch);
 
@@ -436,7 +1141,7 @@ public:
             BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         BEAST_EXPECT(store.getLastRotated() == lastRotated);
 
@@ -448,7 +1153,7 @@ public:
             BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         ledgerCheck(env, ledgerSeq - lastRotated, lastRotated);
 
@@ -469,7 +1174,7 @@ public:
             BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq), true));
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         BEAST_EXPECT(store.getLastRotated() == lastRotated);
 
@@ -481,7 +1186,7 @@ public:
             BEAST_EXPECT(goodLedger(env, ledger, std::to_string(ledgerSeq++), true));
         }
 
-        store.rendezvous();
+        BEAST_EXPECT(syncStore(env));
 
         ledgerCheck(env, ledgerSeq - lastRotated, lastRotated);
 
@@ -493,7 +1198,7 @@ public:
     makeBackendRotating(jtx::Env& env, NodeStoreScheduler& scheduler, std::string path)
     {
         Section section{env.app().config().section(Sections::kNodeDatabase)};
-        boost::filesystem::path newPath;
+        std::filesystem::path newPath;
 
         if (!BEAST_EXPECT(path.size()))
             return {};
@@ -604,13 +1309,631 @@ public:
         BEAST_EXPECT(dbr->getName() == "3");
     }
 
+    void
+    testLedgerGaps()
+    {
+        // Note that this test is intentionally very similar to
+        // LedgerMaster_test::testCompleteLedgerRange, but has a different
+        // focus.
+
+        testcase("Wait for ledger gaps to fill in");
+
+        using namespace test::jtx;
+
+        Env env{*this, envconfig(onlineDelete)};
+
+        auto failureMessage = [&](char const* label, auto expected, auto actual) {
+            std::stringstream ss;
+            ss << label << ": Expected: " << expected << ", Got: " << actual;
+            return ss.str();
+        };
+
+        auto const alice = Account("alice");
+        env.fund(XRP(1000), alice);
+        env.close();
+
+        auto& lm = env.app().getLedgerMaster();
+        LedgerIndex minSeq = 2;
+        auto& store = env.app().getSHAMapStore();
+        auto& netOPs = env.app().getOPs();
+        // Which of the existing complete ledgers the store initializes
+        // lastRotated from is a timing detail, so everything below derives from
+        // the observed value rather than assuming a particular one. Spinning
+        // until it equals a hard-coded value never terminates when a different
+        // one legitimately wins.
+        //
+        // The range check and the initializeStore() one both end the testcase
+        // rather than merely reporting, because lastRotated is the only value
+        // from the store that enters minSeq. A lastRotated of 0 -- the value
+        // getLastRotated() reports until the store has been handed a validated
+        // ledger -- makes minSeq 0 below, and the minSeq - 1 and minSeq - 2
+        // ranges then underflow to first > last, which aborts a Debug build
+        // inside missingFromCompleteLedgerRange().
+        auto const extraCloses = initializeStore(env);
+        if (!BEAST_EXPECT(extraCloses.has_value()))
+            return;
+        LedgerIndex maxSeq = env.closed()->header().seq;
+        LedgerIndex lastRotated = store.getLastRotated();
+        if (!BEAST_EXPECTS(
+                lastRotated >= minSeq && lastRotated <= maxSeq, std::to_string(lastRotated)))
+            return;
+        // The BEAST_EXPECT above already returned if this is nullopt, but that
+        // is invisible to clang-tidy's optional model.
+        // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+        BEAST_EXPECTS(maxSeq == 3 + *extraCloses, std::to_string(maxSeq));
+        std::stringstream initialRange;
+        initialRange << minSeq << "-" << maxSeq;
+        BEAST_EXPECTS(lm.getCompleteLedgers() == initialRange.str(), lm.getCompleteLedgers());
+        BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
+        // The inner range is empty unless initializeStore() had to close extra
+        // ledgers, and missingFromCompleteLedgerRange() treats first > last as a
+        // precondition violation that aborts a Debug build via UNREACHABLE, so
+        // only check it when it is well formed.
+        if (minSeq + 1 <= maxSeq - 1)
+        {
+            BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
+        }
+        BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
+        BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
+        BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
+
+        auto expectedRange =
+            [](LedgerIndex minSeq, std::vector const& deleteSeqs, LedgerIndex maxSeq) {
+                std::stringstream expectedRange;
+                expectedRange << minSeq;
+                auto lastDelete = minSeq - 1;
+                for (auto deleteSeq : deleteSeqs)
+                {
+                    if (deleteSeq <= lastDelete)
+                        continue;
+                    expectedRange << "-" << (deleteSeq - 1);
+                    if (deleteSeq + 1 <= maxSeq)
+                        expectedRange << "," << (deleteSeq + 1);
+                    lastDelete = deleteSeq;
+                }
+                if (lastDelete + 1 < maxSeq)
+                {
+                    expectedRange << "-" << maxSeq;
+                }
+                return expectedRange.str();
+            };
+
+        auto deleteLedgerSeq =
+            [&lm, &store, &netOPs, &minSeq, &lastRotated, &expectedRange, &failureMessage, this](
+                Env& env,
+                LedgerIndex& maxSeq,
+                std::vector& deleteSeqs) -> LedgerIndex {
+            using namespace std::chrono_literals;
+
+            // The next ledger will trigger a rotation. Delete the
+            // current ledger from LedgerMaster.
+
+            netOPs.setMode(OperatingMode::CONNECTED);
+
+            LedgerIndex const deleteSeq = maxSeq;
+            std::size_t iterations = 30;
+            while (!lm.haveLedger(deleteSeq) && --iterations > 0)
+            {
+                std::this_thread::sleep_for(10ms);
+            }
+            // Even the slowest machines should be able to finalize deleteSeq within 10
+            // loops (100ms). If this test ever actually fails feel free to lower this
+            // cutoff. The intent of this test is to flag if the loop takes a very long
+            // time, but still allow the rest of this function to finish.
+            BEAST_EXPECTS(iterations > 20, std::to_string(iterations));
+            if (!BEAST_EXPECT(lm.haveLedger(deleteSeq)))
+                return 0;
+
+            // This test may be timing sensitive, because it's messing with server internals in ways
+            // that they can't be messed with normally. Sleep a little bit to give the server time
+            // to finish any internal work before we delete the ledger.
+            std::this_thread::sleep_for(250ms);
+
+            lm.clearLedger(deleteSeq);
+            deleteSeqs.push_back(deleteSeq);
+            if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
+                return 0;
+
+            BEAST_EXPECTS(
+                lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeqs, maxSeq),
+                failureMessage(
+                    "Complete ledgers",
+                    expectedRange(minSeq, deleteSeqs, maxSeq),
+                    lm.getCompleteLedgers()));
+            BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == deleteSeqs.size());
+
+            if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
+                return 0;
+            // Close another ledger, which will trigger a rotation, but the
+            // rotation will be stuck until the missing ledger is filled in.
+            env.close();
+            // Do not call rendezvous() here without a timeout; it will block until the missing
+            // ledger is backfilled. That will not happen automatically. It's a manual step that
+            // is done later in this test.
+            ++maxSeq;
+
+            if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
+                return 0;
+            netOPs.setMode(OperatingMode::FULL);
+
+            if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
+                return 0;
+            BEAST_EXPECT(!store.rendezvous(10ms));
+            BEAST_EXPECT(netOPs.getOperatingMode() == OperatingMode::FULL);
+
+            // Nothing has changed
+            BEAST_EXPECTS(
+                store.getLastRotated() == lastRotated,
+                failureMessage("lastRotated", lastRotated, store.getLastRotated()));
+            BEAST_EXPECTS(
+                lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeqs, maxSeq),
+                failureMessage(
+                    "Complete ledgers",
+                    expectedRange(minSeq, deleteSeqs, maxSeq),
+                    lm.getCompleteLedgers()));
+
+            return deleteSeq;
+        };
+
+        std::vector deleteSeqs;
+
+        // Close enough ledgers to rotate a few times
+        while (maxSeq < 40)
+        {
+            for (int t = 0; t < 3; ++t)
+            {
+                env(noop(alice));
+            }
+            env.close();
+            BEAST_EXPECT(syncStore(env));
+
+            ++maxSeq;
+
+            if (maxSeq + 1 == lastRotated + kDeleteInterval)
+            {
+                using namespace std::chrono_literals;
+
+                {
+                    // Trigger the circuit breaker in SHAMapStoreImp::healthWait() to ensure it
+                    // doesn't block forever.
+                    LedgerIndex const deleteSeq = deleteLedgerSeq(env, maxSeq, deleteSeqs);
+                    if (!BEAST_EXPECT(deleteSeq > 0))
+                        return;
+                    if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
+                        return;
+
+                    // Close 7 more ledgers, waiting a little bit in between to
+                    // simulate the ledger making progress while online delete waits
+                    // for the missing ledger to be filled in.
+                    // After the 7th ledger, the circuit breaker will trigger and abort the attempt.
+                    while (maxSeq < lastRotated + (kDeleteInterval * 2) - 2)
+                    {
+                        env.close();
+                        ++maxSeq;
+                        // Nothing has changed
+                        BEAST_EXPECTS(
+                            store.getLastRotated() == lastRotated,
+                            failureMessage("lastRotated", lastRotated, store.getLastRotated()));
+                        BEAST_EXPECTS(
+                            lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeqs, maxSeq),
+                            failureMessage(
+                                "Complete Ledgers",
+                                expectedRange(minSeq, deleteSeqs, maxSeq),
+                                lm.getCompleteLedgers()));
+                        // The Store is "stuck" in healthWait() and won't finish the run() loop
+                        // until it's backfilled
+                        if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
+                            return;
+                    }
+
+                    // Close one more ledger, which will NOT trigger the circuit breaker. Wait for
+                    // the full 1 second recovery wait timeout to ensure the circuit breaker is not
+                    // triggered.
+                    env.close();
+                    ++maxSeq;
+                    // The Store is "stuck" in healthWait() and won't finish the run() loop
+                    // until it's backfilled
+                    BEAST_EXPECT(!store.rendezvous(1s));
+
+                    // Close one more ledger, which will trigger the circuit breaker and abort the
+                    // attempt to rotate.
+                    env.close();
+                    ++maxSeq;
+                    // Nothing has changed
+                    BEAST_EXPECTS(
+                        store.getLastRotated() == lastRotated,
+                        failureMessage("lastRotated", lastRotated, store.getLastRotated()));
+                    BEAST_EXPECTS(
+                        lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeqs, maxSeq),
+                        failureMessage(
+                            "Complete Ledgers",
+                            expectedRange(minSeq, deleteSeqs, maxSeq),
+                            lm.getCompleteLedgers()));
+
+                    // The circuit breaker has been triggered.
+                    BEAST_EXPECT(syncStore(env));
+                }
+                {
+                    // Recover before the circuit breaker triggers, so the test can continue.
+                    LedgerIndex const deleteSeq = deleteLedgerSeq(env, maxSeq, deleteSeqs);
+                    if (!BEAST_EXPECT(deleteSeq > 0))
+                        return;
+                    if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
+                        return;
+
+                    // Close 5 more ledgers, waiting a little bit in between to
+                    // simulate the ledger making progress while online delete waits
+                    // for the missing ledger to be filled in.
+                    // This ensures the healthWait check has time to run and
+                    // detect the gap.
+                    for (int l = 0; l < 5; ++l)
+                    {
+                        env.close();
+                        ++maxSeq;
+                        // Nothing has changed
+                        BEAST_EXPECTS(
+                            store.getLastRotated() == lastRotated,
+                            failureMessage("lastRotated", lastRotated, store.getLastRotated()));
+                        BEAST_EXPECTS(
+                            lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeqs, maxSeq),
+                            failureMessage(
+                                "Complete Ledgers",
+                                expectedRange(minSeq, deleteSeqs, maxSeq),
+                                lm.getCompleteLedgers()));
+                        if (!BEAST_EXPECT(!lm.haveLedger(deleteSeq)))
+                            return;
+                    }
+
+                    // The Store is "stuck" in healthWait() and won't finish the run() loop
+                    // until it's backfilled
+                    // Wait for the full 1 second recovery wait timeout to ensure the circuit
+                    // breaker is not triggered, and this isn't some other timing fluke.
+                    BEAST_EXPECT(!store.rendezvous(1s));
+
+                    // Put the missing ledger back in LedgerMaster
+                    lm.setLedgerRangePresent(deleteSeq, deleteSeq);
+                    BEAST_EXPECT(deleteSeqs.back() == deleteSeq);
+                    deleteSeqs.pop_back();
+
+                    // Wait for the rotation to finish
+                    BEAST_EXPECT(syncStore(env));
+
+                    minSeq = lastRotated;
+                    while (deleteSeqs.front() < minSeq)
+                    {
+                        deleteSeqs.erase(deleteSeqs.begin());
+                    }
+                    lastRotated = deleteSeq + 1;
+                }
+            }
+            BEAST_EXPECT(maxSeq != lastRotated + kDeleteInterval);
+            BEAST_EXPECTS(
+                env.closed()->header().seq == maxSeq,
+                failureMessage("maxSeq", maxSeq, env.closed()->header().seq));
+            BEAST_EXPECTS(
+                store.getLastRotated() == lastRotated,
+                failureMessage("lastRotated", lastRotated, store.getLastRotated()));
+            {
+                auto const expected = expectedRange(minSeq, deleteSeqs, maxSeq);
+                BEAST_EXPECTS(
+                    lm.getCompleteLedgers() == expected,
+                    failureMessage("CompleteLedgers", expected, lm.getCompleteLedgers()));
+            }
+            BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == deleteSeqs.size());
+            // missingFromCompleteLedgerRange() treats first > last as a
+            // precondition violation and aborts a Debug build via UNREACHABLE.
+            // The range can only collapse if this test's model of minSeq /
+            // maxSeq has desynced from the store, so report that as a failure
+            // instead of taking down the whole unit test job.
+            if (minSeq + 1 <= maxSeq - 1)
+            {
+                BEAST_EXPECT(
+                    lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == deleteSeqs.size());
+            }
+            else
+            {
+                BEAST_EXPECTS(false, failureMessage("range collapsed", minSeq, maxSeq));
+            }
+            BEAST_EXPECT(
+                lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == deleteSeqs.size() + 2);
+            BEAST_EXPECT(
+                lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == deleteSeqs.size() + 2);
+            BEAST_EXPECT(
+                lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == deleteSeqs.size() + 2);
+        }
+    }
+
+    // Cover the branches of SHAMapStoreImp::healthWait() that decide whether
+    // the server is healthy enough to rotate, and how loudly to complain while
+    // it is not. testLedgerGaps() covers the case where a gap holds the
+    // rotation back until the circuit breaker trips; these cover the rest of
+    // the decision table.
+    void
+    testHealthWaitState()
+    {
+        testcase("healthWait server state");
+
+        using namespace std::chrono_literals;
+        using namespace test::jtx;
+
+        auto logs = std::make_unique();
+        // Not `auto const*`: waitFor() blocks, so it is not const.
+        auto* const log = logs.get();
+        Env env{*this, envconfig(onlineDelete), std::move(logs), beast::Severity::Trace};
+
+        auto& store = env.app().getSHAMapStore();
+        auto& netOPs = env.app().getOPs();
+
+        // No gap: the only thing holding the store back is the operating mode.
+        auto const parked = parkInHealthWait(env, false, OperatingMode::CONNECTED);
+        if (!parked)
+            return;
+
+        // Hold the non-FULL mode until the store has logged that it is waiting
+        // on it. With no gap, a fresh validated ledger and a mode that is not
+        // DISCONNECTED, the only check left that can report unhealthy is
+        // "mode != FULL", and a mode that is not FULL is not expected to fix
+        // itself, so the wait is logged at warn, for the full duration.
+        //
+        // Waiting for the message rather than sleeping past it is what keeps
+        // this from depending on how quickly the store gets around to sampling.
+        // The store cannot leave the wait loop while the mode stays put -- the
+        // validated ledger index does not advance, so the circuit breaker is
+        // never reached -- so the rendezvous() below is not racing it.
+        BEAST_EXPECT(log->waitFor(beast::Severity::Warning, kFullWait, 10s));
+        BEAST_EXPECT(!store.rendezvous(10ms));
+        BEAST_EXPECT(netOPs.getOperatingMode() != OperatingMode::FULL);
+        BEAST_EXPECT(netOPs.getOperatingMode() != OperatingMode::DISCONNECTED);
+        BEAST_EXPECTS(
+            store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
+
+        // Now make the mode FULL but the validated ledger stale. Advancing the
+        // clock without closing a ledger ages the validated ledger past
+        // age_threshold_seconds, which defaults to 60. The mode check can no
+        // longer be the reason the store is unhealthy, so the age check is.
+        //
+        // This one does sleep: what is being asserted is that the store did not
+        // rotate, and 1500ms is long enough for it to have re-sampled the server
+        // at least once -- the full wait is 1000ms -- so the age check, not a
+        // stale sample of the old mode, is what held it back.
+        auto const closeTime = env.now();
+        env.timeKeeper().set(closeTime + 2min);
+        netOPs.setMode(OperatingMode::FULL);
+        BEAST_EXPECT(netOPs.getOperatingMode() == OperatingMode::FULL);
+        BEAST_EXPECT(!store.rendezvous(1500ms));
+        BEAST_EXPECTS(
+            store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
+
+        // Restore the clock. Nothing is wrong any more, so the rotation that
+        // has been waiting all along runs to completion.
+        env.timeKeeper().set(closeTime);
+        BEAST_EXPECT(syncStore(env));
+        BEAST_EXPECTS(
+            store.getLastRotated() == parked->validated, std::to_string(store.getLastRotated()));
+    }
+
+    void
+    testHealthWaitGapLevels()
+    {
+        testcase("healthWait gap wait levels");
+
+        using namespace std::chrono_literals;
+        using namespace test::jtx;
+
+        auto logs = std::make_unique();
+        // Not `auto const*`: waitFor() blocks, so it is not const.
+        auto* const log = logs.get();
+        Env env{*this, envconfig(onlineDelete), std::move(logs), beast::Severity::Trace};
+
+        auto& lm = env.app().getLedgerMaster();
+        auto& store = env.app().getSHAMapStore();
+
+        auto const parked = parkInHealthWait(env, true, OperatingMode::FULL);
+        if (!parked)
+            return;
+
+        // The missing ledger is an older one; the validated ledger itself is
+        // present. The store has no reason to think the gap will close on its
+        // own, so it waits the full duration and says so at info -- not warn,
+        // because the server is otherwise healthy and has not been waiting long
+        // enough to have fallen behind.
+        BEAST_EXPECT(lm.haveLedger(parked->validated));
+        BEAST_EXPECT(!lm.haveLedger(parked->gap));
+        BEAST_EXPECT(log->waitFor(beast::Severity::Info, kFullWait, 10s));
+        // Nothing so far should have looked like a ledger being built.
+        BEAST_EXPECT(log->count(beast::Severity::Trace, kShortWait) == 0);
+        // Nothing fills the gap in, so the store is still in the wait loop.
+        BEAST_EXPECT(!store.rendezvous(10ms));
+
+        // Move the gap onto the validated ledger itself. That is the one case
+        // the store treats as transient -- the ledger is expected to be built
+        // shortly -- so it drops to trace and waits a tenth as long. Asserting
+        // that the shortened wait appears only after this swap is what pins the
+        // branch to the buildingIndex condition, rather than to anything
+        // incidental about a store that happens to be waiting.
+        lm.setLedgerRangePresent(parked->gap, parked->gap);
+        lm.clearLedger(parked->validated);
+        BEAST_EXPECT(lm.haveLedger(parked->gap));
+        BEAST_EXPECT(!lm.haveLedger(parked->validated));
+        BEAST_EXPECT(log->waitFor(beast::Severity::Trace, kShortWait, 10s));
+        BEAST_EXPECT(!store.rendezvous(10ms));
+        BEAST_EXPECTS(
+            store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
+
+        // Fill it in and the rotation completes.
+        lm.setLedgerRangePresent(parked->validated, parked->validated);
+        BEAST_EXPECT(syncStore(env));
+        BEAST_EXPECTS(
+            store.getLastRotated() == parked->validated, std::to_string(store.getLastRotated()));
+    }
+
+    void
+    testHealthWaitDisconnected()
+    {
+        testcase("healthWait disconnected");
+
+        using namespace std::chrono_literals;
+        using namespace test::jtx;
+
+        Env env{*this, envconfig(onlineDelete)};
+
+        auto& lm = env.app().getLedgerMaster();
+        auto& store = env.app().getSHAMapStore();
+        auto& netOPs = env.app().getOPs();
+
+        auto const parked = parkInHealthWait(env, true, OperatingMode::FULL);
+        if (!parked)
+            return;
+
+        // While the server is FULL, the gap holds the rotation back.
+        BEAST_EXPECT(!store.rendezvous(1500ms));
+        BEAST_EXPECTS(
+            store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
+
+        // A disconnected server is not doing any ledger I/O, so the gap cannot
+        // have been caused by its own activity and will not close until it has
+        // peers again. The store deliberately takes advantage of that to get as
+        // much rotation done as possible: this is the one case where a gap does
+        // not hold online delete back at all.
+        netOPs.setMode(OperatingMode::DISCONNECTED);
+        BEAST_EXPECT(netOPs.getOperatingMode() == OperatingMode::DISCONNECTED);
+        BEAST_EXPECT(syncStore(env));
+        BEAST_EXPECTS(
+            store.getLastRotated() == parked->validated, std::to_string(store.getLastRotated()));
+        // The rotation ran with the gap still present -- nothing filled it in.
+        BEAST_EXPECT(!lm.haveLedger(parked->gap));
+    }
+
+    void
+    testHealthWaitStop()
+    {
+        testcase("healthWait stop");
+
+        using namespace test::jtx;
+
+        Env env{*this, envconfig(onlineDelete)};
+
+        auto& store = env.app().getSHAMapStore();
+
+        auto const parked = parkInHealthWait(env, true, OperatingMode::FULL);
+        if (!parked)
+            return;
+
+        // Stopping the store has to break it out of the wait loop, which it
+        // would otherwise never leave: the gap is never filled in and the
+        // validated ledger index never advances to reach the circuit breaker.
+        //
+        // stop() joins the store's thread, so its return is the
+        // synchronisation point here. rendezvous() afterwards is only a
+        // cross-check, and cannot block: the store is parked in the health
+        // check that gates a rotation, so Stopping there merely leaves
+        // readyToRotate false, and run() falls through to the top of its loop,
+        // where it clears working_ and notifies before returning on stop_.
+        store.stop();
+        BEAST_EXPECT(store.rendezvous());
+        BEAST_EXPECTS(
+            store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
+    }
+
+    // The two tests below cover the health check that run() performs between the
+    // stages of a rotation it has already committed to, which is a different
+    // decision from the one that gates the rotation in the first place: giving
+    // up here means abandoning work in progress. run() makes it at four points
+    // -- after clearing prior ledgers, after copying the validated ledger, after
+    // freshening the caches, and after clearing them -- with the same three-way
+    // switch each time, and parkMidRotation() parks the store at the first of
+    // them.
+    void
+    testHealthWaitExpiredMidRotation()
+    {
+        testcase("healthWait circuit breaker mid-rotation");
+
+        using namespace test::jtx;
+
+        auto logs = std::make_unique();
+        auto* const log = logs.get();
+        Env env{*this, envconfig(slowOnlineDelete), std::move(logs), beast::Severity::Trace};
+
+        auto& lm = env.app().getLedgerMaster();
+        auto& store = env.app().getSHAMapStore();
+
+        auto const parked = parkMidRotation(env, *log);
+        if (!parked)
+            return;
+
+        // Advance the validated ledger index past the circuit breaker. The store
+        // has had no successful health check since the gap appeared, so once the
+        // index has moved max_waiting_ledgers on from the last one that did
+        // succeed, it abandons the rotation instead of waiting for the gap
+        // forever. Nothing here fills the gap in.
+        for (int i = 0; i < kMinWaitingLedgers; ++i)
+        {
+            env.close();
+            BEAST_EXPECT(!lm.haveLedger(parked->gap));
+        }
+
+        // Abandoning the rotation returns the store to waiting for work, so it
+        // reports itself idle -- but with lastRotated left where it started,
+        // unlike the completed rotation parkMidRotation() drove first.
+        BEAST_EXPECT(syncStore(env));
+        BEAST_EXPECTS(
+            store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
+        BEAST_EXPECT(log->count(kExpired) > 0);
+        BEAST_EXPECTS(log->count(kFinished) == 1, std::to_string(log->count(kFinished)));
+        BEAST_EXPECT(!lm.haveLedger(parked->gap));
+    }
+
+    void
+    testHealthWaitStopMidRotation()
+    {
+        testcase("healthWait stop mid-rotation");
+
+        using namespace test::jtx;
+
+        auto logs = std::make_unique();
+        auto* const log = logs.get();
+        Env env{*this, envconfig(slowOnlineDelete), std::move(logs), beast::Severity::Trace};
+
+        auto& store = env.app().getSHAMapStore();
+
+        auto const parked = parkMidRotation(env, *log);
+        if (!parked)
+            return;
+
+        // Stopping has to break the store out of the rotation, which it would
+        // otherwise never leave: the gap is never filled in and the validated
+        // ledger index never advances to reach the circuit breaker. Note that
+        // being stopped outranks being healthy -- the health check reports it
+        // even when nothing is wrong with the server -- so this does not depend
+        // on the store still being parked when stop() lands.
+        //
+        // stop() joins the store's thread, so its return is the synchronisation
+        // point. Deliberately do not call the untimed rendezvous() afterwards:
+        // run() returns without clearing working_, so it would block forever.
+        store.stop();
+        BEAST_EXPECTS(
+            store.getLastRotated() == parked->lastRotated, std::to_string(store.getLastRotated()));
+        // The rotation was abandoned rather than completed, and the circuit
+        // breaker was not what abandoned it.
+        BEAST_EXPECTS(log->count(kFinished) == 1, std::to_string(log->count(kFinished)));
+        BEAST_EXPECTS(log->count(kExpired) == 0, std::to_string(log->count(kExpired)));
+    }
+
     void
     run() override
     {
+        testConfig();
         testClear();
         testAutomatic();
         testCanDelete();
         testRotate();
+        testLedgerGaps();
+        testHealthWaitState();
+        testHealthWaitGapLevels();
+        testHealthWaitDisconnected();
+        testHealthWaitStop();
+        testHealthWaitExpiredMidRotation();
+        testHealthWaitStopMidRotation();
     }
 };
 
diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp
index bcd31bc6a0..71d968014f 100644
--- a/src/test/app/Sponsor_test.cpp
+++ b/src/test/app/Sponsor_test.cpp
@@ -376,11 +376,11 @@ public:
     }
 
     void
-    testSingleSigning()
+    testSingleSigning(FeatureBitset features)
     {
         testcase("Single signing");
         using namespace test::jtx;
-        Env env{*this, testableAmendments()};
+        Env env{*this, features};
         Account const alice("alice");
         Account const sponsor("sponsor");
         Account const invalid("invalid");
@@ -415,11 +415,11 @@ public:
     }
 
     void
-    testMultiSigning()
+    testMultiSigning(FeatureBitset features)
     {
         testcase("Multi signing");
         using namespace test::jtx;
-        Env env{*this, testableAmendments()};
+        Env env{*this, features};
         Account const alice("alice");
         Account const bob("bob");
         Account const sponsor("sponsor");
@@ -1073,14 +1073,17 @@ public:
     }
 
     void
-    testTransferSponsor()
+    testTransferSponsor(FeatureBitset features)
     {
-        testcase("Transfer Sponsor");
+        testcase(
+            std::string("Transfer Sponsor ") +
+            (features[fixCleanup3_4_0] ? "(fixCleanup3_4_0 enabled)"
+                                       : "(fixCleanup3_4_0 disabled)"));
         using namespace test::jtx;
 
         // Verify preflight checks
         {
-            Env env{*this, testableAmendments()};
+            Env env{*this, features};
             Account const alice("alice");
             Account const bob("bob");
             Account const sponsor("sponsor");
@@ -1164,7 +1167,7 @@ public:
 
         {
             // Invalid SponsorshipEnd permission (sponsor object/sponsor account)
-            Env env{*this, testableAmendments()};
+            Env env{*this, features};
             Account const alice("alice");
             Account const bob("bob");
             Account const charlie("charlie");
@@ -1209,7 +1212,7 @@ public:
 
         {
             // sponsor account
-            Env env{*this, testableAmendments()};
+            Env env{*this, features};
             Account const alice("alice");
             Account const bob("bob");
             Account const sponsor1("sponsor1");
@@ -1340,7 +1343,7 @@ public:
         }
         {
             // dissolve account sponsorship from sponsor
-            Env env{*this, testableAmendments()};
+            Env env{*this, features};
             Account const alice("alice");
             Account const bob("bob");
             Account const sponsor("sponsor");
@@ -1364,7 +1367,7 @@ public:
 
         {
             // sponsor object (co-signing)
-            Env env{*this, testableAmendments()};
+            Env env{*this, features};
             Account const alice("alice");
             Account const bob("bob");
             Account const sponsor1("sponsor1");
@@ -1473,10 +1476,20 @@ public:
             BEAST_EXPECT(sle2->isFieldPresent(sfSponsor));
             BEAST_EXPECT(sle2->getAccountID(sfSponsor) == sponsor2.id());
 
-            // dissolve sponsor: ending an object sponsorship succeeds even
-            // when the sponsee lacks sufficient reserve to reclaim the object.
+            // dissolve sponsor: ending an object sponsorship now (fixCleanup3_4_0) requires the
+            // sponsee to be able to self-fund the object's reserve.
             adjustAccountXRPBalance(env, alice, reserve(env, 1) - drops(1));
 
+            if (features[fixCleanup3_4_0])
+            {
+                // Under-funded: End is rejected until alice can self-fund.
+                env(sponsor::transfer(alice, tfSponsorshipEnd, checkId),
+                    Ter(tecINSUFFICIENT_RESERVE));
+                env.close();
+
+                adjustAccountXRPBalance(env, alice, reserve(env, 1));
+            }
+
             env(sponsor::transfer(alice, tfSponsorshipEnd, checkId));
             env.close();
 
@@ -1509,7 +1522,7 @@ public:
         }
         {
             // sponsor object (pre-funded + no ltSponsorship entry)
-            Env env{*this, testableAmendments()};
+            Env env{*this, features};
             Account const alice("alice");
             Account const bob("bob");
             Account const sponsor1("sponsor1");
@@ -1543,7 +1556,7 @@ public:
         }
         {
             // sponsor object (pre-funded)
-            Env env{*this, testableAmendments()};
+            Env env{*this, features};
             Account const alice("alice");
             Account const bob("bob");
             Account const sponsor1("sponsor1");
@@ -1646,7 +1659,7 @@ public:
 
         {
             // Dissolve object sponsorship from sponsor(no-ltSponsorship)
-            Env env{*this, testableAmendments()};
+            Env env{*this, features};
             Account const alice("alice");
             Account const bob("bob");
             Account const sponsor("sponsor");
@@ -1686,7 +1699,7 @@ public:
 
         {
             // Dissolve object sponsorship from sponsor (with ltSponsorship)
-            Env env{*this, testableAmendments()};
+            Env env{*this, features};
             Account const alice("alice");
             Account const bob("bob");
             Account const sponsor("sponsor");
@@ -1744,7 +1757,7 @@ public:
 
             for (bool const isIssuerHigh : {false, true})
             {
-                Env env{*this, testableAmendments()};
+                Env env{*this, features};
                 env.fund(XRP(10000), alice, bob, sponsor);
                 env.close();
 
@@ -1788,7 +1801,7 @@ public:
 
         {
             // invalid transfer
-            Env env{*this, testableAmendments()};
+            Env env{*this, features};
             Account const alice("alice");
             Account const bob("bob");
             Account const sponsor("sponsor");
@@ -1825,7 +1838,7 @@ public:
         {
             // existing owner objects that are outside the v1 SponsorshipTransfer
             // object allow-list
-            Env env{*this, testableAmendments()};
+            Env env{*this, features};
             Account const alice("alice");
             Account const sponsor("sponsor");
             env.fund(XRP(10000), alice, sponsor);
@@ -1864,7 +1877,11 @@ public:
 
             PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
             Vault const vault{env};
-            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = xrpAsset});
+            // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
+            // accepts closed-ended vaults; build one and advance past
+            // SubscriptionDate before creating a loan.
+            auto [vaultTx, vaultKeylet, subscriptionDate] =
+                vault.createClosedEnded({.owner = alice, .asset = xrpAsset});
             env(vaultTx);
             env.close();
 
@@ -1872,6 +1889,8 @@ public:
                 {.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(1000)}));
             env.close();
 
+            vault.closePastSubscription(subscriptionDate);
+
             auto const brokerKeylet =
                 keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
             env(loan_broker::set(alice, vaultKeylet.key),
@@ -5448,13 +5467,12 @@ public:
         using namespace test::jtx;
         using namespace std::chrono_literals;
 
-        // Finishing a self-escrow (source == destination) whose trust line
-        // was deleted while the escrow was outstanding auto-creates the line,
-        // and the outcome of that reserve check depends on whether the escrow
-        // reserve is released before delivery (Sponsor) or after (legacy).
-        // With the source's balance in the one-increment window
-        // [reserve(1), reserve(2)), the legacy order requires reserve(2) and
-        // fails, while the Sponsor order requires reserve(1) and succeeds.
+        // Finishing a self-escrow (source == destination) whose trust line was
+        // deleted while the escrow was outstanding auto-creates the line. With
+        // the source's balance in the one-increment window
+        // [reserve(1), reserve(2)), the finish succeeds only when the escrow
+        // reserve is released before delivery, which either featureSponsor or
+        // fixCleanup3_4_0 does.
         auto runTest = [&](FeatureBitset features, TER expected) {
             Account const alice("alice");
             Account const gw("gw");
@@ -5519,11 +5537,9 @@ public:
             }
         };
 
-        // Pre-amendment: legacy order — the escrow still counts against the
-        // reserve while the auto-created line is checked.
-        runTest(testableAmendments() - featureSponsor, tecNO_LINE_INSUF_RESERVE);
-
-        // Post-amendment: the escrow reserve is recycled into the new line.
+        runTest(testableAmendments() - featureSponsor - fixCleanup3_4_0, tecNO_LINE_INSUF_RESERVE);
+        runTest(testableAmendments() - featureSponsor, tesSUCCESS);
+        runTest(testableAmendments() - fixCleanup3_4_0, tesSUCCESS);
         runTest(testableAmendments(), tesSUCCESS);
     }
 
@@ -5566,9 +5582,9 @@ public:
             Ter(tesSUCCESS));
         env.close();
 
-        // The same helper (deltaAssetsTxAccount) drives the withdraw path, so a
-        // fee-sponsored withdrawal back to the depositor's own account also
-        // passes on the destination side.
+        // The same fee-correction logic (ValidVault::deltaAssetsForParty)
+        // drives the withdraw path, so a fee-sponsored withdrawal back to
+        // the depositor's own account also passes on the destination side.
         env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = xrpAsset(50)}),
             Fee(XRP(1)),
             sponsor::As(sponsor, spfSponsorFee),
@@ -5659,8 +5675,12 @@ protected:
         testInvalidSponsorshipSet();
         testPseudoAccountSponsorship();
 
-        testSingleSigning();
-        testMultiSigning();
+        // The signing prefix of an alternate signature field changes with
+        // fixCleanup3_4_0, so sign and verify under both rule sets.
+        testSingleSigning(jtx::testableAmendments());
+        testSingleSigning(jtx::testableAmendments() - fixCleanup3_4_0);
+        testMultiSigning(jtx::testableAmendments());
+        testMultiSigning(jtx::testableAmendments() - fixCleanup3_4_0);
 
         testInvalidSponsorField();
 
@@ -5671,7 +5691,8 @@ protected:
         testPreFundAndCosign();
         testSponsoredFreeTierReserve();
 
-        testTransferSponsor();
+        testTransferSponsor(jtx::testableAmendments());
+        testTransferSponsor(jtx::testableAmendments() - fixCleanup3_4_0);
         testLegacySignerListReserve();
         testSponsorFee();
         testSponsorAccount();
diff --git a/src/test/app/TestHostFunctions.h b/src/test/app/TestHostFunctions.h
deleted file mode 100644
index 9392444341..0000000000
--- a/src/test/app/TestHostFunctions.h
+++ /dev/null
@@ -1,494 +0,0 @@
-#pragma once
-
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-
-namespace xrpl::test {
-
-class TestLedgerDataProvider : public HostFunctions
-{
-    jtx::Env& env_;
-
-public:
-    TestLedgerDataProvider(jtx::Env& env) : HostFunctions(env.journal), env_(env)
-    {
-    }
-
-    [[nodiscard]] std::expected
-    getLedgerSqn() const override
-    {
-        return env_.current()->seq();
-    }
-};
-
-class TestHostFunctions : public HostFunctions
-{
-protected:
-    test::jtx::Env& env_;
-    AccountID accountID_;
-    Bytes data_;
-
-public:
-    TestHostFunctions(test::jtx::Env& env) : HostFunctions(env.journal), env_(env)
-    {
-        accountID_ = env.master.id();
-        std::string t = "10000";
-        data_ = Bytes{t.begin(), t.end()};
-    }
-
-    [[nodiscard]] std::expected
-    getLedgerSqn() const override
-    {
-        return 12345;
-    }
-
-    [[nodiscard]] std::expected
-    getParentLedgerTime() const override
-    {
-        return 67890;
-    }
-
-    [[nodiscard]] std::expected
-    getParentLedgerHash() const override
-    {
-        return env_.current()->header().parentHash;
-    }
-
-    [[nodiscard]] std::expected
-    getBaseFee() const override
-    {
-        return 10;
-    }
-
-    [[nodiscard]] std::expected
-    isAmendmentEnabled(uint256 const& amendmentId) const override
-    {
-        return 1;
-    }
-
-    [[nodiscard]] std::expected
-    isAmendmentEnabled(std::string_view const& amendmentName) const override
-    {
-        return 1;
-    }
-
-    std::expected
-    cacheLedgerObj(uint256 const& objId, int32_t cacheIdx) override
-    {
-        return 1;
-    }
-
-    [[nodiscard]] std::expected
-    getTxField(SField const& fname) const override
-    {
-        if (fname == sfAccount)
-            return Bytes(accountID_.begin(), accountID_.end());
-
-        if (fname == sfFee)
-        {
-            int64_t x = 235;
-            auto const* p = reinterpret_cast(&x);
-            return Bytes{p, p + sizeof(x)};
-        }
-
-        if (fname == sfSequence)
-        {
-            auto const x = getLedgerSqn();
-            if (!x)
-                return std::unexpected(x.error());
-            std::uint32_t const data = x.value();
-            auto const* b = reinterpret_cast(&data);
-            auto const* e = reinterpret_cast(&data + 1);
-            return Bytes{b, e};
-        }
-
-        return Bytes();
-    }
-
-    [[nodiscard]] std::expected
-    getCurrentLedgerObjField(SField const& fname) const override
-    {
-        auto const& sn = fname.getName();
-        if (sn == "Destination" || sn == "Account")
-            return Bytes(accountID_.begin(), accountID_.end());
-        if (sn == "Data")
-            return data_;
-        if (sn == "FinishAfter")
-        {
-            auto t = env_.current()->parentCloseTime().time_since_epoch().count();
-            std::string s = std::to_string(t);
-            return Bytes{s.begin(), s.end()};
-        }
-
-        return std::unexpected(HostFunctionError::Unimplemented);
-    }
-
-    [[nodiscard]] std::expected
-    getLedgerObjField(int32_t, SField const& fname) const override
-    {
-        if (fname == sfBalance)
-        {
-            int64_t x = 10'000;
-            auto const* p = reinterpret_cast(&x);
-            return Bytes{p, p + sizeof(x)};
-        }
-
-        if (fname == sfAccount)
-            return Bytes(accountID_.begin(), accountID_.end());
-
-        return data_;
-    }
-
-    [[nodiscard]] std::expected
-    getTxNestedField(FieldLocator const& locator) const override
-    {
-        if (locator.size() == 1)
-        {
-            int32_t const* l = locator.data();
-            int32_t const sfield = l[0];
-            if (sfield == sfAccount.getCode())
-                return Bytes(accountID_.begin(), accountID_.end());
-        }
-
-        uint8_t const a[] = {0x2b, 0x6a, 0x23, 0x2a, 0xa4, 0xc4, 0xbe, 0x41, 0xbf, 0x49, 0xd2,
-                             0x45, 0x9f, 0xa4, 0xa0, 0x34, 0x7e, 0x1b, 0x54, 0x3a, 0x4c, 0x92,
-                             0xfc, 0xee, 0x08, 0x21, 0xc0, 0x20, 0x1e, 0x2e, 0x9a, 0x00};
-        return Bytes(&a[0], &a[sizeof(a)]);
-    }
-
-    [[nodiscard]] std::expected
-    getCurrentLedgerObjNestedField(FieldLocator const& locator) const override
-    {
-        if (locator.size() == 1)
-        {
-            int32_t const* l = locator.data();
-            int32_t const sfield = l[0];
-            if (sfield == sfAccount.getCode())
-                return Bytes(accountID_.begin(), accountID_.end());
-        }
-
-        uint8_t const a[] = {0x2b, 0x6a, 0x23, 0x2a, 0xa4, 0xc4, 0xbe, 0x41, 0xbf, 0x49, 0xd2,
-                             0x45, 0x9f, 0xa4, 0xa0, 0x34, 0x7e, 0x1b, 0x54, 0x3a, 0x4c, 0x92,
-                             0xfc, 0xee, 0x08, 0x21, 0xc0, 0x20, 0x1e, 0x2e, 0x9a, 0x00};
-        return Bytes(&a[0], &a[sizeof(a)]);
-    }
-
-    [[nodiscard]] std::expected
-    getLedgerObjNestedField(int32_t cacheIdx, FieldLocator const& locator) const override
-    {
-        if (locator.size() == 1)
-        {
-            int32_t const* l = locator.data();
-            int32_t const sfield = l[0];
-            if (sfield == sfAccount.getCode())
-                return Bytes(accountID_.begin(), accountID_.end());
-        }
-
-        uint8_t const a[] = {0x2b, 0x6a, 0x23, 0x2a, 0xa4, 0xc4, 0xbe, 0x41, 0xbf, 0x49, 0xd2,
-                             0x45, 0x9f, 0xa4, 0xa0, 0x34, 0x7e, 0x1b, 0x54, 0x3a, 0x4c, 0x92,
-                             0xfc, 0xee, 0x08, 0x21, 0xc0, 0x20, 0x1e, 0x2e, 0x9a, 0x00};
-        return Bytes(&a[0], &a[sizeof(a)]);
-    }
-
-    [[nodiscard]] std::expected
-    getTxArrayLen(SField const& fname) const override
-    {
-        return 32;
-    }
-
-    [[nodiscard]] std::expected
-    getCurrentLedgerObjArrayLen(SField const& fname) const override
-    {
-        return 32;
-    }
-
-    [[nodiscard]] std::expected
-    getLedgerObjArrayLen(int32_t cacheIdx, SField const& fname) const override
-    {
-        return 32;
-    }
-
-    [[nodiscard]] std::expected
-    getTxNestedArrayLen(FieldLocator const& locator) const override
-    {
-        return 32;
-    }
-
-    [[nodiscard]] std::expected
-    getCurrentLedgerObjNestedArrayLen(FieldLocator const& locator) const override
-    {
-        return 32;
-    }
-
-    [[nodiscard]] std::expected
-    getLedgerObjNestedArrayLen(int32_t cacheIdx, FieldLocator const& locator) const override
-    {
-        return 32;
-    }
-
-    std::expected
-    updateData(Slice const& data) override
-    {
-        return data.size();
-    }
-
-    [[nodiscard]] std::expected
-    checkSignature(Slice const& message, Slice const& signature, Slice const& pubkey) const override
-    {
-        return 1;
-    }
-
-    [[nodiscard]] std::expected
-    computeSha512HalfHash(Slice const& data) const override
-    {
-        return env_.current()->header().parentHash;
-    }
-
-    [[nodiscard]] std::expected
-    accountKeylet(AccountID const& account) const override
-    {
-        if (!account)
-            return std::unexpected(HostFunctionError::InvalidAccount);
-        auto const keylet = keylet::account(account);
-        return Bytes{keylet.key.begin(), keylet.key.end()};
-    }
-
-    [[nodiscard]] std::expected
-    ammKeylet(Asset const& issue1, Asset const& issue2) const override
-    {
-        if (issue1 == issue2)
-            return std::unexpected(HostFunctionError::InvalidParams);
-        if (issue1.holds() || issue2.holds())
-            return std::unexpected(HostFunctionError::InvalidParams);
-        auto const keylet = keylet::amm(issue1, issue2);
-        return Bytes{keylet.key.begin(), keylet.key.end()};
-    }
-
-    [[nodiscard]] std::expected
-    checkKeylet(AccountID const& account, std::uint32_t seq) const override
-    {
-        if (!account)
-            return std::unexpected(HostFunctionError::InvalidAccount);
-        auto const keylet = keylet::check(account, SeqProxy::rawSequence(seq));
-        return Bytes{keylet.key.begin(), keylet.key.end()};
-    }
-
-    [[nodiscard]] std::expected
-    credentialKeylet(AccountID const& subject, AccountID const& issuer, Slice const& credentialType)
-        const override
-    {
-        if (!subject || !issuer || credentialType.empty() ||
-            credentialType.size() > kMaxCredentialTypeLength)
-            return std::unexpected(HostFunctionError::InvalidAccount);
-        auto const keylet = keylet::credential(subject, issuer, credentialType);
-        return Bytes{keylet.key.begin(), keylet.key.end()};
-    }
-
-    [[nodiscard]] std::expected
-    escrowKeylet(AccountID const& account, std::uint32_t seq) const override
-    {
-        if (!account)
-            return std::unexpected(HostFunctionError::InvalidAccount);
-        auto const keylet = keylet::escrow(account, SeqProxy::rawSequence(seq));
-        return Bytes{keylet.key.begin(), keylet.key.end()};
-    }
-
-    [[nodiscard]] std::expected
-    oracleKeylet(AccountID const& account, std::uint32_t documentId) const override
-    {
-        if (!account)
-            return std::unexpected(HostFunctionError::InvalidAccount);
-        auto const keylet = keylet::oracle(account, documentId);
-        return Bytes{keylet.key.begin(), keylet.key.end()};
-    }
-
-    [[nodiscard]] std::expected
-    getNFT(AccountID const& account, uint256 const& nftId) const override
-    {
-        if (!account || !nftId)
-            return std::unexpected(HostFunctionError::InvalidParams);
-
-        std::string s = "https://ripple.com";
-        return Bytes(s.begin(), s.end());
-    }
-
-    [[nodiscard]] std::expected
-    getNFTIssuer(uint256 const& nftId) const override
-    {
-        return Bytes(accountID_.begin(), accountID_.end());
-    }
-
-    [[nodiscard]] std::expected
-    getNFTTaxon(uint256 const& nftId) const override
-    {
-        return 4;
-    }
-
-    [[nodiscard]] std::expected
-    getNFTFlags(uint256 const& nftId) const override
-    {
-        return 8;
-    }
-
-    [[nodiscard]] std::expected
-    getNFTTransferFee(uint256 const& nftId) const override
-    {
-        return 10;
-    }
-
-    [[nodiscard]] std::expected
-    getNFTSequence(uint256 const& nftId) const override
-    {
-        return 4;
-    }
-
-    template 
-    void
-    log(std::string_view const& msg, F&& dataFn) const
-    {
-#ifdef DEBUG_OUTPUT
-        auto& j = std::cerr;
-#else
-        if (!getJournal().active(beast::Severity::Trace))
-            return;
-        auto j = getJournal().trace();
-#endif
-        j << "WasmTrace: " << msg << " " << dataFn();
-
-#ifdef DEBUG_OUTPUT
-        j << std::endl;
-#endif
-    }
-
-    void
-    trace(std::string_view const& msg, std::string_view const& data) const override
-    {
-        log(msg, [&data] { return data; });
-    }
-
-    [[nodiscard]] std::expected
-    floatFromInt(int64_t x, int32_t mode) const override
-    {
-        return wasm_float::floatFromIntImpl(x, mode);
-    }
-
-    [[nodiscard]] std::expected
-    floatFromUint(uint64_t x, int32_t mode) const override
-    {
-        return wasm_float::floatFromUintImpl(x, mode);
-    }
-
-    [[nodiscard]] std::expected
-    floatFromSTAmount(STAmount const& x, int32_t mode) const override
-    {
-        return wasm_float::floatFromSTAmountImpl(x, mode);
-    }
-
-    [[nodiscard]] std::expected
-    floatFromSTNumber(STNumber const& x, int32_t mode) const override
-    {
-        return wasm_float::floatFromSTNumberImpl(x, mode);
-    }
-
-    [[nodiscard]] std::expected
-    floatToInt(Slice const& x, int32_t mode) const override
-    {
-        return wasm_float::floatToIntImpl(x, mode);
-    }
-
-    [[nodiscard]] std::expected
-    floatToMantExp(Slice const& x) const override
-    {
-        return wasm_float::floatToMantExpImpl(x);
-    }
-
-    [[nodiscard]] std::expected
-    floatFromMantExp(int64_t mantissa, int32_t exponent, int32_t mode) const override
-    {
-        return wasm_float::floatFromMantExpImpl(mantissa, exponent, mode);
-    }
-
-    [[nodiscard]] std::expected
-    floatCompare(Slice const& x, Slice const& y) const override
-    {
-        return wasm_float::floatCompareImpl(x, y);
-    }
-
-    [[nodiscard]] std::expected
-    floatAdd(Slice const& x, Slice const& y, int32_t mode) const override
-    {
-        return wasm_float::floatAddImpl(x, y, mode);
-    }
-
-    [[nodiscard]] std::expected
-    floatSubtract(Slice const& x, Slice const& y, int32_t mode) const override
-    {
-        return wasm_float::floatSubtractImpl(x, y, mode);
-    }
-
-    [[nodiscard]] std::expected
-    floatMultiply(Slice const& x, Slice const& y, int32_t mode) const override
-    {
-        return wasm_float::floatMultiplyImpl(x, y, mode);
-    }
-
-    [[nodiscard]] std::expected
-    floatDivide(Slice const& x, Slice const& y, int32_t mode) const override
-    {
-        return wasm_float::floatDivideImpl(x, y, mode);
-    }
-
-    [[nodiscard]] std::expected
-    floatRoot(Slice const& x, int32_t n, int32_t mode) const override
-    {
-        return wasm_float::floatRootImpl(x, n, mode);
-    }
-
-    [[nodiscard]] std::expected
-    floatPower(Slice const& x, int32_t n, int32_t mode) const override
-    {
-        return wasm_float::floatPowerImpl(x, n, mode);
-    }
-};
-
-class TestHostFunctionsSink : public TestHostFunctions
-{
-    test::StreamSink sink_;
-
-public:
-    explicit TestHostFunctionsSink(test::jtx::Env& env)
-        : TestHostFunctions(env), sink_(beast::Severity::Debug)
-    {
-        j_ = beast::Journal(sink_);
-    }
-
-    test::StreamSink&
-    getSink()
-    {
-        return sink_;
-    }
-};
-
-}  // namespace xrpl::test
diff --git a/src/test/app/ValidatorList_test.cpp b/src/test/app/ValidatorList_test.cpp
index 323c77c780..d2e6cb24aa 100644
--- a/src/test/app/ValidatorList_test.cpp
+++ b/src/test/app/ValidatorList_test.cpp
@@ -2253,8 +2253,7 @@ private:
     {
         testcase("Sha512 hashing");
         // Tests that ValidatorList hash_append helpers with a single blob
-        // returns the same result as xrpl::Sha512Half used by the
-        // TMValidatorList protocol message handler
+        // return the same result as xrpl::Sha512Half
         std::string const manifest = "This is not really a manifest";
         std::string const blob = "This is not really a blob";
         std::string const signature = "This is not really a signature";
@@ -2275,17 +2274,6 @@ private:
             BEAST_EXPECT(global != sha512Half(blob, blobMap, version));
         }
 
-        {
-            protocol::TMValidatorList msg1;
-            msg1.set_manifest(manifest);
-            msg1.set_blob(blob);
-            msg1.set_signature(signature);
-            msg1.set_version(version);
-            BEAST_EXPECT(global == sha512Half(msg1));
-            msg1.set_signature(blob);
-            BEAST_EXPECT(global != sha512Half(msg1));
-        }
-
         {
             protocol::TMValidatorListCollection msg2;
             msg2.set_manifest(manifest);
@@ -2323,19 +2311,7 @@ private:
             BEAST_EXPECT(!ec);
             return std::make_pair(header, buffers);
         };
-        auto extractProtocolMessage1 = [this, &extractHeader](Message& message) {
-            auto [header, buffers] = extractHeader(message);
-            if (BEAST_EXPECT(header) &&
-                BEAST_EXPECT(header->messageType == protocol::mtVALIDATOR_LIST))
-            {
-                auto const msg =
-                    detail::parseMessageContent(*header, buffers.data());
-                BEAST_EXPECT(msg);
-                return msg;
-            }
-            return std::shared_ptr();
-        };
-        auto extractProtocolMessage2 = [this, &extractHeader](Message& message) {
+        auto extractProtocolMessage = [this, &extractHeader](Message& message) {
             auto [header, buffers] = extractHeader(message);
             if (BEAST_EXPECT(header) &&
                 BEAST_EXPECT(header->messageType == protocol::mtVALIDATOR_LIST_COLLECTION))
@@ -2347,92 +2323,55 @@ private:
             }
             return std::shared_ptr();
         };
-        auto verifyMessage =
-            [this, manifestCutoff, &extractProtocolMessage1, &extractProtocolMessage2](
-                auto const version,
-                auto const& manifest,
-                auto const& blobInfos,
-                auto const& messages,
-                std::vector>> expectedInfo) {
-                BEAST_EXPECT(messages.size() == expectedInfo.size());
-                auto msgIter = expectedInfo.begin();
-                for (auto const& messageWithHash : messages)
+        auto verifyMessage = [this, manifestCutoff, &extractProtocolMessage](
+                                 auto const version,
+                                 auto const& manifest,
+                                 auto const& blobInfos,
+                                 auto const& messages,
+                                 std::vector> expectedInfo) {
+            BEAST_EXPECT(messages.size() == expectedInfo.size());
+            auto msgIter = expectedInfo.begin();
+            for (auto const& messageWithHash : messages)
+            {
+                if (!BEAST_EXPECT(msgIter != expectedInfo.end()))
+                    break;
+                if (!BEAST_EXPECT(messageWithHash.message))
+                    continue;
+                auto const& expectedSeqs = *msgIter;
+                auto seqIter = expectedSeqs.begin();
                 {
-                    if (!BEAST_EXPECT(msgIter != expectedInfo.end()))
-                        break;
-                    if (!BEAST_EXPECT(messageWithHash.message))
-                        continue;
-                    auto const& expectedSeqs = msgIter->second;
-                    auto seqIter = expectedSeqs.begin();
-                    auto const size =
-                        messageWithHash.message->getBuffer(compression::Compressed::Off).size();
-                    // This size is arbitrary, but shouldn't change
-                    BEAST_EXPECT(size == msgIter->first);
-                    if (expectedSeqs.size() == 1)
+                    std::vector hashingBlobs;
+                    hashingBlobs.reserve(expectedSeqs.size());
+
+                    auto const msg = extractProtocolMessage(*messageWithHash.message);
+                    if (BEAST_EXPECT(msg))
                     {
-                        auto const msg = extractProtocolMessage1(*messageWithHash.message);
-                        auto const expectedVersion = 1;
-                        if (BEAST_EXPECT(msg))
+                        BEAST_EXPECT(msg->version() == version);
+                        BEAST_EXPECT(msg->manifest() == manifest);
+                        for (auto const& blobInfo : msg->blobs())
                         {
-                            BEAST_EXPECT(msg->version() == expectedVersion);
                             if (!BEAST_EXPECT(seqIter != expectedSeqs.end()))
-                                continue;
+                                break;
                             auto const& expectedBlob = blobInfos.at(*seqIter);
-                            BEAST_EXPECT((*seqIter < manifestCutoff) == !!expectedBlob.manifest);
-                            auto const expectedManifest =
-                                *seqIter < manifestCutoff && expectedBlob.manifest
-                                ? *expectedBlob.manifest
-                                : manifest;
-                            BEAST_EXPECT(msg->manifest() == expectedManifest);
-                            BEAST_EXPECT(msg->blob() == expectedBlob.blob);
-                            BEAST_EXPECT(msg->signature() == expectedBlob.signature);
+                            hashingBlobs.push_back(expectedBlob);
+                            BEAST_EXPECT(blobInfo.has_manifest() == !!expectedBlob.manifest);
+                            BEAST_EXPECT(blobInfo.has_manifest() == (*seqIter < manifestCutoff));
+
+                            if (*seqIter < manifestCutoff)
+                                BEAST_EXPECT(blobInfo.manifest() == *expectedBlob.manifest);
+                            BEAST_EXPECT(blobInfo.blob() == expectedBlob.blob);
+                            BEAST_EXPECT(blobInfo.signature() == expectedBlob.signature);
                             ++seqIter;
-                            BEAST_EXPECT(seqIter == expectedSeqs.end());
-
-                            BEAST_EXPECT(
-                                messageWithHash.hash ==
-                                sha512Half(
-                                    expectedManifest,
-                                    expectedBlob.blob,
-                                    expectedBlob.signature,
-                                    expectedVersion));
                         }
+                        BEAST_EXPECT(seqIter == expectedSeqs.end());
                     }
-                    else
-                    {
-                        std::vector hashingBlobs;
-                        hashingBlobs.reserve(msgIter->second.size());
-
-                        auto const msg = extractProtocolMessage2(*messageWithHash.message);
-                        if (BEAST_EXPECT(msg))
-                        {
-                            BEAST_EXPECT(msg->version() == version);
-                            BEAST_EXPECT(msg->manifest() == manifest);
-                            for (auto const& blobInfo : msg->blobs())
-                            {
-                                if (!BEAST_EXPECT(seqIter != expectedSeqs.end()))
-                                    break;
-                                auto const& expectedBlob = blobInfos.at(*seqIter);
-                                hashingBlobs.push_back(expectedBlob);
-                                BEAST_EXPECT(blobInfo.has_manifest() == !!expectedBlob.manifest);
-                                BEAST_EXPECT(
-                                    blobInfo.has_manifest() == (*seqIter < manifestCutoff));
-
-                                if (*seqIter < manifestCutoff)
-                                    BEAST_EXPECT(blobInfo.manifest() == *expectedBlob.manifest);
-                                BEAST_EXPECT(blobInfo.blob() == expectedBlob.blob);
-                                BEAST_EXPECT(blobInfo.signature() == expectedBlob.signature);
-                                ++seqIter;
-                            }
-                            BEAST_EXPECT(seqIter == expectedSeqs.end());
-                        }
-                        BEAST_EXPECT(
-                            messageWithHash.hash == sha512Half(manifest, hashingBlobs, version));
-                    }
-                    ++msgIter;
+                    BEAST_EXPECT(
+                        messageWithHash.hash == sha512Half(manifest, hashingBlobs, version));
                 }
-                BEAST_EXPECT(msgIter == expectedInfo.end());
-            };
+                ++msgIter;
+            }
+            BEAST_EXPECT(msgIter == expectedInfo.end());
+        };
         auto verifyBuildMessages = [this](
                                        std::pair const& result,
                                        std::size_t expectedSequence,
@@ -2471,66 +2410,10 @@ private:
 
         std::vector messages;
 
-        // Version 1
-
-        // This peer has a VL ahead of our "current"
-        verifyBuildMessages(
-            ValidatorList::buildValidatorListMessages(
-                1, 8, maxSequence, version, manifest, blobInfos, messages),
-            0,
-            0);
-        BEAST_EXPECT(messages.empty());
-
-        // Don't repeat the work if messages is populated, even though the
-        // peerSequence provided indicates it should. Note that this
-        // situation is contrived for this test and should never happen in
-        // real code.
-        messages.emplace_back();
-        verifyBuildMessages(
-            ValidatorList::buildValidatorListMessages(
-                1, 3, maxSequence, version, manifest, blobInfos, messages),
-            5,
-            0);
-        BEAST_EXPECT(messages.size() == 1 && !messages.front().message);
-
-        // Generate a version 1 message
-        messages.clear();
-        verifyBuildMessages(
-            ValidatorList::buildValidatorListMessages(
-                1, 3, maxSequence, version, manifest, blobInfos, messages),
-            5,
-            1);
-        if (BEAST_EXPECT(messages.size() == 1) && BEAST_EXPECT(messages.front().message))
-        {
-            auto const& messageWithHash = messages.front();
-            auto const msg = extractProtocolMessage1(*messageWithHash.message);
-            auto const size =
-                messageWithHash.message->getBuffer(compression::Compressed::Off).size();
-            // This size is arbitrary, but shouldn't change
-            BEAST_EXPECT(size == 108);
-            auto const& expected = blobInfos.at(5);
-            if (BEAST_EXPECT(msg))
-            {
-                BEAST_EXPECT(msg->version() == 1);
-                // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
-                BEAST_EXPECT(msg->manifest() == *expected.manifest);
-                BEAST_EXPECT(msg->blob() == expected.blob);
-                BEAST_EXPECT(msg->signature() == expected.signature);
-            }
-            BEAST_EXPECT(
-                messageWithHash.hash ==
-                // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
-                sha512Half(*expected.manifest, expected.blob, expected.signature, 1));
-        }
-
-        // Version 2
-
-        messages.clear();
-
         // This peer has a VL ahead of us.
         verifyBuildMessages(
             ValidatorList::buildValidatorListMessages(
-                2, maxSequence * 2, maxSequence, version, manifest, blobInfos, messages),
+                maxSequence * 2, maxSequence, version, manifest, blobInfos, messages),
             0,
             0);
         BEAST_EXPECT(messages.empty());
@@ -2542,19 +2425,19 @@ private:
         messages.emplace_back();
         verifyBuildMessages(
             ValidatorList::buildValidatorListMessages(
-                2, 3, maxSequence, version, manifest, blobInfos, messages),
+                3, maxSequence, version, manifest, blobInfos, messages),
             maxSequence,
             0);
         BEAST_EXPECT(messages.size() == 1 && !messages.front().message);
 
-        // Generate a version 2 message. Don't send the current
+        // Generate a message. Don't send the current
         messages.clear();
         verifyBuildMessages(
             ValidatorList::buildValidatorListMessages(
-                2, 5, maxSequence, version, manifest, blobInfos, messages),
+                5, maxSequence, version, manifest, blobInfos, messages),
             maxSequence,
             4);
-        verifyMessage(version, manifest, blobInfos, messages, {{372, {6, 7, 10, 12}}});
+        verifyMessage(version, manifest, blobInfos, messages, {{6, 7, 10, 12}});
 
         // Test message splitting on size limits.
 
@@ -2562,50 +2445,39 @@ private:
         messages.clear();
         verifyBuildMessages(
             ValidatorList::buildValidatorListMessages(
-                2, 5, maxSequence, version, manifest, blobInfos, messages, 300),
+                5, maxSequence, version, manifest, blobInfos, messages, 300),
             maxSequence,
             4);
-        verifyMessage(version, manifest, blobInfos, messages, {{212, {6, 7}}, {192, {10, 12}}});
+        verifyMessage(version, manifest, blobInfos, messages, {{6, 7}, {10, 12}});
 
         // Set a limit between the size of the two earlier messages so one
         // will split and the other won't
         messages.clear();
         verifyBuildMessages(
             ValidatorList::buildValidatorListMessages(
-                2, 5, maxSequence, version, manifest, blobInfos, messages, 200),
+                5, maxSequence, version, manifest, blobInfos, messages, 200),
             maxSequence,
             4);
-        verifyMessage(
-            version, manifest, blobInfos, messages, {{108, {6}}, {108, {7}}, {192, {10, 12}}});
+        verifyMessage(version, manifest, blobInfos, messages, {{6}, {7}, {10, 12}});
 
         // Set a limit so that all the VLs are sent individually
         messages.clear();
         verifyBuildMessages(
             ValidatorList::buildValidatorListMessages(
-                2, 5, maxSequence, version, manifest, blobInfos, messages, 150),
+                5, maxSequence, version, manifest, blobInfos, messages, 150),
             maxSequence,
             4);
-        verifyMessage(
-            version,
-            manifest,
-            blobInfos,
-            messages,
-            {{108, {6}}, {108, {7}}, {110, {10}}, {110, {12}}});
+        verifyMessage(version, manifest, blobInfos, messages, {{6}, {7}, {10}, {12}});
 
         // Set a limit smaller than some of the messages. Because single
         // messages send regardless, they will all still be sent
         messages.clear();
         verifyBuildMessages(
             ValidatorList::buildValidatorListMessages(
-                2, 5, maxSequence, version, manifest, blobInfos, messages, 108),
+                5, maxSequence, version, manifest, blobInfos, messages, 108),
             maxSequence,
             4);
-        verifyMessage(
-            version,
-            manifest,
-            blobInfos,
-            messages,
-            {{108, {6}}, {108, {7}}, {110, {10}}, {110, {12}}});
+        verifyMessage(version, manifest, blobInfos, messages, {{6}, {7}, {10}, {12}});
     }
 
     void
diff --git a/src/test/app/ValidatorSite_test.cpp b/src/test/app/ValidatorSite_test.cpp
index 8400f2d794..8373efe85b 100644
--- a/src/test/app/ValidatorSite_test.cpp
+++ b/src/test/app/ValidatorSite_test.cpp
@@ -15,13 +15,12 @@
 #include 
 
 #include 
-#include 
-#include 
 #include 
 
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -704,7 +703,7 @@ public:
                   .effectiveOverlap = detail::kDefaultEffectiveOverlap,
                   .expectedRefreshMin = 60 * 24}});  // max of 24 hours
         }
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
         for (auto const& file : directory_iterator(good.subdir()))
         {
             remove_all(file);
diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp
deleted file mode 100644
index 70527f570d..0000000000
--- a/src/test/app/Vault_test.cpp
+++ /dev/null
@@ -1,8436 +0,0 @@
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-namespace xrpl {
-
-class Vault_test : public beast::unit_test::Suite
-{
-    using PrettyAsset = xrpl::test::jtx::PrettyAsset;
-    using PrettyAmount = xrpl::test::jtx::PrettyAmount;
-
-    static constexpr auto kNegativeAmount = [](PrettyAsset const& asset) -> PrettyAmount {
-        return {STAmount{asset.raw(), 1ul, 0, true, STAmount::Unchecked{}}, ""};
-    };
-
-    void
-    testSequences()
-    {
-        using namespace test::jtx;
-        Account const issuer{"issuer"};
-        Account const owner{"owner"};
-        Account const depositor{"depositor"};
-        Account const charlie{"charlie"};  // authorized 3rd party
-        Account const dave{"dave"};
-
-        auto const testSequence = [&, this](
-                                      std::string const& prefix,
-                                      Env& env,
-                                      Vault& vault,
-                                      PrettyAsset const& asset) {
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            tx[sfData] = "AFEED00E";
-            tx[sfAssetsMaximum] = asset(100).number();
-            env(tx);
-            env.close();
-            BEAST_EXPECT(env.le(keylet));
-            std::uint64_t const scale = asset.raw().holds() ? 1 : 1e6;
-
-            auto const [share, vaultAccount] =
-                [&env, keylet = keylet, asset, this]() -> std::tuple {
-                auto const vault = env.le(keylet);
-                BEAST_EXPECT(vault != nullptr);
-                if (!asset.integral())
-                {
-                    BEAST_EXPECT(vault->at(sfScale) == 6);
-                }
-                else
-                {
-                    BEAST_EXPECT(vault->at(sfScale) == 0);
-                }
-                auto const shares = env.le(keylet::mptokenIssuance(vault->at(sfShareMPTID)));
-                BEAST_EXPECT(shares != nullptr);
-                if (!asset.integral())
-                {
-                    BEAST_EXPECT(shares->at(sfAssetScale) == 6);
-                }
-                else
-                {
-                    BEAST_EXPECT(shares->at(sfAssetScale) == 0);
-                }
-                return {MPTIssue(vault->at(sfShareMPTID)), Account("vault", vault->at(sfAccount))};
-            }();
-            auto const shares = share.raw().get();
-            env.memoize(vaultAccount);
-
-            // Several 3rd party accounts which cannot receive funds
-            Account const alice{"alice"};
-            Account const erin{"erin"};  // not authorized by issuer
-            env.fund(XRP(1000), alice, erin);
-            env(fset(alice, asfDepositAuth));
-            env.close();
-
-            {
-                testcase(prefix + " fail to deposit more than assets held");
-                auto tx = vault.deposit(
-                    {.depositor = depositor, .id = keylet.key, .amount = asset(10000)});
-                env(tx, Ter(tecINSUFFICIENT_FUNDS));
-                env.close();
-            }
-
-            {
-                testcase(prefix + " deposit non-zero amount");
-                auto tx =
-                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(depositor, shares) == share(50 * scale));
-            }
-
-            {
-                testcase(prefix + " deposit non-zero amount again");
-                auto tx =
-                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(depositor, shares) == share(100 * scale));
-            }
-
-            {
-                testcase(prefix + " fail to delete non-empty vault");
-                auto tx = vault.del({.owner = owner, .id = keylet.key});
-                env(tx, Ter(tecHAS_OBLIGATIONS));
-                env.close();
-            }
-
-            {
-                testcase(prefix + " fail to update because wrong owner");
-                auto tx = vault.set({.owner = issuer, .id = keylet.key});
-                tx[sfAssetsMaximum] = asset(50).number();
-                env(tx, Ter(tecNO_PERMISSION));
-                env.close();
-            }
-
-            {
-                testcase(prefix + " fail to set maximum lower than current amount");
-                auto tx = vault.set({.owner = owner, .id = keylet.key});
-                tx[sfAssetsMaximum] = asset(50).number();
-                env(tx, Ter(tecLIMIT_EXCEEDED));
-                env.close();
-            }
-
-            {
-                testcase(prefix + " set maximum higher than current amount");
-                auto tx = vault.set({.owner = owner, .id = keylet.key});
-                tx[sfAssetsMaximum] = asset(150).number();
-                env(tx);
-                env.close();
-            }
-
-            {
-                testcase(prefix + " set maximum is idempotent, set it again");
-                auto tx = vault.set({.owner = owner, .id = keylet.key});
-                tx[sfAssetsMaximum] = asset(150).number();
-                env(tx);
-                env.close();
-            }
-
-            {
-                testcase(prefix + " set data");
-                auto tx = vault.set({.owner = owner, .id = keylet.key});
-                tx[sfData] = "0";
-                env(tx);
-                env.close();
-            }
-
-            {
-                testcase(prefix + " fail to set domain on public vault");
-                auto tx = vault.set({.owner = owner, .id = keylet.key});
-                tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
-                env(tx, Ter{tecNO_PERMISSION});
-                env.close();
-            }
-
-            {
-                testcase(prefix + " fail to deposit more than maximum");
-                auto tx =
-                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-                env(tx, Ter(tecLIMIT_EXCEEDED));
-                env.close();
-            }
-
-            {
-                testcase(prefix + " reset maximum to zero i.e. not enforced");
-                auto tx = vault.set({.owner = owner, .id = keylet.key});
-                tx[sfAssetsMaximum] = asset(0).number();
-                env(tx);
-                env.close();
-            }
-
-            {
-                testcase(prefix + " fail to withdraw more than assets held");
-                auto tx = vault.withdraw(
-                    {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
-                env(tx, Ter(tecINSUFFICIENT_FUNDS));
-                env.close();
-            }
-
-            {
-                testcase(prefix + " deposit some more");
-                auto tx =
-                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(depositor, shares) == share(200 * scale));
-            }
-
-            {
-                testcase(prefix + " clawback some");
-                auto code = asset.raw().native() ? Ter(temMALFORMED) : Ter(tesSUCCESS);
-                auto tx = vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(10)});
-                env(tx, code);
-                env.close();
-                if (!asset.raw().native())
-                {
-                    BEAST_EXPECT(env.balance(depositor, shares) == share(190 * scale));
-                }
-            }
-
-            {
-                testcase(prefix + " clawback all");
-                auto code = asset.raw().native() ? Ter(tecNO_PERMISSION) : Ter(tesSUCCESS);
-                auto tx = vault.clawback({.issuer = issuer, .id = keylet.key, .holder = depositor});
-                env(tx, code);
-                env.close();
-                if (!asset.raw().native())
-                {
-                    BEAST_EXPECT(env.balance(depositor, shares) == share(0));
-
-                    {
-                        auto tx = vault.clawback(
-                            {.issuer = issuer,
-                             .id = keylet.key,
-                             .holder = depositor,
-                             .amount = asset(10)});
-                        env(tx, Ter{tecPRECISION_LOSS});
-                        env.close();
-                    }
-
-                    {
-                        auto tx = vault.withdraw(
-                            {.depositor = depositor, .id = keylet.key, .amount = asset(10)});
-                        env(tx, Ter{tecPRECISION_LOSS});
-                        env.close();
-                    }
-                }
-            }
-
-            if (!asset.raw().native())
-            {
-                testcase(prefix + " deposit again");
-                auto tx =
-                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(200)});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(depositor, shares) == share(200 * scale));
-            }
-            else
-            {
-                testcase(prefix + " deposit/withdrawal same or less than fee");
-                auto const amount = env.current()->fees().base;
-
-                auto tx =
-                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount});
-                env(tx);
-                env.close();
-
-                tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = amount});
-                env(tx);
-                env.close();
-
-                tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount});
-                env(tx);
-                env.close();
-
-                // Withdraw to 3rd party
-                tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = amount});
-                tx[sfDestination] = charlie.human();
-                env(tx);
-                env.close();
-
-                tx =
-                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount - 1});
-                env(tx);
-                env.close();
-
-                tx = vault.withdraw(
-                    {.depositor = depositor, .id = keylet.key, .amount = amount - 1});
-                env(tx);
-                env.close();
-            }
-
-            {
-                testcase(prefix + " fail to withdraw to 3rd party lsfDepositAuth");
-                auto tx = vault.withdraw(
-                    {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-                tx[sfDestination] = alice.human();
-                env(tx, Ter{tecNO_PERMISSION});
-                env.close();
-            }
-
-            {
-                testcase(prefix + " fail to withdraw to zero destination");
-                auto tx = vault.withdraw(
-                    {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
-                tx[sfDestination] = "0";
-                env(tx, Ter(temMALFORMED));
-                env.close();
-            }
-
-            if (!asset.raw().native())
-            {
-                testcase(prefix + " fail to withdraw to 3rd party no authorization");
-                auto tx = vault.withdraw(
-                    {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-                tx[sfDestination] = erin.human();
-                env(tx, Ter{asset.raw().holds() ? tecNO_LINE : tecNO_AUTH});
-                env.close();
-            }
-
-            {
-                testcase(prefix + " fail to withdraw to 3rd party lsfRequireDestTag");
-                auto tx = vault.withdraw(
-                    {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-                tx[sfDestination] = dave.human();
-                env(tx, Ter{tecDST_TAG_NEEDED});
-                env.close();
-            }
-
-            {
-                testcase(prefix + " withdraw to 3rd party lsfRequireDestTag");
-                auto tx =
-                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-                tx[sfDestination] = dave.human();
-                tx[sfDestinationTag] = "0";
-                env(tx);
-                env.close();
-            }
-
-            {
-                testcase(prefix + " deposit again");
-                auto tx = vault.deposit({.depositor = dave, .id = keylet.key, .amount = asset(50)});
-                env(tx);
-                env.close();
-            }
-
-            {
-                testcase(prefix + " fail to withdraw lsfRequireDestTag");
-                auto tx =
-                    vault.withdraw({.depositor = dave, .id = keylet.key, .amount = asset(50)});
-                env(tx, Ter{tecDST_TAG_NEEDED});
-                env.close();
-            }
-
-            {
-                testcase(prefix + " withdraw with tag");
-                auto tx =
-                    vault.withdraw({.depositor = dave, .id = keylet.key, .amount = asset(50)});
-                tx[sfDestinationTag] = "0";
-                env(tx);
-                env.close();
-            }
-
-            {
-                testcase(prefix + " withdraw to authorized 3rd party");
-                auto tx =
-                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-                tx[sfDestination] = charlie.human();
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(depositor, shares) == share(100 * scale));
-            }
-
-            {
-                testcase(prefix + " withdraw to issuer");
-                auto tx =
-                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-                tx[sfDestination] = issuer.human();
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(depositor, shares) == share(50 * scale));
-            }
-
-            if (!asset.raw().native())
-            {
-                testcase(prefix + " issuer deposits");
-                auto tx =
-                    vault.deposit({.depositor = issuer, .id = keylet.key, .amount = asset(10)});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(issuer, shares) == share(10 * scale));
-
-                testcase(prefix + " issuer withdraws");
-                tx = vault.withdraw(
-                    {.depositor = issuer, .id = keylet.key, .amount = share(10 * scale)});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(issuer, shares) == share(0 * scale));
-            }
-
-            {
-                testcase(prefix + " withdraw remaining assets");
-                auto tx =
-                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(depositor, shares) == share(0));
-
-                if (!asset.raw().native())
-                {
-                    auto tx = vault.clawback(
-                        {.issuer = issuer,
-                         .id = keylet.key,
-                         .holder = depositor,
-                         .amount = asset(0)});
-                    env(tx, Ter{tecPRECISION_LOSS});
-                    env.close();
-                }
-
-                {
-                    auto tx = vault.withdraw(
-                        {.depositor = depositor, .id = keylet.key, .amount = share(10)});
-                    env(tx, Ter{tecINSUFFICIENT_FUNDS});
-                    env.close();
-                }
-            }
-
-            if (!asset.integral())
-            {
-                testcase(prefix + " temporary authorization for 3rd party");
-                env(trust(erin, asset(1000)));
-                env(trust(issuer, asset(0), erin, tfSetfAuth));
-                env(pay(issuer, erin, asset(10)));
-
-                // Erin deposits all in vault, then sends shares to depositor
-                auto tx = vault.deposit({.depositor = erin, .id = keylet.key, .amount = asset(10)});
-                env(tx);
-                env.close();
-                {
-                    auto tx = pay(erin, depositor, share(10 * scale));
-
-                    // depositor no longer has MPToken for shares
-                    env(tx, Ter{tecNO_AUTH});
-                    env.close();
-
-                    // depositor will gain MPToken for shares again
-                    env(vault.deposit(
-                        {.depositor = depositor, .id = keylet.key, .amount = asset(1)}));
-                    env.close();
-
-                    env(tx);
-                    env.close();
-                }
-
-                testcase(prefix + " withdraw to authorized 3rd party");
-                // Depositor withdraws assets, destined to Erin
-                tx =
-                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
-                tx[sfDestination] = erin.human();
-                env(tx);
-                env.close();
-
-                // Erin returns assets to issuer
-                env(pay(erin, issuer, asset(10)));
-                env.close();
-
-                testcase(prefix + " fail to pay to unauthorized 3rd party");
-                env(trust(erin, asset(0)));
-                env.close();
-
-                // Erin has MPToken but is no longer authorized to hold assets
-                env(pay(depositor, erin, share(1)), Ter{tecNO_LINE});
-                env.close();
-
-                // Depositor withdraws remaining single asset
-                tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)});
-                env(tx);
-                env.close();
-            }
-
-            {
-                testcase(prefix + " fail to delete because wrong owner");
-                auto tx = vault.del({.owner = issuer, .id = keylet.key});
-                env(tx, Ter(tecNO_PERMISSION));
-                env.close();
-            }
-
-            {
-                testcase(prefix + " delete empty vault");
-                auto tx = vault.del({.owner = owner, .id = keylet.key});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(!env.le(keylet));
-            }
-        };
-
-        auto testCases = [&, this](
-                             std::string prefix, std::function setup) {
-            Env env{*this, testableAmendments()};
-
-            Vault vault{env};
-            env.fund(XRP(1000), issuer, owner, depositor, charlie, dave);
-            env.close();
-            env(fset(issuer, asfAllowTrustLineClawback));
-            env(fset(issuer, asfRequireAuth));
-            env(fset(dave, asfRequireDest));
-            env.close();
-            env.require(Flags(issuer, asfAllowTrustLineClawback));
-            env.require(Flags(issuer, asfRequireAuth));
-
-            PrettyAsset const asset = setup(env);
-            testSequence(prefix, env, vault, asset);
-        };
-
-        testCases("XRP", [&](Env& env) -> PrettyAsset { return {xrpIssue(), 1'000'000}; });
-
-        testCases("IOU", [&](Env& env) -> Asset {
-            PrettyAsset const asset = issuer["IOU"];
-            env(trust(owner, asset(1000)));
-            env(trust(depositor, asset(1000)));
-            env(trust(charlie, asset(1000)));
-            env(trust(dave, asset(1000)));
-            env(trust(issuer, asset(0), owner, tfSetfAuth));
-            env(trust(issuer, asset(0), depositor, tfSetfAuth));
-            env(trust(issuer, asset(0), charlie, tfSetfAuth));
-            env(trust(issuer, asset(0), dave, tfSetfAuth));
-            env(pay(issuer, depositor, asset(1000)));
-            env.close();
-            return asset;
-        });
-
-        testCases("MPT", [&](Env& env) -> Asset {
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
-            PrettyAsset const asset = mptt.issuanceID();
-            mptt.authorize({.account = depositor});
-            mptt.authorize({.account = charlie});
-            mptt.authorize({.account = dave});
-            env(pay(issuer, depositor, asset(1000)));
-            env.close();
-            return asset;
-        });
-    }
-
-    void
-    testPreflight()
-    {
-        using namespace test::jtx;
-
-        struct CaseArgs
-        {
-            FeatureBitset features = testableAmendments();
-        };
-
-        auto testCase = [&, this](
-                            std::function test,
-                            CaseArgs args = {}) {
-            Env env{*this, args.features};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            Vault vault{env};
-            env.fund(XRP(1000), issuer, owner);
-            env.close();
-
-            env(fset(issuer, asfAllowTrustLineClawback));
-            env(fset(issuer, asfRequireAuth));
-            env.close();
-
-            PrettyAsset const asset = issuer["IOU"];
-            env(trust(owner, asset(1000)));
-            env(trust(issuer, asset(0), owner, tfSetfAuth));
-            env(pay(issuer, owner, asset(1000)));
-            env.close();
-
-            test(env, issuer, owner, asset, vault);
-        };
-
-        auto testDisabled = [&](TER resultAfterCreate = temDISABLED) {
-            return [&, resultAfterCreate](
-                       Env& env,
-                       Account const& issuer,
-                       Account const& owner,
-                       Asset const& asset,
-                       Vault& vault) {
-                testcase("disabled single asset vault");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                env(tx, Ter{temDISABLED});
-
-                {
-                    auto tx = vault.set({.owner = owner, .id = keylet.key});
-                    env(tx, kData("test"), Ter{resultAfterCreate});
-                }
-
-                {
-                    auto tx =
-                        vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                    env(tx, Ter{resultAfterCreate});
-                }
-
-                {
-                    auto tx =
-                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                    env(tx, Ter{resultAfterCreate});
-                }
-
-                {
-                    auto tx = vault.clawback(
-                        {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(10)});
-                    env(tx, Ter{resultAfterCreate});
-                }
-
-                {
-                    auto tx = vault.del({.owner = owner, .id = keylet.key});
-                    env(tx, Ter{resultAfterCreate});
-                }
-            };
-        };
-
-        testCase(testDisabled(), {.features = testableAmendments() - featureSingleAssetVault});
-
-        testCase(testDisabled(tecNO_ENTRY), {.features = testableAmendments() - featureMPTokensV1});
-
-        testCase(
-            [&](Env& env,
-                Account const& issuer,
-                Account const& owner,
-                Asset const& asset,
-                Vault& vault) {
-                testcase("disabled permissioned domains");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                env(tx);
-
-                tx[sfFlags] = tx[sfFlags].asUInt() | tfVaultPrivate;
-                tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
-                env(tx, Ter{temDISABLED});
-
-                {
-                    auto tx = vault.set({.owner = owner, .id = keylet.key});
-                    env(tx, kData("Test"));
-
-                    tx[sfDomainID] = to_string(BaseUInt<256>(13ul));
-                    env(tx, Ter{temDISABLED});
-                }
-            },
-            {.features = testableAmendments() - featurePermissionedDomains});
-
-        testCase([&](Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Asset const& asset,
-                     Vault& vault) {
-            testcase("invalid flags");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            tx[sfFlags] = tfClearDeepFreeze;
-            env(tx, Ter{temINVALID_FLAG});
-
-            {
-                auto tx = vault.set({.owner = owner, .id = keylet.key});
-                tx[sfFlags] = tfClearDeepFreeze;
-                env(tx, Ter{temINVALID_FLAG});
-            }
-
-            {
-                auto tx =
-                    vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                tx[sfFlags] = tfClearDeepFreeze;
-                env(tx, Ter{temINVALID_FLAG});
-            }
-
-            {
-                auto tx =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                tx[sfFlags] = tfClearDeepFreeze;
-                env(tx, Ter{temINVALID_FLAG});
-            }
-
-            {
-                auto tx = vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(10)});
-                tx[sfFlags] = tfClearDeepFreeze;
-                env(tx, Ter{temINVALID_FLAG});
-            }
-
-            {
-                auto tx = vault.del({.owner = owner, .id = keylet.key});
-                tx[sfFlags] = tfClearDeepFreeze;
-                env(tx, Ter{temINVALID_FLAG});
-            }
-        });
-
-        testCase([&](Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Asset const& asset,
-                     Vault& vault) {
-            testcase("invalid fee");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            tx[jss::Fee] = "-1";
-            env(tx, Ter{temBAD_FEE});
-
-            {
-                auto tx = vault.set({.owner = owner, .id = keylet.key});
-                tx[jss::Fee] = "-1";
-                env(tx, Ter{temBAD_FEE});
-            }
-
-            {
-                auto tx =
-                    vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                tx[jss::Fee] = "-1";
-                env(tx, Ter{temBAD_FEE});
-            }
-
-            {
-                auto tx =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                tx[jss::Fee] = "-1";
-                env(tx, Ter{temBAD_FEE});
-            }
-
-            {
-                auto tx = vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(10)});
-                tx[jss::Fee] = "-1";
-                env(tx, Ter{temBAD_FEE});
-            }
-
-            {
-                auto tx = vault.del({.owner = owner, .id = keylet.key});
-                tx[jss::Fee] = "-1";
-                env(tx, Ter{temBAD_FEE});
-            }
-        });
-
-        testCase(
-            [&](Env& env, Account const&, Account const& owner, Asset const&, Vault& vault) {
-                testcase("disabled permissioned domain");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpIssue()});
-                tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
-                env(tx, Ter{temDISABLED});
-
-                {
-                    auto tx = vault.set({.owner = owner, .id = keylet.key});
-                    tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
-                    env(tx, Ter{temDISABLED});
-                }
-
-                {
-                    auto tx = vault.set({.owner = owner, .id = keylet.key});
-                    tx[sfDomainID] = "0";
-                    env(tx, Ter{temDISABLED});
-                }
-            },
-            {.features = (testableAmendments()) - featurePermissionedDomains});
-
-        testCase([&](Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Asset const& asset,
-                     Vault& vault) {
-            testcase("use zero vault");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpIssue()});
-
-            {
-                auto tx = vault.set({
-                    .owner = owner,
-                    .id = beast::kZero,
-                });
-                env(tx, Ter{temMALFORMED});
-            }
-
-            {
-                auto tx =
-                    vault.deposit({.depositor = owner, .id = beast::kZero, .amount = asset(10)});
-                env(tx, Ter(temMALFORMED));
-            }
-
-            {
-                auto tx =
-                    vault.withdraw({.depositor = owner, .id = beast::kZero, .amount = asset(10)});
-                env(tx, Ter{temMALFORMED});
-            }
-
-            {
-                auto tx = vault.clawback(
-                    {.issuer = issuer, .id = beast::kZero, .holder = owner, .amount = asset(10)});
-                env(tx, Ter{temMALFORMED});
-            }
-
-            {
-                auto tx = vault.del({
-                    .owner = owner,
-                    .id = beast::kZero,
-                });
-                env(tx, Ter{temMALFORMED});
-            }
-        });
-
-        testCase(
-            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
-                testcase("withdraw to bad destination");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-
-                {
-                    auto tx =
-                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                    tx[jss::Destination] = "0";
-                    env(tx, Ter{temMALFORMED});
-                }
-            });
-
-        testCase(
-            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
-                testcase("create with Scale");
-
-                {
-                    auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                    tx[sfScale] = 255;
-                    env(tx, Ter(temMALFORMED));
-                }
-
-                {
-                    auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                    tx[sfScale] = 19;
-                    env(tx, Ter(temMALFORMED));
-                }
-
-                // accepted range from 0 to 18
-                {
-                    auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                    tx[sfScale] = 18;
-                    env(tx);
-                    env.close();
-                    auto const sleVault = env.le(keylet);
-                    BEAST_EXPECT(sleVault);
-                    BEAST_EXPECT((*sleVault)[sfScale] == 18);
-                }
-
-                {
-                    auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                    tx[sfScale] = 0;
-                    env(tx);
-                    env.close();
-                    auto const sleVault = env.le(keylet);
-                    BEAST_EXPECT(sleVault);
-                    BEAST_EXPECT((*sleVault)[sfScale] == 0);
-                }
-
-                {
-                    auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                    env(tx);
-                    env.close();
-                    auto const sleVault = env.le(keylet);
-                    BEAST_EXPECT(sleVault);
-                    BEAST_EXPECT((*sleVault)[sfScale] == 6);
-                }
-            });
-
-        testCase(
-            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
-                testcase("create or set invalid data");
-
-                auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
-
-                {
-                    auto tx = tx1;
-                    tx[sfData] = "";
-                    env(tx, Ter(temMALFORMED));
-                }
-
-                {
-                    auto tx = tx1;
-                    // A hexadecimal string of 257 bytes.
-                    tx[sfData] = std::string(514, 'A');
-                    env(tx, Ter(temMALFORMED));
-                }
-
-                {
-                    auto tx = vault.set({.owner = owner, .id = keylet.key});
-                    tx[sfData] = "";
-                    env(tx, Ter{temMALFORMED});
-                }
-
-                {
-                    auto tx = vault.set({.owner = owner, .id = keylet.key});
-                    // A hexadecimal string of 257 bytes.
-                    tx[sfData] = std::string(514, 'A');
-                    env(tx, Ter{temMALFORMED});
-                }
-            });
-
-        testCase(
-            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
-                testcase("set nothing updated");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-
-                {
-                    auto tx = vault.set({.owner = owner, .id = keylet.key});
-                    env(tx, Ter{temMALFORMED});
-                }
-            });
-
-        testCase(
-            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
-                testcase("create with invalid metadata");
-
-                auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
-
-                {
-                    auto tx = tx1;
-                    tx[sfMPTokenMetadata] = "";
-                    env(tx, Ter(temMALFORMED));
-                }
-
-                {
-                    auto tx = tx1;
-                    // This metadata is for the share token.
-                    // A hexadecimal string of 1025 bytes.
-                    tx[sfMPTokenMetadata] = std::string(2050, 'B');
-                    env(tx, Ter(temMALFORMED));
-                }
-            });
-
-        testCase(
-            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
-                testcase("set negative maximum");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-
-                {
-                    auto tx = vault.set({.owner = owner, .id = keylet.key});
-                    tx[sfAssetsMaximum] = kNegativeAmount(asset).number();
-                    env(tx, Ter{temMALFORMED});
-                }
-            });
-
-        testCase(
-            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
-                testcase("invalid deposit amount");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-
-                {
-                    auto tx = vault.deposit(
-                        {.depositor = owner, .id = keylet.key, .amount = kNegativeAmount(asset)});
-                    env(tx, Ter(temBAD_AMOUNT));
-                }
-
-                {
-                    auto tx =
-                        vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(0)});
-                    env(tx, Ter(temBAD_AMOUNT));
-                }
-            });
-
-        testCase(
-            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
-                testcase("invalid set immutable flag");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-
-                {
-                    auto tx = vault.set({.owner = owner, .id = keylet.key});
-                    tx[sfFlags] = tfVaultPrivate;
-                    env(tx, Ter(temINVALID_FLAG));
-                }
-            });
-
-        testCase(
-            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
-                testcase("invalid withdraw amount");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-
-                {
-                    auto tx = vault.withdraw(
-                        {.depositor = owner, .id = keylet.key, .amount = kNegativeAmount(asset)});
-                    env(tx, Ter(temBAD_AMOUNT));
-                }
-
-                {
-                    auto tx =
-                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(0)});
-                    env(tx, Ter(temBAD_AMOUNT));
-                }
-            });
-
-        testCase([&](Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Asset const& asset,
-                     Vault& vault) {
-            testcase("invalid clawback");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-
-            // Preclaim only checks for native assets.
-            if (asset.native())
-            {
-                auto tx = vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)});
-                env(tx, Ter(temMALFORMED));
-            }
-
-            {
-                auto tx = vault.clawback(
-                    {.issuer = issuer,
-                     .id = keylet.key,
-                     .holder = owner,
-                     .amount = kNegativeAmount(asset)});
-                env(tx, Ter(temBAD_AMOUNT));
-            }
-        });
-
-        testCase(
-            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
-                testcase("invalid create");
-
-                auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
-
-                {
-                    auto tx = tx1;
-                    tx[sfWithdrawalPolicy] = 0;
-                    env(tx, Ter(temMALFORMED));
-                }
-
-                {
-                    auto tx = tx1;
-                    tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
-                    env(tx, Ter{temMALFORMED});
-                }
-
-                {
-                    auto tx = tx1;
-                    tx[sfAssetsMaximum] = kNegativeAmount(asset).number();
-                    env(tx, Ter{temMALFORMED});
-                }
-
-                {
-                    auto tx = tx1;
-                    tx[sfFlags] = tfVaultPrivate;
-                    tx[sfDomainID] = "0";
-                    env(tx, Ter{temMALFORMED});
-                }
-            });
-    }
-
-    // Test for non-asset specific behaviors.
-    void
-    testCreateFailXRP()
-    {
-        using namespace test::jtx;
-
-        auto testCase = [this](
-                            std::function test) {
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            Account const depositor{"depositor"};
-
-            env.fund(XRP(1000), issuer, owner, depositor);
-            env.close();
-            Vault vault{env};
-            Asset const asset = xrpIssue();
-
-            test(env, issuer, owner, depositor, asset, vault);
-        };
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     PrettyAsset const& asset,
-                     Vault& vault) {
-            testcase("nothing to set");
-            auto tx = vault.set({.owner = owner, .id = keylet::skip().key});
-            tx[sfAssetsMaximum] = asset(0).number();
-            env(tx, Ter(tecNO_ENTRY));
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     PrettyAsset const& asset,
-                     Vault& vault) {
-            testcase("nothing to deposit to");
-            auto tx = vault.deposit(
-                {.depositor = depositor, .id = keylet::skip().key, .amount = asset(10)});
-            env(tx, Ter(tecNO_ENTRY));
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     PrettyAsset const& asset,
-                     Vault& vault) {
-            testcase("nothing to withdraw from");
-            auto tx = vault.withdraw(
-                {.depositor = depositor, .id = keylet::skip().key, .amount = asset(10)});
-            env(tx, Ter(tecNO_ENTRY));
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault) {
-            testcase("nothing to delete");
-            auto tx = vault.del({.owner = owner, .id = keylet::skip().key});
-            env(tx, Ter(tecNO_ENTRY));
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault) {
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            testcase("transaction is good");
-            env(tx);
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault) {
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            tx[sfWithdrawalPolicy] = 1;
-            testcase("explicitly select withdrawal policy");
-            env(tx);
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault) {
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            testcase("insufficient fee");
-            env(tx, Fee(env.current()->fees().base - 1), Ter(telINSUF_FEE_P));
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault) {
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            testcase("insufficient reserve");
-            // It is possible to construct a complicated mathematical
-            // expression for this amount, but it is sadly not easy.
-            env(pay(owner, issuer, XRP(775)));
-            env.close();
-            env(tx, Ter(tecINSUFFICIENT_RESERVE));
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault) {
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            tx[sfFlags] = tfVaultPrivate;
-            tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
-            testcase("non-existing domain");
-            env(tx, Ter{tecOBJECT_NOT_FOUND});
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault) {
-            testcase("cannot set Scale=0");
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            tx[sfScale] = 0;
-            env(tx, Ter{temMALFORMED});
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault) {
-            testcase("cannot set Scale=1");
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            tx[sfScale] = 1;
-            env(tx, Ter{temMALFORMED});
-        });
-    }
-
-    void
-    testCreateFailIOU()
-    {
-        using namespace test::jtx;
-        {
-            {
-                testcase("IOU fail because MPT is disabled");
-                Env env{*this, (testableAmendments() - featureMPTokensV1)};
-                Account const issuer{"issuer"};
-                Account const owner{"owner"};
-                env.fund(XRP(1000), issuer, owner);
-                env.close();
-
-                Vault const vault{env};
-                Asset const asset = issuer["IOU"].asset();
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-
-                env(tx, Ter(temDISABLED));
-                env.close();
-            }
-
-            {
-                testcase("IOU fail create frozen");
-                Env env{*this, testableAmendments()};
-                Account const issuer{"issuer"};
-                Account const owner{"owner"};
-                env.fund(XRP(1000), issuer, owner);
-                env.close();
-                env(fset(issuer, asfGlobalFreeze));
-                env.close();
-
-                Vault const vault{env};
-                Asset const asset = issuer["IOU"].asset();
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-
-                env(tx, Ter(tecFROZEN));
-                env.close();
-            }
-
-            {
-                testcase("IOU fail create no ripling");
-                Env env{*this, testableAmendments()};
-                Account const issuer{"issuer"};
-                Account const owner{"owner"};
-                env.fund(XRP(1000), issuer, owner);
-                env.close();
-                env(fclear(issuer, asfDefaultRipple));
-                env.close();
-
-                Vault const vault{env};
-                Asset const asset = issuer["IOU"].asset();
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                env(tx, Ter(terNO_RIPPLE));
-                env.close();
-            }
-
-            {
-                testcase("IOU no issuer");
-                Env env{*this, testableAmendments()};
-                Account const issuer{"issuer"};
-                Account const owner{"owner"};
-                env.fund(XRP(1000), owner);
-                env.close();
-
-                Vault const vault{env};
-                Asset const asset = issuer["IOU"].asset();
-                {
-                    auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                    env(tx, Ter(terNO_ACCOUNT));
-                    env.close();
-                }
-            }
-        }
-
-        {
-            testcase("IOU fail create vault for AMM LPToken");
-            Env env{*this, testableAmendments()};
-            Account const gw("gateway");
-            Account const alice("alice");
-            Account const carol("carol");
-            IOU const usd = gw["USD"];
-
-            auto const [asset1, asset2] = std::pair(XRP(10000), usd(10000));
-            auto toFund = [&](STAmount const& a) -> STAmount {
-                if (a.native())
-                {
-                    auto const defXRP = XRP(30000);
-                    if (a <= defXRP)
-                        return defXRP;
-                    return a + XRP(1000);
-                }
-                auto defIOU = STAmount{a.asset(), 30000};
-                if (a <= defIOU)
-                    return defIOU;
-                return a + STAmount{a.asset(), 1000};
-            };
-            auto const toFund1 = toFund(asset1);
-            auto const toFund2 = toFund(asset2);
-            BEAST_EXPECT(asset1 <= toFund1 && asset2 <= toFund2);
-
-            if (!asset1.native() && !asset2.native())
-            {
-                fund(env, gw, {alice, carol}, {toFund1, toFund2}, Fund::All);
-            }
-            else if (asset1.native())
-            {
-                fund(env, gw, {alice, carol}, toFund1, {toFund2}, Fund::All);
-            }
-            else if (asset2.native())
-            {
-                fund(env, gw, {alice, carol}, toFund2, {toFund1}, Fund::All);
-            }
-
-            AMM const ammAlice(env, alice, asset1, asset2, CreateArg{.log = false, .tfee = 0});
-
-            Account const owner{"owner"};
-            env.fund(XRP(1000000), owner);
-
-            Vault const vault{env};
-            auto [tx, k] = vault.create({.owner = owner, .asset = ammAlice.lptIssue()});
-            env(tx, Ter{tecWRONG_ASSET});
-            env.close();
-        }
-    }
-
-    void
-    testCreateFailMPT()
-    {
-        using namespace test::jtx;
-
-        auto testCase = [this](
-                            std::function test) {
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            Account const depositor{"depositor"};
-            env.fund(XRP(1000), issuer, owner, depositor);
-            env.close();
-            Vault vault{env};
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            // Locked because that is the default flag.
-            mptt.create();
-            Asset const asset = mptt.issuanceID();
-
-            test(env, issuer, owner, depositor, asset, vault);
-        };
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault) {
-            testcase("MPT no authorization");
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx, Ter(tecNO_AUTH));
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault) {
-            testcase("MPT cannot set Scale=0");
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            tx[sfScale] = 0;
-            env(tx, Ter{temMALFORMED});
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault) {
-            testcase("MPT cannot set Scale=1");
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            tx[sfScale] = 1;
-            env(tx, Ter{temMALFORMED});
-        });
-    }
-
-    void
-    testNonTransferableShares()
-    {
-        using namespace test::jtx;
-
-        Env env{*this, testableAmendments()};
-        Account const issuer{"issuer"};
-        Account const owner{"owner"};
-        Account const depositor{"depositor"};
-        env.fund(XRP(1000), issuer, owner, depositor);
-        env.close();
-
-        Vault const vault{env};
-        PrettyAsset const asset = issuer["IOU"];
-        env.trust(asset(1000), owner);
-        env(pay(issuer, owner, asset(100)));
-        env.trust(asset(1000), depositor);
-        env(pay(issuer, depositor, asset(100)));
-        env.close();
-
-        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-        tx[sfFlags] = tfVaultShareNonTransferable;
-        env(tx);
-        env.close();
-
-        {
-            testcase("nontransferable deposits");
-            auto tx1 =
-                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(40)});
-            env(tx1);
-
-            auto tx2 = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(60)});
-            env(tx2);
-            env.close();
-        }
-
-        auto const vaultAccount =  //
-            [&env, key = keylet.key, this]() -> AccountID {
-            auto jvVault = env.rpc("vault_info", strHex(key));
-
-            BEAST_EXPECT(jvVault[jss::result][jss::vault][sfAssetsTotal] == "100");
-            BEAST_EXPECT(
-                jvVault[jss::result][jss::vault][jss::shares][sfOutstandingAmount] == "100000000");
-
-            // Vault pseudo-account
-            return parseBase58(jvVault[jss::result][jss::vault][jss::Account].asString())
-                .value();
-        }();
-
-        auto const mptId = makeMptID(1, vaultAccount);
-        Asset const shares = mptId;
-
-        {
-            testcase("nontransferable shares cannot be moved");
-            env(pay(owner, depositor, shares(10)), Ter{tecNO_AUTH});
-            env(pay(depositor, owner, shares(10)), Ter{tecNO_AUTH});
-        }
-
-        {
-            testcase("nontransferable shares can be used to withdraw");
-            auto tx1 =
-                vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(20)});
-            env(tx1);
-
-            auto tx2 = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(30)});
-            env(tx2);
-            env.close();
-        }
-
-        {
-            testcase("nontransferable shares balance check");
-            auto jvVault = env.rpc("vault_info", strHex(keylet.key));
-            BEAST_EXPECT(jvVault[jss::result][jss::vault][sfAssetsTotal] == "50");
-            BEAST_EXPECT(
-                jvVault[jss::result][jss::vault][jss::shares][sfOutstandingAmount] == "50000000");
-        }
-
-        {
-            testcase("nontransferable shares withdraw rest");
-            auto tx1 =
-                vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(20)});
-            env(tx1);
-
-            auto tx2 = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(30)});
-            env(tx2);
-            env.close();
-        }
-
-        {
-            testcase("nontransferable shares delete empty vault");
-            auto tx = vault.del({.owner = owner, .id = keylet.key});
-            env(tx);
-            BEAST_EXPECT(!env.le(keylet));
-        }
-    }
-
-    void
-    testWithMPT()
-    {
-        using namespace test::jtx;
-
-        struct CaseArgs
-        {
-            bool enableClawback = true;
-            bool requireAuth = true;
-            int initialXRP = 1000;
-            FeatureBitset features = testableAmendments();
-        };
-
-        auto testCase = [this](
-                            std::function test,
-                            CaseArgs args = {}) {
-            Env env{*this, args.features};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            Account const depositor{"depositor"};
-            env.fund(XRP(args.initialXRP), issuer, owner, depositor);
-            env.close();
-            Vault vault{env};
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            auto const kNone = LedgerSpecificFlags(0);
-            mptt.create(
-                {.flags = tfMPTCanTransfer | tfMPTCanLock |
-                     (args.enableClawback ? tfMPTCanClawback : kNone) |
-                     (args.requireAuth ? tfMPTRequireAuth : kNone)});
-            PrettyAsset const asset = mptt.issuanceID();
-            mptt.authorize({.account = owner});
-            mptt.authorize({.account = depositor});
-            if (args.requireAuth)
-            {
-                mptt.authorize({.account = issuer, .holder = owner});
-                mptt.authorize({.account = issuer, .holder = depositor});
-            }
-
-            env(pay(issuer, depositor, asset(1000)));
-            env.close();
-
-            test(env, issuer, owner, depositor, asset, vault, mptt);
-        };
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     PrettyAsset const& asset,
-                     Vault& vault,
-                     MPTTester& mptt) {
-            testcase("MPT nothing to clawback from");
-            auto tx = vault.clawback(
-                {.issuer = issuer,
-                 .id = keylet::skip().key,
-                 .holder = depositor,
-                 .amount = asset(10)});
-            env(tx, Ter(tecNO_ENTRY));
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault,
-                     MPTTester& mptt) {
-            testcase("MPT global lock blocks create");
-            mptt.set({.account = issuer, .flags = tfMPTLock});
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx, Ter(tecLOCKED));
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     PrettyAsset const& asset,
-                     Vault& vault,
-                     MPTTester& mptt) {
-            testcase("MPT only issuer can clawback");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-            env(tx);
-            env.close();
-
-            {
-                auto tx = vault.clawback({
-                    .issuer = depositor,
-                    .id = keylet.key,
-                    .holder = depositor,
-                });
-                env(tx, Ter(tecNO_PERMISSION));
-            }
-
-            {
-                auto tx = vault.clawback({
-                    .issuer = owner,
-                    .id = keylet.key,
-                    .holder = depositor,
-                });
-                env(tx, Ter(tecNO_PERMISSION));
-            }
-        });
-
-        testCase(
-            [this](
-                Env& env,
-                Account const& issuer,
-                Account const& owner,
-                Account const& depositor,
-                PrettyAsset const& asset,
-                Vault& vault,
-                MPTTester& mptt) {
-                testcase("MPT depositor without MPToken, auth required");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                env(tx);
-                env.close();
-
-                tx = vault.deposit(
-                    {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
-                env(tx);
-                env.close();
-
-                {
-                    // Remove depositor MPToken and it will not be re-created
-                    mptt.authorize({.account = depositor, .flags = tfMPTUnauthorize});
-                    env.close();
-
-                    auto const mptoken = keylet::mptoken(mptt.issuanceID(), depositor);
-                    auto const sleMPT1 = env.le(mptoken);
-                    BEAST_EXPECT(sleMPT1 == nullptr);
-
-                    tx = vault.withdraw(
-                        {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-                    env(tx, Ter{tecNO_AUTH});
-                    env.close();
-
-                    auto const sleMPT2 = env.le(mptoken);
-                    BEAST_EXPECT(sleMPT2 == nullptr);
-                }
-
-                {
-                    // Set destination to 3rd party without MPToken
-                    Account const charlie{"charlie"};
-                    env.fund(XRP(1000), charlie);
-                    env.close();
-
-                    tx = vault.withdraw(
-                        {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-                    tx[sfDestination] = charlie.human();
-                    env(tx, Ter(tecNO_AUTH));
-                }
-            },
-            {.requireAuth = true});
-
-        testCase(
-            [this](
-                Env& env,
-                Account const& issuer,
-                Account const& owner,
-                Account const& depositor,
-                PrettyAsset const& asset,
-                Vault& vault,
-                MPTTester& mptt) {
-                testcase("MPT depositor without MPToken, no auth required");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                env(tx);
-                env.close();
-                auto v = env.le(keylet);
-                BEAST_EXPECT(v);
-
-                tx = vault.deposit(
-                    {.depositor = depositor,
-                     .id = keylet.key,
-                     .amount = asset(1000)});  // all assets held by depositor
-                env(tx);
-                env.close();
-
-                {
-                    // Remove depositor's MPToken and it will be re-created
-                    mptt.authorize({.account = depositor, .flags = tfMPTUnauthorize});
-                    env.close();
-
-                    auto const mptoken = keylet::mptoken(mptt.issuanceID(), depositor);
-                    auto const sleMPT1 = env.le(mptoken);
-                    BEAST_EXPECT(sleMPT1 == nullptr);
-
-                    tx = vault.withdraw(
-                        {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-                    env(tx);
-                    env.close();
-
-                    auto const sleMPT2 = env.le(mptoken);
-                    BEAST_EXPECT(sleMPT2 != nullptr);
-                    BEAST_EXPECT(sleMPT2->at(sfMPTAmount) == 100);
-                }
-
-                {
-                    // Remove 3rd party MPToken and it will not be re-created
-                    mptt.authorize({.account = owner, .flags = tfMPTUnauthorize});
-                    env.close();
-
-                    auto const mptoken = keylet::mptoken(mptt.issuanceID(), owner);
-                    auto const sleMPT1 = env.le(mptoken);
-                    BEAST_EXPECT(sleMPT1 == nullptr);
-
-                    tx = vault.withdraw(
-                        {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-                    tx[sfDestination] = owner.human();
-                    env(tx, Ter(tecNO_AUTH));
-                    env.close();
-
-                    auto const sleMPT2 = env.le(mptoken);
-                    BEAST_EXPECT(sleMPT2 == nullptr);
-                }
-            },
-            {.requireAuth = false});
-
-        auto const [acctReserve, incReserve] = [this]() -> std::pair {
-            Env const env{*this, testableAmendments()};
-            return {
-                env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(),
-                env.current()->fees().increment.drops() / kDropsPerXrp.drops()};
-        }();
-
-        testCase(
-            [&, this](
-                Env& env,
-                Account const& issuer,
-                Account const& owner,
-                Account const& depositor,
-                PrettyAsset const& asset,
-                Vault& vault,
-                MPTTester& mptt) {
-                testcase("MPT fail reserve to re-create MPToken");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                env(tx);
-                env.close();
-                auto v = env.le(keylet);
-                BEAST_EXPECT(v);
-
-                env(pay(depositor, owner, asset(1000)));
-                env.close();
-
-                tx = vault.deposit(
-                    {.depositor = owner,
-                     .id = keylet.key,
-                     .amount = asset(1000)});  // all assets held by owner
-                env(tx);
-                env.close();
-
-                {
-                    // Remove owners's MPToken and it will not be re-created
-                    mptt.authorize({.account = owner, .flags = tfMPTUnauthorize});
-                    env.close();
-
-                    auto const mptoken = keylet::mptoken(mptt.issuanceID(), owner);
-                    auto const sleMPT = env.le(mptoken);
-                    BEAST_EXPECT(sleMPT == nullptr);
-
-                    // Use one reserve so the next transaction fails
-                    env(ticket::create(owner, 1));
-                    env.close();
-
-                    // No reserve to create MPToken for asset in VaultWithdraw
-                    tx = vault.withdraw(
-                        {.depositor = owner, .id = keylet.key, .amount = asset(100)});
-                    env(tx, Ter{tecINSUFFICIENT_RESERVE});
-                    env.close();
-
-                    env(pay(depositor, owner, XRP(incReserve)));
-                    env.close();
-
-                    // Withdraw can now create asset MPToken, tx will succeed
-                    env(tx);
-                    env.close();
-                }
-            },
-            {.requireAuth = false, .initialXRP = acctReserve + (incReserve * 4) + 1});
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     PrettyAsset const& asset,
-                     Vault& vault,
-                     MPTTester& mptt) {
-            testcase("MPT issuance deleted");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
-            env(tx);
-            env.close();
-
-            {
-                auto tx = vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
-                env(tx);
-            }
-
-            mptt.destroy({.issuer = issuer, .id = mptt.issuanceID()});
-            env.close();
-
-            {
-                auto [tx, keylet] = vault.create({.owner = depositor, .asset = asset});
-                env(tx, Ter{tecOBJECT_NOT_FOUND});
-            }
-
-            {
-                auto tx =
-                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
-                env(tx, Ter{tecOBJECT_NOT_FOUND});
-            }
-
-            {
-                auto tx =
-                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
-                env(tx, Ter{tecOBJECT_NOT_FOUND});
-            }
-
-            {
-                auto tx = vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
-                env(tx, Ter{tecOBJECT_NOT_FOUND});
-            }
-
-            env(vault.del({.owner = owner, .id = keylet.key}));
-        });
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     PrettyAsset const& asset,
-                     Vault& vault,
-                     MPTTester& mptt) {
-            testcase("MPT vault owner can receive shares unless unauthorized");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
-            env(tx);
-            env.close();
-
-            auto const issuanceId = [&env](xrpl::Keylet keylet) -> MPTID {
-                auto const vault = env.le(keylet);
-                return vault->at(sfShareMPTID);
-            }(keylet);
-            PrettyAsset const shares = MPTIssue(issuanceId);
-
-            {
-                // owner has MPToken for shares they did not explicitly create
-                env(pay(depositor, owner, shares(1)));
-                env.close();
-
-                tx = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = shares(1)});
-                env(tx);
-                env.close();
-
-                // owner's MPToken for vault shares not destroyed by withdraw
-                env(pay(depositor, owner, shares(1)));
-                env.close();
-
-                tx = vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)});
-                env(tx);
-                env.close();
-
-                // owner's MPToken for vault shares not destroyed by clawback
-                env(pay(depositor, owner, shares(1)));
-                env.close();
-
-                // pay back, so we can destroy owner's MPToken now
-                env(pay(owner, depositor, shares(1)));
-                env.close();
-
-                {
-                    // explicitly destroy vault owners MPToken with zero balance
-                    json::Value jv;
-                    jv[sfAccount] = owner.human();
-                    jv[sfMPTokenIssuanceID] = to_string(issuanceId);
-                    jv[sfFlags] = tfMPTUnauthorize;
-                    jv[sfTransactionType] = jss::MPTokenAuthorize;
-                    env(jv);
-                    env.close();
-                }
-
-                // owner no longer has MPToken for vault shares
-                tx = pay(depositor, owner, shares(1));
-                env(tx, Ter{tecNO_AUTH});
-                env.close();
-
-                // destroy all remaining shares, so we can delete vault
-                tx = vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
-                env(tx);
-                env.close();
-
-                // will soft fail destroying MPToken for vault owner
-                env(vault.del({.owner = owner, .id = keylet.key}));
-                env.close();
-            }
-        });
-
-        testCase(
-            [this](
-                Env& env,
-                Account const& issuer,
-                Account const& owner,
-                Account const& depositor,
-                PrettyAsset const& asset,
-                Vault& vault,
-                MPTTester& mptt) {
-                testcase("MPT clawback disabled");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                env(tx);
-                env.close();
-
-                tx = vault.deposit(
-                    {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
-                env(tx);
-                env.close();
-
-                {
-                    auto tx = vault.clawback(
-                        {.issuer = issuer,
-                         .id = keylet.key,
-                         .holder = depositor,
-                         .amount = asset(0)});
-                    env(tx, Ter{tecNO_PERMISSION});
-                }
-            },
-            {.enableClawback = false});
-
-        testCase([this](
-                     Env& env,
-                     Account const& issuer,
-                     Account const& owner,
-                     Account const& depositor,
-                     Asset const& asset,
-                     Vault& vault,
-                     MPTTester& mptt) {
-            testcase("MPT un-authorization");
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
-            env(tx);
-            env.close();
-
-            mptt.authorize({.account = issuer, .holder = depositor, .flags = tfMPTUnauthorize});
-            env.close();
-
-            {
-                auto tx = vault.withdraw(
-                    {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-                env(tx, Ter(tecNO_AUTH));
-
-                // Withdrawal to other (authorized) accounts works
-                tx[sfDestination] = issuer.human();
-                env(tx);
-                env.close();
-
-                tx[sfDestination] = owner.human();
-                env(tx);
-                env.close();
-            }
-
-            {
-                // Cannot deposit some more
-                auto tx =
-                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-                env(tx, Ter(tecNO_AUTH));
-            }
-
-            {
-                // Cannot clawback if issuer is the holder
-                tx = vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = issuer, .amount = asset(800)});
-                env(tx, Ter(tecNO_PERMISSION));
-            }
-            // Clawback works
-            tx = vault.clawback(
-                {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(800)});
-            env(tx);
-            env.close();
-
-            env(vault.del({.owner = owner, .id = keylet.key}));
-        });
-
-        {
-            testcase("MPT shares to a vault");
-
-            Env env{*this, testableAmendments()};
-            Account const owner{"owner"};
-            Account const issuer{"issuer"};
-            env.fund(XRP(1000000), owner, issuer);
-            env.close();
-            Vault const vault{env};
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create(
-                {.flags = tfMPTCanTransfer | tfMPTCanLock | lsfMPTCanClawback | tfMPTRequireAuth});
-            mptt.authorize({.account = owner});
-            mptt.authorize({.account = issuer, .holder = owner});
-            PrettyAsset const asset = mptt.issuanceID();
-            env(pay(issuer, owner, asset(100)));
-            auto [tx1, k1] = vault.create({.owner = owner, .asset = asset});
-            env(tx1);
-            env.close();
-
-            auto const shares = [&env, keylet = k1, this]() -> Asset {
-                auto const vault = env.le(keylet);
-                BEAST_EXPECT(vault != nullptr);
-                return MPTIssue(vault->at(sfShareMPTID));
-            }();
-
-            auto [tx2, k2] = vault.create({.owner = owner, .asset = shares});
-            env(tx2, Ter{tecWRONG_ASSET});
-            env.close();
-        }
-
-        {
-            testcase("MPT locked: vault shares inherit underlying lock");
-
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            Account const alice{"alice"};
-            Account const bob{"bob"};
-            Account const carol{"carol"};
-            env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
-            env.close();
-            Vault const vault{env};
-
-            MPTTester asset{
-                {.env = env,
-                 .issuer = issuer,
-                 .holders = {owner, alice, bob, carol},
-                 .flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock}};
-            env(pay(issuer, alice, asset(1'000)));
-            env(pay(issuer, bob, asset(1'000)));
-            env.close();
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
-            // Bob also deposits so he has a share MPToken to receive into.
-            env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
-            env.close();
-
-            auto const shares = [&]() -> PrettyAsset {
-                auto const sle = env.le(keylet);
-                BEAST_EXPECT(sle != nullptr);
-                return MPTIssue(sle->at(sfShareMPTID));
-            }();
-            auto const shareMptID = shares.raw().get().getMptID();
-            auto const shareBalance = [&](Account const& account) {
-                auto const sle = env.le(keylet::mptoken(shareMptID, account));
-                return sle ? sle->at(sfMPTAmount) : 0;
-            };
-
-            // Sanity: before the underlying lock, peer-to-peer share
-            // transfers are allowed.
-            env(pay(alice, bob, shares(1)));
-            env.close();
-
-            // Create the offer while shares are spendable, then lock the
-            // underlying to test whether a stale offer can still be crossed.
-            env(offer(alice, XRP(1), shares(1)));
-            env.close();
-
-            // Lock the underlying after the vault and share balances exist.
-            asset.set({.account = issuer, .flags = tfMPTLock});
-            env.close();
-
-            // Direct vault share payment inherits the underlying lock via
-            // sfReferenceHolding.
-            BEAST_EXPECT(shareBalance(alice) == 499);
-            BEAST_EXPECT(shareBalance(bob) == 501);
-            env(pay(alice, bob, shares(1)), Ter{tecLOCKED});
-            env.close();
-            BEAST_EXPECT(shareBalance(alice) == 499);
-            BEAST_EXPECT(shareBalance(bob) == 501);
-
-            // The same inherited lock must also block DEX payment paths that
-            // would consume an offer selling vault shares.
-            env(pay(carol, bob, shares(1)),
-                Sendmax(XRP(1)),
-                Path(BookSpec{shares.raw()}),
-                Ter{tecPATH_PARTIAL});
-            env.close();
-            BEAST_EXPECT(shareBalance(alice) == 499);
-            BEAST_EXPECT(shareBalance(bob) == 501);
-            BEAST_EXPECT(expectOffers(env, alice, 1));
-        }
-
-        {
-            testcase("MPT CanTrade governance: share inherits underlying on DEX and AMM");
-
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            Account const alice{"alice"};
-            Account const bob{"bob"};
-            env.fund(XRP(100'000), issuer, owner, alice, bob);
-            env.close();
-            Vault const vault{env};
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
-            PrettyAsset const asset = mptt.issuanceID();
-            mptt.authorize({.account = owner});
-            mptt.authorize({.account = alice});
-            mptt.authorize({.account = bob});
-            env(pay(issuer, alice, asset(10'000)));
-            env(pay(issuer, bob, asset(10'000)));
-            env.close();
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            // Seed shares so we can later place them on trading venues.
-            env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(5'000)}));
-            env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(5'000)}));
-            env.close();
-
-            auto const shares = [&]() -> PrettyAsset {
-                auto const sle = env.le(keylet);
-                BEAST_EXPECT(sle != nullptr);
-                return MPTIssue(sle->at(sfShareMPTID));
-            }();
-
-            // CanTrade is not set on the underlying, both the asset and
-            // the vault share are blocked on the DEX.
-            env(offer(alice, XRP(1), asset(10)), Ter{tecNO_PERMISSION});
-            env(offer(alice, XRP(1), shares(1)), Ter{tecNO_PERMISSION});
-            env.close();
-
-            // Deposit still works before enabling CanTrade.
-            env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(100)}));
-            env.close();
-
-            // Peer-to-peer share transfers still work (CanTransfer is set on
-            // both layers).
-            env(pay(alice, bob, shares(1)));
-            env.close();
-
-            // Withdraw still works before enabling CanTrade.
-            env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = asset(100)}));
-            env.close();
-
-            // Enable CanTrade on the underlying.
-            mptt.set({.flags = tfMPTSetCanTrade});
-            env.close();
-
-            env(offer(alice, XRP(1), asset(10)));
-            env(offer(alice, XRP(1), shares(1)));
-            env.close();
-
-            AMM const ammUnderlying(env, alice, XRP(1'000), asset(1'000));
-        }
-
-        {
-            testcase("MPT OutstandingAmount > MaximumAmount");
-
-            Env env{*this, testableAmendments() | featureSingleAssetVault};
-            Account const alice{"alice"};
-            Account const issuer{"issuer"};
-            env.fund(XRP(1'000), alice, issuer);
-            env.close();
-            Vault const vault{env};
-
-            MPTTester const btc({.env = env, .issuer = issuer, .holders = {alice}, .maxAmt = 100});
-
-            auto [tx, k] = vault.create({.owner = issuer, .asset = btc});
-            env(tx);
-            env.close();
-
-            tx = vault.deposit({.depositor = issuer, .id = k.key, .amount = btc(110)});
-            // accountHolds is the first check and the issuer has only BTC(100)
-            // available
-            env(tx, Ter{tecINSUFFICIENT_FUNDS});
-            env.close();
-
-            // OutstandingAmount == MaximumAmount
-            env(pay(issuer, alice, btc(100)));
-            env.close();
-
-            tx = vault.deposit({.depositor = issuer, .id = k.key, .amount = btc(100)});
-            // the issuer has BTC(0) available
-            env(tx, Ter{tecINSUFFICIENT_FUNDS});
-            env.close();
-
-            tx = vault.deposit({.depositor = alice, .id = k.key, .amount = btc(100)});
-            // alice transfers BTC(100), OutstandingAmount is 100
-            env(tx);
-            env.close();
-        }
-    }
-
-    void
-    testWithIOU()
-    {
-        using namespace test::jtx;
-
-        struct CaseArgs
-        {
-            int initialXRP = 1000;
-            Number initialIOU = 200;
-            double transferRate = 1.0;
-            bool charlieRipple = true;
-            FeatureBitset features = testableAmendments();
-        };
-
-        auto testCase = [&, this](
-                            std::function vaultAccount,
-                                Vault& vault,
-                                PrettyAsset const& asset,
-                                std::function issuanceId)> test,
-                            CaseArgs args = {}) {
-            Env env{*this, args.features};
-            Account const owner{"owner"};
-            Account const issuer{"issuer"};
-            Account const charlie{"charlie"};
-            Vault vault{env};
-            env.fund(XRP(args.initialXRP), issuer, owner, charlie);
-            env(fset(issuer, asfAllowTrustLineClawback));
-            env.close();
-
-            PrettyAsset const asset = issuer["IOU"];
-            env.trust(asset(1000), owner);
-            env(pay(issuer, owner, asset(args.initialIOU)));
-            env.close();
-            if (!args.charlieRipple)
-            {
-                env(fset(issuer, 0, asfDefaultRipple));
-                env.close();
-                env.trust(asset(1000), charlie);
-                env.close();
-                env(pay(issuer, charlie, asset(args.initialIOU)));
-                env.close();
-                env(fset(issuer, asfDefaultRipple));
-            }
-            else
-            {
-                env.trust(asset(1000), charlie);
-            }
-            env.close();
-            env(rate(issuer, args.transferRate));
-            env.close();
-
-            auto const vaultAccount = [&env](xrpl::Keylet keylet) -> Account {
-                return Account("vault", env.le(keylet)->at(sfAccount));
-            };
-            auto const issuanceId = [&env](xrpl::Keylet keylet) -> MPTID {
-                return env.le(keylet)->at(sfShareMPTID);
-            };
-
-            test(env, owner, issuer, charlie, vaultAccount, vault, asset, issuanceId);
-        };
-
-        testCase([&, this](
-                     Env& env,
-                     Account const& owner,
-                     Account const& issuer,
-                     Account const&,
-                     auto vaultAccount,
-                     Vault& vault,
-                     PrettyAsset const& asset,
-                     auto&&...) {
-            testcase("IOU cannot use different asset");
-            PrettyAsset const foo = issuer["FOO"];
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            {
-                // Cannot create new trustline to a vault
-                auto tx = [&, account = vaultAccount(keylet)]() {
-                    json::Value jv;
-                    jv[jss::Account] = issuer.human();
-                    {
-                        auto& ja = jv[jss::LimitAmount] =
-                            foo(0).value().getJson(JsonOptions::Values::None);
-                        ja[jss::issuer] = toBase58(account);
-                    }
-                    jv[jss::TransactionType] = jss::TrustSet;
-                    jv[jss::Flags] = tfSetFreeze;
-                    return jv;
-                }();
-                env(tx, Ter{tecNO_PERMISSION});
-                env.close();
-            }
-
-            {
-                auto tx = vault.deposit({.depositor = issuer, .id = keylet.key, .amount = foo(20)});
-                env(tx, Ter{tecWRONG_ASSET});
-                env.close();
-            }
-
-            {
-                auto tx =
-                    vault.withdraw({.depositor = issuer, .id = keylet.key, .amount = foo(20)});
-                env(tx, Ter{tecWRONG_ASSET});
-                env.close();
-            }
-
-            env(vault.del({.owner = owner, .id = keylet.key}));
-            env.close();
-        });
-
-        testCase(
-            [&, this](
-                Env& env,
-                Account const& owner,
-                Account const& issuer,
-                Account const& charlie,
-                auto vaultAccount,
-                Vault& vault,
-                PrettyAsset const& asset,
-                auto issuanceId) {
-                testcase("IOU transfer fees not applied");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                env(tx);
-                env.close();
-
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
-                env.close();
-
-                auto const issue = asset.raw().get();
-                Asset const share = Asset(issuanceId(keylet));
-
-                // transfer fees ignored on deposit
-                BEAST_EXPECT(env.balance(owner, issue) == asset(100));
-                BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(100));
-
-                {
-                    auto tx = vault.clawback(
-                        {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)});
-                    env(tx);
-                    env.close();
-                }
-
-                // transfer fees ignored on clawback
-                BEAST_EXPECT(env.balance(owner, issue) == asset(100));
-                BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(50));
-
-                env(vault.withdraw(
-                    {.depositor = owner, .id = keylet.key, .amount = share(20'000'000)}));
-
-                // transfer fees ignored on withdraw
-                BEAST_EXPECT(env.balance(owner, issue) == asset(120));
-                BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(30));
-
-                {
-                    auto tx = vault.withdraw(
-                        {.depositor = owner, .id = keylet.key, .amount = share(30'000'000)});
-                    tx[sfDestination] = charlie.human();
-                    env(tx);
-                }
-
-                // transfer fees ignored on withdraw to 3rd party
-                BEAST_EXPECT(env.balance(owner, issue) == asset(120));
-                BEAST_EXPECT(env.balance(charlie, issue) == asset(30));
-                BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(0));
-
-                env(vault.del({.owner = owner, .id = keylet.key}));
-                env.close();
-            },
-            CaseArgs{.transferRate = 1.25});
-
-        testCase([&, this](
-                     Env& env,
-                     Account const& owner,
-                     Account const& issuer,
-                     Account const& charlie,
-                     auto,
-                     Vault& vault,
-                     PrettyAsset const& asset,
-                     auto&&...) {
-            testcase("IOU no trust line to 3rd party");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
-            env.close();
-
-            Account const erin{"erin"};
-            env.fund(XRP(1000), erin);
-            env.close();
-
-            // Withdraw to 3rd party without trust line
-            auto const tx1 = [&](xrpl::Keylet keylet) {
-                auto tx =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                tx[sfDestination] = erin.human();
-                return tx;
-            }(keylet);
-            env(tx1, Ter{tecNO_LINE});
-        });
-
-        testCase([&, this](
-                     Env& env,
-                     Account const& owner,
-                     Account const& issuer,
-                     Account const& charlie,
-                     auto,
-                     Vault& vault,
-                     PrettyAsset const& asset,
-                     auto&&...) {
-            testcase("IOU no trust line to depositor");
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            // reset limit, so deposit of all funds will delete the trust line
-            env.trust(asset(0), owner);
-            env.close();
-
-            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(200)}));
-            env.close();
-
-            auto trustline = env.le(keylet::trustLine(owner, asset.raw().get()));
-            BEAST_EXPECT(trustline == nullptr);
-
-            // Withdraw without trust line, will succeed
-            auto const tx1 = [&](xrpl::Keylet keylet) {
-                auto tx =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                return tx;
-            }(keylet);
-            env(tx1);
-        });
-
-        testCase(
-            [&, this](
-                Env& env,
-                Account const& owner,
-                Account const& issuer,
-                Account const& charlie,
-                auto vaultAccount,
-                Vault& vault,
-                PrettyAsset const& asset,
-                std::function issuanceId) {
-                testcase("IOU non-transferable");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                tx[sfScale] = 0;
-                env(tx);
-                env.close();
-
-                // Turn on noripple on the pseudo account's trust line.
-                // Charlie's is already set.
-                env(trust(issuer, vaultAccount(keylet)["IOU"], tfSetNoRipple));
-
-                {
-                    // Charlie cannot deposit
-                    auto tx = vault.deposit(
-                        {.depositor = charlie, .id = keylet.key, .amount = asset(100)});
-                    env(tx, Ter{terNO_RIPPLE});
-                    env.close();
-                }
-
-                {
-                    PrettyAsset const shares = issuanceId(keylet);
-                    auto tx1 =
-                        vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)});
-                    env(tx1);
-                    env.close();
-
-                    // Charlie cannot receive funds
-                    auto tx2 = vault.withdraw(
-                        {.depositor = owner, .id = keylet.key, .amount = shares(100)});
-                    tx2[sfDestination] = charlie.human();
-                    env(tx2, Ter{terNO_RIPPLE});
-                    env.close();
-
-                    {
-                        // Create MPToken for shares held by Charlie
-                        json::Value tx{json::ValueType::Object};
-                        tx[sfAccount] = charlie.human();
-                        tx[sfMPTokenIssuanceID] =
-                            to_string(shares.raw().get().getMptID());
-                        tx[sfTransactionType] = jss::MPTokenAuthorize;
-                        env(tx);
-                        env.close();
-                    }
-                    // Behavioral shift introduced by share inheritance:
-                    // before fixCleanup3_2_0 this share Payment succeeded
-                    // and the underlying IOU's NoRipple restriction surfaced
-                    // only later on Charlie's withdrawal (terNO_RIPPLE).
-                    // Post-amendment, canTransfer reads the share's
-                    // sfReferenceHolding and dispatches to the underlying IOU;
-                    // rippling is disabled between owner and charlie so the
-                    // share payment itself is now blocked. tecPATH_DRY is
-                    // the path-find layer's translation of the underlying
-                    // terNO_RIPPLE under featureMPTokensV2.
-                    env(pay(owner, charlie, shares(100)), Ter{tecPATH_DRY});
-                    env.close();
-                }
-
-                tx = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(100)});
-                env(tx);
-                env.close();
-
-                // Delete vault with zero balance
-                env(vault.del({.owner = owner, .id = keylet.key}));
-            },
-            {.charlieRipple = false});
-
-        testCase(
-            [&, this](
-                Env& env,
-                Account const& owner,
-                Account const& issuer,
-                Account const& charlie,
-                auto const& vaultAccount,
-                Vault& vault,
-                PrettyAsset const& asset,
-                auto&&...) {
-                testcase("IOU calculation rounding");
-
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                tx[sfScale] = 1;
-                env(tx);
-                env.close();
-
-                auto const startingOwnerBalance = env.balance(owner, asset);
-                BEAST_EXPECT((startingOwnerBalance.value() == STAmount{asset, 11875, -2}));
-
-                // This operation (first deposit 100, then 3.75 x 5) is known to
-                // have triggered calculation rounding errors in Number
-                // (addition and division), causing the last deposit to be
-                // blocked by Vault invariants.
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
-
-                auto const tx1 = vault.deposit(
-                    {.depositor = owner, .id = keylet.key, .amount = asset(Number(375, -2))});
-                for (auto i = 0; i < 5; ++i)
-                {
-                    env(tx1);
-                }
-                env.close();
-
-                {
-                    STAmount const xfer{asset, 1185, -1};
-                    BEAST_EXPECT(env.balance(owner, asset) == startingOwnerBalance.value() - xfer);
-                    BEAST_EXPECT(env.balance(vaultAccount(keylet), asset) == xfer);
-
-                    auto const vault = env.le(keylet);
-                    BEAST_EXPECT(vault->at(sfAssetsAvailable) == xfer);
-                    BEAST_EXPECT(vault->at(sfAssetsTotal) == xfer);
-                }
-
-                // Total vault balance should be 118.5 IOU. Withdraw and delete
-                // the vault to verify this exact amount was deposited and the
-                // owner has matching shares
-                env(vault.withdraw(
-                    {.depositor = owner,
-                     .id = keylet.key,
-                     .amount = asset(Number(1000 + (37 * 5), -1))}));
-
-                {
-                    BEAST_EXPECT(env.balance(owner, asset) == startingOwnerBalance.value());
-                    BEAST_EXPECT(env.balance(vaultAccount(keylet), asset) == beast::kZero);
-                    auto const vault = env.le(keylet);
-                    BEAST_EXPECT(vault->at(sfAssetsAvailable) == beast::kZero);
-                    BEAST_EXPECT(vault->at(sfAssetsTotal) == beast::kZero);
-                }
-
-                env(vault.del({.owner = owner, .id = keylet.key}));
-                env.close();
-            },
-            {.initialIOU = Number(11875, -2)});
-
-        auto const [acctReserve, incReserve] = [this]() -> std::pair {
-            Env const env{*this, testableAmendments()};
-            return {
-                env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(),
-                env.current()->fees().increment.drops() / kDropsPerXrp.drops()};
-        }();
-
-        testCase(
-            [&, this](
-                Env& env,
-                Account const& owner,
-                Account const& issuer,
-                Account const& charlie,
-                auto,
-                Vault& vault,
-                PrettyAsset const& asset,
-                auto&&...) {
-                testcase("IOU no trust line to depositor no reserve");
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                env(tx);
-                env.close();
-
-                // reset limit, so deposit of all funds will delete the trust
-                // line
-                env.trust(asset(0), owner);
-                env.close();
-
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(200)}));
-                env.close();
-
-                auto trustline = env.le(keylet::trustLine(owner, asset.raw().get()));
-                BEAST_EXPECT(trustline == nullptr);
-
-                env(ticket::create(owner, 1));
-                env.close();
-
-                // Fail because not enough reserve to create trust line
-                tx = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
-                env(tx, Ter{tecNO_LINE_INSUF_RESERVE});
-                env.close();
-
-                env(pay(charlie, owner, XRP(incReserve)));
-                env.close();
-
-                // Withdraw can now create trust line, will succeed
-                env(tx);
-                env.close();
-            },
-            CaseArgs{.initialXRP = acctReserve + (incReserve * 4) + 1});
-
-        testCase(
-            [&, this](
-                Env& env,
-                Account const& owner,
-                Account const& issuer,
-                Account const& charlie,
-                auto,
-                Vault& vault,
-                PrettyAsset const& asset,
-                auto&&...) {
-                testcase("IOU no reserve for share MPToken");
-                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-                env(tx);
-                env.close();
-
-                env(pay(owner, charlie, asset(100)));
-                env.close();
-
-                env(ticket::create(charlie, 3));
-                env.close();
-
-                // Fail because not enough reserve to create MPToken for shares
-                tx = vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(100)});
-                env(tx, Ter{tecINSUFFICIENT_RESERVE});
-                env.close();
-
-                env(pay(issuer, charlie, XRP(incReserve)));
-                env.close();
-
-                // Deposit can now create MPToken, will succeed
-                env(tx);
-                env.close();
-            },
-            CaseArgs{.initialXRP = acctReserve + (incReserve * 4) + 1});
-    }
-
-    void
-    testWithDomainCheck()
-    {
-        using namespace test::jtx;
-
-        testcase("private vault");
-
-        Env env{*this, testableAmendments()};
-        Account const issuer{"issuer"};
-        Account const owner{"owner"};
-        Account const depositor{"depositor"};
-        Account const charlie{"charlie"};
-        Account const pdOwner{"pdOwner"};
-        Account const credIssuer1{"credIssuer1"};
-        Account const credIssuer2{"credIssuer2"};
-        std::string const credType = "credential";
-        Vault const vault{env};
-        env.fund(XRP(1000), issuer, owner, depositor, charlie, pdOwner, credIssuer1, credIssuer2);
-        env.close();
-        env(fset(issuer, asfAllowTrustLineClawback));
-        env.close();
-        env.require(Flags(issuer, asfAllowTrustLineClawback));
-
-        PrettyAsset const asset = issuer["IOU"];
-        env.trust(asset(1000), owner);
-        env(pay(issuer, owner, asset(500)));
-        env.trust(asset(1000), depositor);
-        env(pay(issuer, depositor, asset(500)));
-        env.trust(asset(1000), charlie);
-        env(pay(issuer, charlie, asset(5)));
-        env.close();
-
-        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
-        env(tx);
-        env.close();
-        BEAST_EXPECT(env.le(keylet));
-
-        {
-            testcase("private vault owner can deposit");
-            auto tx = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(50)});
-            env(tx);
-        }
-
-        {
-            testcase("private vault depositor not authorized yet");
-            auto tx =
-                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-            env(tx, Ter{tecNO_AUTH});
-        }
-
-        {
-            testcase("private vault cannot set non-existing domain");
-            auto tx = vault.set({.owner = owner, .id = keylet.key});
-            tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
-            env(tx, Ter{tecOBJECT_NOT_FOUND});
-        }
-
-        {
-            testcase("private vault set domainId");
-
-            {
-                pdomain::Credentials const credentials1{
-                    {.issuer = credIssuer1, .credType = credType}};
-
-                env(pdomain::setTx(pdOwner, credentials1));
-                auto const domainId1 = [&]() {
-                    auto tx = env.tx()->getJson(JsonOptions::Values::None);
-                    return pdomain::getNewDomain(env.meta());
-                }();
-
-                auto tx = vault.set({.owner = owner, .id = keylet.key});
-                tx[sfDomainID] = to_string(domainId1);
-                env(tx);
-                env.close();
-
-                // Update domain second time, should be harmless
-                env(tx);
-                env.close();
-            }
-
-            {
-                pdomain::Credentials const credentials{
-                    {.issuer = credIssuer1, .credType = credType},
-                    {.issuer = credIssuer2, .credType = credType}};
-
-                env(pdomain::setTx(pdOwner, credentials));
-                auto const domainId = [&]() {
-                    auto tx = env.tx()->getJson(JsonOptions::Values::None);
-                    return pdomain::getNewDomain(env.meta());
-                }();
-
-                auto tx = vault.set({.owner = owner, .id = keylet.key});
-                tx[sfDomainID] = to_string(domainId);
-                env(tx);
-                env.close();
-
-                // Should be idempotent
-                tx = vault.set({.owner = owner, .id = keylet.key});
-                tx[sfDomainID] = to_string(domainId);
-                env(tx);
-                env.close();
-            }
-        }
-
-        {
-            testcase("private vault depositor still not authorized");
-            auto tx =
-                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-            env(tx, Ter{tecNO_AUTH});
-            env.close();
-        }
-
-        auto const credKeylet = credentials::keylet(depositor, credIssuer1, credType);
-        {
-            testcase("private vault depositor now authorized");
-            env(credentials::create(depositor, credIssuer1, credType));
-            env(credentials::accept(depositor, credIssuer1, credType));
-            env(credentials::create(charlie, credIssuer1, credType));
-            // charlie's credential not accepted
-            env.close();
-            auto credSle = env.le(credKeylet);
-            BEAST_EXPECT(credSle != nullptr);
-
-            auto tx =
-                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-            env(tx);
-            env.close();
-
-            tx = vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(50)});
-            env(tx, Ter{tecNO_AUTH});
-            env.close();
-        }
-
-        {
-            testcase("private vault depositor lost authorization");
-            env(credentials::deleteCred(credIssuer1, depositor, credIssuer1, credType));
-            env(credentials::deleteCred(credIssuer1, charlie, credIssuer1, credType));
-            env.close();
-            auto credSle = env.le(credKeylet);
-            BEAST_EXPECT(credSle == nullptr);
-
-            auto tx =
-                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-            env(tx, Ter{tecNO_AUTH});
-            env.close();
-        }
-
-        auto const shares = [&env, keylet = keylet, this]() -> Asset {
-            auto const vault = env.le(keylet);
-            BEAST_EXPECT(vault != nullptr);
-            return MPTIssue(vault->at(sfShareMPTID));
-        }();
-
-        {
-            testcase("private vault expired authorization");
-            uint32_t const closeTime =
-                env.current()->header().parentCloseTime.time_since_epoch().count();
-            {
-                auto tx0 = credentials::create(depositor, credIssuer2, credType);
-                tx0[sfExpiration] = closeTime + 20;
-                env(tx0);
-                tx0 = credentials::create(charlie, credIssuer2, credType);
-                tx0[sfExpiration] = closeTime + 20;
-                env(tx0);
-                env.close();
-
-                env(credentials::accept(depositor, credIssuer2, credType));
-                env(credentials::accept(charlie, credIssuer2, credType));
-                env.close();
-            }
-
-            {
-                auto tx1 =
-                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-                env(tx1);
-                env.close();
-
-                auto const tokenKeylet =
-                    keylet::mptoken(shares.get().getMptID(), depositor.id());
-                BEAST_EXPECT(env.le(tokenKeylet) != nullptr);
-            }
-
-            {
-                // time advance
-                env.close();
-                env.close();
-                env.close();
-
-                auto const credsKeylet = credentials::keylet(depositor, credIssuer2, credType);
-                BEAST_EXPECT(env.le(credsKeylet) != nullptr);
-
-                auto tx2 =
-                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1)});
-                env(tx2, Ter{tecEXPIRED});
-                env.close();
-
-                BEAST_EXPECT(env.le(credsKeylet) == nullptr);
-            }
-
-            {
-                auto const credsKeylet = credentials::keylet(charlie, credIssuer2, credType);
-                BEAST_EXPECT(env.le(credsKeylet) != nullptr);
-                auto const tokenKeylet =
-                    keylet::mptoken(shares.get().getMptID(), charlie.id());
-                BEAST_EXPECT(env.le(tokenKeylet) == nullptr);
-
-                auto tx3 =
-                    vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(2)});
-                env(tx3, Ter{tecEXPIRED});
-
-                env.close();
-                BEAST_EXPECT(env.le(credsKeylet) == nullptr);
-                BEAST_EXPECT(env.le(tokenKeylet) == nullptr);
-            }
-        }
-
-        {
-            testcase("private vault reset domainId");
-            auto tx = vault.set({.owner = owner, .id = keylet.key});
-            tx[sfDomainID] = "0";
-            env(tx);
-            env.close();
-
-            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-            env(tx, Ter{tecNO_AUTH});
-            env.close();
-
-            tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-            env(tx);
-            env.close();
-
-            tx = vault.clawback(
-                {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
-            env(tx);
-
-            tx = vault.clawback(
-                {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)});
-            env(tx);
-            env.close();
-
-            tx = vault.del({
-                .owner = owner,
-                .id = keylet.key,
-            });
-            env(tx);
-        }
-    }
-
-    void
-    testWithDomainChecXRP()
-    {
-        using namespace test::jtx;
-
-        testcase("private XRP vault");
-
-        Env env{*this, testableAmendments()};
-        Account const owner{"owner"};
-        Account const depositor{"depositor"};
-        Account const alice{"charlie"};
-        std::string const credType = "credential";
-        Vault const vault{env};
-        env.fund(XRP(100000), owner, depositor, alice);
-        env.close();
-
-        PrettyAsset const asset = xrpIssue();
-        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
-        env(tx);
-        env.close();
-
-        auto const [vaultAccount, issuanceId] =
-            [&env, keylet = keylet, this]() -> std::tuple {
-            auto const vault = env.le(keylet);
-            BEAST_EXPECT(vault != nullptr);
-            return {vault->at(sfAccount), vault->at(sfShareMPTID)};
-        }();
-        BEAST_EXPECT(env.le(keylet::account(vaultAccount)));
-        BEAST_EXPECT(env.le(keylet::mptokenIssuance(issuanceId)));
-        PrettyAsset const shares{issuanceId};
-
-        {
-            testcase("private XRP vault owner can deposit");
-            auto tx = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(50)});
-            env(tx);
-            env.close();
-        }
-
-        {
-            testcase("private XRP vault cannot pay shares to depositor yet");
-            env(pay(owner, depositor, shares(1)), Ter{tecNO_AUTH});
-        }
-
-        {
-            testcase("private XRP vault depositor not authorized yet");
-            auto tx =
-                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-            env(tx, Ter{tecNO_AUTH});
-        }
-
-        {
-            testcase("private XRP vault set DomainID");
-            pdomain::Credentials const credentials{{.issuer = owner, .credType = credType}};
-
-            env(pdomain::setTx(owner, credentials));
-            auto const domainId = [&]() {
-                auto tx = env.tx()->getJson(JsonOptions::Values::None);
-                return pdomain::getNewDomain(env.meta());
-            }();
-
-            auto tx = vault.set({.owner = owner, .id = keylet.key});
-            tx[sfDomainID] = to_string(domainId);
-            env(tx);
-            env.close();
-        }
-
-        auto const credKeylet = credentials::keylet(depositor, owner, credType);
-        {
-            testcase("private XRP vault depositor now authorized");
-            env(credentials::create(depositor, owner, credType));
-            env(credentials::accept(depositor, owner, credType));
-            env.close();
-
-            BEAST_EXPECT(env.le(credKeylet));
-            auto tx =
-                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
-            env(tx);
-            env.close();
-        }
-
-        {
-            testcase("private XRP vault can pay shares to depositor");
-            env(pay(owner, depositor, shares(1)));
-        }
-
-        {
-            testcase("private XRP vault cannot pay shares to 3rd party");
-            json::Value jv;
-            jv[sfAccount] = alice.human();
-            jv[sfTransactionType] = jss::MPTokenAuthorize;
-            jv[sfMPTokenIssuanceID] = to_string(issuanceId);
-            env(jv);
-            env.close();
-
-            env(pay(owner, alice, shares(1)), Ter{tecNO_AUTH});
-        }
-    }
-
-    void
-    testFailedPseudoAccount()
-    {
-        using namespace test::jtx;
-
-        testcase("fail pseudo-account allocation");
-        Env env{*this, testableAmendments()};
-        Account const owner{"owner"};
-        Vault const vault{env};
-        env.fund(XRP(1000), owner);
-
-        auto const keylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-        for (int i = 0; i < 256; ++i)
-        {
-            AccountID const accountId = xrpl::pseudoAccountAddress(*env.current(), keylet.key);
-
-            env(pay(env.master.id(), accountId, XRP(1000)),
-                Seq(kAutofill),
-                Fee(kAutofill),
-                Sig(kAutofill));
-        }
-
-        auto [tx, keylet1] = vault.create({.owner = owner, .asset = xrpIssue()});
-        BEAST_EXPECT(keylet.key == keylet1.key);
-        env(tx, Ter{terADDRESS_COLLISION});
-    }
-
-    void
-    testScaleIOU()
-    {
-        using namespace test::jtx;
-
-        struct Data
-        {
-            Account const& owner;
-            Account const& issuer;
-            Account const& depositor;
-            Account const& vaultAccount;
-            MPTIssue shares;
-            PrettyAsset const& share;
-            Vault& vault;
-            xrpl::Keylet keylet;
-            Issue assets;
-            PrettyAsset const& asset;
-            std::function)> peek;
-        };
-
-        auto testCase = [&, this](
-                            std::uint8_t scale, std::function test) {
-            Env env{*this, testableAmendments()};
-            Account const owner{"owner"};
-            Account const issuer{"issuer"};
-            Account const depositor{"depositor"};
-            Vault vault{env};
-            env.fund(XRP(1000), issuer, owner, depositor);
-            env(fset(issuer, asfAllowTrustLineClawback));
-            env.close();
-
-            PrettyAsset const asset = issuer["IOU"];
-            env.trust(asset(1000), owner);
-            env.trust(asset(1000), depositor);
-            env(pay(issuer, owner, asset(200)));
-            env(pay(issuer, depositor, asset(200)));
-            env.close();
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            tx[sfScale] = scale;
-            env(tx);
-
-            auto const [vaultAccount, issuanceId] =
-                [&env](xrpl::Keylet keylet) -> std::tuple {
-                auto const vault = env.le(keylet);
-                return {Account("vault", vault->at(sfAccount)), vault->at(sfShareMPTID)};
-            }(keylet);
-            MPTIssue const shares(issuanceId);
-            env.memoize(vaultAccount);
-
-            auto const peek = [keylet, &env, this](std::function fn) -> bool {
-                return env.app().getOpenLedger().modify(
-                    [&](OpenView& view, beast::Journal j) -> bool {
-                        Sandbox sb(&view, TapNone);
-                        auto vault = sb.peek(keylet::vault(keylet.key));
-                        if (!BEAST_EXPECT(vault))
-                            return false;
-                        auto shares = sb.peek(keylet::mptokenIssuance(vault->at(sfShareMPTID)));
-                        if (!BEAST_EXPECT(shares))
-                            return false;
-                        if (fn(*vault, *shares))
-                        {
-                            sb.update(vault);
-                            sb.update(shares);
-                            sb.apply(view);
-                            return true;
-                        }
-                        return false;
-                    });
-            };
-
-            test(
-                env,
-                {.owner = owner,
-                 .issuer = issuer,
-                 .depositor = depositor,
-                 .vaultAccount = vaultAccount,
-                 .shares = shares,
-                 .share = PrettyAsset(shares),
-                 .vault = vault,
-                 .keylet = keylet,
-                 .assets = asset.raw().get(),
-                 .asset = asset,
-                 .peek = peek});
-        };
-
-        testCase(18, [&, this](Env& env, Data d) {
-            testcase("Scale deposit overflow on first deposit");
-            auto tx = d.vault.deposit(
-                {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(10)});
-            env(tx, Ter{tecPATH_DRY});
-            env.close();
-        });
-
-        testCase(18, [&, this](Env& env, Data d) {
-            testcase("Scale deposit overflow on second deposit");
-
-            {
-                auto tx = d.vault.deposit(
-                    {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
-                env(tx);
-                env.close();
-            }
-
-            {
-                auto tx = d.vault.deposit(
-                    {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(10)});
-                env(tx, Ter{tecPATH_DRY});
-                env.close();
-            }
-        });
-
-        testCase(18, [&, this](Env& env, Data d) {
-            testcase("Scale deposit overflow on total shares");
-
-            {
-                auto tx = d.vault.deposit(
-                    {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
-                env(tx);
-                env.close();
-            }
-
-            {
-                auto tx = d.vault.deposit(
-                    {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
-                env(tx, Ter{tecPATH_DRY});
-                env.close();
-            }
-        });
-
-        testCase(1, [&, this](Env& env, Data d) {
-            testcase("Scale deposit exact");
-
-            auto const start = env.balance(d.depositor, d.assets).number();
-            auto tx = d.vault.deposit(
-                {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(1)});
-            env(tx);
-            env.close();
-            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(10));
-            BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start - 1));
-        });
-
-        testCase(1, [&, this](Env& env, Data d) {
-            testcase("Scale deposit insignificant amount");
-
-            auto tx = d.vault.deposit(
-                {.depositor = d.depositor,
-                 .id = d.keylet.key,
-                 .amount = STAmount(d.asset, Number(9, -2))});
-            env(tx, Ter{tecPRECISION_LOSS});
-        });
-
-        testCase(1, [&, this](Env& env, Data d) {
-            testcase("Scale deposit exact, using full precision");
-
-            auto const start = env.balance(d.depositor, d.assets).number();
-            auto tx = d.vault.deposit(
-                {.depositor = d.depositor,
-                 .id = d.keylet.key,
-                 .amount = STAmount(d.asset, Number(15, -1))});
-            env(tx);
-            env.close();
-            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(15));
-            BEAST_EXPECT(
-                env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(15, -1)));
-        });
-
-        testCase(1, [&, this](Env& env, Data d) {
-            testcase("Scale deposit exact, truncating from .5");
-
-            auto const start = env.balance(d.depositor, d.assets).number();
-            // Each of the cases below will transfer exactly 1.2 IOU to the
-            // vault and receive 12 shares in exchange
-            {
-                auto tx = d.vault.deposit(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, Number(125, -2))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(12));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) ==
-                    STAmount(d.asset, start - Number(12, -1)));
-            }
-
-            {
-                auto tx = d.vault.deposit(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, Number(1201, -3))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(24));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) ==
-                    STAmount(d.asset, start - Number(24, -1)));
-            }
-
-            {
-                auto tx = d.vault.deposit(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, Number(1299, -3))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(36));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) ==
-                    STAmount(d.asset, start - Number(36, -1)));
-            }
-        });
-
-        testCase(1, [&, this](Env& env, Data d) {
-            testcase("Scale deposit exact, truncating from .01");
-
-            auto const start = env.balance(d.depositor, d.assets).number();
-            // round to 12
-            auto tx = d.vault.deposit(
-                {.depositor = d.depositor,
-                 .id = d.keylet.key,
-                 .amount = STAmount(d.asset, Number(1201, -3))});
-            env(tx);
-            env.close();
-            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(12));
-            BEAST_EXPECT(
-                env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(12, -1)));
-
-            {
-                // round to 6
-                auto tx = d.vault.deposit(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, Number(69, -2))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(18));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) ==
-                    STAmount(d.asset, start - Number(18, -1)));
-            }
-        });
-
-        testCase(1, [&, this](Env& env, Data d) {
-            testcase("Scale deposit exact, truncating from .99");
-
-            auto const start = env.balance(d.depositor, d.assets).number();
-            // round to 12
-            auto tx = d.vault.deposit(
-                {.depositor = d.depositor,
-                 .id = d.keylet.key,
-                 .amount = STAmount(d.asset, Number(1299, -3))});
-            env(tx);
-            env.close();
-            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(12));
-            BEAST_EXPECT(
-                env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(12, -1)));
-
-            {
-                // round to 6
-                auto tx = d.vault.deposit(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, Number(62, -2))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(18));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) ==
-                    STAmount(d.asset, start - Number(18, -1)));
-            }
-        });
-
-        testCase(1, [&, this](Env& env, Data d) {
-            // initial setup: deposit 100 IOU, receive 1000 shares
-            auto const start = env.balance(d.depositor, d.assets).number();
-            auto tx = d.vault.deposit(
-                {.depositor = d.depositor,
-                 .id = d.keylet.key,
-                 .amount = STAmount(d.asset, Number(100, 0))});
-            env(tx);
-            env.close();
-            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
-            BEAST_EXPECT(
-                env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(100, 0)));
-            BEAST_EXPECT(
-                env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(100, 0)));
-            BEAST_EXPECT(
-                env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-1000, 0)));
-
-            {
-                testcase("Scale redeem exact");
-                // sharesToAssetsWithdraw:
-                //  assets = assetsTotal * (shares / sharesTotal)
-                //  assets = 100 * 100 / 1000 = 100 * 0.1 = 10
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-                auto tx = d.vault.withdraw(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.share, Number(100, 0))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(10, 0)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(90, 0)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-900, 0)));
-            }
-
-            {
-                testcase("Scale redeem with rounding");
-                // sharesToAssetsWithdraw:
-                //  assets = assetsTotal * (shares / sharesTotal)
-                //  assets = 90 * 25 / 900 = 90 * 0.02777... = 2.5
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-                d.peek([](SLE& vault, auto&) -> bool {
-                    vault[sfAssetsAvailable] = Number(1);
-                    return true;
-                });
-
-                // Note, this transaction fails first (because of above change
-                // in the open ledger) but then succeeds when the ledger is
-                // closed (because a modification like above is not persistent),
-                // which is why the checks below are expected to pass.
-                auto tx = d.vault.withdraw(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.share, Number(25, 0))});
-                env(tx, Ter{tecINSUFFICIENT_FUNDS});
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900 - 25));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) ==
-                    STAmount(d.asset, start + Number(25, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) ==
-                    STAmount(d.asset, Number(900 - 25, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) ==
-                    STAmount(d.share, -Number(900 - 25, 0)));
-            }
-
-            {
-                testcase("Scale redeem exact");
-                // sharesToAssetsWithdraw:
-                //  assets = assetsTotal * (shares / sharesTotal)
-                //  assets = 87.5 * 21 / 875 = 87.5 * 0.024 = 2.1
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-
-                tx = d.vault.withdraw(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.share, Number(21, 0))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(875 - 21));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) ==
-                    STAmount(d.asset, start + Number(21, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) ==
-                    STAmount(d.asset, Number(875 - 21, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) ==
-                    STAmount(d.share, -Number(875 - 21, 0)));
-            }
-
-            {
-                testcase("Scale redeem rest");
-                auto const rest = env.balance(d.depositor, d.shares).number();
-
-                tx = d.vault.withdraw(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.share, rest)});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares).number() == 0);
-                BEAST_EXPECT(env.balance(d.vaultAccount, d.assets).number() == 0);
-                BEAST_EXPECT(env.balance(d.vaultAccount, d.shares).number() == 0);
-            }
-        });
-
-        testCase(18, [&, this](Env& env, Data d) {
-            testcase("Scale withdraw overflow");
-
-            {
-                auto tx = d.vault.deposit(
-                    {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
-                env(tx);
-                env.close();
-            }
-
-            {
-                auto tx = d.vault.withdraw(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, Number(10, 0))});
-                env(tx, Ter{tecPATH_DRY});
-                env.close();
-            }
-        });
-
-        testCase(1, [&, this](Env& env, Data d) {
-            // initial setup: deposit 100 IOU, receive 1000 shares
-            auto const start = env.balance(d.depositor, d.assets).number();
-            auto tx = d.vault.deposit(
-                {.depositor = d.depositor,
-                 .id = d.keylet.key,
-                 .amount = STAmount(d.asset, Number(100, 0))});
-            env(tx);
-            env.close();
-            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
-            BEAST_EXPECT(
-                env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(100, 0)));
-            BEAST_EXPECT(
-                env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(100, 0)));
-            BEAST_EXPECT(
-                env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-1000, 0)));
-
-            {
-                testcase("Scale withdraw exact");
-                // assetsToSharesWithdraw:
-                //  shares = sharesTotal * (assets / assetsTotal)
-                //  shares = 1000 * 10 / 100 = 1000 * 0.1 = 100
-                // sharesToAssetsWithdraw:
-                //  assets = assetsTotal * (shares / sharesTotal)
-                //  assets = 100 * 100 / 1000 = 100 * 0.1 = 10
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-                auto tx = d.vault.withdraw(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, Number(10, 0))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(10, 0)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(90, 0)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-900, 0)));
-            }
-
-            {
-                testcase("Scale withdraw insignificant amount");
-                auto tx = d.vault.withdraw(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, Number(4, -2))});
-                env(tx, Ter{tecPRECISION_LOSS});
-            }
-
-            {
-                testcase("Scale withdraw with rounding assets");
-                // assetsToSharesWithdraw:
-                //  shares = sharesTotal * (assets / assetsTotal)
-                //  shares = 900 * 2.5 / 90 = 900 * 0.02777... = 25
-                // sharesToAssetsWithdraw:
-                //  assets = assetsTotal * (shares / sharesTotal)
-                //  assets = 90 * 25 / 900 = 90 * 0.02777... = 2.5
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-                d.peek([](SLE& vault, auto&) -> bool {
-                    vault[sfAssetsAvailable] = Number(1);
-                    return true;
-                });
-
-                // Note, this transaction fails first (because of above change
-                // in the open ledger) but then succeeds when the ledger is
-                // closed (because a modification like above is not persistent),
-                // which is why the checks below are expected to pass.
-                auto tx = d.vault.withdraw(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, Number(25, -1))});
-                env(tx, Ter{tecINSUFFICIENT_FUNDS});
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900 - 25));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) ==
-                    STAmount(d.asset, start + Number(25, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) ==
-                    STAmount(d.asset, Number(900 - 25, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) ==
-                    STAmount(d.share, -Number(900 - 25, 0)));
-            }
-
-            {
-                testcase("Scale withdraw with rounding shares up");
-                // assetsToSharesWithdraw:
-                //  shares = sharesTotal * (assets / assetsTotal)
-                //  shares = 875 * 3.75 / 87.5 = 875 * 0.042857... = 37.5
-                // sharesToAssetsWithdraw:
-                //  assets = assetsTotal * (shares / sharesTotal)
-                //  assets = 87.5 * 38 / 875 = 87.5 * 0.043428... = 3.8
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-                auto tx = d.vault.withdraw(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, Number(375, -2))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(875 - 38));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) ==
-                    STAmount(d.asset, start + Number(38, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) ==
-                    STAmount(d.asset, Number(875 - 38, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) ==
-                    STAmount(d.share, -Number(875 - 38, 0)));
-            }
-
-            {
-                testcase("Scale withdraw with rounding shares down");
-                // assetsToSharesWithdraw:
-                //  shares = sharesTotal * (assets / assetsTotal)
-                //  shares = 837 * 3.72 / 83.7 = 837 * 0.04444... = 37.2
-                // sharesToAssetsWithdraw:
-                //  assets = assetsTotal * (shares / sharesTotal)
-                //  assets = 83.7 * 37 / 837 = 83.7 * 0.044205... = 3.7
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-                auto tx = d.vault.withdraw(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, Number(372, -2))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(837 - 37));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) ==
-                    STAmount(d.asset, start + Number(37, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) ==
-                    STAmount(d.asset, Number(837 - 37, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) ==
-                    STAmount(d.share, -Number(837 - 37, 0)));
-            }
-
-            {
-                testcase("Scale withdraw tiny amount");
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-                auto tx = d.vault.withdraw(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, Number(9, -2))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(800 - 1));
-                BEAST_EXPECT(
-                    env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(1, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) ==
-                    STAmount(d.asset, Number(800 - 1, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) ==
-                    STAmount(d.share, -Number(800 - 1, 0)));
-            }
-
-            {
-                testcase("Scale withdraw rest");
-                auto const rest = env.balance(d.vaultAccount, d.assets).number();
-
-                tx = d.vault.withdraw(
-                    {.depositor = d.depositor,
-                     .id = d.keylet.key,
-                     .amount = STAmount(d.asset, rest)});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares).number() == 0);
-                BEAST_EXPECT(env.balance(d.vaultAccount, d.assets).number() == 0);
-                BEAST_EXPECT(env.balance(d.vaultAccount, d.shares).number() == 0);
-            }
-        });
-
-        testCase(18, [&, this](Env& env, Data d) {
-            testcase("Scale clawback overflow");
-
-            {
-                auto tx = d.vault.deposit(
-                    {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
-                env(tx);
-                env.close();
-            }
-
-            {
-                auto tx = d.vault.clawback(
-                    {.issuer = d.issuer,
-                     .id = d.keylet.key,
-                     .holder = d.depositor,
-                     .amount = STAmount(d.asset, Number(10, 0))});
-                env(tx, Ter{tecPATH_DRY});
-                env.close();
-            }
-        });
-
-        testCase(1, [&, this](Env& env, Data d) {
-            // initial setup: deposit 100 IOU, receive 1000 shares
-            auto const start = env.balance(d.depositor, d.assets).number();
-            auto tx = d.vault.deposit(
-                {.depositor = d.depositor,
-                 .id = d.keylet.key,
-                 .amount = STAmount(d.asset, Number(100, 0))});
-            env(tx);
-            env.close();
-            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
-            BEAST_EXPECT(
-                env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(100, 0)));
-            BEAST_EXPECT(
-                env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(100, 0)));
-            BEAST_EXPECT(
-                env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(1000, 0)));
-            {
-                testcase("Scale clawback exact");
-                // assetsToSharesWithdraw:
-                //  shares = sharesTotal * (assets / assetsTotal)
-                //  shares = 1000 * 10 / 100 = 1000 * 0.1 = 100
-                // sharesToAssetsWithdraw:
-                //  assets = assetsTotal * (shares / sharesTotal)
-                //  assets = 100 * 100 / 1000 = 100 * 0.1 = 10
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-                auto tx = d.vault.clawback(
-                    {.issuer = d.issuer,
-                     .id = d.keylet.key,
-                     .holder = d.depositor,
-                     .amount = STAmount(d.asset, Number(10, 0))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900));
-                BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(90, 0)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(900, 0)));
-            }
-
-            {
-                testcase("Scale clawback insignificant amount");
-                auto tx = d.vault.clawback(
-                    {.issuer = d.issuer,
-                     .id = d.keylet.key,
-                     .holder = d.depositor,
-                     .amount = STAmount(d.asset, Number(4, -2))});
-                env(tx, Ter{tecPRECISION_LOSS});
-            }
-
-            {
-                testcase("Scale clawback with rounding assets");
-                // assetsToSharesWithdraw:
-                //  shares = sharesTotal * (assets / assetsTotal)
-                //  shares = 900 * 2.5 / 90 = 900 * 0.02777... = 25
-                // sharesToAssetsWithdraw:
-                //  assets = assetsTotal * (shares / sharesTotal)
-                //  assets = 90 * 25 / 900 = 90 * 0.02777... = 2.5
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-                auto tx = d.vault.clawback(
-                    {.issuer = d.issuer,
-                     .id = d.keylet.key,
-                     .holder = d.depositor,
-                     .amount = STAmount(d.asset, Number(25, -1))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900 - 25));
-                BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) ==
-                    STAmount(d.asset, Number(900 - 25, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) ==
-                    STAmount(d.share, -Number(900 - 25, 0)));
-            }
-
-            {
-                testcase("Scale clawback with rounding shares up");
-                // assetsToSharesWithdraw:
-                //  shares = sharesTotal * (assets / assetsTotal)
-                //  shares = 875 * 3.75 / 87.5 = 875 * 0.042857... = 37.5
-                // sharesToAssetsWithdraw:
-                //  assets = assetsTotal * (shares / sharesTotal)
-                //  assets = 87.5 * 38 / 875 = 87.5 * 0.043428... = 3.8
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-                auto tx = d.vault.clawback(
-                    {.issuer = d.issuer,
-                     .id = d.keylet.key,
-                     .holder = d.depositor,
-                     .amount = STAmount(d.asset, Number(375, -2))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(875 - 38));
-                BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) ==
-                    STAmount(d.asset, Number(875 - 38, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) ==
-                    STAmount(d.share, -Number(875 - 38, 0)));
-            }
-
-            {
-                testcase("Scale clawback with rounding shares down");
-                // assetsToSharesWithdraw:
-                //  shares = sharesTotal * (assets / assetsTotal)
-                //  shares = 837 * 3.72 / 83.7 = 837 * 0.04444... = 37.2
-                // sharesToAssetsWithdraw:
-                //  assets = assetsTotal * (shares / sharesTotal)
-                //  assets = 83.7 * 37 / 837 = 83.7 * 0.044205... = 3.7
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-                auto tx = d.vault.clawback(
-                    {.issuer = d.issuer,
-                     .id = d.keylet.key,
-                     .holder = d.depositor,
-                     .amount = STAmount(d.asset, Number(372, -2))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(837 - 37));
-                BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) ==
-                    STAmount(d.asset, Number(837 - 37, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) ==
-                    STAmount(d.share, -Number(837 - 37, 0)));
-            }
-
-            {
-                testcase("Scale clawback tiny amount");
-
-                auto const start = env.balance(d.depositor, d.assets).number();
-                auto tx = d.vault.clawback(
-                    {.issuer = d.issuer,
-                     .id = d.keylet.key,
-                     .holder = d.depositor,
-                     .amount = STAmount(d.asset, Number(9, -2))});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(800 - 1));
-                BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.assets) ==
-                    STAmount(d.asset, Number(800 - 1, -1)));
-                BEAST_EXPECT(
-                    env.balance(d.vaultAccount, d.shares) ==
-                    STAmount(d.share, -Number(800 - 1, 0)));
-            }
-
-            {
-                testcase("Scale clawback rest");
-                auto const rest = env.balance(d.vaultAccount, d.assets).number();
-                d.peek([](SLE& vault, auto&) -> bool {
-                    vault[sfAssetsAvailable] = Number(5);
-                    return true;
-                });
-
-                // Note, this transaction yields two different results:
-                // * in the open ledger, with AssetsAvailable = 5
-                // * when the ledger is closed with unmodified AssetsAvailable
-                //   because a modification like above is not persistent.
-                tx = d.vault.clawback(
-                    {.issuer = d.issuer,
-                     .id = d.keylet.key,
-                     .holder = d.depositor,
-                     .amount = STAmount(d.asset, rest)});
-                env(tx);
-                env.close();
-                BEAST_EXPECT(env.balance(d.depositor, d.shares).number() == 0);
-                BEAST_EXPECT(env.balance(d.vaultAccount, d.assets).number() == 0);
-                BEAST_EXPECT(env.balance(d.vaultAccount, d.shares).number() == 0);
-            }
-        });
-
-        // Non-1:1 ratio (scale=1, 10:1 shares:assets) with an outstanding loan.
-        // Deposit 100 IOU → 1000 shares. Borrow 40 → assetsAvailable=60.
-        // Clawback 80 IOU → clamped to 60, then share math uses truncation.
-        testCase(1, [&, this](Env& env, Data d) {
-            using namespace loan_broker;
-            using namespace loan;
-
-            testcase("Scale clawback clamped with outstanding loan");
-
-            auto tx = d.vault.deposit(
-                {.depositor = d.depositor,
-                 .id = d.keylet.key,
-                 .amount = STAmount(d.asset, Number(100, 0))});
-            env(tx);
-            env.close();
-            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
-
-            // Create a loan broker backed by this vault
-            auto const brokerKeylet =
-                keylet::loanBroker(d.owner.id(), SeqProxy::rawSequence(env.seq(d.owner)));
-            env(set(d.owner, d.keylet.key));
-            env.close();
-
-            // Borrow 40: assetsAvailable=60, assetsTotal=100
-            env(set(d.depositor, brokerKeylet.key, STAmount(d.asset, Number(40, 0))),
-                loan::kInterestRate(TenthBips32(0)),
-                kGracePeriod(60),
-                kPaymentInterval(120),
-                kPaymentTotal(10),
-                Sig(sfCounterpartySignature, d.owner),
-                Fee(env.current()->fees().base * 2),
-                Ter(tesSUCCESS));
-            env.close();
-
-            {
-                auto const sle = env.le(d.keylet);
-                BEAST_EXPECT(sle->at(sfAssetsAvailable) == STAmount(d.asset, Number(60, 0)));
-                BEAST_EXPECT(sle->at(sfAssetsTotal) == STAmount(d.asset, Number(100, 0)));
-            }
-
-            // Request 80 IOU clawback — clamped to assetsAvailable (60)
-            // With scale=1 (10:1), 60 assets = 600 shares destroyed
-            tx = d.vault.clawback(
-                {.issuer = d.issuer,
-                 .id = d.keylet.key,
-                 .holder = d.depositor,
-                 .amount = STAmount(d.asset, Number(80, 0))});
-            env(tx, Ter(tesSUCCESS));
-            env.close();
-
-            {
-                auto const sle = env.le(d.keylet);
-                BEAST_EXPECT(sle != nullptr);
-                BEAST_EXPECT(sle->at(sfAssetsAvailable) == STAmount(d.asset, Number(0, 0)));
-                BEAST_EXPECT(sle->at(sfAssetsTotal) == STAmount(d.asset, Number(40, 0)));
-
-                // 600 of 1000 shares destroyed, 400 remain
-                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(400));
-            }
-        });
-    }
-
-    void
-    testRPC()
-    {
-        using namespace test::jtx;
-
-        testcase("RPC");
-        Env env{*this, testableAmendments()};
-        Account const owner{"owner"};
-        Account const issuer{"issuer"};
-        Vault const vault{env};
-        env.fund(XRP(1000), issuer, owner);
-        env.close();
-
-        PrettyAsset const asset = issuer["IOU"];
-        env.trust(asset(1000), owner);
-        env(pay(issuer, owner, asset(200)));
-        env.close();
-
-        auto const sequence = env.seq(owner);
-        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-        env(tx);
-        env.close();
-
-        // Set some fields
-        {
-            auto tx1 = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(50)});
-            env(tx1);
-
-            auto tx2 = vault.set({.owner = owner, .id = keylet.key});
-            tx2[sfAssetsMaximum] = asset(1000).number();
-            env(tx2);
-            env.close();
-        }
-
-        auto const sleVault = [&env, keylet = keylet, this]() {
-            auto const vault = env.le(keylet);
-            BEAST_EXPECT(vault != nullptr);
-            return vault;
-        }();
-
-        auto const check = [&, keylet = keylet, sle = sleVault, this](
-                               json::Value const& vault,
-                               json::Value const& issuance = json::ValueType::Null) {
-            BEAST_EXPECT(vault.isObject());
-
-            static constexpr auto kCheckString =
-                [](auto& node, SField const& field, std::string v) -> bool {
-                return node.isMember(field.fieldName) && node[field.fieldName].isString() &&
-                    node[field.fieldName] == v;
-            };
-            static constexpr auto kCheckObject =
-                [](auto& node, SField const& field, json::Value v) -> bool {
-                return node.isMember(field.fieldName) && node[field.fieldName].isObject() &&
-                    node[field.fieldName] == v;
-            };
-            static constexpr auto kCheckInt = [](auto& node, SField const& field, int v) -> bool {
-                return node.isMember(field.fieldName) &&
-                    ((node[field.fieldName].isInt() && node[field.fieldName] == json::Int(v)) ||
-                     (node[field.fieldName].isUInt() && node[field.fieldName] == json::UInt(v)));
-            };
-
-            BEAST_EXPECT(vault["LedgerEntryType"].asString() == "Vault");
-            BEAST_EXPECT(vault[jss::index].asString() == strHex(keylet.key));
-            BEAST_EXPECT(kCheckInt(vault, sfFlags, 0));
-            // Ignore all other standard fields, this test doesn't care
-
-            BEAST_EXPECT(kCheckString(vault, sfAccount, toBase58(sle->at(sfAccount))));
-            BEAST_EXPECT(kCheckObject(vault, sfAsset, toJson(sle->at(sfAsset))));
-            BEAST_EXPECT(kCheckString(vault, sfAssetsAvailable, "50"));
-            BEAST_EXPECT(kCheckString(vault, sfAssetsMaximum, "1000"));
-            BEAST_EXPECT(kCheckString(vault, sfAssetsTotal, "50"));
-            BEAST_EXPECT(!vault.isMember(sfLossUnrealized.getJsonName()));
-
-            auto const strShareID = strHex(sle->at(sfShareMPTID));
-            BEAST_EXPECT(kCheckString(vault, sfShareMPTID, strShareID));
-            BEAST_EXPECT(kCheckString(vault, sfOwner, toBase58(owner.id())));
-            BEAST_EXPECT(kCheckInt(vault, sfSequence, sequence));
-            BEAST_EXPECT(kCheckInt(vault, sfWithdrawalPolicy, kVaultStrategyFirstComeFirstServe));
-
-            if (issuance.isObject())
-            {
-                BEAST_EXPECT(issuance["LedgerEntryType"].asString() == "MPTokenIssuance");
-                BEAST_EXPECT(issuance[jss::mpt_issuance_id].asString() == strShareID);
-                BEAST_EXPECT(kCheckInt(issuance, sfSequence, 1));
-                BEAST_EXPECT(kCheckInt(
-                    issuance, sfFlags, int(lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer)));
-                BEAST_EXPECT(kCheckString(issuance, sfOutstandingAmount, "50000000"));
-            }
-        };
-
-        {
-            testcase("RPC ledger_entry selected by key");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault] = strHex(keylet.key);
-            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
-
-            BEAST_EXPECT(!jvVault[jss::result].isMember(jss::error));
-            BEAST_EXPECT(jvVault[jss::result].isMember(jss::node));
-            check(jvVault[jss::result][jss::node]);
-        }
-
-        {
-            testcase("RPC ledger_entry selected by owner and seq");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault][jss::owner] = owner.human();
-            jvParams[jss::vault][jss::seq] = sequence;
-            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
-
-            BEAST_EXPECT(!jvVault[jss::result].isMember(jss::error));
-            BEAST_EXPECT(jvVault[jss::result].isMember(jss::node));
-            check(jvVault[jss::result][jss::node]);
-        }
-
-        {
-            testcase("RPC ledger_entry cannot find vault by key");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault] = to_string(uint256(42));
-            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
-            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "entryNotFound");
-        }
-
-        {
-            testcase("RPC ledger_entry cannot find vault by owner and seq");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault][jss::owner] = issuer.human();
-            jvParams[jss::vault][jss::seq] = 1'000'000;
-            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
-            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "entryNotFound");
-        }
-
-        {
-            testcase("RPC ledger_entry malformed key");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault] = 42;
-            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
-            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC ledger_entry malformed owner");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault][jss::owner] = 42;
-            jvParams[jss::vault][jss::seq] = sequence;
-            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
-            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedOwner");
-        }
-
-        {
-            testcase("RPC ledger_entry malformed seq");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault][jss::owner] = issuer.human();
-            jvParams[jss::vault][jss::seq] = "foo";
-            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
-            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC ledger_entry negative seq");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault][jss::owner] = issuer.human();
-            jvParams[jss::vault][jss::seq] = -1;
-            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
-            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC ledger_entry oversized seq");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault][jss::owner] = issuer.human();
-            jvParams[jss::vault][jss::seq] = 1e20;
-            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
-            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC ledger_entry bool seq");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault][jss::owner] = issuer.human();
-            jvParams[jss::vault][jss::seq] = true;
-            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
-            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC account_objects");
-
-            json::Value jvParams;
-            jvParams[jss::account] = owner.human();
-            jvParams[jss::type] = jss::vault;
-            auto jv = env.rpc("json", "account_objects", to_string(jvParams))[jss::result];
-
-            BEAST_EXPECT(jv[jss::account_objects].size() == 1);
-            check(jv[jss::account_objects][0u]);
-        }
-
-        {
-            testcase("RPC ledger_data");
-
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::binary] = false;
-            jvParams[jss::type] = jss::vault;
-            json::Value jv = env.rpc("json", "ledger_data", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::state].size() == 1);
-            check(jv[jss::result][jss::state][0u]);
-        }
-
-        {
-            testcase("RPC vault_info command line");
-            json::Value jv = env.rpc("vault_info", strHex(keylet.key), "validated");
-
-            BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
-            BEAST_EXPECT(jv[jss::result].isMember(jss::vault));
-            check(jv[jss::result][jss::vault], jv[jss::result][jss::vault][jss::shares]);
-        }
-
-        {
-            testcase("RPC vault_info json");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault_id] = strHex(keylet.key);
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-
-            BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
-            BEAST_EXPECT(jv[jss::result].isMember(jss::vault));
-            check(jv[jss::result][jss::vault], jv[jss::result][jss::vault][jss::shares]);
-        }
-
-        {
-            testcase("RPC vault_info invalid vault_id");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault_id] = "foobar";
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info json invalid index");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault_id] = 0;
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info json by owner and sequence");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::owner] = owner.human();
-            jvParams[jss::seq] = sequence;
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-
-            BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
-            BEAST_EXPECT(jv[jss::result].isMember(jss::vault));
-            check(jv[jss::result][jss::vault], jv[jss::result][jss::vault][jss::shares]);
-        }
-
-        {
-            testcase("RPC vault_info json malformed sequence");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::owner] = owner.human();
-            jvParams[jss::seq] = "foobar";
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info json invalid sequence");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::owner] = owner.human();
-            jvParams[jss::seq] = 0;
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info json negative sequence");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::owner] = owner.human();
-            jvParams[jss::seq] = -1;
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info json oversized sequence");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::owner] = owner.human();
-            jvParams[jss::seq] = 1e20;
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info json bool sequence");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::owner] = owner.human();
-            jvParams[jss::seq] = true;
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info json malformed owner");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::owner] = "foobar";
-            jvParams[jss::seq] = sequence;
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info json invalid combination only owner");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::owner] = owner.human();
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info json invalid combination only seq");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::seq] = sequence;
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info json invalid combination seq vault_id");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault_id] = strHex(keylet.key);
-            jvParams[jss::seq] = sequence;
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info json invalid combination owner vault_id");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault_id] = strHex(keylet.key);
-            jvParams[jss::owner] = owner.human();
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase(
-                "RPC vault_info json invalid combination owner seq "
-                "vault_id");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            jvParams[jss::vault_id] = strHex(keylet.key);
-            jvParams[jss::seq] = sequence;
-            jvParams[jss::owner] = owner.human();
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info json no input");
-            json::Value jvParams;
-            jvParams[jss::ledger_index] = jss::validated;
-            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info command line invalid index");
-            json::Value jv = env.rpc("vault_info", "foobar", "validated");
-            BEAST_EXPECT(jv[jss::error].asString() == "invalidParams");
-        }
-
-        {
-            testcase("RPC vault_info command line invalid index");
-            json::Value jv = env.rpc("vault_info", "0", "validated");
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "malformedRequest");
-        }
-
-        {
-            testcase("RPC vault_info command line invalid index");
-            json::Value jv = env.rpc("vault_info", strHex(uint256(42)), "validated");
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "entryNotFound");
-        }
-
-        {
-            testcase("RPC vault_info command line invalid ledger");
-            json::Value jv = env.rpc("vault_info", strHex(keylet.key), "0");
-            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "lgrNotFound");
-        }
-    }
-
-    void
-    testVaultClawbackBurnShares()
-    {
-        using namespace test::jtx;
-        using namespace loan_broker;
-        using namespace loan;
-        Env env(*this, beast::Severity::Warning);
-
-        auto const vaultAssetBalance = [&](Keylet const& vaultKeylet) {
-            auto const sleVault = env.le(vaultKeylet);
-            BEAST_EXPECT(sleVault != nullptr);
-
-            return std::make_pair(sleVault->at(sfAssetsAvailable), sleVault->at(sfAssetsTotal));
-        };
-
-        auto const vaultShareBalance = [&](Keylet const& vaultKeylet) {
-            auto const sleVault = env.le(vaultKeylet);
-            BEAST_EXPECT(sleVault != nullptr);
-
-            auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
-            BEAST_EXPECT(sleIssuance != nullptr);
-
-            return sleIssuance->at(sfOutstandingAmount);
-        };
-
-        auto const setupVault = [&](PrettyAsset const& asset,
-                                    Account const& owner,
-                                    Account const& depositor) -> std::pair {
-            Vault const vault{env};
-
-            auto const& [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx, Ter(tesSUCCESS));
-            env.close();
-
-            auto const& vaultSle = env.le(vaultKeylet);
-            BEAST_EXPECT(vaultSle != nullptr);
-
-            Asset const share = vaultSle->at(sfShareMPTID);
-
-            env(vault.deposit(
-                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
-                Ter(tesSUCCESS));
-            env.close();
-
-            auto const& [availablePreDefault, totalPreDefault] = vaultAssetBalance(vaultKeylet);
-            BEAST_EXPECT(availablePreDefault == totalPreDefault);
-            BEAST_EXPECT(availablePreDefault == asset(100).value());
-
-            // attempt to clawback shares while there are assets fails
-            env(vault.clawback(
-                    {.issuer = owner,
-                     .id = vaultKeylet.key,
-                     .holder = depositor,
-                     .amount = share(0).value()}),
-                Ter(tecNO_PERMISSION));
-            env.close();
-
-            auto const& sharesAvailable = vaultShareBalance(vaultKeylet);
-            auto const& brokerKeylet =
-                keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-
-            env(set(owner, vaultKeylet.key));
-            env.close();
-
-            auto const& loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
-
-            // Create a simple Loan for the full amount of Vault assets
-            env(set(depositor, brokerKeylet.key, asset(100).value()),
-                loan::kInterestRate(TenthBips32(0)),
-                kGracePeriod(60),
-                kPaymentInterval(120),
-                kPaymentTotal(10),
-                Sig(sfCounterpartySignature, owner),
-                Fee(env.current()->fees().base * 2),
-                Ter(tesSUCCESS));
-            env.close();
-
-            // attempt to clawback shares while there assetsAvailable == 0 and
-            // assetsTotal > 0 fails
-            env(vault.clawback(
-                    {.issuer = owner,
-                     .id = vaultKeylet.key,
-                     .holder = depositor,
-                     .amount = share(0).value()}),
-                Ter(tecNO_PERMISSION));
-            env.close();
-
-            env.close(std::chrono::seconds{120 + 60});
-
-            env(manage(owner, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
-
-            auto const& [availablePostDefault, totalPostDefault] = vaultAssetBalance(vaultKeylet);
-
-            BEAST_EXPECT(availablePostDefault == totalPostDefault);
-            BEAST_EXPECT(availablePostDefault == asset(0).value());
-            BEAST_EXPECT(vaultShareBalance(vaultKeylet) == sharesAvailable);
-
-            return std::make_pair(vault, vaultKeylet);
-        };
-
-        auto const testCase = [&](PrettyAsset const& asset,
-                                  std::string const& prefix,
-                                  Account const& owner,
-                                  Account const& depositor) {
-            {
-                testcase("VaultClawback (share) - " + prefix + " owner asset clawback fails");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
-                // when asset is XRP or owner is not issuer clawback fail
-                // when owner is issuer precision loss occurs as vault is
-                // empty
-                auto const expectedTer = [&]() {
-                    if (asset.native())
-                        return Ter(temMALFORMED);
-                    if (asset.raw().getIssuer() != owner.id())
-                        return Ter(tecNO_PERMISSION);
-                    return Ter(tecPRECISION_LOSS);
-                }();
-                env(vault.clawback({
-                        .issuer = owner,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                        .amount = asset(100).value(),
-                    }),
-                    expectedTer);
-                env.close();
-            }
-
-            {
-                testcase(
-                    "VaultClawback (share) - " + prefix + " owner incomplete share clawback fails");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
-                auto const& vaultSle = env.le(vaultKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-                Asset const share = vaultSle->at(sfShareMPTID);
-                env(vault.clawback({
-                        .issuer = owner,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                        .amount = share(1).value(),
-                    }),
-                    Ter(tecLIMIT_EXCEEDED));
-                env.close();
-            }
-
-            {
-                testcase(
-                    "VaultClawback (share) - " + prefix +
-                    " owner implicit complete share clawback");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
-                env(vault.clawback({
-                        .issuer = owner,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                    }),
-                    // when owner is issuer implicit clawback fails
-                    asset.native() || asset.raw().getIssuer() != owner.id() ? Ter(tesSUCCESS)
-                                                                            : Ter(tecWRONG_ASSET));
-                env.close();
-            }
-
-            {
-                testcase(
-                    "VaultClawback (share) - " + prefix +
-                    " owner explicit complete share clawback succeeds");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
-                auto const& vaultSle = env.le(vaultKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-                Asset const share = vaultSle->at(sfShareMPTID);
-                env(vault.clawback({
-                        .issuer = owner,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                        .amount = share(vaultShareBalance(vaultKeylet)).value(),
-                    }),
-                    Ter(tesSUCCESS));
-                env.close();
-            }
-            {
-                testcase("VaultClawback (share) - " + prefix + " owner can clawback own shares");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, owner);
-                auto const& vaultSle = env.le(vaultKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-                Asset const share = vaultSle->at(sfShareMPTID);
-                env(vault.clawback({
-                        .issuer = owner,
-                        .id = vaultKeylet.key,
-                        .holder = owner,
-                        .amount = share(vaultShareBalance(vaultKeylet)).value(),
-                    }),
-                    Ter(tesSUCCESS));
-                env.close();
-            }
-
-            {
-                testcase("VaultClawback (share) - " + prefix + " empty vault share clawback fails");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, owner);
-                auto const& vaultSle = env.le(vaultKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-                Asset const share = vaultSle->at(sfShareMPTID);
-                env(vault.clawback({
-                        .issuer = owner,
-                        .id = vaultKeylet.key,
-                        .holder = owner,
-                        .amount = share(vaultShareBalance(vaultKeylet)).value(),
-                    }),
-                    Ter(tesSUCCESS));
-
-                // Now the vault is empty, clawback again fails
-                env(vault.clawback({
-                        .issuer = owner,
-                        .id = vaultKeylet.key,
-                        .holder = owner,
-                        .amount = share(vaultShareBalance(vaultKeylet)).value(),
-                    }),
-                    Ter(tecNO_PERMISSION));
-                env.close();
-            }
-        };
-
-        Account const owner{"alice"};
-        Account const depositor{"bob"};
-        Account const issuer{"issuer"};
-
-        env.fund(XRP(10000), issuer, owner, depositor);
-        env.close();
-
-        // Test XRP
-        PrettyAsset const xrp = xrpIssue();
-        testCase(xrp, "XRP", owner, depositor);
-        testCase(xrp, "XRP (depositor is owner)", owner, owner);
-
-        // Test IOU
-        PrettyAsset const iou = issuer["IOU"];
-        env(fset(issuer, asfAllowTrustLineClawback));
-        env.close();
-
-        env.trust(iou(1000), owner);
-        env.trust(iou(1000), depositor);
-        env(pay(issuer, owner, iou(100)));
-        env(pay(issuer, depositor, iou(100)));
-        env.close();
-        testCase(iou, "IOU", owner, depositor);
-        testCase(iou, "IOU (owner is issuer)", issuer, depositor);
-
-        // Test MPT
-        MPTTester mptt{env, issuer, kMptInitNoFund};
-        mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
-        PrettyAsset const mpt = mptt.issuanceID();
-        mptt.authorize({.account = owner});
-        mptt.authorize({.account = depositor});
-        env(pay(issuer, owner, mpt(1000)));
-        env(pay(issuer, depositor, mpt(1000)));
-        env.close();
-        testCase(mpt, "MPT", owner, depositor);
-        testCase(mpt, "MPT (owner is issuer)", issuer, depositor);
-    }
-
-    void
-    testVaultClawbackAssets()
-    {
-        using namespace test::jtx;
-        using namespace loan_broker;
-        using namespace loan;
-        Env env(*this);
-        env.enableFeature(fixCleanup3_1_3);
-
-        auto const setupVault = [&](PrettyAsset const& asset,
-                                    Account const& owner,
-                                    Account const& depositor,
-                                    Account const& issuer) -> std::pair {
-            Vault const vault{env};
-
-            auto const& [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx, Ter(tesSUCCESS));
-            env.close();
-
-            auto const& vaultSle = env.le(vaultKeylet);
-            BEAST_EXPECT(vaultSle != nullptr);
-            env.memoize(Account("vault", vaultSle->at(sfAccount)));
-            env(vault.deposit(
-                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
-                Ter(tesSUCCESS));
-            env.close();
-
-            return std::make_pair(vault, vaultKeylet);
-        };
-
-        auto const testCase = [&](PrettyAsset const& asset,
-                                  std::string const& prefix,
-                                  Account const& owner,
-                                  Account const& depositor,
-                                  Account const& issuer) {
-            if (asset.native())
-            {
-                testcase("VaultClawback (asset) - " + prefix + " issuer XRP clawback fails");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
-                // If the asset is XRP, clawback with amount fails as malformed
-                // when asset is specified.
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = issuer,
-                        .amount = asset(1).value(),
-                    }),
-                    Ter(temMALFORMED));
-                // When asset is implicit, clawback fails as no permission.
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = issuer,
-                    }),
-                    Ter(tecNO_PERMISSION));
-                return;
-            }
-
-            {
-                testcase(
-                    "VaultClawback (asset) - " + prefix + " clawback for different asset fails");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
-
-                Account const issuer2{"issuer2"};
-                PrettyAsset const asset2 = issuer2["FOO"];
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                        .amount = asset2(1).value(),
-                    }),
-                    Ter(tecWRONG_ASSET));
-            }
-
-            {
-                testcase(
-                    "VaultClawback (asset) - " + prefix +
-                    " ambiguous owner/issuer asset clawback fails");
-                auto [vault, vaultKeylet] = setupVault(asset, issuer, depositor, issuer);
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = issuer,
-                    }),
-                    Ter(tecWRONG_ASSET));
-            }
-
-            {
-                testcase("VaultClawback (asset) - " + prefix + " non-issuer asset clawback fails");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
-
-                env(vault.clawback({
-                        .issuer = owner,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                    }),
-                    Ter(tecNO_PERMISSION));
-
-                env(vault.clawback({
-                        .issuer = owner,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                        .amount = asset(1).value(),
-                    }),
-                    Ter(tecNO_PERMISSION));
-            }
-
-            {
-                testcase("VaultClawback (asset) - " + prefix + " issuer clawback from self fails");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, issuer, issuer);
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = issuer,
-                    }),
-                    Ter(tecNO_PERMISSION));
-            }
-
-            {
-                testcase("VaultClawback (asset) - " + prefix + " issuer share clawback fails");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
-                auto const& vaultSle = env.le(vaultKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-                Asset const share = vaultSle->at(sfShareMPTID);
-
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                        .amount = share(1).value(),
-                    }),
-                    Ter(tecNO_PERMISSION));
-            }
-
-            {
-                testcase(
-                    "VaultClawback (asset) - " + prefix +
-                    " partial issuer asset clawback succeeds");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
-
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                        .amount = asset(1).value(),
-                    }),
-                    Ter(tesSUCCESS));
-            }
-
-            {
-                testcase(
-                    "VaultClawback (asset) - " + prefix + " full issuer asset clawback succeeds");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
-
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                        .amount = asset(100).value(),
-                    }),
-                    Ter(tesSUCCESS));
-            }
-
-            {
-                testcase(
-                    "VaultClawback (asset) - " + prefix +
-                    " implicit full issuer asset clawback succeeds");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
-
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                    }),
-                    Ter(tesSUCCESS));
-            }
-
-            {
-                testcase(
-                    "VaultClawback (asset) - " + prefix +
-                    " zero-amount clawback clamped with outstanding loan");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
-
-                auto const vaultSle = env.le(vaultKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-
-                PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
-
-                // Create a loan broker backed by this vault
-                auto const brokerKeylet =
-                    keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-                env(set(owner, vaultKeylet.key));
-                env.close();
-
-                // Depositor borrows 40 units, reducing assetsAvailable to 60
-                // while assetsTotal stays at 100
-                env(set(depositor, brokerKeylet.key, asset(40).value()),
-                    loan::kInterestRate(TenthBips32(0)),
-                    kGracePeriod(60),
-                    kPaymentInterval(120),
-                    kPaymentTotal(10),
-                    Sig(sfCounterpartySignature, owner),
-                    Fee(env.current()->fees().base * 2),
-                    Ter(tesSUCCESS));
-                env.close();
-
-                {
-                    auto const sle = env.le(vaultKeylet);
-                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
-                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
-                }
-
-                // Zero-amount clawback (= "clawback all") should succeed,
-                // clamped to assetsAvailable (60) rather than the full
-                // share value (100).
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                    }),
-                    Ter(tesSUCCESS));
-                env.close();
-
-                // Only 60 assets clawed back; loan's 40 still outstanding
-                {
-                    auto const sle = env.le(vaultKeylet);
-                    BEAST_EXPECT(sle != nullptr);
-                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
-                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
-
-                    // 60 of 100 shares destroyed (1:1 ratio), 40 remain
-                    auto const sharesAfter = env.balance(depositor, shares);
-                    BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1}));
-                }
-            }
-
-            {
-                testcase(
-                    "VaultClawback (asset) - " + prefix +
-                    " non-zero clawback clamped with outstanding loan");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
-
-                auto const vaultSle = env.le(vaultKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-                PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
-
-                // Create a loan broker backed by this vault
-                auto const brokerKeylet =
-                    keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-                env(set(owner, vaultKeylet.key));
-                env.close();
-
-                // Depositor borrows 40 units
-                env(set(depositor, brokerKeylet.key, asset(40).value()),
-                    loan::kInterestRate(TenthBips32(0)),
-                    kGracePeriod(60),
-                    kPaymentInterval(120),
-                    kPaymentTotal(10),
-                    Sig(sfCounterpartySignature, owner),
-                    Fee(env.current()->fees().base * 2),
-                    Ter(tesSUCCESS));
-                env.close();
-
-                {
-                    auto const sle = env.le(vaultKeylet);
-                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
-                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
-                }
-
-                // Request 100 but only 60 available — clamped to 60
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                        .amount = asset(100).value(),
-                    }),
-                    Ter(tesSUCCESS));
-                env.close();
-
-                {
-                    auto const sle = env.le(vaultKeylet);
-                    BEAST_EXPECT(sle != nullptr);
-                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
-                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
-
-                    // 60 of 100 shares destroyed (1:1 ratio), 40 remain
-                    auto const sharesAfter = env.balance(depositor, shares);
-                    BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1}));
-                }
-            }
-
-            {
-                testcase(
-                    "VaultClawback (asset) - " + prefix +
-                    " partial clawback below available with outstanding loan");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
-
-                auto const vaultSle = env.le(vaultKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-                PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
-
-                // Create a loan broker backed by this vault
-                auto const brokerKeylet =
-                    keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-                env(set(owner, vaultKeylet.key));
-                env.close();
-
-                // Depositor borrows 40 units: assetsAvailable=60, assetsTotal=100
-                env(set(depositor, brokerKeylet.key, asset(40).value()),
-                    loan::kInterestRate(TenthBips32(0)),
-                    kGracePeriod(60),
-                    kPaymentInterval(120),
-                    kPaymentTotal(10),
-                    Sig(sfCounterpartySignature, owner),
-                    Fee(env.current()->fees().base * 2),
-                    Ter(tesSUCCESS));
-                env.close();
-
-                {
-                    auto const sle = env.le(vaultKeylet);
-                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
-                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
-                }
-
-                // Clawback 30 — well under available (60), no clamping needed
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                        .amount = asset(30).value(),
-                    }),
-                    Ter(tesSUCCESS));
-                env.close();
-
-                {
-                    auto const sle = env.le(vaultKeylet);
-                    BEAST_EXPECT(sle != nullptr);
-                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(30).value());
-                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(70).value());
-
-                    // 30 of 100 shares destroyed (1:1 ratio), 70 remain
-                    auto const sharesAfter = env.balance(depositor, shares);
-                    BEAST_EXPECT(sharesAfter == shares(Number{7, sle->at(sfScale) + 1}));
-                }
-            }
-
-            {
-                testcase(
-                    "VaultClawback (asset) - " + prefix +
-                    " clawback exactly equal to available with outstanding loan");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
-
-                auto const vaultSle = env.le(vaultKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-                PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
-
-                auto const brokerKeylet =
-                    keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-                env(set(owner, vaultKeylet.key));
-                env.close();
-
-                // Depositor borrows 40 units: assetsAvailable=60, assetsTotal=100
-                env(set(depositor, brokerKeylet.key, asset(40).value()),
-                    loan::kInterestRate(TenthBips32(0)),
-                    kGracePeriod(60),
-                    kPaymentInterval(120),
-                    kPaymentTotal(10),
-                    Sig(sfCounterpartySignature, owner),
-                    Fee(env.current()->fees().base * 2),
-                    Ter(tesSUCCESS));
-                env.close();
-
-                // Clawback exactly 60 — at the boundary, no clamping needed
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                        .amount = asset(60).value(),
-                    }),
-                    Ter(tesSUCCESS));
-                env.close();
-
-                {
-                    auto const sle = env.le(vaultKeylet);
-                    BEAST_EXPECT(sle != nullptr);
-                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
-                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
-
-                    // 60 of 100 shares destroyed (1:1 ratio), 40 remain
-                    auto const sharesAfter = env.balance(depositor, shares);
-                    BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1}));
-                }
-            }
-
-            {
-                testcase(
-                    "VaultClawback (asset) - " + prefix +
-                    " clawback with zero available (fully borrowed)");
-                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
-
-                auto const vaultSle = env.le(vaultKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-                PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
-
-                auto const brokerKeylet =
-                    keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-                env(set(owner, vaultKeylet.key));
-                env.close();
-
-                // Depositor borrows all 100 units: assetsAvailable=0, assetsTotal=100
-                env(set(depositor, brokerKeylet.key, asset(100).value()),
-                    loan::kInterestRate(TenthBips32(0)),
-                    kGracePeriod(60),
-                    kPaymentInterval(120),
-                    kPaymentTotal(10),
-                    Sig(sfCounterpartySignature, owner),
-                    Fee(env.current()->fees().base * 2),
-                    Ter(tesSUCCESS));
-                env.close();
-
-                {
-                    auto const sle = env.le(vaultKeylet);
-                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
-                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
-                }
-
-                auto const sharesBefore = env.balance(depositor, shares);
-
-                // Zero-amount clawback — nothing available, clamped to 0,
-                // resulting in zero shares destroyed → tecPRECISION_LOSS
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                    }),
-                    Ter(tecPRECISION_LOSS));
-                env.close();
-
-                // Explicit amount clawback — also nothing available
-                env(vault.clawback({
-                        .issuer = issuer,
-                        .id = vaultKeylet.key,
-                        .holder = depositor,
-                        .amount = asset(50).value(),
-                    }),
-                    Ter(tecPRECISION_LOSS));
-                env.close();
-
-                {
-                    // Nothing changed — vault and shares unchanged
-                    auto const sle = env.le(vaultKeylet);
-                    BEAST_EXPECT(sle != nullptr);
-                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
-                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
-                    auto const sharesAfter = env.balance(depositor, shares);
-                    BEAST_EXPECT(sharesAfter == sharesBefore);
-                }
-            }
-        };
-
-        Account const owner{"alice"};
-        Account const depositor{"bob"};
-        Account const issuer{"issuer"};
-
-        env.fund(XRP(10000), issuer, owner, depositor);
-        env.close();
-
-        // Test XRP
-        PrettyAsset const xrp = xrpIssue();
-        testCase(xrp, "XRP", owner, depositor, issuer);
-
-        // Test IOU
-        PrettyAsset const iou = issuer["IOU"];
-        env(fset(issuer, asfAllowTrustLineClawback));
-        env.close();
-        env.trust(iou(2000), owner);
-        env.trust(iou(2000), depositor);
-        env(pay(issuer, owner, iou(2000)));
-        env(pay(issuer, depositor, iou(2000)));
-        env.close();
-        testCase(iou, "IOU", owner, depositor, issuer);
-
-        // Test MPT
-        MPTTester mptt{env, issuer, kMptInitNoFund};
-        mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
-
-        PrettyAsset const mpt = mptt.issuanceID();
-        mptt.authorize({.account = owner});
-        mptt.authorize({.account = depositor});
-        env(pay(issuer, depositor, mpt(2000)));
-        env.close();
-        testCase(mpt, "MPT", owner, depositor, issuer);
-
-        // Test pre-fixCleanup3_1_3 legacy path: zero-amount clawback
-        // returns early without clamping to assetsAvailable.
-        {
-            testcase(
-                "VaultClawback (asset) - IOU pre-fixCleanup3_1_3"
-                " zero-amount clawback unclamped with outstanding loan");
-
-            env.disableFeature(fixCleanup3_1_3);
-
-            auto [vault, vaultKeylet] = setupVault(iou, owner, depositor, issuer);
-
-            auto const vaultSle = env.le(vaultKeylet);
-            BEAST_EXPECT(vaultSle != nullptr);
-            if (!vaultSle)
-                return;
-
-            PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
-
-            // Create a loan broker backed by this vault
-            auto const brokerKeylet =
-                keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-            env(set(owner, vaultKeylet.key));
-            env.close();
-
-            // Depositor borrows 40 units, reducing assetsAvailable to 60
-            // while assetsTotal stays at 100
-            env(set(depositor, brokerKeylet.key, iou(40).value()),
-                loan::kInterestRate(TenthBips32(0)),
-                kGracePeriod(60),
-                kPaymentInterval(120),
-                kPaymentTotal(10),
-                Sig(sfCounterpartySignature, owner),
-                Fee(env.current()->fees().base * 2),
-                Ter(tesSUCCESS));
-            env.close();
-
-            {
-                auto const sle = env.le(vaultKeylet);
-                BEAST_EXPECT(sle->at(sfAssetsAvailable) == iou(60).value());
-                BEAST_EXPECT(sle->at(sfAssetsTotal) == iou(100).value());
-            }
-
-            auto const sharesBefore = env.balance(depositor, shares);
-
-            // Legacy: zero-amount clawback tries to recover the full
-            // share value (100) without clamping to assetsAvailable (60).
-            // This causes the vault balance to go negative, triggering
-            // the sanity check in doApply → tefINTERNAL.
-            env(vault.clawback({
-                    .issuer = issuer,
-                    .id = vaultKeylet.key,
-                    .holder = depositor,
-                }),
-                Ter(tefINTERNAL));
-            env.close();
-
-            {
-                // Transaction rolled back — vault and shares unchanged
-                auto const sle = env.le(vaultKeylet);
-                BEAST_EXPECT(sle != nullptr);
-                BEAST_EXPECT(sle->at(sfAssetsAvailable) == iou(60).value());
-                BEAST_EXPECT(sle->at(sfAssetsTotal) == iou(100).value());
-                auto const sharesAfter = env.balance(depositor, shares);
-                BEAST_EXPECT(sharesAfter == sharesBefore);
-            }
-
-            env.enableFeature(fixCleanup3_1_3);
-        }
-    }
-
-    void
-    testAssetsMaximum()
-    {
-        testcase("Assets Maximum");
-
-        using namespace test::jtx;
-
-        Env env{*this, testableAmendments()};
-        Account const owner{"owner"};
-        Account const issuer{"issuer"};
-
-        Vault const vault{env};
-        env.fund(XRP(1'000'000), issuer, owner);
-        env.close();
-
-        auto const maxInt64 = std::to_string(std::numeric_limits::max());
-        BEAST_EXPECT(maxInt64 == "9223372036854775807");
-
-        auto const maxInt64Plus1 = std::to_string(
-            static_cast(std::numeric_limits::max()) + 1);
-        BEAST_EXPECT(maxInt64Plus1 == "9223372036854775808");
-
-        // Naming things is hard
-        auto const maxInt64Plus2 = std::to_string(
-            static_cast(std::numeric_limits::max()) + 2);
-        BEAST_EXPECT(maxInt64Plus2 == "9223372036854775809");
-
-        auto const initialXRP = to_string(kInitialXrp);
-        BEAST_EXPECT(initialXRP == "100000000000000000");
-
-        auto const initialXRPPlus1 = to_string(kInitialXrp + 1);
-        BEAST_EXPECT(initialXRPPlus1 == "100000000000000001");
-
-        {
-            testcase("Assets Maximum: XRP");
-
-            PrettyAsset const xrpAsset = xrpIssue();
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
-            tx[sfData] = "4D65746144617461";
-
-            tx[sfAssetsMaximum] = maxInt64;
-            env(tx, Ter(tefEXCEPTION));
-            env.close();
-
-            tx[sfAssetsMaximum] = initialXRPPlus1;
-            env(tx, Ter(tefEXCEPTION));
-            env.close();
-
-            tx[sfAssetsMaximum] = initialXRP;
-            env(tx);
-            env.close();
-
-            // There are several parse failures expected in this function, so just disable it once.
-            env.setParseFailureExpected(true);
-            try
-            {
-                tx[sfAssetsMaximum] = maxInt64Plus1;
-                env(tx, Ter(tefEXCEPTION));
-                env.close();
-                // should throw in parser
-                fail();
-            }
-            catch (std::exception const& e)
-            {
-                BEAST_EXPECT(
-                    std::string(e.what()) ==
-                    "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
-            }
-
-            try
-            {
-                tx[sfAssetsMaximum] = maxInt64Plus2;
-                env(tx, Ter(tefEXCEPTION));
-                // should throw in parser
-                fail();
-            }
-            catch (std::exception const& e)
-            {
-                BEAST_EXPECT(
-                    std::string(e.what()) ==
-                    "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
-            }
-
-            auto const newKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-            try
-            {
-                auto const insertAt = maxInt64Plus2.size() - 3;
-                auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." +
-                    maxInt64Plus2.substr(insertAt);  // (max int64+2) / 1000
-                BEAST_EXPECT(decimalTest == "9223372036854775.809");
-                tx[sfAssetsMaximum] = decimalTest;
-                env(tx);
-                // should throw in parser
-                fail();
-            }
-            catch (std::exception const& e)
-            {
-                BEAST_EXPECT(
-                    std::string(e.what()) ==
-                    "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
-            }
-
-            auto const vaultSle = env.le(newKeylet);
-            BEAST_EXPECT(!vaultSle);
-        }
-
-        {
-            testcase("Assets Maximum: MPT");
-
-            PrettyAsset const mptAsset = [&]() {
-                MPTTester mptt{env, issuer, kMptInitNoFund};
-                mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
-                env.close();
-                PrettyAsset const mptAsset = mptt["MPT"];
-                mptt.authorize({.account = owner});
-                env.close();
-                return mptAsset;
-            }();
-
-            env(pay(issuer, owner, mptAsset(100'000)));
-            env.close();
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = mptAsset});
-            tx[sfData] = "4D65746144617461";
-
-            tx[sfAssetsMaximum] = maxInt64;
-            env(tx);
-            env.close();
-
-            tx[sfAssetsMaximum] = initialXRPPlus1;
-            env(tx);
-            env.close();
-
-            tx[sfAssetsMaximum] = initialXRP;
-            env(tx);
-            env.close();
-
-            try
-            {
-                tx[sfAssetsMaximum] = maxInt64Plus2;
-                env(tx, Ter(tefEXCEPTION));
-                // should throw in parser
-                fail();
-            }
-            catch (std::exception const& e)
-            {
-                BEAST_EXPECT(
-                    std::string(e.what()) ==
-                    "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
-            }
-
-            auto const newKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-            try
-            {
-                auto const insertAt = maxInt64Plus2.size() - 1;
-                auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." +
-                    maxInt64Plus2.substr(insertAt);  // (max int64+2) / 10
-                BEAST_EXPECT(decimalTest == "922337203685477580.9");
-                tx[sfAssetsMaximum] = decimalTest;
-                env(tx);
-                // should throw in parser
-                fail();
-            }
-            catch (std::exception const& e)
-            {
-                BEAST_EXPECT(
-                    std::string(e.what()) ==
-                    "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
-            }
-
-            auto const vaultSle = env.le(newKeylet);
-            BEAST_EXPECT(!vaultSle);
-        }
-
-        {
-            testcase("Assets Maximum: IOU");
-
-            // Almost anything goes with IOUs
-            PrettyAsset const iouAsset = issuer["IOU"];
-            env.trust(iouAsset(1000), owner);
-            env(pay(issuer, owner, iouAsset(200)));
-            env.close();
-
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = iouAsset});
-            tx[sfData] = "4D65746144617461";
-
-            tx[sfAssetsMaximum] = maxInt64;
-            env(tx);
-            env.close();
-
-            tx[sfAssetsMaximum] = initialXRPPlus1;
-            env(tx);
-            env.close();
-
-            tx[sfAssetsMaximum] = initialXRP;
-            env(tx);
-            env.close();
-
-            // Since several tests are expected to have parser failures, leave this flag set for the
-            // remainder of this function.
-            env.setParseFailureExpected(true);
-            try
-            {
-                tx[sfAssetsMaximum] = maxInt64Plus2;
-                env(tx);
-                // should throw in parser
-                fail();
-            }
-            catch (std::exception const& e)
-            {
-                BEAST_EXPECT(
-                    std::string(e.what()) ==
-                    "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
-            }
-
-            tx[sfAssetsMaximum] = "1000000000000000e80";
-            env.close();
-
-            tx[sfAssetsMaximum] = "1000000000000000e-96";
-            env.close();
-
-            // These values will be rounded to 15 significant digits
-            {
-                auto const newKeylet =
-                    keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-                try
-                {
-                    auto const insertAt = maxInt64Plus2.size() - 1;
-                    auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." +
-                        maxInt64Plus2.substr(insertAt);  // (max int64+2) / 10
-                    BEAST_EXPECT(decimalTest == "922337203685477580.9");
-                    tx[sfAssetsMaximum] = decimalTest;
-                    env(tx);
-                    // should throw in parser
-                    fail();
-                }
-                catch (std::exception const& e)
-                {
-                    BEAST_EXPECT(
-                        std::string(e.what()) ==
-                        "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
-                }
-
-                auto const vaultSle = env.le(newKeylet);
-                BEAST_EXPECT(!vaultSle);
-            }
-            {
-                tx[sfAssetsMaximum] = "9223372036854775807e40";  // max int64 * 10^40
-                auto const newKeylet =
-                    keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-                env(tx);
-                env.close();
-
-                auto const vaultSle = env.le(newKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-
-                BEAST_EXPECT(
-                    (vaultSle->at(sfAssetsMaximum) ==
-                     Number{9223372036854776, 43, Number::Normalized{}}));
-            }
-            {
-                tx[sfAssetsMaximum] = "9223372036854775807e-40";  // max int64 * 10^-40
-                auto const newKeylet =
-                    keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-                env(tx);
-                env.close();
-
-                auto const vaultSle = env.le(newKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-
-                BEAST_EXPECT(
-                    (vaultSle->at(sfAssetsMaximum) ==
-                     Number{9223372036854776, -37, Number::Normalized{}}));
-            }
-            {
-                tx[sfAssetsMaximum] = "9223372036854775807e-100";  // max int64 * 10^-100
-                auto const newKeylet =
-                    keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-                env(tx);
-                env.close();
-
-                // Field 'AssetsMaximum' may not be explicitly set to default.
-                auto const vaultSle = env.le(newKeylet);
-                if (!BEAST_EXPECT(vaultSle))
-                    return;
-
-                BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == kNumZero);
-            }
-
-            // What _can't_ IOUs do?
-            // 1. Exceed maximum exponent / offset
-            tx[sfAssetsMaximum] = "1000000000000000e81";
-            env(tx, Ter(tefEXCEPTION));
-            env.close();
-
-            // 2. Mantissa larger than uint64 max
-            try
-            {
-                auto const g = env.getParseFailureGuard(true);
-                tx[sfAssetsMaximum] = "18446744073709551617e5";  // uint64 max + 1
-                env(tx);
-                BEAST_EXPECTS(false, "Expected parse_error for mantissa larger than uint64 max");
-            }
-            catch (ParseError const& e)
-            {
-                using namespace std::string_literals;
-                BEAST_EXPECT(
-                    e.what() == "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."s);
-            }
-        }
-    }
-
-    void
-    testVaultEscrowedMPT()
-    {
-        using namespace test::jtx;
-        using namespace std::literals;
-
-        // Verify vault deposit/withdraw/clawback respect sfLockedAmount.
-        // When MPT tokens are escrowed, sfMPTAmount is reduced and
-        // sfLockedAmount is increased. Vault operations go through
-        // accountSend/accountHolds which read sfMPTAmount, so escrowed
-        // tokens are naturally excluded.
-
-        {
-            testcase("Vault deposit fails when MPT asset is escrowed");
-
-            Env env{*this, testableAmendments()};
-            auto const baseFee = env.current()->fees().base;
-            Account const owner{"owner"};
-            Account const depositor{"depositor"};
-            Account const issuer{"issuer"};
-            Account const bob{"bob"};
-
-            env.fund(XRP(10000), issuer, owner, depositor, bob);
-            env.close();
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create(
-                {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow});
-            mptt.authorize({.account = owner});
-            mptt.authorize({.account = depositor});
-            mptt.authorize({.account = bob});
-            PrettyAsset const asset = mptt.issuanceID();
-            env(pay(issuer, depositor, asset(100)));
-            env.close();
-
-            // Escrow 60 of 100 MPT tokens: sfMPTAmount drops to 40
-            auto const escrowSeq = env.seq(depositor);
-            env(escrow::create(depositor, bob, asset(60)),
-                escrow::kCondition(escrow::kCb1),
-                escrow::kFinishTime(env.now() + 1s),
-                Fee(baseFee * 150),
-                Ter(tesSUCCESS));
-            env.close();
-
-            Vault const vault{env};
-            auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx, Ter(tesSUCCESS));
-            env.close();
-
-            // Deposit 100 should fail — only 40 spendable
-            env(vault.deposit(
-                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
-                Ter(tecINSUFFICIENT_FUNDS));
-            env.close();
-
-            // Deposit 40 (the unlocked balance) should succeed
-            env(vault.deposit({.depositor = depositor, .id = vaultKeylet.key, .amount = asset(40)}),
-                Ter(tesSUCCESS));
-            env.close();
-
-            {
-                auto const sle = env.le(vaultKeylet);
-                BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
-            }
-
-            // Clean up escrow
-            env(escrow::finish(bob, depositor, escrowSeq),
-                escrow::kCondition(escrow::kCb1),
-                escrow::kFulfillment(escrow::kFb1),
-                Fee(baseFee * 150),
-                Ter(tesSUCCESS));
-            env.close();
-        }
-
-        {
-            testcase("Vault withdraw respects escrowed shares");
-
-            Env env{*this, testableAmendments()};
-            auto const baseFee = env.current()->fees().base;
-            Account const owner{"owner"};
-            Account const depositor{"depositor"};
-            Account const issuer{"issuer"};
-            Account const bob{"bob"};
-
-            env.fund(XRP(10000), issuer, owner, depositor, bob);
-            env.close();
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create(
-                {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow});
-            mptt.authorize({.account = owner});
-            mptt.authorize({.account = depositor});
-            PrettyAsset const asset = mptt.issuanceID();
-            env(pay(issuer, depositor, asset(100)));
-            env.close();
-
-            Vault const vault{env};
-            auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx, Ter(tesSUCCESS));
-            env.close();
-
-            // Deposit 100 → get shares
-            env(vault.deposit(
-                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
-                Ter(tesSUCCESS));
-            env.close();
-
-            auto const vaultSle = env.le(vaultKeylet);
-            if (!BEAST_EXPECT(vaultSle))
-                return;
-            env.memoize(Account("vault", vaultSle->at(sfAccount)));
-            PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
-
-            // Authorize bob for share MPT so he can receive escrowed shares
-            auto const shareMPTID = vaultSle->at(sfShareMPTID);
-            {
-                json::Value jv;
-                jv[jss::Account] = bob.human();
-                jv[sfMPTokenIssuanceID] = to_string(shareMPTID);
-                jv[jss::TransactionType] = jss::MPTokenAuthorize;
-                env(jv, Ter(tesSUCCESS));
-                env.close();
-            }
-
-            // Escrow 60% of shares
-            auto const escrowAmount = shares(Number{6, vaultSle->at(sfScale) + 1});
-            env(escrow::create(depositor, bob, escrowAmount),
-                escrow::kCondition(escrow::kCb1),
-                escrow::kFinishTime(env.now() + 1s),
-                Fee(baseFee * 150),
-                Ter(tesSUCCESS));
-            env.close();
-
-            // Withdraw all 100 should fail — only 40% of shares are unlocked
-            env(vault.withdraw(
-                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
-                Ter(tecINSUFFICIENT_FUNDS));
-            env.close();
-
-            // Withdraw 40 (matching unlocked shares) should succeed
-            env(vault.withdraw(
-                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(40)}),
-                Ter(tesSUCCESS));
-            env.close();
-
-            {
-                auto const sle = env.le(vaultKeylet);
-                BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(60).value());
-            }
-        }
-
-        {
-            testcase("Vault clawback only recovers unlocked shares");
-
-            Env env{*this, testableAmendments() | fixCleanup3_1_3};
-            auto const baseFee = env.current()->fees().base;
-            Account const owner{"owner"};
-            Account const depositor{"depositor"};
-            Account const issuer{"issuer"};
-            Account const bob{"bob"};
-
-            env.fund(XRP(10000), issuer, owner, depositor, bob);
-            env.close();
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create(
-                {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow});
-            mptt.authorize({.account = owner});
-            mptt.authorize({.account = depositor});
-            PrettyAsset const asset = mptt.issuanceID();
-            env(pay(issuer, depositor, asset(100)));
-            env.close();
-
-            Vault const vault{env};
-            auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx, Ter(tesSUCCESS));
-            env.close();
-
-            // Deposit 100 → get shares
-            env(vault.deposit(
-                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
-                Ter(tesSUCCESS));
-            env.close();
-
-            auto const vaultSle = env.le(vaultKeylet);
-            if (!BEAST_EXPECT(vaultSle))
-                return;
-            env.memoize(Account("vault", vaultSle->at(sfAccount)));
-            PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
-
-            // Authorize bob for share MPT so he can receive escrowed shares
-            auto const shareMPTID = vaultSle->at(sfShareMPTID);
-            {
-                json::Value jv;
-                jv[jss::Account] = bob.human();
-                jv[sfMPTokenIssuanceID] = to_string(shareMPTID);
-                jv[jss::TransactionType] = jss::MPTokenAuthorize;
-                env(jv, Ter(tesSUCCESS));
-                env.close();
-            }
-
-            // Escrow 60% of shares
-            auto const escrowAmount = shares(Number{6, vaultSle->at(sfScale) + 1});
-            env(escrow::create(depositor, bob, escrowAmount),
-                escrow::kCondition(escrow::kCb1),
-                escrow::kFinishTime(env.now() + 1s),
-                Fee(baseFee * 150),
-                Ter(tesSUCCESS));
-            env.close();
-
-            // Zero-amount clawback ("all") — should only recover assets
-            // corresponding to unlocked shares (40%)
-            env(vault.clawback({
-                    .issuer = issuer,
-                    .id = vaultKeylet.key,
-                    .holder = depositor,
-                }),
-                Ter(tesSUCCESS));
-            env.close();
-
-            {
-                auto const sle = env.le(vaultKeylet);
-                BEAST_EXPECT(sle != nullptr);
-                // Only 40 of 100 assets recovered (matching 40% unlocked shares)
-                BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(60).value());
-                BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
-
-                // Depositor's unlocked shares are now 0
-                auto const sharesAfter = env.balance(depositor, shares);
-                BEAST_EXPECT(sharesAfter == shares(0));
-            }
-        }
-    }
-
-    // Reproduction: canWithdraw IOU limit check bypassed when
-    // withdrawal amount is specified in shares (MPT) rather than in assets.
-    void
-    testBug6LimitBypassWithShares()
-    {
-        using namespace test::jtx;
-        testcase("Bug6 - limit bypass with share-denominated withdrawal");
-
-        auto const allAmendments = testableAmendments() | featureSingleAssetVault;
-
-        for (auto const& features : {allAmendments, allAmendments - fixCleanup3_1_3})
-        {
-            bool const withFix = features[fixCleanup3_1_3];
-
-            Env env{*this, features};
-            Account const owner{"owner"};
-            Account const issuer{"issuer"};
-            Account const depositor{"depositor"};
-            Account const charlie{"charlie"};
-            Vault const vault{env};
-
-            env.fund(XRP(1000), issuer, owner, depositor, charlie);
-            env(fset(issuer, asfAllowTrustLineClawback));
-            env.close();
-
-            PrettyAsset const asset = issuer["IOU"];
-            env.trust(asset(1000), owner);
-            env.trust(asset(1000), depositor);
-            env(pay(issuer, owner, asset(200)));
-            env(pay(issuer, depositor, asset(200)));
-            env.close();
-
-            // Charlie gets a LOW trustline limit of 5
-            env.trust(asset(5), charlie);
-            env.close();
-
-            auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            auto const depositTx =
-                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
-            env(depositTx);
-            env.close();
-
-            // Get the share MPT info
-            auto const vaultSle = env.le(keylet);
-            if (!BEAST_EXPECT(vaultSle))
-                return;
-            auto const mptIssuanceID = vaultSle->at(sfShareMPTID);
-            MPTIssue const shares(mptIssuanceID);
-            PrettyAsset const share(shares);
-
-            // CONTROL: Withdraw 10 IOU (asset-denominated) to charlie.
-            // Charlie's limit is 5, so this should be rejected with tecNO_LINE
-            // regardless of the amendment.
-            {
-                auto withdrawTx =
-                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
-                withdrawTx[sfDestination] = charlie.human();
-                env(withdrawTx, Ter{tecNO_LINE});
-                env.close();
-            }
-            auto const charlieBalanceBefore = env.balance(charlie, asset.raw().get());
-
-            // Withdraw the equivalent amount in shares to charlie.
-            // Post-fix: rejected (tecNO_LINE) because the share amount is
-            //   converted to assets and the trustline limit is checked.
-            // Pre-fix: succeeds (tesSUCCESS) because the limit check was
-            //   skipped for share-denominated withdrawals.
-            {
-                auto withdrawTx = vault.withdraw(
-                    {.depositor = depositor,
-                     .id = keylet.key,
-                     .amount = STAmount(share, 10'000'000)});
-                withdrawTx[sfDestination] = charlie.human();
-                env(withdrawTx, Ter{withFix ? TER{tecNO_LINE} : TER{tesSUCCESS}});
-                env.close();
-
-                auto const charlieBalanceAfter = env.balance(charlie, asset.raw().get());
-                if (withFix)
-                {
-                    // Post-fix: charlie's balance is unchanged — the withdrawal
-                    // was correctly rejected despite being share-denominated.
-                    BEAST_EXPECT(charlieBalanceAfter == charlieBalanceBefore);
-                }
-                else
-                {
-                    // Pre-fix: charlie received the assets, bypassing the
-                    // trustline limit.
-                    BEAST_EXPECT(charlieBalanceAfter > charlieBalanceBefore);
-                }
-            }
-        }
-    }
-
-    void
-    testRemoveEmptyHoldingLockedAmount()
-    {
-        testcase("removeEmptyHolding deletes MPToken with sfLockedAmount");
-        using namespace test::jtx;
-        using namespace std::literals;
-
-        auto const amendments = testableAmendments();
-        auto runTest = [&](FeatureBitset f) {
-            Env env{*this, f};
-            auto const baseFee = env.current()->fees().base;
-
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            Account const depositor{"depositor"};
-            Account const bob{"bob"};
-
-            env.fund(XRP(100000), issuer, owner, depositor, bob);
-            env.close();
-
-            Vault const vault{env};
-
-            // Create an MPT asset for the vault
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
-            PrettyAsset const asset = mptt.issuanceID();
-            mptt.authorize({.account = owner});
-            mptt.authorize({.account = depositor});
-            env(pay(issuer, depositor, asset(1000)));
-            env.close();
-
-            // Create vault
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            auto const vaultSle = env.le(keylet);
-            BEAST_EXPECT(vaultSle != nullptr);
-            auto const shareMptID = vaultSle->at(sfShareMPTID);
-            MPTIssue const shareIssue{shareMptID};
-
-            // Depositor deposits 1000 asset units into vault, receiving shares
-            env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)}));
-            env.close();
-
-            // Check depositor has shares
-            {
-                auto const sleMpt = env.le(keylet::mptoken(shareMptID, depositor));
-                BEAST_EXPECT(sleMpt != nullptr);
-                BEAST_EXPECT(sleMpt->at(sfMPTAmount) == 1000);
-            }
-
-            // Escrow 500 of those shares
-            env(escrow::create(depositor, bob, STAmount{shareIssue, 500}),
-                escrow::kCondition(escrow::kCb1),
-                escrow::kFinishTime(env.now() + 1s),
-                Fee(baseFee * 150),
-                Ter(tesSUCCESS));
-            env.close();
-
-            // Verify: sfMPTAmount=500, sfLockedAmount=500
-            {
-                auto const sleMpt = env.le(keylet::mptoken(shareMptID, depositor));
-                BEAST_EXPECT(sleMpt != nullptr);
-                BEAST_EXPECT(sleMpt->at(sfLockedAmount) == 500);
-                BEAST_EXPECT(sleMpt->at(sfMPTAmount) == 500);
-            }
-
-            // Withdraw remaining spendable shares — triggers removeEmptyHolding
-            env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(500)}),
-                Ter(tesSUCCESS));
-            env.close();
-
-            auto const sleMptAfter = env.le(keylet::mptoken(shareMptID, depositor));
-            if (!f[fixCleanup3_1_3])
-            {
-                // Without the fix, removeEmptyHolding deletes the MPToken
-                // even though sfLockedAmount > 0, leaving the escrow's locked
-                // amount untracked.
-                BEAST_EXPECT(sleMptAfter == nullptr);
-            }
-            else
-            {
-                // With the fix, MPToken must still exist with sfLockedAmount > 0
-                // and sfMPTAmount == 0 (all spendable shares withdrawn).
-                BEAST_EXPECT(sleMptAfter != nullptr);
-                if (sleMptAfter)
-                {
-                    BEAST_EXPECT(sleMptAfter->at(sfLockedAmount) == 500);
-                    BEAST_EXPECT(sleMptAfter->at(sfMPTAmount) == 0);
-                }
-            }
-        };
-
-        runTest(amendments - fixCleanup3_1_3);
-        runTest(amendments);
-    }
-
-    void
-    testRemoveEmptyHoldingConfidentialBalances()
-    {
-        testcase("removeEmptyHolding keeps MPToken with confidential balances");
-        using namespace test::jtx;
-
-        Env env{*this, testableAmendments()};
-
-        Account const issuer{"issuer"};
-        Account const holder{"holder"};
-        MPTTester mpt{env, issuer, {.holders = {holder}}};
-        mpt.create({.authorize = MPTCreate::allHolders});
-
-        auto const tokenKeylet = keylet::mptoken(mpt.issuanceID(), holder.id());
-        auto const encryptedBalanceFields = {
-            &sfConfidentialBalanceInbox,
-            &sfConfidentialBalanceSpending,
-            &sfIssuerEncryptedBalance,
-            &sfAuditorEncryptedBalance};
-
-        env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
-            for (auto const field : encryptedBalanceFields)
-            {
-                Sandbox sb(&view, TapNone);
-                auto const token = sb.peek(tokenKeylet);
-                if (!BEAST_EXPECT(token))
-                    return false;
-
-                token->setFieldVL(*field, gMakeZeroBuffer(kEcGamalEncryptedTotalLength));
-                sb.update(token);
-
-                auto const dummyTx = *env.jt(noop(holder)).stx;
-                BEAST_EXPECT(
-                    removeEmptyHolding({sb, dummyTx}, holder.id(), MPTIssue(mpt.issuanceID()), j) ==
-                    tecHAS_OBLIGATIONS);
-                BEAST_EXPECT(sb.peek(tokenKeylet) != nullptr);
-            }
-            return true;
-        });
-    }
-
-    // -----------------------------------------------------------------------
-    // Helpers and tests: sole-shareholder / stuck-depositor (XLS-0065 +
-    // fixCleanup3_2_0). The vault-level withdraw behavior is tested here;
-    // the loan-protocol setup is incidental.
-    // -----------------------------------------------------------------------
-
-    FeatureBitset const all_{test::jtx::testableAmendments()};
-    std::string const iouCurrency_{"IOU"};
-
-    // design doc:
-    //     AssetsAvailable ≈ 3,333.50
-    //     AssetsTotal     ≈ 6,666.50  (3,333.50 cash + 3,333 receivable)
-    //     LossUnrealized  =  3,333
-    //     OutstandingShares = sharesLender   (5e9 at IOU scale 1e6)
-    struct StuckDepositorFixture
-    {
-        test::jtx::Account issuer{"issuer"};
-        test::jtx::Account lender{"lender"};
-        test::jtx::Account bob{"bob"};
-        test::jtx::Account borrower{"borrower"};
-        std::optional asset;
-        std::optional vaultKeylet;
-        uint256 brokerID;
-        std::optional loanKeylet;
-        MPTID shareAsset;
-        std::uint64_t sharesLender = 0;
-    };
-
-    static constexpr std::int64_t kStuckFunding = 1'000'000;
-    static constexpr std::int64_t kStuckDepositorIOU = 1'000'000;
-    static constexpr std::int64_t kStuckBorrowerIOU = 100'000;
-    static constexpr std::int64_t kStuckDeposit = 5'000;
-    static constexpr std::int64_t kStuckPrincipal = 3'333;
-    static constexpr std::uint32_t kStuckPayInterval = 600;
-    static constexpr std::uint32_t kStuckPayTotal = 2;
-
-    [[nodiscard]] StuckDepositorFixture
-    setupStuckDepositor(test::jtx::Env& env)
-    {
-        using namespace test::jtx;
-
-        StuckDepositorFixture f;
-        f.asset = f.issuer[iouCurrency_];
-
-        env.fund(XRP(kStuckFunding), f.issuer, f.lender, f.bob, f.borrower);
-        env.close();
-
-        env(trust(f.lender, (*f.asset)(10'000'000)));
-        env(trust(f.bob, (*f.asset)(10'000'000)));
-        env(trust(f.borrower, (*f.asset)(10'000'000)));
-        env.close();
-
-        env(pay(f.issuer, f.lender, (*f.asset)(kStuckDepositorIOU)));
-        env(pay(f.issuer, f.bob, (*f.asset)(kStuckDepositorIOU)));
-        env(pay(f.issuer, f.borrower, (*f.asset)(kStuckBorrowerIOU)));
-        env.close();
-
-        // Vault: Lender creates and seeds it; Bob matches the deposit for a
-        // clean 50/50 split.
-        Vault const v{env};
-        auto [createTx, vaultKeylet] = v.create({.owner = f.lender, .asset = *f.asset});
-        env(createTx);
-        env.close();
-        if (!BEAST_EXPECT(env.le(vaultKeylet)))
-            return f;
-        f.vaultKeylet = vaultKeylet;
-
-        env(v.deposit({
-                .depositor = f.lender,
-                .id = vaultKeylet.key,
-                .amount = (*f.asset)(kStuckDeposit),
-            }),
-            Ter(tesSUCCESS));
-        env(v.deposit({
-                .depositor = f.bob,
-                .id = vaultKeylet.key,
-                .amount = (*f.asset)(kStuckDeposit),
-            }),
-            Ter(tesSUCCESS));
-        env.close();
-
-        // Loan broker: no cover, no management fee, debt cap 10x principal.
-        f.brokerID =
-            keylet::loanBroker(f.lender.id(), SeqProxy::rawSequence(env.seq(f.lender))).key;
-        {
-            using namespace loan_broker;
-            env(set(f.lender, vaultKeylet.key),
-                kDebtMaximum((*f.asset)(kStuckPrincipal * 10).value()));
-            env.close();
-        }
-
-        // Loan: 3,333 USD principal, impaired immediately.
-        auto const sleBroker = env.le(keylet::loanBroker(f.brokerID));
-        if (!BEAST_EXPECT(sleBroker))
-            return f;
-        f.loanKeylet =
-            keylet::loan(f.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
-
-        {
-            using namespace loan;
-            env(set(f.borrower, f.brokerID, kStuckPrincipal),
-                Sig(sfCounterpartySignature, f.lender),
-                kPaymentTotal(kStuckPayTotal),
-                kPaymentInterval(kStuckPayInterval),
-                Fee(env.current()->fees().base * 2),
-                Ter(tesSUCCESS));
-            env.close();
-            env(manage(f.lender, f.loanKeylet->key, tfLoanImpair), Ter(tesSUCCESS));
-            env.close();
-        }
-
-        auto const vaultSle = env.le(vaultKeylet);
-        if (!BEAST_EXPECT(vaultSle))
-            return f;
-        BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == (*f.asset)(kStuckPrincipal).value());
-
-        f.shareAsset = vaultSle->at(sfShareMPTID);
-
-        auto const tokenBob = env.le(keylet::mptoken(f.shareAsset, f.bob.id()));
-        if (!BEAST_EXPECT(tokenBob))
-            return f;
-        std::uint64_t const sharesBob = tokenBob->getFieldU64(sfMPTAmount);
-
-        // Bob (non-sole) exits at the discounted rate. Always succeeds.
-        STAmount const bobShareAmt{MPTIssue{f.shareAsset}, Number(sharesBob)};
-        env(v.withdraw({
-                .depositor = f.bob,
-                .id = vaultKeylet.key,
-                .amount = bobShareAmt,
-            }),
-            Ter(tesSUCCESS));
-        env.close();
-
-        auto const tokenLender = env.le(keylet::mptoken(f.shareAsset, f.lender.id()));
-        if (!BEAST_EXPECT(tokenLender))
-            return f;
-        f.sharesLender = tokenLender->getFieldU64(sfMPTAmount);
-
-        auto const sleIssuance = env.le(keylet::mptokenIssuance(f.shareAsset));
-        if (!BEAST_EXPECT(sleIssuance))
-            return f;
-        BEAST_EXPECT(sleIssuance->getFieldU64(sfOutstandingAmount) == f.sharesLender);
-
-        auto const vaultAfterBob = env.le(vaultKeylet);
-        if (!BEAST_EXPECT(vaultAfterBob))
-            return f;
-        // After Bob's exit: loss is unchanged (3,333 receivable), and the
-        // gap between assetsTotal and assetsAvailable equals exactly that
-        // receivable.
-        BEAST_EXPECT(vaultAfterBob->at(sfLossUnrealized) == (*f.asset)(kStuckPrincipal).value());
-        BEAST_EXPECT(
-            vaultAfterBob->at(sfAssetsTotal) - vaultAfterBob->at(sfAssetsAvailable) ==
-            vaultAfterBob->at(sfLossUnrealized));
-
-        return f;
-    }
-
-    // Reproduces the worked example from the XLS-0065 design doc. The sole
-    // remaining shareholder asks (via fixed-asset input) for the vault's
-    // entire AssetsAvailable. Pre-fix this fails with the zero-sized-vault
-    // invariant violation. Post-fix the full-price exchange rate burns
-    // only a portion of the shares, the depositor receives all of
-    // AssetsAvailable, and the residual shares remain backed by the
-    // impaired-loan receivable.
-    void
-    testWithdrawSoleShareholderFixedAssetExit(FeatureBitset features)
-    {
-        using namespace test::jtx;
-
-        bool const withFix = features[fixCleanup3_2_0];
-        testcase(
-            std::string{"Vault withdraw: sole shareholder exits via "
-                        "fixed-asset amount with impaired loan"} +
-            (withFix ? " (fixCleanup3_2_0)" : " (pre-fix)"));
-
-        std::string logs;
-        Env env(*this, features, std::make_unique(&logs));
-        auto const f = setupStuckDepositor(env);
-        if (!f.vaultKeylet || !f.asset || f.sharesLender == 0)
-        {
-            BEAST_EXPECT(false);
-            return;
-        }
-        Keylet const& vaultKey = *f.vaultKeylet;
-        PrettyAsset const& asset = *f.asset;
-
-        auto const vaultBefore = env.le(vaultKey);
-        if (!BEAST_EXPECT(vaultBefore))
-            return;
-        Number const availableBefore = vaultBefore->at(sfAssetsAvailable);
-        Number const totalBefore = vaultBefore->at(sfAssetsTotal);
-        Number const lossBefore = vaultBefore->at(sfLossUnrealized);
-
-        STAmount const lenderBalanceBefore = env.balance(f.lender, asset);
-
-        // The requested amount differs between feature regimes because
-        // the two regimes are testing different behaviors:
-        //
-        // - Pre-fix: request the full AssetsAvailable (3,333.50). Under
-        //   the discounted formula this would burn every outstanding
-        //   share, hitting the zero-sized-vault invariant. The
-        //   transaction is rejected with tecINVARIANT_FAILED — the
-        //   stuck-depositor bug.
-        //
-        // - Post-fix: request a strictly smaller amount (1,000 USD).
-        //   The full-price formula burns only ~30% of the outstanding
-        //   shares; the vault retains the rest, backed by the impaired
-        //   receivable. Requesting *exactly* AssetsAvailable post-fix
-        //   would currently fail with tecINSUFFICIENT_FUNDS due to the
-        //   round-to-nearest used by assetsToSharesWithdraw (the
-        //   recomputed payout can overshoot the request by a few ULPs).
-        //   The "force payout to AssetsAvailable" branch in doApply
-        //   only triggers when every share is burned, which is covered
-        //   by the loan-repayment test.
-        STAmount const requestAssets =
-            withFix ? asset(1000).value() : STAmount{asset.raw(), availableBefore};
-        Vault const v{env};
-        env(v.withdraw({
-                .depositor = f.lender,
-                .id = vaultKey.key,
-                .amount = requestAssets,
-            }),
-            Ter(withFix ? TER{tesSUCCESS} : TER{tecINVARIANT_FAILED}));
-        env.close();
-
-        auto const vaultAfter = env.le(vaultKey);
-        if (!BEAST_EXPECT(vaultAfter))
-            return;
-        auto const issuanceAfter = env.le(keylet::mptokenIssuance(f.shareAsset));
-        if (!BEAST_EXPECT(issuanceAfter))
-            return;
-
-        std::uint64_t const sharesAfter = issuanceAfter->getFieldU64(sfOutstandingAmount);
-        Number const availableAfter = vaultAfter->at(sfAssetsAvailable);
-        Number const totalAfter = vaultAfter->at(sfAssetsTotal);
-        Number const lossAfter = vaultAfter->at(sfLossUnrealized);
-
-        if (!withFix)
-        {
-            // Pre-fix: rejected — vault state unchanged.
-            BEAST_EXPECT(sharesAfter == f.sharesLender);
-            BEAST_EXPECT(availableAfter == availableBefore);
-            BEAST_EXPECT(totalAfter == totalBefore);
-            BEAST_EXPECT(lossAfter == lossBefore);
-            return;
-        }
-
-        // Post-fix exact-value derivation (fixture: sharesLender=5e9,
-        // totalBefore=6666.5, request=1000):
-        //   sharesRedeemed = round(sharesLender * request / totalBefore)
-        //                  = round(750,018,750.469) = 750,018,750
-        //   received       = totalBefore * sharesRedeemed / sharesLender
-        //                  = 999.999999375  (slightly under 1,000 due to
-        //                                    integer-share rounding)
-        constexpr std::uint64_t kExpectedSharesRedeemed = 750'018'750;
-        Number const expectedReceived =
-            totalBefore * Number(kExpectedSharesRedeemed) / Number(f.sharesLender);
-
-        BEAST_EXPECT(sharesAfter == f.sharesLender - kExpectedSharesRedeemed);
-
-        // LossUnrealized is unchanged: the loan-protocol side is untouched.
-        BEAST_EXPECT(lossAfter == lossBefore);
-
-        // The entire (total - available) gap is the impaired receivable,
-        // i.e. equal to lossUnrealized.
-        BEAST_EXPECT(totalAfter - availableAfter == lossAfter);
-
-        STAmount const lenderBalanceAfter = env.balance(f.lender, asset);
-        Number const received{lenderBalanceAfter - lenderBalanceBefore};
-        BEAST_EXPECT(received == expectedReceived);
-
-        // Conservation: assets removed from the vault equal what the
-        // depositor received.
-        BEAST_EXPECT(totalBefore - totalAfter == received);
-        BEAST_EXPECT(availableBefore - availableAfter == received);
-    }
-
-    // Sole shareholder attempts to burn ALL outstanding shares via
-    // fixed-shares input while the vault still holds an impaired
-    // receivable. Pre-fix this fails with the zero-sized-vault invariant
-    // violation. Post-fix the full-price rate causes assetsWithdrawn to
-    // equal assetsTotal, which exceeds assetsAvailable, so the transaction
-    // is rejected with tecINSUFFICIENT_FUNDS.
-    void
-    testWithdrawSoleShareholderFullSharesRejected(FeatureBitset features)
-    {
-        using namespace test::jtx;
-
-        bool const withFix = features[fixCleanup3_2_0];
-        testcase(
-            std::string{"Vault withdraw: sole shareholder full-shares "
-                        "burn is rejected while loss outstanding"} +
-            (withFix ? " (fixCleanup3_2_0)" : " (pre-fix)"));
-
-        std::string logs;
-        Env env(*this, features, std::make_unique(&logs));
-        auto const f = setupStuckDepositor(env);
-        if (!f.vaultKeylet || f.sharesLender == 0)
-        {
-            BEAST_EXPECT(false);
-            return;
-        }
-        Keylet const& vaultKey = *f.vaultKeylet;
-
-        auto const vaultBefore = env.le(vaultKey);
-        if (!BEAST_EXPECT(vaultBefore))
-            return;
-        Number const availableBefore = vaultBefore->at(sfAssetsAvailable);
-        Number const totalBefore = vaultBefore->at(sfAssetsTotal);
-        Number const lossBefore = vaultBefore->at(sfLossUnrealized);
-
-        // Fixed-shares input: ask for ALL outstanding shares.
-        STAmount const shareAmt{MPTIssue{f.shareAsset}, Number(f.sharesLender)};
-        Vault const v{env};
-        env(v.withdraw({
-                .depositor = f.lender,
-                .id = vaultKey.key,
-                .amount = shareAmt,
-            }),
-            Ter(withFix ? TER{tecINSUFFICIENT_FUNDS} : TER{tecINVARIANT_FAILED}));
-        env.close();
-
-        // Either way the transaction was rejected; vault state unchanged.
-        auto const vaultAfter = env.le(vaultKey);
-        if (!BEAST_EXPECT(vaultAfter))
-            return;
-        auto const issuanceAfter = env.le(keylet::mptokenIssuance(f.shareAsset));
-        if (!BEAST_EXPECT(issuanceAfter))
-            return;
-        BEAST_EXPECT(issuanceAfter->getFieldU64(sfOutstandingAmount) == f.sharesLender);
-        BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == availableBefore);
-        BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore);
-        BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
-    }
-
-    // Post-fix end-to-end resolution: after the sole-shareholder partial
-    // exit, the loan is repaid in full. With unrealized loss cleared and
-    // all assets back as cash, the depositor can burn all remaining
-    // shares and fully exit the vault. The final withdrawal hits the
-    // "force payout to assetsAvailable" branch in doApply.
-    void
-    testWithdrawSoleShareholderLoanRepaymentExit()
-    {
-        using namespace test::jtx;
-        using namespace loan;
-
-        testcase(
-            "Vault withdraw: sole shareholder fully exits after impaired "
-            "loan is repaid (fixCleanup3_2_0)");
-
-        Env env(*this, all_ | fixCleanup3_2_0);
-        auto const f = setupStuckDepositor(env);
-        if (!f.vaultKeylet || !f.asset || !f.loanKeylet || f.sharesLender == 0)
-        {
-            BEAST_EXPECT(false);
-            return;
-        }
-        Keylet const& vaultKey = *f.vaultKeylet;
-        Keylet const& loanKey = *f.loanKeylet;
-        PrettyAsset const& asset = *f.asset;
-
-        Vault const v{env};
-
-        // Sole-shareholder partial exit (see comment in
-        // testWithdrawSoleShareholderFixedAssetExit for why we request
-        // less than full AssetsAvailable).
-        {
-            STAmount const requestAssets = asset(1000).value();
-            env(v.withdraw({
-                    .depositor = f.lender,
-                    .id = vaultKey.key,
-                    .amount = requestAssets,
-                }),
-                Ter(tesSUCCESS));
-            env.close();
-        }
-
-        // Confirm the "dormant-but-alive" state from the design doc. The
-        // partial exit burned exactly 750,018,750 shares (see derivation
-        // in testWithdrawSoleShareholderFixedAssetExit).
-        auto const tokenAfterExit = env.le(keylet::mptoken(f.shareAsset, f.lender.id()));
-        if (!BEAST_EXPECT(tokenAfterExit))
-            return;
-        std::uint64_t const retainedShares = tokenAfterExit->getFieldU64(sfMPTAmount);
-        BEAST_EXPECT(retainedShares == f.sharesLender - 750'018'750);
-
-        // Borrower repays the loan in full (pays more than the outstanding
-        // total; the loan transactor caps the receivable).
-        env(pay(f.borrower, loanKey.key, asset(kStuckPrincipal * 2)), Ter(tesSUCCESS));
-        env.close();
-
-        auto const vaultAfterRepay = env.le(vaultKey);
-        if (!BEAST_EXPECT(vaultAfterRepay))
-            return;
-        // Repayment converts the 3,333 receivable back to cash; assetsTotal
-        // is unchanged but assetsAvailable jumps by exactly the same amount,
-        // and lossUnrealized clears to zero.
-        BEAST_EXPECT(vaultAfterRepay->at(sfLossUnrealized) == beast::kZero);
-        BEAST_EXPECT(vaultAfterRepay->at(sfAssetsAvailable) == vaultAfterRepay->at(sfAssetsTotal));
-
-        STAmount const lenderBalanceBeforeFinal = env.balance(f.lender, asset);
-        Number const availableBeforeFinal = vaultAfterRepay->at(sfAssetsAvailable);
-
-        // Burn all remaining shares — the clean-state preconditions of
-        // the "final withdrawal" guard are now satisfied.
-        STAmount const allShares{MPTIssue{f.shareAsset}, Number(retainedShares)};
-        env(v.withdraw({
-                .depositor = f.lender,
-                .id = vaultKey.key,
-                .amount = allShares,
-            }),
-            Ter(tesSUCCESS));
-        env.close();
-
-        auto const vaultFinal = env.le(vaultKey);
-        if (!BEAST_EXPECT(vaultFinal))
-            return;
-        auto const issuanceFinal = env.le(keylet::mptokenIssuance(f.shareAsset));
-        if (!BEAST_EXPECT(issuanceFinal))
-            return;
-
-        // Zero-sized vault invariant satisfied: 0 shares, 0 assets.
-        BEAST_EXPECT(issuanceFinal->getFieldU64(sfOutstandingAmount) == 0);
-        BEAST_EXPECT(vaultFinal->at(sfAssetsTotal) == beast::kZero);
-        BEAST_EXPECT(vaultFinal->at(sfAssetsAvailable) == beast::kZero);
-        BEAST_EXPECT(vaultFinal->at(sfLossUnrealized) == beast::kZero);
-
-        // The final payout equals exactly the AssetsAvailable that
-        // existed before the call (the "force payout" branch).
-        STAmount const lenderBalanceAfter = env.balance(f.lender, asset);
-        Number const finalReceived{lenderBalanceAfter - lenderBalanceBeforeFinal};
-        BEAST_EXPECT(finalReceived == availableBeforeFinal);
-    }
-
-    // Clean-state regression: with no impaired loan, a sole shareholder
-    // burning all their shares fully empties the vault under both the
-    // pre-fix and post-fix code paths. Confirms the new logic doesn't
-    // break the existing happy-path close-out.
-    void
-    testWithdrawSoleShareholderCleanVaultUnaffected(FeatureBitset features)
-    {
-        using namespace test::jtx;
-
-        bool const withFix = features[fixCleanup3_2_0];
-        testcase(
-            std::string{"Vault withdraw: sole shareholder clean-state "
-                        "close-out unchanged"} +
-            (withFix ? " (fixCleanup3_2_0)" : " (pre-fix)"));
-
-        Env env(*this, features);
-
-        Account const issuer{"issuer"};
-        Account const lender{"lender"};
-
-        env.fund(XRP(kStuckFunding), issuer, lender);
-        env.close();
-
-        PrettyAsset const asset = issuer[iouCurrency_];
-        env(trust(lender, asset(10'000'000)));
-        env.close();
-        env(pay(issuer, lender, asset(kStuckDepositorIOU)));
-        env.close();
-
-        // Sole shareholder of a clean vault — no loan broker needed.
-        Vault const v{env};
-        auto [createTx, vaultKeylet] = v.create({.owner = lender, .asset = asset});
-        env(createTx);
-        env.close();
-
-        env(v.deposit({
-                .depositor = lender,
-                .id = vaultKeylet.key,
-                .amount = asset(kStuckDeposit),
-            }),
-            Ter(tesSUCCESS));
-        env.close();
-
-        auto const vaultBefore = env.le(vaultKeylet);
-        if (!BEAST_EXPECT(vaultBefore))
-            return;
-        auto const shareAsset = vaultBefore->at(sfShareMPTID);
-        auto const tokenLender = env.le(keylet::mptoken(shareAsset, lender.id()));
-        if (!BEAST_EXPECT(tokenLender))
-            return;
-        std::uint64_t const sharesLender = tokenLender->getFieldU64(sfMPTAmount);
-
-        // Sole shareholder, no loans, no loss. Burn everything.
-        STAmount const allShares{MPTIssue{shareAsset}, Number(sharesLender)};
-        env(v.withdraw({
-                .depositor = lender,
-                .id = vaultKeylet.key,
-                .amount = allShares,
-            }),
-            Ter(tesSUCCESS));
-        env.close();
-
-        auto const vaultFinal = env.le(vaultKeylet);
-        if (!BEAST_EXPECT(vaultFinal))
-            return;
-        auto const issuanceFinal = env.le(keylet::mptokenIssuance(shareAsset));
-        if (!BEAST_EXPECT(issuanceFinal))
-            return;
-        BEAST_EXPECT(issuanceFinal->getFieldU64(sfOutstandingAmount) == 0);
-        BEAST_EXPECT(vaultFinal->at(sfAssetsTotal) == beast::kZero);
-        BEAST_EXPECT(vaultFinal->at(sfAssetsAvailable) == beast::kZero);
-        BEAST_EXPECT(vaultFinal->at(sfLossUnrealized) == beast::kZero);
-
-        // (Pre-fix path takes the regular code path; post-fix path enters
-        // the new final-withdrawal guard, which forces payout to exactly
-        // assetsAvailable. Either way the result is identical for a clean
-        // vault.)
-        (void)withFix;
-    }
-
-    // Sole shareholder in an impaired vault redeems a *partial* count of
-    // shares via fixed-shares input. Pre-fix the discounted formula is
-    // used; post-fix the full-price formula is used (waiveUnrealizedLoss
-    // = Yes). The relative payout therefore differs, and post-fix the
-    // depositor recovers proportionally more of the residual cash for
-    // the shares burned. In both cases the vault is left in a valid
-    // (non-empty) state.
-    void
-    testWithdrawSoleShareholderPartialFixedSharesUsesFullPrice()
-    {
-        using namespace test::jtx;
-
-        testcase(
-            "Vault withdraw: sole-shareholder partial fixed-shares uses "
-            "full-price rate (fixCleanup3_2_0)");
-
-        Env env(*this, all_ | fixCleanup3_2_0);
-        auto const f = setupStuckDepositor(env);
-        if (!f.vaultKeylet || !f.asset || f.sharesLender == 0)
-        {
-            BEAST_EXPECT(false);
-            return;
-        }
-        Keylet const& vaultKey = *f.vaultKeylet;
-        PrettyAsset const& asset = *f.asset;
-
-        auto const vaultBefore = env.le(vaultKey);
-        if (!BEAST_EXPECT(vaultBefore))
-            return;
-        Number const totalBefore = vaultBefore->at(sfAssetsTotal);
-        Number const availableBefore = vaultBefore->at(sfAssetsAvailable);
-        Number const lossBefore = vaultBefore->at(sfLossUnrealized);
-
-        // Burn exactly half of the outstanding shares.
-        std::uint64_t const halfShares = f.sharesLender / 2;
-        STAmount const halfAmt{MPTIssue{f.shareAsset}, Number(halfShares)};
-
-        STAmount const lenderBalanceBefore = env.balance(f.lender, asset);
-
-        Vault const v{env};
-        env(v.withdraw({
-                .depositor = f.lender,
-                .id = vaultKey.key,
-                .amount = halfAmt,
-            }),
-            Ter(tesSUCCESS));
-        env.close();
-
-        // Expected payout under the full-price formula:
-        //   assets = totalBefore * halfShares / sharesLender
-        // which (with halfShares == sharesLender/2) is roughly
-        //   totalBefore / 2.
-        STAmount const lenderBalanceAfter = env.balance(f.lender, asset);
-        Number const received{lenderBalanceAfter - lenderBalanceBefore};
-        Number const expected = totalBefore * Number(halfShares) / Number(f.sharesLender);
-        BEAST_EXPECT(received == expected);
-
-        // The full-price payout exceeds the discounted formula by exactly
-        // lossBefore * halfShares / sharesLender — that's the whole point
-        // of the waive.
-        Number const discounted =
-            (totalBefore - lossBefore) * Number(halfShares) / Number(f.sharesLender);
-        Number const expectedDelta = lossBefore * Number(halfShares) / Number(f.sharesLender);
-        BEAST_EXPECT(received - discounted == expectedDelta);
-
-        auto const vaultAfter = env.le(vaultKey);
-        if (!BEAST_EXPECT(vaultAfter))
-            return;
-        auto const issuanceAfter = env.le(keylet::mptokenIssuance(f.shareAsset));
-        if (!BEAST_EXPECT(issuanceAfter))
-            return;
-
-        // Vault remains valid: half the shares remain, lossUnrealized
-        // is untouched, and the entire (total - available) gap is still
-        // the impaired receivable.
-        BEAST_EXPECT(
-            issuanceAfter->getFieldU64(sfOutstandingAmount) == f.sharesLender - halfShares);
-        BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore - received);
-        BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
-        BEAST_EXPECT(
-            vaultAfter->at(sfAssetsTotal) - vaultAfter->at(sfAssetsAvailable) ==
-            vaultAfter->at(sfLossUnrealized));
-
-        // Conservation: vault delta matches the depositor's gain.
-        BEAST_EXPECT(totalBefore - vaultAfter->at(sfAssetsTotal) == received);
-        BEAST_EXPECT(availableBefore - vaultAfter->at(sfAssetsAvailable) == received);
-    }
-
-    // Bug: DeltaInfo::makeDelta uses max(scale(after), scale(before)) for the
-    // sfAssetsTotal and sfAssetsAvailable deltas, and visitEntry applies the
-    // same max() for the vault pseudo-account RippleState.  When
-    // sfAssetsTotal sits exactly at 1e16 (IOU exponent 1, ULP = 10) and a
-    // withdrawal of 5 USD brings it to 9.999...995e15 (IOU exponent 0,
-    // ULP = 1), all three computations pick the anterior coarser scale 1.
-    // roundToAsset(-5, scale=1) collapses to 0, so the invariant check
-    // vaultPseudoDeltaAssets >= kZero fires even though the state change is
-    // valid and fully consistent at IOU precision.
-    //
-    // Fix (fixCleanup3_2_0): finalize compares the vault pseudo-account and
-    // sfAssetsTotal/Available deltas directly in Number space, bypassing
-    // scale-coarsened rounding.
-    void
-    testBugMakeDeltaAnteriorScale()
-    {
-        using namespace test::jtx;
-
-        auto runScenario = [this](FeatureBitset features, TER expected) {
-            std::string logs;
-            Env env(*this, features, std::make_unique(&logs));
-
-            Account const issuer{"issuer"};
-            Account const alice{"alice"};
-
-            env.fund(XRP(100'000), issuer, alice);
-            env.close();
-            env(fset(issuer, asfDefaultRipple));
-            env.close();
-
-            PrettyAsset const usd{issuer["USD"]};
-            // Trust limit of 2e16, fund exactly 1e16 so deposit lands at the
-            // IOU scale-1 boundary (exponent 1, ULP = 10).
-            STAmount const fundAndDeposit{usd.raw(), Number{1, 16}};
-
-            env(trust(alice, STAmount{usd.raw(), 2, 16}));
-            env.close();
-            env(pay(issuer, alice, fundAndDeposit));
-            env.close();
-
-            Vault const vault{env};
-            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
-            vaultTx[sfScale] = 0;
-            env(vaultTx);
-            env.close();
-
-            // sfAssetsTotal = sfAssetsAvailable = 1e16 (exponent 1, ULP = 10).
-            env(vault.deposit(
-                {.depositor = alice, .id = vaultKeylet.key, .amount = fundAndDeposit}));
-            env.close();
-
-            // Withdraw 5 USD: -5 is sub-ULP at the anterior scale (ULP = 10)
-            // but exact at the posterior scale (ULP = 1).  The state change is
-            // consistent; only the invariant's scale selection is wrong.
-            env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(5)}),
-                Ter(expected));
-            env.close();
-        };
-
-        {
-            testcase(
-                "bug: VaultWithdraw across IOU scale boundary fires invariant "
-                "(pre-fixCleanup3_2_0)");
-            runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
-        }
-        {
-            testcase(
-                "bug: VaultWithdraw across IOU scale boundary succeeds "
-                "(post-fixCleanup3_2_0)");
-            runScenario(testableAmendments(), tesSUCCESS);
-        }
-    }
-
-    // Bug: DeltaInfo::makeDelta uses max(scale(after), scale(before)) for
-    // sfAssetsTotal/Available deltas.  This is symmetric to
-    // testBugMakeDeltaAnteriorScale but in the opposite direction: a deposit
-    // pushes assetsTotal from just below 1e16 (IOU exponent 0, ULP = 1) to just
-    // above it (exponent 1, ULP = 10).  makeDelta picks the coarser *posterior*
-    // scale 1.  The trust line balance rounds from atEdge + 2 = 10,000,000,000,000,001
-    // → 1e16, so the pseudo-account delta is only +1 in IOU space.
-    // roundToAsset(+1, scale=1) = 0 fires "deposit must increase vault balance"
-    // even though the state change is consistent at every precision boundary.
-    //
-    // Fix (fixCleanup3_2_0): computeVaultMinScale uses the posterior Number-space
-    // scale of sfAssetsTotal (which retains the full value 10,000,000,000,000,001,
-    // exponent 0), giving minScale = 0.  roundToAsset(+1, scale=0) = 1 > 0 and
-    // the invariant passes.  However the transactor's own precision guard fires
-    // first (bob pays 2 USD, vault receives only 1 due to IOU rounding), so the
-    // post-amendment result is tecPRECISION_LOSS rather than tesSUCCESS —
-    // the depositor is protected from silently losing 1 USD to rounding.
-    void
-    testBugMakeDeltaPosteriorScale()
-    {
-        using namespace test::jtx;
-
-        auto runScenario = [this](FeatureBitset features, TER expected) {
-            std::string logs;
-            Env env(*this, features, std::make_unique(&logs));
-
-            Account const issuer{"issuer"};
-            Account const alice{"alice"};
-            Account const bob{"bob"};
-
-            env.fund(XRP(100'000), issuer, alice, bob);
-            env.close();
-            env(fset(issuer, asfDefaultRipple));
-            env.close();
-
-            PrettyAsset const usd{issuer["USD"]};
-            // atEdge is the largest IOU value with exponent 0 (ULP = 1).
-            // A deposit of 2 USD brings assetsTotal to 10,000,000,000,000,001
-            // in Number space, crossing the 1e16 boundary in IOU space.
-            STAmount const atEdge{usd.raw(), Number{9'999'999'999'999'999LL}};
-
-            env(trust(alice, STAmount{usd.raw(), 2, 16}));
-            env(trust(bob, usd(100)));
-            env.close();
-            env(pay(issuer, alice, atEdge));
-            env(pay(issuer, bob, usd(2)));
-            env.close();
-
-            Vault const vault{env};
-            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
-            vaultTx[sfScale] = 0;
-            env(vaultTx);
-            env.close();
-
-            // sfAssetsTotal = sfAssetsAvailable = atEdge (exponent 0, ULP = 1)
-            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = atEdge}));
-            env.close();
-
-            // Deposit 2 USD: +2 is sub-ULP at the posterior IOU scale (ULP = 10)
-            // but exact at the Number scale retained by sfAssetsTotal.
-            env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = usd(2)}),
-                Ter(expected));
-            env.close();
-        };
-
-        {
-            testcase(
-                "bug: VaultDeposit across IOU scale boundary fires invariant "
-                "(pre-fixCleanup3_2_0)");
-            runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
-        }
-        {
-            testcase(
-                "bug: VaultDeposit across IOU scale boundary succeeds "
-                "(post-fixCleanup3_2_0)");
-            runScenario(testableAmendments(), tecPRECISION_LOSS);
-        }
-    }
-
-    // Bug: ValidVault::visitEntry computes destinationDelta.scale as
-    // max(before_exponent, after_exponent) for RippleState entries.  When a
-    // withdrawal credits a destination whose IOU balance sits just below a
-    // power-of-10 boundary (atEdge = 9'999'999'999'999'999), the post-credit
-    // STAmount rounds up one exponent (exponent 0 → 1), making
-    // destinationDelta.scale = 1.  The invariant then calls
-    // roundToAsset(+2 USD, scale=1) = 0 and incorrectly fires
-    // "withdrawal must increase destination balance".
-    //
-    // Fix (fixCleanup3_2_0): finalize compares destination delta directly in
-    // Number space, bypassing scale-coarsened rounding.  The transaction
-    // itself succeeds because the effective IOU credit is non-trivial at
-    // Number precision even though the STAmount exponent shifted.
-    void
-    testVaultWithdrawCanonicalizeToZero()
-    {
-        using namespace test::jtx;
-
-        enum class DestKind : bool { ThirdParty = false, Self = true };
-
-        auto runScenario = [this](FeatureBitset features, DestKind destKind, TER expected) {
-            std::string logs;
-            Env env(*this, features, std::make_unique(&logs));
-
-            Account const issuer{"issuer"};
-            Account const alice{"alice"};
-            Account const bob{"bob"};
-
-            env.fund(XRP(100'000), issuer, alice, bob);
-            env.close();
-            env(fset(issuer, asfDefaultRipple));
-            env.close();
-
-            PrettyAsset const usd{issuer["USD"]};
-            STAmount const aliceLimit{usd.raw(), 2, 16};
-            STAmount const bobLimit{usd.raw(), 2, 16};
-            STAmount const atEdge{usd.raw(), Number{9'999'999'999'999'999LL}};
-
-            env(trust(alice, aliceLimit));
-            if (destKind == DestKind::ThirdParty)
-                env(trust(bob, bobLimit));
-            env.close();
-
-            env(pay(issuer, alice, usd(1'000)));
-            if (destKind == DestKind::ThirdParty)
-                env(pay(issuer, bob, atEdge));
-            env.close();
-
-            Vault const vault{env};
-            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
-            vaultTx[sfScale] = 0;
-            env(vaultTx);
-            env.close();
-
-            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1'000)}));
-            env.close();
-
-            // For the self-destination case, push alice's own trust line to
-            // the IOU edge so the next withdraw inflow crosses the boundary.
-            if (destKind == DestKind::Self)
-            {
-                env(pay(issuer, alice, atEdge));
-                env.close();
-            }
-
-            auto tx = vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(2)});
-            if (destKind == DestKind::ThirdParty)
-                tx[sfDestination] = bob.human();
-            env(tx, Ter(expected));
-            env.close();
-        };
-
-        {
-            testcase(
-                "bug: VaultWithdraw to third-party at IOU edge fires invariant "
-                "(pre-fixCleanup3_2_0)");
-            runScenario(
-                testableAmendments() - fixCleanup3_2_0, DestKind::ThirdParty, tecINVARIANT_FAILED);
-        }
-        {
-            testcase(
-                "bug: VaultWithdraw to third-party at IOU edge succeeds "
-                "(post-fixCleanup3_2_0)");
-            runScenario(testableAmendments(), DestKind::ThirdParty, tesSUCCESS);
-        }
-        {
-            testcase(
-                "bug: VaultWithdraw to self at IOU edge fires invariant "
-                "(pre-fixCleanup3_2_0)");
-            runScenario(
-                testableAmendments() - fixCleanup3_2_0, DestKind::Self, tecINVARIANT_FAILED);
-        }
-        {
-            testcase(
-                "bug: VaultWithdraw to self at IOU edge succeeds "
-                "(post-fixCleanup3_2_0)");
-            runScenario(testableAmendments(), DestKind::Self, tesSUCCESS);
-        }
-    }
-
-    // Bug: the equality check (vault outflow == destination inflow) was
-    // skipped whenever the destination delta rounded to zero at localMinScale,
-    // including cases where the vault outflow rounded to a non-zero value and
-    // a representable amount of value was genuinely destroyed.
-    //
-    // Scenario: Bob's IOU balance sits 5 units below the 10^16 STAmount
-    // precision boundary (atEdge2 = 9,999,999,999,999,995).  A withdrawal of
-    // 6 USD shifts his balance across that boundary: the exponent increments
-    // (0 → 1), so his effective inflow in Number space is only +5 — 1 USD is
-    // consumed by the precision-boundary rounding and cannot be credited.
-    //
-    // The destroyed amount (1 USD) is sub-ULP at destinationScale=1 (step=10),
-    // so the check treats it as an unavoidable IOU-precision artefact and
-    // lets the transaction succeed.
-    //
-    // Contrast: if 15 USD were destroyed at the same scale (destroyed ≥ step),
-    // floor(15/10)=1 ≠ 0 and the invariant would fire — that discrepancy IS
-    // representable and indicates a real accounting bug.
-    //
-    // Pre-fixCleanup3_2_0: the "must increase destination balance" check fires
-    // because roundedDestinationDelta = 0 ≤ 0.
-    void
-    testVaultWithdrawEqualityEnforced()
-    {
-        using namespace test::jtx;
-
-        auto runScenario = [this](FeatureBitset features, TER expected) {
-            std::string logs;
-            Env env(*this, features, std::make_unique(&logs));
-
-            Account const issuer{"issuer"};
-            Account const alice{"alice"};
-            Account const bob{"bob"};
-
-            env.fund(XRP(100'000), issuer, alice, bob);
-            env.close();
-            env(fset(issuer, asfDefaultRipple));
-            env.close();
-
-            PrettyAsset const usd{issuer["USD"]};
-            STAmount const aliceLimit{usd.raw(), 2, 16};
-            STAmount const bobLimit{usd.raw(), 2, 16};
-            // Bob's balance sits 5 units below the 10^16 STAmount precision
-            // boundary.  Receiving 6 USD shifts his exponent 0 → 1; the
-            // STAmount records +5, not +6 (1 USD is lost to rounding).
-            STAmount const atEdge2{usd.raw(), Number{9'999'999'999'999'995LL}};
-
-            env(trust(alice, aliceLimit));
-            env(trust(bob, bobLimit));
-            env.close();
-
-            env(pay(issuer, alice, usd(1'000)));
-            env(pay(issuer, bob, atEdge2));
-            env.close();
-
-            Vault const vault{env};
-            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
-            vaultTx[sfScale] = 0;
-            env(vaultTx);
-            env.close();
-
-            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1'000)}));
-            env.close();
-
-            // Withdraw 6 USD to Bob: vault loses 6, Bob gains only 5.
-            // Destroyed amount = 1 USD, which is sub-ULP at destinationScale=1.
-            auto tx = vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(6)});
-            tx[sfDestination] = bob.human();
-            env(tx, Ter(expected));
-            env.close();
-        };
-
-        {
-            testcase(
-                "bug: VaultWithdraw to destination at IOU precision boundary fires "
-                "invariant (pre-fixCleanup3_2_0)");
-            runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
-        }
-        {
-            testcase(
-                "bug: VaultWithdraw to destination at IOU precision boundary succeeds "
-                "when destroyed amount is sub-ULP (post-fixCleanup3_2_0)");
-            runScenario(testableAmendments(), tesSUCCESS);
-        }
-    }
-
-    // Bug: when a depositor's IOU trustline balance is very large (e.g.
-    // ~1e17), adding a small deposit (e.g. 1 USD) leaves sfAssetsTotal
-    // unchanged at IOU precision because the increment is sub-ULP at the
-    // vault's current asset scale.  The vault records the deposit, mints
-    // shares, and decrements the depositor's trustline, but sfAssetsTotal
-    // does not change — the conservation invariant fires because the rail
-    // delta is zero.
-    //
-    // Two sub-cases are exercised:
-    //   1. First-ever deposit into an empty vault: the depositor's own
-    //      trustline has a large balance so 1 USD canonicalizes to zero
-    //      when written back through the IOU rail.
-    //   2. Subsequent deposit after the vault already holds a large
-    //      sfAssetsTotal: a different depositor (bob, with a small balance)
-    //      sends 1 USD, which again rounds to zero at the vault's coarse
-    //      asset scale.
-    //
-    // Fix (fixCleanup3_2_0): the deposit transactor checks whether
-    // roundToAsset(amount, vault_scale) == 0 and rejects early with
-    // tecPRECISION_LOSS before any state is modified.
-    void
-    testVaultDepositCanonicalizeToZero()
-    {
-        using namespace test::jtx;
-        auto runScenario = [this](FeatureBitset features, TER expected) {
-            std::string logs;
-            Env env(*this, features, std::make_unique(&logs));
-
-            Account const issuer{"issuer"};
-            Account const alice{"alice"};
-            Account const bob{"bob"};
-
-            env.fund(XRP(100'000), issuer, alice, bob);
-            env.close();
-
-            env(fset(issuer, asfDefaultRipple));
-            env.close();
-
-            PrettyAsset const usd{issuer["USD"]};
-
-            STAmount const trustLimit{usd.raw(), Number{99'999'999'999'999'999LL}};
-            STAmount const aliceFund{usd.raw(), Number{99'999'999'999'999'999LL}};
-
-            env(trust(alice, trustLimit));
-            env(trust(bob, trustLimit));
-            env.close();
-
-            env(pay(issuer, alice, aliceFund));
-            env(pay(issuer, bob, usd(1000)));
-            env.close();
-
-            Vault const vault{env};
-
-            // Scale=0 so sfAssetsTotal stores whole USD
-            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
-            vaultTx[sfScale] = 0;
-            env(vaultTx);
-            env.close();
-
-            // Alice's deposit canonicalizes to zero at her own trustline scale
-            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1)}),
-                Ter(expected));
-
-            // Increase vault-scale
-            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = aliceFund}));
-            env.close();
-
-            env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = usd(1)}),
-                Ter(expected));
-            env.close();
-        };
-
-        {
-            testcase(
-                "bug: VaultDeposit below Vault precision canonicalized to zero "
-                "(pre-fixCleanup3_2_0)");
-            runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
-        }
-        {
-            testcase(
-                "bug: VaultDeposit below Vault precision canonicalized to zero "
-                "(post-fixCleanup3_2_0)");
-            runScenario(testableAmendments(), tecPRECISION_LOSS);
-        }
-    }
-
-    // VaultDeposit by issuer with the vault parked at the IOU 16-digit
-    // edge (9.999e15). Issuer mints 2 more USD; the vault trust line
-    // goes 9.999e15 → 10^16, gaining 1 unit instead of 2 (canonicalization).
-    //
-    // Pre-fixCleanup3_2_0: the proactive check is absent; the deposit
-    // applies, then VaultInvariant's "deposit must increase vault
-    // balance" assertion fires at finalize time on the rounded vault
-    // delta of zero, returning tecINVARIANT_FAILED.
-    // Post-amendment: reject deposit that is not representable at Vault scale.
-    void
-    testBugIssuerVaultDepositAtEdge()
-    {
-        using namespace test::jtx;
-
-        auto runScenario = [this](FeatureBitset features, TER expected) {
-            std::string logs;
-            Env env(*this, features, std::make_unique(&logs));
-
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-
-            env.fund(XRP(100'000), issuer, owner);
-            env.close();
-            env(fset(issuer, asfDefaultRipple));
-            env.close();
-
-            PrettyAsset const usd{issuer["USD"]};
-            STAmount const trustLimit{usd.raw(), 2, 16};
-            STAmount const ownerFund{usd.raw(), Number{9'999'999'999'999'999LL}};
-
-            env(trust(owner, trustLimit));
-            env.close();
-            env(pay(issuer, owner, ownerFund));
-            env.close();
-
-            Vault const vault{env};
-            auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = usd});
-            vaultTx[sfScale] = 0;
-            env(vaultTx);
-            env.close();
-            env(vault.deposit({.depositor = owner, .id = vaultKeylet.key, .amount = ownerFund}));
-            env.close();
-
-            // Vault pseudo-account is now at 9.999e15. Issuer mints 2
-            // more USD. Pre: tecINVARIANT_FAILED at finalize. Post:
-            // tecPRECISION_LOSS proactively. Either way, no value moves.
-            env(vault.deposit({.depositor = issuer, .id = vaultKeylet.key, .amount = usd(2)}),
-                Ter(expected));
-            env.close();
-        };
-
-        {
-            testcase(
-                "bug: VaultDeposit by issuer at IOU edge fires "
-                "tecINVARIANT_FAILED at finalize (pre-fixCleanup3_2_0)");
-            runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
-        }
-        {
-            testcase(
-                "bug: VaultDeposit by issuer at IOU edge rejects with "
-                "tecPRECISION_LOSS proactively (post-fixCleanup3_2_0)");
-            runScenario(testableAmendments(), tecPRECISION_LOSS);
-        }
-    }
-
-    void
-    testReferenceHolding()
-    {
-        using namespace test::jtx;
-
-        auto readReferenceHolding = [&](Env const& env,
-                                        Keylet const& vaultKeylet) -> std::optional {
-            auto const sleVault = env.le(vaultKeylet);
-            if (!sleVault)
-                return std::nullopt;
-            auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
-            if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
-                return std::nullopt;
-            return sleIssuance->getFieldH256(sfReferenceHolding);
-        };
-
-        // Post-fixCleanup3_2_0: vault share carries sfReferenceHolding
-        // pointing to the vault pseudo's MPToken (for MPT-backed vaults)
-        // or RippleState (for IOU-backed vaults).
-        {
-            testcase("sfReferenceHolding: MPT-backed vault, post-amendment");
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            env.fund(XRP(10'000), issuer, owner);
-            env.close();
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
-            PrettyAsset const asset = mptt.issuanceID();
-            mptt.authorize({.account = owner});
-
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            auto const sleVault = env.le(keylet);
-            BEAST_EXPECT(sleVault != nullptr);
-            auto const pseudoId = sleVault->at(sfAccount);
-            auto const expected = keylet::mptoken(mptt.issuanceID(), pseudoId).key;
-
-            auto const stored = readReferenceHolding(env, keylet);
-            BEAST_EXPECT(stored.has_value());
-            BEAST_EXPECT(stored && *stored == expected);
-            // The pointed-to MPToken must actually exist.
-            BEAST_EXPECT(env.le(keylet::mptoken(mptt.issuanceID(), pseudoId)) != nullptr);
-        }
-
-        {
-            testcase("sfReferenceHolding: IOU-backed vault, post-amendment");
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            env.fund(XRP(10'000), issuer, owner);
-            env(fset(issuer, asfDefaultRipple));
-            env.close();
-
-            PrettyAsset const asset = issuer["IOU"];
-            env.trust(asset(1'000'000), owner);
-            env.close();
-
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            auto const sleVault = env.le(keylet);
-            BEAST_EXPECT(sleVault != nullptr);
-            auto const pseudoId = sleVault->at(sfAccount);
-            auto const expected = keylet::trustLine(pseudoId, asset.raw().get()).key;
-
-            auto const stored = readReferenceHolding(env, keylet);
-            BEAST_EXPECT(stored.has_value());
-            BEAST_EXPECT(stored && *stored == expected);
-            // The pointed-to RippleState must actually exist.
-            BEAST_EXPECT(env.le(keylet::trustLine(pseudoId, asset.raw().get())) != nullptr);
-        }
-
-        // XRP-backed vaults leave the field absent: XRP has no separate
-        // holding ledger entry and no transferability concept to inherit.
-        {
-            testcase("sfReferenceHolding: XRP-backed vault, field absent");
-            Env env{*this, testableAmendments()};
-            Account const owner{"owner"};
-            env.fund(XRP(10'000), owner);
-            env.close();
-
-            PrettyAsset const asset{xrpIssue(), 1'000'000};
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            BEAST_EXPECT(!readReferenceHolding(env, keylet).has_value());
-        }
-
-        // Pre-fixCleanup3_2_0: vault share has the field absent regardless
-        // of underlying type.
-        {
-            testcase("sfReferenceHolding: vault share, pre-amendment");
-            Env env{*this, testableAmendments() - fixCleanup3_2_0};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            env.fund(XRP(10'000), issuer, owner);
-            env.close();
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
-            PrettyAsset const asset = mptt.issuanceID();
-            mptt.authorize({.account = owner});
-
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            BEAST_EXPECT(!readReferenceHolding(env, keylet).has_value());
-        }
-
-        // Plain MPTokenIssuanceCreate (not a vault share) must never
-        // populate the field. Only the post-amendment case is
-        // interesting; pre-amendment nothing writes the field at all.
-        {
-            testcase("sfReferenceHolding: plain MPT issuance never set");
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            env.fund(XRP(10'000), issuer);
-            env.close();
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
-            env.close();
-
-            auto const sleIssuance = env.le(keylet::mptokenIssuance(mptt.issuanceID()));
-            if (BEAST_EXPECT(sleIssuance))
-                BEAST_EXPECT(!sleIssuance->isFieldPresent(sfReferenceHolding));
-        }
-    }
-
-    // Probe every transactor surface that might delete the vault pseudo-
-    // account's underlying holding (the MPToken or RippleState pointed to
-    // by sfReferenceHolding). Each scenario asserts either that the
-    // existing pseudo-account guards stop the deletion at preclaim, or
-    // that the ledger leaves the holding intact afterwards. This is a
-    // regression guard: if any of these guards regresses, the share's
-    // sfReferenceHolding pointer would dangle and the new ValidMPTIssuance
-    // invariant would catch it - but we want to fail much earlier, at
-    // the transactor's preclaim / doApply, not at invariant time.
-    void
-    testHoldingDeletionBlocked()
-    {
-        using namespace test::jtx;
-
-        // Helper: read the share's referenced holding and confirm the
-        // pointed-to SLE still exists after the probe.
-        auto referencedHoldingExists = [&](Env const& env, Keylet const& vaultKeylet) -> bool {
-            auto const sleVault = env.le(vaultKeylet);
-            if (!sleVault)
-                return false;
-            auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
-            if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
-                return false;
-            auto const holdingKey = sleIssuance->getFieldH256(sfReferenceHolding);
-            return env.le(keylet::unchecked(holdingKey)) != nullptr;
-        };
-
-        // ---- MPT-backed vault ----------------------------------------
-        {
-            testcase("vault pseudo MPToken: Clawback blocked by tecPSEUDO_ACCOUNT");
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            Account const depositor{"depositor"};
-            env.fund(XRP(10'000), issuer, owner, depositor);
-            env.close();
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback});
-            PrettyAsset const asset = mptt.issuanceID();
-            mptt.authorize({.account = owner});
-            mptt.authorize({.account = depositor});
-            env(pay(issuer, depositor, asset(1'000)));
-            env.close();
-
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(500)}));
-            env.close();
-
-            BEAST_EXPECT(referencedHoldingExists(env, keylet));
-
-            Account const pseudoAccount{"vault-pseudo", env.le(keylet)->at(sfAccount)};
-            // Issuer attempts to claw back the FULL underlying balance
-            // (500) directly from the vault pseudo-account. With the
-            // full amount, the doApply path would drain the pseudo's
-            // MPToken to zero and removeEmptyHolding would erase it -
-            // if doApply ever ran. SAV's pseudo-account guard at
-            // Clawback.cpp:201 refuses at preclaim with
-            // tecPSEUDO_ACCOUNT before any state change.
-            env(claw(issuer, asset(500), pseudoAccount), Ter{tecPSEUDO_ACCOUNT});
-            env.close();
-            BEAST_EXPECT(referencedHoldingExists(env, keylet));
-            // Sanity: pseudo's full balance is intact.
-            BEAST_EXPECT(env.balance(pseudoAccount, asset).number() == 500);
-        }
-
-        {
-            testcase("vault pseudo MPToken: Issuer cannot Unauthorize pseudo");
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            env.fund(XRP(10'000), issuer, owner);
-            env.close();
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
-            PrettyAsset const asset = mptt.issuanceID();
-            mptt.authorize({.account = owner});
-            mptt.authorize({.account = issuer, .holder = owner});
-            env.close();
-
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            BEAST_EXPECT(referencedHoldingExists(env, keylet));
-
-            auto const pseudoId = env.le(keylet)->at(sfAccount);
-            // Issuer attempts MPTokenAuthorize against the pseudo with
-            // tfMPTUnauthorize. MPTokenAuthorize.cpp blocks pseudo
-            // accounts via isPseudoAccount; the pseudo's MPToken is
-            // preserved. Construct the tx manually since the pseudo
-            // lacks a signing key, and the issuer-driven flavour is
-            // expressed via sfHolder.
-            json::Value jv;
-            jv[sfAccount] = issuer.human();
-            jv[sfHolder] = toBase58(pseudoId);
-            jv[sfMPTokenIssuanceID] = to_string(mptt.issuanceID());
-            jv[sfFlags] = tfMPTUnauthorize;
-            jv[sfTransactionType] = jss::MPTokenAuthorize;
-            env(jv, Ter{tecNO_PERMISSION});
-            env.close();
-            BEAST_EXPECT(referencedHoldingExists(env, keylet));
-        }
-
-        {
-            testcase("vault pseudo MPToken: MPTokenIssuanceDestroy blocked while vault holds");
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            Account const depositor{"depositor"};
-            env.fund(XRP(10'000), issuer, owner, depositor);
-            env.close();
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
-            PrettyAsset const asset = mptt.issuanceID();
-            mptt.authorize({.account = owner});
-            mptt.authorize({.account = depositor});
-            env(pay(issuer, depositor, asset(1'000)));
-            env.close();
-
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(500)}));
-            env.close();
-
-            BEAST_EXPECT(referencedHoldingExists(env, keylet));
-
-            // While the vault holds outstanding underlying, the issuer
-            // cannot destroy the issuance. tecHAS_OBLIGATIONS confirms
-            // the protection - and as a side effect, the share's
-            // sfReferenceHolding pointer cannot be left pointing at a
-            // ghost issuance.
-            mptt.destroy({.id = mptt.issuanceID(), .err = tecHAS_OBLIGATIONS});
-            env.close();
-            BEAST_EXPECT(referencedHoldingExists(env, keylet));
-        }
-
-        // ---- IOU-backed vault ----------------------------------------
-        {
-            testcase("vault pseudo trust line: Clawback blocked by tecPSEUDO_ACCOUNT");
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            env.fund(XRP(10'000), issuer, owner);
-            env(fset(issuer, asfAllowTrustLineClawback));
-            env.close();
-
-            PrettyAsset const asset = issuer["IOU"];
-            env.trust(asset(1'000'000), owner);
-            env(pay(issuer, owner, asset(1'000)));
-            env.close();
-
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(500)}));
-            env.close();
-
-            BEAST_EXPECT(referencedHoldingExists(env, keylet));
-
-            Account const pseudoAccount{"vault-pseudo", env.le(keylet)->at(sfAccount)};
-            // Issuer attempts to claw back the FULL IOU balance (500)
-            // directly from the vault pseudo. With the full amount, the
-            // doApply path would drain the trust line to zero and (if
-            // both reserve flags clear) trustDelete would erase it - if
-            // doApply ever ran. The same SAV pseudo-account guard
-            // refuses at preclaim with tecPSEUDO_ACCOUNT. The amount's
-            // STAmount issuer field is the holder, per IOU clawback
-            // convention.
-            env(claw(issuer, pseudoAccount["IOU"](500)), Ter{tecPSEUDO_ACCOUNT});
-            env.close();
-            BEAST_EXPECT(referencedHoldingExists(env, keylet));
-            // Sanity: pseudo's full balance is intact.
-            BEAST_EXPECT(env.balance(pseudoAccount, asset).number() == 500);
-        }
-
-        {
-            testcase("vault pseudo trust line: TrustSet limit=0 from issuer preserves line");
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            env.fund(XRP(10'000), issuer, owner);
-            env(fset(issuer, asfDefaultRipple));
-            env.close();
-
-            PrettyAsset const asset = issuer["IOU"];
-            env.trust(asset(1'000'000), owner);
-            env(pay(issuer, owner, asset(1'000)));
-            env.close();
-
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(500)}));
-            env.close();
-
-            BEAST_EXPECT(referencedHoldingExists(env, keylet));
-
-            // Issuer submits TrustSet with limit=0 against the vault
-            // pseudo. The pseudo's side of the line still has the
-            // original (non-zero) limit and a non-zero balance, so the
-            // line is preserved - even though the issuer cleared its
-            // own side. trustDelete only fires when both limits clear
-            // and the balance is zero.
-            Account const pseudoAccount{"vault-pseudo", env.le(keylet)->at(sfAccount)};
-            env(trust(issuer, pseudoAccount["IOU"](0)));
-            env.close();
-            BEAST_EXPECT(referencedHoldingExists(env, keylet));
-        }
-
-        // ---- Positive control: VaultDelete is the only legitimate path
-        {
-            testcase("vault pseudo holding: VaultDelete is the legitimate cleanup path");
-            Env env{*this, testableAmendments()};
-            Account const issuer{"issuer"};
-            Account const owner{"owner"};
-            env.fund(XRP(10'000), issuer, owner);
-            env.close();
-
-            MPTTester mptt{env, issuer, kMptInitNoFund};
-            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
-            PrettyAsset const asset = mptt.issuanceID();
-            mptt.authorize({.account = owner});
-
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-            env(tx);
-            env.close();
-
-            BEAST_EXPECT(referencedHoldingExists(env, keylet));
-            auto const pseudoId = env.le(keylet)->at(sfAccount);
-            auto const sharedMptId = env.le(keylet)->at(sfShareMPTID);
-            auto const holdingKeylet = keylet::mptoken(mptt.issuanceID(), pseudoId);
-
-            // VaultDelete tears down the vault pseudo's holding, the
-            // share issuance, and the pseudo-account itself. Invariant
-            // permits this because the tx is ttVAULT_DELETE.
-            env(vault.del({.owner = owner, .id = keylet.key}));
-            env.close();
-
-            BEAST_EXPECT(env.le(keylet) == nullptr);
-            BEAST_EXPECT(env.le(holdingKeylet) == nullptr);
-            BEAST_EXPECT(env.le(keylet::mptokenIssuance(sharedMptId)) == nullptr);
-        }
-    }
-
-    // VaultDeposit::preclaim uses accountHolds(..., SpendableHandling::
-    // shFULL_BALANCE), which for an IOU asset adds the counterparty's
-    // LowLimit/HighLimit to the depositor's raw balance (TokenHelpers.cpp:
-    // getTrustLineBalance with includeOppositeLimit=true). When the
-    // depositor's raw balance < deposit amount but raw + opposite limit >=
-    // amount, preclaim is satisfied. doApply then calls
-    // directSendNoFeeIOU, which unconditionally subtracts saAmount from
-    // saBalance — driving the trust line negative — and returns tesSUCCESS.
-    // The post-send sanity check uses the default shSIMPLE_BALANCE (no
-    // opposite-limit add), sees a negative balance, and returns tefINTERNAL.
-    void
-    testVaultDepositNegativeBalanceFromOppositeLimit()
-    {
-        auto runTest = [&](FeatureBitset f, TER expected) {
-            using namespace test::jtx;
-            using namespace std::literals;
-
-            Env env{*this, f};
-            Account const gw{"gateway"};
-            Account const owner{"owner"};
-            Account const depositor{"depositor"};
-
-            env.fund(XRP(10000), gw, owner, depositor);
-            env.close();
-
-            // Gateway with DefaultRipple so vault creation on its IOU works.
-            env(fset(gw, asfDefaultRipple));
-            env.close();
-
-            // Depositor opens a trust line to gateway and receives a small
-            // balance.
-            PrettyAsset const usd = gw["USD"];
-            env.trust(usd(1000), depositor);
-            env(pay(gw, depositor, usd(100)));  // raw trust-line balance: 100
-            env.close();
-
-            // Key precondition: gateway sets a non-zero limit on the same
-            // RippleState — the "opposite field" from depositor's perspective.
-            // This is what inflates shFULL_BALANCE in preclaim above the raw
-            // balance.
-            env(trust(gw, depositor["USD"](1000)));
-            env.close();
-
-            // Create the IOU vault.
-            Vault const vault{env};
-            auto [vaultTx, keylet] = vault.create({.owner = owner, .asset = usd});
-            env(vaultTx);
-            env.close();
-
-            // Submit a deposit of 500 USD:
-            //   - raw balance:                100 USD
-            //   - opposite limit (gw's side): 1000 USD
-            //   - preclaim sees 100 + 1000 = 1100, passes (>= 500)
-            //   - doApply transfers 500, depositor's trust-line balance
-            //     becomes -400
-            //   - sanity check at VaultDeposit.cpp:256 fires
-            //   - tx returns tefINTERNAL (BUG — should be tesSUCCESS.
-            auto depositTx =
-                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = usd(500)});
-            env(depositTx, Ter(expected));
-            env.close();
-        };
-
-        {
-            testcase(
-                "IOU vault deposit exceeding depositor's balance but "
-                "within counterparty's trust limit, pre-fixCleanup3_2_0 "
-                "(tefINTERNAL)");
-            runTest(test::jtx::testableAmendments() - fixCleanup3_2_0, tefINTERNAL);
-        }
-        {
-            testcase(
-                "IOU vault deposit exceeding depositor's balance but "
-                "within counterparty's trust limit, post-fixCleanup3_2_0 "
-                "(tesSUCCESS)");
-            runTest(test::jtx::testableAmendments(), tesSUCCESS);
-        }
-    }
-
-    void
-    testVaultDeleteMemoData()
-    {
-        using namespace test::jtx;
-
-        Env env{*this};
-
-        Account const owner{"owner"};
-        env.fund(XRP(1'000'000), owner);
-        env.close();
-
-        Vault const vault{env};
-
-        auto const keylet = keylet::vault(owner.id(), SeqProxy::rawSequence(1));
-        auto delTx = vault.del({.owner = owner, .id = keylet.key});
-
-        // Test VaultDelete with featureLendingProtocolV1_1 disabled
-        // Transaction fails if the data field is provided
-        {
-            testcase("VaultDelete memo data featureLendingProtocolV1_1 disabled");
-            env.disableFeature(featureLendingProtocolV1_1);
-            delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
-            env(delTx, Ter(temDISABLED));
-            env.enableFeature(featureLendingProtocolV1_1);
-            env.close();
-        }
-
-        // Transaction fails if the data field is too large
-        {
-            testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data too large");
-            delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength + 1, 'A'));
-            env(delTx, Ter(temMALFORMED));
-            env.close();
-        }
-
-        // Transaction fails if the data field is set, but is empty
-        {
-            testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data empty");
-            delTx[sfMemoData] = strHex(std::string());
-            env(delTx, Ter(temMALFORMED));
-            env.close();
-        }
-
-        {
-            testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled no vault");
-            auto const keylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
-
-            // Recreate the transaction as the vault keylet changed
-            auto delTx = vault.del({.owner = owner, .id = keylet.key});
-            delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
-            env(delTx, Ter(tecNO_ENTRY));
-            env.close();
-        }
-
-        {
-            testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data valid");
-            PrettyAsset const xrpAsset = xrpIssue();
-            auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
-            env(tx, Ter(tesSUCCESS));
-            env.close();
-            // Recreate the transaction as the vault keylet changed
-            auto delTx = vault.del({.owner = owner, .id = keylet.key});
-            delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
-            env(delTx, Ter(tesSUCCESS));
-            env.close();
-        }
-    }
-
-    void
-    testVaultCreateLEVersion()
-    {
-        using namespace test::jtx;
-
-        Account const owner{"owner"};
-        PrettyAsset const xrpAsset = xrpIssue();
-
-        {
-            testcase("VaultCreate LEVersion: featureLendingProtocolV1_1 disabled, field absent");
-            Env env{*this};
-            env.disableFeature(featureLendingProtocolV1_1);
-            env.fund(XRP(1'000'000), owner);
-            env.close();
-
-            Vault const vault{env};
-            auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
-            env(tx, Ter(tesSUCCESS));
-            env.close();
-
-            auto const sleVault = env.le(keylet);
-            BEAST_EXPECT(sleVault);
-            BEAST_EXPECT(!sleVault->isFieldPresent(sfLEVersion));
-        }
-
-        {
-            testcase(
-                "VaultCreate LEVersion: featureLendingProtocolV1_1 enabled, LEVersion == "
-                "VaultVersion::CashBasis");
-            Env env{*this};
-            env.fund(XRP(1'000'000), owner);
-            env.close();
-
-            Vault const vault{env};
-            auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
-            env(tx, Ter(tesSUCCESS));
-            env.close();
-
-            auto const sleVault = env.le(keylet);
-            BEAST_EXPECT(sleVault);
-            BEAST_EXPECT(sleVault->isFieldPresent(sfLEVersion));
-            BEAST_EXPECT(sleVault->at(sfLEVersion) == std::to_underlying(VaultVersion::CashBasis));
-        }
-
-        {
-            testcase("VaultCreate rejects LEVersion set in the transaction");
-            Env env{*this};
-            env.fund(XRP(1'000'000), owner);
-            env.close();
-
-            Vault const vault{env};
-            auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
-            tx[sfLEVersion] = 2;
-            env(tx, Ter(temMALFORMED));
-            env.close();
-
-            BEAST_EXPECT(!env.le(keylet));
-        }
-
-        {
-            testcase("VaultSet rejects LEVersion set in the transaction");
-            Env env{*this};
-            env.fund(XRP(1'000'000), owner);
-            env.close();
-
-            Vault const vault{env};
-            auto const [createTx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
-            env(createTx, Ter(tesSUCCESS));
-            env.close();
-
-            auto setTx = vault.set({.owner = owner, .id = keylet.key});
-            setTx[sfLEVersion] = 2;
-            env(setTx, Ter(temMALFORMED));
-            env.close();
-        }
-    }
-
-    void
-    testVaultDepositFreezeIOU()
-    {
-        using namespace test::jtx;
-        testcase("VaultDeposit IOU freeze checks");
-
-        Account const issuer{"issuer"};
-        Account const owner{"owner"};
-        Env env{*this};
-        Vault vault{env};
-
-        env.fund(XRP(100'000), issuer, owner);
-        env(fset(issuer, asfAllowTrustLineClawback));
-        env.close();
-        PrettyAsset const asset = issuer["IOU"];
-        env.trust(asset(1'000'000), owner);
-        env(pay(issuer, owner, asset(100'000)));
-        env.close();
-
-        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-        env(tx);
-        env.close();
-        auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
-
-        // Initial deposit so the vault pseudo-account has a trustline
-        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
-        env.close();
-
-        auto runTests = [&]() {
-            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
-
-            // Global freeze
-            {
-                testcase("VaultDeposit IOU global freeze");
-                env(fset(issuer, asfGlobalFreeze));
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                    Ter(tecFROZEN));
-                env(fclear(issuer, asfGlobalFreeze));
-            }
-
-            // Depositor freeze
-            {
-                testcase("VaultDeposit IOU depositor freeze");
-                env(trust(issuer, asset(0), owner, tfSetFreeze));
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                    Ter(tecFROZEN));
-                env(trust(issuer, asset(0), owner, tfClearFreeze));
-            }
-
-            // Depositor deep freeze
-            {
-                testcase("VaultDeposit IOU depositor deep freeze");
-                env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze));
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                    Ter(tecFROZEN));
-                env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze));
-            }
-
-            // Vault-account freeze
-            // Post-fix: checkDepositFreeze catches it → tecFROZEN
-            // Pre-fix: not checked directly, but the transitive share
-            //          check triggers → tecLOCKED
-            {
-                testcase("VaultDeposit IOU pseudo-account freeze");
-                auto trustSet = [&]() {
-                    json::Value jv;
-                    jv[jss::Account] = issuer.human();
-                    {
-                        auto& ja = jv[jss::LimitAmount] =
-                            asset(0).value().getJson(JsonOptions::Values::None);
-                        ja[jss::issuer] = toBase58(vaultAcct.id());
-                    }
-                    jv[jss::TransactionType] = jss::TrustSet;
-                    return jv;
-                }();
-
-                trustSet[jss::Flags] = tfSetFreeze;
-                env(trustSet);
-                env.close();
-
-                TER const expected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                    Ter(expected));
-
-                trustSet[jss::Flags] = tfClearFreeze;
-                env(trustSet);
-                env.close();
-            }
-
-            // Vault-account deep freeze
-            {
-                testcase("VaultDeposit IOU pseudo-account deep freeze");
-                auto trustSet = [&]() {
-                    json::Value jv;
-                    jv[jss::Account] = issuer.human();
-                    {
-                        auto& ja = jv[jss::LimitAmount] =
-                            asset(0).value().getJson(JsonOptions::Values::None);
-                        ja[jss::issuer] = toBase58(vaultAcct.id());
-                    }
-                    jv[jss::TransactionType] = jss::TrustSet;
-                    return jv;
-                }();
-
-                trustSet[jss::Flags] = tfSetFreeze | tfSetDeepFreeze;
-                env(trustSet);
-                env.close();
-
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                    Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
-
-                trustSet[jss::Flags] = tfClearFreeze | tfClearDeepFreeze;
-                env(trustSet);
-                env.close();
-            }
-
-            // Clawback works while frozen
-            {
-                testcase("VaultDeposit IOU freeze clawback unaffected");
-                env(fset(issuer, asfGlobalFreeze));
-                env(vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)}));
-                env(fclear(issuer, asfGlobalFreeze));
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
-                env.close();
-            }
-        };
-
-        runTests();
-        env.disableFeature(fixCleanup3_3_0);
-        runTests();
-        env.enableFeature(fixCleanup3_3_0);
-    }
-
-    void
-    testVaultDepositFreezeMPT()
-    {
-        using namespace test::jtx;
-        testcase("VaultDeposit MPT lock checks");
-
-        Account const issuer{"issuer"};
-        Account const owner{"owner"};
-        Env env{*this};
-        Vault vault{env};
-
-        env.fund(XRP(100'000), issuer, owner);
-        env.close();
-
-        MPTTester mptt{env, issuer, kMptInitNoFund};
-        mptt.create(
-            {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
-        PrettyAsset const mpt{mptt.issuanceID()};
-
-        mptt.authorize({.account = owner});
-        mptt.authorize({.account = issuer, .holder = owner});
-        env.close();
-        env(pay(issuer, owner, mpt(100'000)));
-        env.close();
-
-        auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt});
-        env(tx);
-        env.close();
-        auto const vaultAcctID = env.le(keylet)->at(sfAccount);
-        Account const vaultAcct("vault", vaultAcctID);
-
-        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)}));
-        env.close();
-
-        // For MPT isDeepFrozen == isFrozen, so all locks block in
-        // both pre- and post-fix.
-        auto runTests = [&]() {
-            // Global lock
-            {
-                testcase("VaultDeposit MPT global lock");
-                mptt.set({.flags = tfMPTLock});
-                env.close();
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
-                    Ter(tecLOCKED));
-                mptt.set({.flags = tfMPTUnlock});
-                env.close();
-            }
-
-            // Depositor individual lock
-            {
-                testcase("VaultDeposit MPT depositor lock");
-                mptt.set({.holder = owner, .flags = tfMPTLock});
-                env.close();
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
-                    Ter(tecLOCKED));
-                mptt.set({.holder = owner, .flags = tfMPTUnlock});
-                env.close();
-            }
-
-            // Vault pseudo-account individual lock
-            {
-                testcase("VaultDeposit MPT pseudo-account lock");
-                mptt.set({.holder = vaultAcct, .flags = tfMPTLock});
-                env.close();
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
-                    Ter(tecLOCKED));
-                mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock});
-                env.close();
-            }
-
-            // Clawback works while locked
-            {
-                testcase("VaultDeposit MPT lock clawback unaffected");
-                mptt.set({.flags = tfMPTLock});
-                env.close();
-                env(vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = mpt(1)}));
-                mptt.set({.flags = tfMPTUnlock});
-                env.close();
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
-                env.close();
-            }
-        };
-
-        runTests();
-        env.disableFeature(fixCleanup3_3_0);
-        runTests();
-        env.enableFeature(fixCleanup3_3_0);
-    }
-
-    // Focused demonstration: a depositor under an individual IOU freeze
-    // can still withdraw to themselves (self-withdrawal), but is blocked from
-    // withdrawing to a third party.
-    //
-    // Pre-fixCleanup3_3_0: both the self-withdrawal AND the third-party
-    // withdrawal were blocked because the old code checked checkFrozen on the
-    // destination regardless of whether it was the submitter.
-    // Post-fixCleanup3_3_0: checkWithdrawFreeze skips the submitter freeze
-    // check when submitter == destination, so self-withdrawal succeeds.
-    void
-    testVaultSelfWithdrawWhileFrozen()
-    {
-        testcase("VaultWithdraw IOU self-withdrawal while individually frozen");
-
-        using namespace test::jtx;
-
-        Account const issuer{"issuer"};
-        Account const owner{"owner"};
-        Account const charlie{"charlie"};
-        Env env{*this};
-        Vault vault{env};
-
-        env.fund(XRP(100'000), issuer, owner, charlie);
-        env(fset(issuer, asfAllowTrustLineClawback));
-        env.close();
-
-        PrettyAsset const asset = issuer["IOU"];
-        env.trust(asset(1'000'000), owner);
-        env.trust(asset(1'000'000), charlie);
-        env(pay(issuer, owner, asset(100'000)));
-        env.close();
-
-        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-        env(tx);
-        env.close();
-
-        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)}));
-        env.close();
-
-        auto runTests = [&]() {
-            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
-
-            // Set an individual freeze on the owner's IOU trustline.
-            env(trust(issuer, asset(0), owner, tfSetFreeze));
-            env.close();
-
-            // Self-withdrawal: submitter == destination, so the submitter
-            // freeze check is skipped.
-            // Post-fix: tesSUCCESS.  Pre-fix: tecFROZEN.
-            env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
-
-            // Withdrawal to a third party is blocked: submitter != destination
-            // so the submitter freeze check applies.
-            {
-                auto withdrawToCharlie =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
-                withdrawToCharlie[sfDestination] = charlie.human();
-                // Post-fix: tecFROZEN (checkIndividualFrozen on submitter).
-                // Pre-fix: tecLOCKED (isFrozen on the vault share).
-                env(withdrawToCharlie, Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
-            }
-
-            env(trust(issuer, asset(0), owner, tfClearFreeze));
-            env.close();
-        };
-
-        runTests();
-        env.disableFeature(fixCleanup3_3_0);
-        runTests();
-        env.enableFeature(fixCleanup3_3_0);
-    }
-
-    void
-    testVaultWithdrawFreezeIOU()
-    {
-        using namespace test::jtx;
-        testcase("VaultWithdraw IOU freeze checks");
-
-        Account const issuer{"issuer"};
-        Account const owner{"owner"};
-        Env env{*this};
-        Vault const vault{env};
-
-        env.fund(XRP(100'000), issuer, owner);
-        env(fset(issuer, asfAllowTrustLineClawback));
-        env.close();
-        PrettyAsset const asset = issuer["IOU"];
-        env.trust(asset(1'000'000), owner);
-        env(pay(issuer, owner, asset(100'000)));
-        env.close();
-
-        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
-        env(tx);
-        env.close();
-        auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
-
-        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
-        env.close();
-
-        Account const charlie{"charlie"};
-        env.fund(XRP(10'000), charlie);
-        env.trust(asset(1'000'000), charlie);
-        env.close();
-
-        auto runTests = [&]() {
-            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
-            // Global freeze → self-withdraw
-            {
-                testcase("VaultWithdraw IOU global freeze");
-                env(fset(issuer, asfGlobalFreeze));
-                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                    Ter(tecFROZEN));
-                // Global freeze → withdraw to 3rd party
-
-                auto withdrawToCharlie =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
-                withdrawToCharlie[sfDestination] = charlie.human();
-                env(withdrawToCharlie, Ter(tecFROZEN));
-
-                env(fclear(issuer, asfGlobalFreeze));
-            }
-
-            // Vault-account freeze
-            {
-                testcase("VaultWithdraw IOU pseudo-account freeze");
-                auto trustSet = [&]() {
-                    json::Value jv;
-                    jv[jss::Account] = issuer.human();
-                    {
-                        auto& ja = jv[jss::LimitAmount] =
-                            asset(0).value().getJson(JsonOptions::Values::None);
-                        ja[jss::issuer] = toBase58(vaultAcct.id());
-                    }
-                    jv[jss::TransactionType] = jss::TrustSet;
-                    return jv;
-                }();
-
-                trustSet[jss::Flags] = tfSetFreeze;
-                env(trustSet);
-                env.close();
-
-                TER const terExpected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
-
-                // Self-withdraw
-                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                    Ter(terExpected));
-                // Withdraw to 3rd party
-
-                auto withdrawToCharlie =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
-                withdrawToCharlie[sfDestination] = charlie.human();
-                env(withdrawToCharlie, Ter(terExpected));
-
-                trustSet[jss::Flags] = tfClearFreeze;
-                env(trustSet);
-                env.close();
-            }
-
-            // Depositor freeze, self-withdraw
-            {
-                testcase("VaultWithdraw IOU self-withdraw freeze check");
-                env(trust(issuer, asset(0), owner, tfSetFreeze));
-
-                // Post-fix: self-withdraw allowed (submitter==dst skip)
-                // Pre-fix: isFrozen(depositor, iou) catches it
-                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                    Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
-
-                // Depositor freeze withdraw to 3rd party
-                auto withdrawTo3rd =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
-                withdrawTo3rd[sfDestination] = charlie.human();
-
-                // Post-fix: submitter freeze blocks withdraw to 3rd party
-                // Pre-fix: submitter's IOU freeze not checked, but checkFrozen(depositor,
-                // share) triggers tecLOCKED
-                env(withdrawTo3rd, Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
-
-                env(trust(issuer, asset(0), owner, tfClearFreeze));
-                // Replenish what was withdrawn
-                if (fix330Enabled)
-                {
-                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
-                }
-                env.close();
-            }
-
-            // Depositor deep freeze → self-withdraw blocked
-            {
-                testcase("VaultWithdraw IOU depositor deep freeze");
-                env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze));
-
-                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
-                    Ter(tecFROZEN));
-
-                env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze));
-            }
-
-            // Destination freeze → withdraw to 3rd party
-            {
-                testcase("VaultWithdraw IOU freeze withdraw to 3rd party");
-
-                env(trust(issuer, asset(0), charlie, tfSetFreeze));
-
-                // Self-withdraw unaffected by charlie's freeze
-                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
-
-                auto withdrawToCharlie =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
-                withdrawToCharlie[sfDestination] = charlie.human();
-
-                // Post-fix: freeze on dst allowed
-                // Pre-fix: checkFrozen(dst, iou) catches it
-                env(withdrawToCharlie, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
-
-                env(trust(issuer, asset(0), charlie, tfClearFreeze));
-
-                // Replenish: 1 for self-withdraw + 1 if charlie withdraw succeeded
-                env(vault.deposit(
-                    {.depositor = owner,
-                     .id = keylet.key,
-                     .amount = asset(fix330Enabled ? 2 : 1)}));
-                env.close();
-            }
-
-            // Destination deep freeze → withdraw to 3rd party blocked
-            {
-                testcase("VaultWithdraw IOU deep freeze withdraw to 3rd party");
-
-                env(trust(issuer, asset(0), charlie, tfSetFreeze | tfSetDeepFreeze));
-
-                auto withdrawToCharlie =
-                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
-                withdrawToCharlie[sfDestination] = charlie.human();
-                env(withdrawToCharlie, Ter(tecFROZEN));
-
-                // Destination deep freeze → self-withdraw unaffected
-                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
-
-                env(trust(issuer, asset(0), charlie, tfClearFreeze | tfClearDeepFreeze));
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
-                env.close();
-            }
-
-            // Clawback works while frozen
-            {
-                testcase("VaultWithdraw IOU freeze clawback unaffected");
-                env(fset(issuer, asfGlobalFreeze));
-
-                env(vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)}));
-
-                env(fclear(issuer, asfGlobalFreeze));
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
-                env.close();
-            }
-        };
-
-        runTests();
-        env.disableFeature(fixCleanup3_3_0);
-        runTests();
-        env.enableFeature(fixCleanup3_3_0);
-    }
-
-    void
-    testVaultWithdrawFreezeMPT()
-    {
-        using namespace test::jtx;
-        testcase("VaultWithdraw MPT lock checks");
-
-        Account const issuer{"issuer"};
-        Account const owner{"owner"};
-        Env env{*this};
-        Vault vault{env};
-
-        env.fund(XRP(100'000), issuer, owner);
-        env.close();
-
-        MPTTester mptt{env, issuer, kMptInitNoFund};
-        mptt.create(
-            {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
-        PrettyAsset const mpt{mptt.issuanceID()};
-
-        mptt.authorize({.account = owner});
-        mptt.authorize({.account = issuer, .holder = owner});
-        env.close();
-        env(pay(issuer, owner, mpt(100'000)));
-        env.close();
-
-        auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt});
-        env(tx);
-        env.close();
-        Account const vaultAcct("vault", env.le(keylet)->at(sfAccount));
-
-        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)}));
-        env.close();
-
-        Account const charlie{"charlie"};
-        env.fund(XRP(10'000), charlie);
-        env.close();
-        mptt.authorize({.account = charlie});
-        mptt.authorize({.account = issuer, .holder = charlie});
-        env.close();
-
-        auto runTests = [&]() {
-            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
-
-            // Global lock
-            {
-                testcase("VaultWithdraw MPT global lock");
-                mptt.set({.flags = tfMPTLock});
-                env.close();
-                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
-                    Ter(tecLOCKED));
-
-                // Global lock → withdraw to issuer
-                // Post-fix: bypasses freeze checks, but accountHolds
-                //           on the pseudo returns 0 under global lock
-                // Pre-fix: checkFrozen(dst=issuer) catches global lock
-                {
-                    auto withdrawToIssuer =
-                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
-                    withdrawToIssuer[sfDestination] = issuer.human();
-                    env(withdrawToIssuer, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecLOCKED)));
-                }
-                mptt.set({.flags = tfMPTUnlock});
-                env.close();
-                if (fix330Enabled)
-                {
-                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
-                }
-                env.close();
-            }
-
-            // Vault pseudo-account individual lock
-            {
-                testcase("VaultWithdraw MPT pseudo-account lock");
-                mptt.set({.holder = vaultAcct, .flags = tfMPTLock});
-                env.close();
-                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
-                    Ter(tecLOCKED));
-                mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock});
-                env.close();
-            }
-
-            // Depositor individual lock → self-withdraw blocked
-            // (isDeepFrozen == isFrozen for MPT)
-            {
-                testcase("VaultWithdraw MPT depositor lock");
-                mptt.set({.holder = owner, .flags = tfMPTLock});
-                env.close();
-                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
-                    Ter(tecLOCKED));
-                // Depositor lock → withdraw to 3rd party also blocked
-                {
-                    auto withdrawToCharlie =
-                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
-                    withdrawToCharlie[sfDestination] = charlie.human();
-                    env(withdrawToCharlie, Ter(tecLOCKED));
-                }
-
-                // Depositor lock → withdraw to issuer
-                // Post-fix: issuer bypass in checkWithdrawFreezes
-                // Pre-fix: checkFrozen(depositor, share) blocks transitively
-                {
-                    auto withdrawToIssuer =
-                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
-                    withdrawToIssuer[sfDestination] = issuer.human();
-                    env(withdrawToIssuer, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecLOCKED)));
-                }
-                mptt.set({.holder = owner, .flags = tfMPTUnlock});
-                env.close();
-                if (fix330Enabled)
-                {
-                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
-                }
-                env.close();
-            }
-
-            // 3rd party destination lock → withdraw to 3rd party blocked
-            {
-                testcase("VaultWithdraw MPT 3rd party destination lock");
-                mptt.set({.holder = charlie, .flags = tfMPTLock});
-                env.close();
-                {
-                    auto withdrawToCharlie =
-                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
-                    withdrawToCharlie[sfDestination] = charlie.human();
-                    env(withdrawToCharlie, Ter{tecLOCKED});
-                }
-                // 3rd party lock → self-withdraw unaffected
-                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
-                mptt.set({.holder = charlie, .flags = tfMPTUnlock});
-                env.close();
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
-                env.close();
-            }
-
-            // Clawback works while locked
-            {
-                testcase("VaultWithdraw MPT lock clawback unaffected");
-                mptt.set({.flags = tfMPTLock});
-                env.close();
-                env(vault.clawback(
-                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = mpt(1)}));
-                mptt.set({.flags = tfMPTUnlock});
-                env.close();
-                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
-                env.close();
-            }
-        };
-
-        runTests();
-        env.disableFeature(fixCleanup3_3_0);
-        runTests();
-        env.enableFeature(fixCleanup3_3_0);
-    }
-
-public:
-    void
-    run() override
-    {
-        testVaultWithdrawEqualityEnforced();
-        testBugIssuerVaultDepositAtEdge();
-        testBugMakeDeltaPosteriorScale();
-        testBugMakeDeltaAnteriorScale();
-        testVaultDepositCanonicalizeToZero();
-        testVaultWithdrawCanonicalizeToZero();
-        testVaultDepositNegativeBalanceFromOppositeLimit();
-        testSequences();
-        testPreflight();
-        testCreateFailXRP();
-        testCreateFailIOU();
-        testCreateFailMPT();
-        testWithMPT();
-        testWithIOU();
-        testWithDomainCheck();
-        testWithDomainChecXRP();
-        testNonTransferableShares();
-        testFailedPseudoAccount();
-        testScaleIOU();
-        testRPC();
-        testVaultClawbackBurnShares();
-        testVaultClawbackAssets();
-        testVaultEscrowedMPT();
-        testAssetsMaximum();
-        testVaultDeleteMemoData();
-        testVaultCreateLEVersion();
-        testBug6LimitBypassWithShares();
-        testRemoveEmptyHoldingLockedAmount();
-        testRemoveEmptyHoldingConfidentialBalances();
-
-        testWithdrawSoleShareholderFixedAssetExit(all_ - fixCleanup3_2_0);
-        testWithdrawSoleShareholderFixedAssetExit(all_);
-        testWithdrawSoleShareholderFullSharesRejected(all_ - fixCleanup3_2_0);
-        testWithdrawSoleShareholderFullSharesRejected(all_);
-        testWithdrawSoleShareholderCleanVaultUnaffected(all_ - fixCleanup3_2_0);
-        testWithdrawSoleShareholderCleanVaultUnaffected(all_);
-        testWithdrawSoleShareholderPartialFixedSharesUsesFullPrice();
-        testWithdrawSoleShareholderLoanRepaymentExit();
-
-        testVaultDepositFreezeIOU();
-        testVaultDepositFreezeMPT();
-        testVaultWithdrawFreezeIOU();
-        testVaultWithdrawFreezeMPT();
-        testVaultSelfWithdrawWhileFrozen();
-
-        testReferenceHolding();
-        testHoldingDeletionBlocked();
-    }
-};
-
-BEAST_DEFINE_TESTSUITE_PRIO(Vault, app, xrpl, 1);
-
-}  // namespace xrpl
diff --git a/src/test/app/Wasm_test.cpp b/src/test/app/Wasm_test.cpp
deleted file mode 100644
index 50481875d4..0000000000
--- a/src/test/app/Wasm_test.cpp
+++ /dev/null
@@ -1,1607 +0,0 @@
-#include 
-#ifdef _DEBUG
-// #define DEBUG_OUTPUT 1
-#endif
-
-#include 
-#include 
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include   // IWYU pragma: keep
-#include 
-#include 
-#include 
-
-#include 
-
-#include 
-
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-#include 
-
-namespace xrpl::test {
-
-bool
-testGetDataIncrement();
-
-using Add_proto = int32_t(int32_t, int32_t);
-static wasm_trap_t*
-add(HostFunctions&, wasm_val_vec_t const* params, wasm_val_vec_t* results)
-{
-    int32_t const val1 = params->data[0].of.i32;
-    int32_t const val2 = params->data[1].of.i32;
-    // printf("Host function \"Add\": %d + %d\n", Val1, Val2);
-    results->data[0] = WASM_I32_VAL(val1 + val2);
-    return nullptr;
-}
-
-std::vector
-hexToBytes(std::string const& hex)
-{
-    auto const ws = boost::algorithm::unhex(hex);
-    return Bytes(ws.begin(), ws.end());
-}
-
-template 
-unsigned
-uleb128(IT& it, T val)
-{
-    unsigned count = 0;
-    do
-    {
-        std::uint8_t byte = val & 0x7f;
-        val >>= 7;
-        if (val)
-            byte |= 0x80;
-        *it++ = byte;
-        ++count;
-    } while (val != 0);
-
-    return count;
-}
-
-template 
-std::pair
-uleb128(IT&& it)
-{
-    static_assert(sizeof(*it) == 1, "invalid iterator type");
-    std::uint64_t val = 0;
-    std::uint64_t byte = 0;
-    unsigned shift = 0;
-    unsigned count = 0;
-
-    do
-    {
-        if (shift > (sizeof(std::uint64_t) * 8) - 7)
-            return {0, 0};
-        byte = *it++;
-        val |= (byte & 0x7F) << shift;
-        shift += 7;
-        ++count;
-    } while (byte >= 0x80);
-
-    return {val, count};
-}
-
-static std::pair
-getSection(Bytes const& module, std::uint8_t n)
-{
-    static std::uint8_t const kHdr[] = {0x00, 0x61, 0x73, 0x6D};
-    static std::uint8_t const kVer[] = {0x01, 0x00, 0x00, 0x00};
-    static std::uint8_t const kLastSec = 12;
-
-    // sections:
-    // 0: "Custom", 1: "Type", 2: "Import", 3: "Function", 4: "Table", 5: "Memory", 6: "Global",
-    // 7: "Export", 8: "Start", 9: "Element", 10: "Code", 11: "Data", 12: "DataCount"
-
-    if (module.size() < sizeof(kHdr) + sizeof(kVer) + 2)
-        return {0, 0};
-    if (memcmp(module.data(), kHdr, sizeof(kHdr)) != 0)
-        return {0, 0};
-    if (memcmp(module.data() + sizeof(kHdr), kVer, sizeof(kVer)) != 0)
-        return {0, 0};
-
-    unsigned pos = sizeof(kHdr) + sizeof(kVer);  // sections start
-    for (; pos < module.size();)
-    {
-        auto const start = pos;
-        std::uint8_t const byte = module[pos++];
-        if (byte > kLastSec)
-            return {0, 0};
-
-        auto [sz, cnt] = uleb128(module.cbegin() + pos);
-        if (cnt == 0u)
-            return {0, 0};
-        if (pos + cnt + sz > module.size())
-            return {0, 0};
-        pos += cnt + sz;
-
-        if (byte == n)
-            return {start, pos};
-    }
-    return {0, 0};
-}
-
-static std::optional
-runFinish(std::string const& code)
-{
-    auto& engine = WasmEngine::instance();
-    auto const wasm = hexToBytes(code);
-    HostFunctions hfs;
-    auto const re = engine.run(wasm, hfs, 10'000'000, escrowFunctionName);
-    if (re.has_value())
-    {
-        return std::optional(re->result);
-    }
-
-    return std::nullopt;
-}
-
-static bool
-finishFunctionReturns(std::string const& code, int32_t expected)
-{
-    auto const result = runFinish(code);
-    return result.has_value() && *result == expected;
-}
-
-struct Wasm_test : public beast::unit_test::Suite
-{
-    void
-    checkResult(
-        std::expected, WasmTER> re,
-        int32_t expectedResult,
-        int64_t expectedCost,
-        std::source_location const location = std::source_location::current())
-    {
-        auto const lineStr = " (" + std::to_string(location.line()) + ")";
-        if (BEAST_EXPECTS(re.has_value(), transToken(re.error().ter) + lineStr))
-        {
-            BEAST_EXPECTS(re->result == expectedResult, std::to_string(re->result) + lineStr);
-            BEAST_EXPECTS(re->cost == expectedCost, std::to_string(re->cost) + lineStr);
-        }
-    }
-
-    void
-    testGetDataHelperFunctions()
-    {
-        testcase("getData helper functions");
-        BEAST_EXPECT(testGetDataIncrement());
-    }
-
-    void
-    testWasmLib()
-    {
-        testcase("wasm lib test");
-        // clang-format off
-        /* The WASM module buffer. */
-        Bytes const wasm = {/* WASM header */
-                          0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00,
-                          /* Type section */
-                          0x01, 0x07, 0x01,
-                          /* function type {i32, i32} -> {i32} */
-                          0x60, 0x02, 0x7F, 0x7F, 0x01, 0x7F,
-                          /* Import section */
-                          0x02, 0x13, 0x01,
-                          /* module name: "extern" */
-                          0x06, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6E,
-                          /* extern name: "func-add" */
-                          0x08, 0x66, 0x75, 0x6E, 0x63, 0x2D, 0x61, 0x64, 0x64,
-                          /* import desc: func 0 */
-                          0x00, 0x00,
-                          /* Function section */
-                          0x03, 0x02, 0x01, 0x00,
-                          /* Export section */
-                          0x07, 0x0A, 0x01,
-                          /* export name: "addTwo" */
-                          0x06, 0x61, 0x64, 0x64, 0x54, 0x77, 0x6F,
-                          /* export desc: func 0 */
-                          0x00, 0x01,
-                          /* Code section */
-                          0x0A, 0x0A, 0x01,
-                          /* code body */
-                          0x08, 0x00, 0x20, 0x00, 0x20, 0x01, 0x10, 0x00, 0x0B};
-        // clang-format on
-        auto& vm = WasmEngine::instance();
-
-        HostFunctions hfs;
-        ImportVec imports;
-        WasmImpFunc(imports, "func-add", add, hfs);
-
-        auto re = vm.run(wasm, hfs, 10'000'000, "addTwo", wasmParams(1234, 5678), imports);
-
-        // if (res) printf("invokeAdd get the result: %d\n", res.value());
-
-        checkResult(re, 6'912, 59);
-    }
-
-    void
-    testBadWasm()
-    {
-        testcase("bad wasm test");
-
-        using namespace test::jtx;
-
-        Env const env{*this};
-        HostFunctions hfs(env.journal);
-
-        {
-            auto wasm = hexToBytes("00000000");
-            std::string const funcName("mock_escrow");
-
-            auto re = runEscrowWasm(wasm, hfs, 15, funcName, {});
-            BEAST_EXPECT(!re);
-        }
-
-        {
-            auto wasm = hexToBytes("00112233445566778899AA");
-            std::string const funcName("mock_escrow");
-
-            auto const re = preflightEscrowWasm(wasm, hfs, funcName);
-            BEAST_EXPECT(!isTesSuccess(re));
-        }
-
-        {
-            // Bytecode wrong function name
-            // pub fn bad() -> bool {
-            //     unsafe { host_lib::getLedgerSqn() >= 5 }
-            // }
-            auto const badWasm = hexToBytes(
-                "0061736d010000000105016000017f02190108686f73745f6c69620c6765"
-                "744c656467657253716e00000302010005030100100611027f00418080c0"
-                "000b7f00418080c0000b072b04066d656d6f727902000362616400010a5f"
-                "5f646174615f656e6403000b5f5f686561705f6261736503010a09010700"
-                "100041044a0b004d0970726f64756365727302086c616e67756167650104"
-                "52757374000c70726f6365737365642d6279010572757374631d312e3835"
-                "2e31202834656231363132353020323032352d30332d31352900490f7461"
-                "726765745f6665617475726573042b0f6d757461626c652d676c6f62616c"
-                "732b087369676e2d6578742b0f7265666572656e63652d74797065732b0a"
-                "6d756c746976616c7565");
-
-            auto const re = preflightEscrowWasm(badWasm, hfs, escrowFunctionName);
-            BEAST_EXPECT(!isTesSuccess(re));
-        }
-    }
-
-    void
-    testWasmLedgerSqn()
-    {
-        testcase("Wasm get ledger sequence");
-
-        auto ledgerSqnWasm = hexToBytes(kLedgerSqnWasmHex);
-
-        using namespace test::jtx;
-
-        Env env{*this};
-        TestLedgerDataProvider hfs(env);
-        ImportVec imports;
-        WASM_IMPORT_FUNC2(imports, getLedgerSqn, "ldgr_index", hfs, 33);
-        auto& engine = WasmEngine::instance();
-
-        auto re =
-            engine.run(ledgerSqnWasm, hfs, 1'000'000, escrowFunctionName, {}, imports, env.journal);
-
-        checkResult(re, 0, 440);
-
-        env.close();
-        env.close();
-
-        // empty module, throwing exception
-        re = engine.run({}, hfs, 1'000'000, escrowFunctionName, {}, imports, env.journal);
-        BEAST_EXPECT(!re);
-        env.close();
-    }
-
-    void
-    testImpExp()
-    {
-        testcase("Wasm import/export functions");
-
-        auto impExpWasm = hexToBytes(kImpExpHex);
-
-        using namespace test::jtx;
-
-        Env env{*this};
-        TestLedgerDataProvider hfs(env);
-        ImportVec imports;
-        WASM_IMPORT_FUNC2(imports, getLedgerSqn, "get_ledger_sqn", hfs, 33);
-        WASM_IMPORT_FUNC2(imports, getParentLedgerHash, "get_parent_ledger_hash", hfs, 60);
-        auto& engine = WasmEngine::instance();
-
-        // Test exp_func1() - should return 1
-        auto re = engine.run(impExpWasm, hfs, 1'000'000, "exp_func1", {}, imports, env.journal);
-        checkResult(re, 1, 30);
-
-        // Test exp_func2(5) - should return 2 * 5 = 10
-        re = engine.run(
-            impExpWasm, hfs, 1'000'000, "exp_func2", wasmParams(5), imports, env.journal);
-        checkResult(re, 10, 52);
-
-        // Test test_imports() - should call get_ledger_sqn and get_parent_ledger_hash
-        re = engine.run(impExpWasm, hfs, 1'000'000, "test_imports", {}, imports, env.journal);
-        // Should return the ledger sequence number (3 by default in test env)
-        checkResult(re, 3, 294);
-
-        // Test corrupted import/export sections - invert each byte and expect failure
-        testcase("Wasm import/export section corruption");
-        {
-            // Import section(#2): bytes [26, 79) - 53 bytes
-            // Export section(#7): bytes [90, 141) - 51 bytes
-            auto [importStart, importEnd] = getSection(impExpWasm, 2);
-            auto [exportStart, exportEnd] = getSection(impExpWasm, 7);
-
-            BEAST_EXPECTS(importStart == 26, std::to_string(importStart));
-            BEAST_EXPECTS(importEnd == 79, std::to_string(importEnd));
-            BEAST_EXPECTS(exportStart == 90, std::to_string(exportStart));
-            BEAST_EXPECTS(exportEnd == 141, std::to_string(exportEnd));
-
-            auto testInv = [&](unsigned i) {
-                auto corruptedWasm = impExpWasm;
-                corruptedWasm[i] = ~corruptedWasm[i];  // Invert byte
-
-                // Try to run any function - should fail due to corruption
-                auto result = engine.run(
-                    corruptedWasm, hfs, 1'000'000, "exp_func1", {}, imports, env.journal);
-                BEAST_EXPECT(!result);
-            };
-
-            // Test each byte in import section
-            for (unsigned i = importStart; i < importEnd; ++i)
-                testInv(i);
-
-            // Test each byte in export section
-            for (unsigned i = exportStart; i < exportEnd; ++i)
-                testInv(i);
-        }
-
-        env.close();
-    }
-
-    void
-    testWasmFib()
-    {
-        testcase("Wasm fibo");
-
-        auto const fibWasm = hexToBytes(kFibWasmHex);
-        auto& engine = WasmEngine::instance();
-        HostFunctions hfs;
-
-        auto const re = engine.run(fibWasm, hfs, 10'000'000, "fib", wasmParams(10));
-
-        checkResult(re, 55, 1'137);
-    }
-
-    void
-    testHFCost()
-    {
-        testcase("wasm test host functions cost");
-
-        using namespace test::jtx;
-
-        Env env(*this);
-        {
-            auto const allHostFuncWasm = hexToBytes(kAllHostFunctionsWasmHex);
-
-            auto& engine = WasmEngine::instance();
-
-            TestHostFunctions hfs(env);
-            auto imp = createWasmImport(hfs);
-            for (auto& i : imp)
-                i.second.second.gas = 0;
-
-            auto re = engine.run(
-                allHostFuncWasm, hfs, 1'000'000, escrowFunctionName, {}, imp, env.journal);
-
-            checkResult(re, 1, 30'760);
-
-            env.close();
-        }
-
-        env.close();
-        env.close();
-        env.close();
-        env.close();
-        env.close();
-
-        {
-            auto const allHostFuncWasm = hexToBytes(kAllHostFunctionsWasmHex);
-
-            auto& engine = WasmEngine::instance();
-
-            TestHostFunctions hfs(env);
-            auto const imp = createWasmImport(hfs);
-
-            auto re = engine.run(
-                allHostFuncWasm, hfs, 1'000'000, escrowFunctionName, {}, imp, env.journal);
-
-            checkResult(re, 1, 48'580);
-
-            env.close();
-        }
-
-        // not enough gas
-        {
-            auto const allHostFuncWasm = hexToBytes(kAllHostFunctionsWasmHex);
-
-            auto& engine = WasmEngine::instance();
-
-            TestHostFunctions hfs(env);
-            auto const imp = createWasmImport(hfs);
-
-            auto re =
-                engine.run(allHostFuncWasm, hfs, 200, escrowFunctionName, {}, imp, env.journal);
-
-            if (BEAST_EXPECT(!re))
-            {
-                // Running out of gas now terminates with tecOUT_OF_GAS (was
-                // previously collapsed into tecFAILED_PROCESSING).
-                BEAST_EXPECTS(re.error().ter == tecOUT_OF_GAS, transToken(re.error().ter));
-            }
-
-            env.close();
-        }
-    }
-
-    void
-    testEscrowWasmDN()
-    {
-        testcase("escrow wasm devnet test");
-
-        auto const allHFWasm = hexToBytes(kAllHostFunctionsWasmHex);
-
-        using namespace test::jtx;
-        Env env{*this};
-        {
-            TestHostFunctions hfs(env);
-            auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName, {});
-            checkResult(re, 1, 48'580);
-        }
-
-        {
-            // Invalid gas limit (0) should be rejected (boundary condition)
-            TestHostFunctions hfs(env);
-            auto re = runEscrowWasm(allHFWasm, hfs, -1, escrowFunctionName, {});
-            BEAST_EXPECT(!re.has_value());
-            BEAST_EXPECT(re.error().ter == temBAD_AMOUNT);
-        }
-
-        {
-            // Invalid gas limit (-1) should be rejected
-            TestHostFunctions hfs(env);
-            auto re = runEscrowWasm(allHFWasm, hfs, 0, escrowFunctionName, {});
-            BEAST_EXPECT(!re.has_value());
-            BEAST_EXPECT(re.error().ter == temBAD_AMOUNT);
-        }
-
-        {
-            // max() gas
-            TestHostFunctions hfs(env);
-            auto re = runEscrowWasm(
-                allHFWasm, hfs, std::numeric_limits::max(), escrowFunctionName, {});
-            checkResult(re, 1, 48'580);
-        }
-
-        {  // fail because trying to access nonexistent field
-            struct FieldNotFoundHostFunctions : public TestHostFunctions
-            {
-                explicit FieldNotFoundHostFunctions(Env& env) : TestHostFunctions(env)
-                {
-                }
-                [[nodiscard]] std::expected
-                getTxField(SField const& fname) const override
-                {
-                    return std::unexpected(HostFunctionError::FieldNotFound);
-                }
-            };
-
-            FieldNotFoundHostFunctions hfs(env);
-            auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName, {});
-            checkResult(re, -201, 28'329);
-        }
-
-        {  // fail because trying to allocate more than MAX_PAGES memory
-            struct OversizedFieldHostFunctions : public TestHostFunctions
-            {
-                explicit OversizedFieldHostFunctions(Env& env) : TestHostFunctions(env)
-                {
-                }
-                [[nodiscard]] std::expected
-                getTxField(SField const& fname) const override
-                {
-                    return Bytes((128 + 1) * 64 * 1024, 1);
-                }
-            };
-
-            OversizedFieldHostFunctions hfs(env);
-            auto re = runEscrowWasm(allHFWasm, hfs, 100'000, escrowFunctionName, {});
-            checkResult(re, -201, 28'329);
-        }
-
-// This test use log output, so DEBUG_OUTPUT  must be disabled.
-#ifndef DEBUG_OUTPUT
-        {  // fail because recursion too deep
-
-            auto const deepWasm = hexToBytes(kDeepRecursionHex);
-
-            TestHostFunctionsSink hfs(env);
-            std::string const funcName(escrowFunctionName);
-            auto re = runEscrowWasm(deepWasm, hfs, 1'000'000'000, funcName, {});
-            BEAST_EXPECT(!re && re.error().ter);
-            // std::cout << "bad case (deep recursion) result " << re.error()
-            //             << std::endl;
-
-            auto const& sink = hfs.getSink();
-            auto countSubstr = [](std::string const& str, std::string const& substr) {
-                std::size_t pos = 0;
-                int occurrences = 0;
-                while ((pos = str.find(substr, pos)) != std::string::npos)
-                {
-                    occurrences++;
-                    pos += substr.length();
-                }
-                return occurrences;
-            };
-
-            auto const s = sink.messages().str();
-            BEAST_EXPECT(countSubstr(s, "WASMI Error: failure to call func") == 1);
-            BEAST_EXPECT(countSubstr(s, "TrapCode(StackOverflow)") > 0);
-        }
-#endif
-
-        {  // infinite loop
-            auto const infiniteLoopWasm = hexToBytes(kInfiniteLoopWasmHex);
-            std::string const funcName("loop");
-            TestHostFunctions hfs(env);
-
-            // infinite loop should be caught and fail
-            auto const re = runEscrowWasm(infiniteLoopWasm, hfs, 1'000'000, funcName, {});
-            if (BEAST_EXPECT(!re.has_value()))
-            {
-                BEAST_EXPECT(re.error().ter == tecOUT_OF_GAS);
-            }
-        }
-
-        {
-            // expected import not provided
-            auto const lgrSqnWasm = hexToBytes(kLedgerSqnWasmHex);
-            TestLedgerDataProvider hfs(env);
-            ImportVec imports;
-            WASM_IMPORT_FUNC2(imports, getLedgerSqn, "get_ledger_sqn2", hfs);
-
-            auto& engine = WasmEngine::instance();
-
-            auto re = engine.run(
-                lgrSqnWasm, hfs, 1'000'000, escrowFunctionName, {}, imports, env.journal);
-
-            BEAST_EXPECT(!re);
-        }
-
-        {
-            // HF unsync between import and VM
-            auto const lgrSqnWasm = hexToBytes(kLedgerSqnWasmHex);
-            TestLedgerDataProvider hfs(env);
-            TestLedgerDataProvider hfs2(env);
-            ImportVec imports;
-            WASM_IMPORT_FUNC2(imports, getLedgerSqn, "get_ledger_sqn", hfs2);
-
-            auto& engine = WasmEngine::instance();
-
-            auto re = engine.run(
-                lgrSqnWasm, hfs, 1'000'000, escrowFunctionName, {}, imports, env.journal);
-
-            BEAST_EXPECT(!re);
-        }
-
-        {
-            // bad function name
-            auto const lgrSqnWasm = hexToBytes(kLedgerSqnWasmHex);
-            TestLedgerDataProvider hfs(env);
-            ImportVec imports;
-            WASM_IMPORT_FUNC2(imports, getLedgerSqn, "get_ledger_sqn", hfs);
-
-            auto& engine = WasmEngine::instance();
-            auto re = engine.run(lgrSqnWasm, hfs, 1'000'000, "func1", {}, imports, env.journal);
-
-            BEAST_EXPECT(!re);
-        }
-    }
-
-    // TODO: testFloat is disabled until the float fixtures are regenerated.
-    //
-    // kFloatTestsWasmHex and kFloat0Hex were built against the old trace ABI,
-    // where the trace_* host functions returned i32. They now return void, so
-    // neither module instantiates and both blocks below fail with tecINTERNAL.
-    //
-    // Regenerating them is not just a rebuild: float_tests/ and float_0/ are
-    // still pinned to xrpl-wasm-stdlib @ "renames" and use APIs that no longer
-    // exist on xrpl-common-stdlib @ "error-and-trace"
-    // (FLOAT_ROUNDING_MODES_TO_NEAREST became RoundingMode,
-    // core::locator::Locator moved to fields::locator::Locator, and
-    // trace_data/DataRepr became trace_float/trace_hex). The fixture sources
-    // have to be ported first. The expected gas costs below are also stale and
-    // will need recomputing once the modules run again.
-    //
-    // void
-    // testFloat()
-    // {
-    //     testcase("float point");
-    //
-    //     std::string const funcName(escrowFunctionName);
-    //
-    //     using namespace test::jtx;
-    //
-    //     Env env(*this);
-    //     {
-    //         auto const floatTestWasm = hexToBytes(kFloatTestsWasmHex);
-    //
-    //         TestHostFunctions hfs(env);
-    //         auto re = runEscrowWasm(floatTestWasm, hfs, 200'000, funcName,
-    //         {}); checkResult(re, 1, 134'402); env.close();
-    //     }
-    //
-    //     {
-    //         auto const float0Wasm = hexToBytes(kFloat0Hex);
-    //
-    //         TestHostFunctions hfs(env);
-    //         auto re = runEscrowWasm(float0Wasm, hfs, 100'000, funcName, {});
-    //         checkResult(re, 1, 2'775);
-    //         env.close();
-    //     }
-    // }
-
-    void
-    testCodecovWasm()
-    {
-        testcase("Codecov wasm test");
-
-        using namespace test::jtx;
-
-        Env env{*this};
-
-        auto const codecovWasm = hexToBytes(kCodecovTestsWasmHex);
-        TestHostFunctions hfs(env);
-
-        auto const allowance = 264'467;
-        auto re = runEscrowWasm(codecovWasm, hfs, allowance, escrowFunctionName, {});
-
-        checkResult(re, 1, allowance);
-    }
-
-    void
-    testDisabledFloat()
-    {
-        testcase("disabled float");
-
-        using namespace test::jtx;
-        Env env{*this};
-
-        auto disabledFloatWasm = hexToBytes(kDisabledFloatHex);
-        std::string const funcName(escrowFunctionName);
-        TestHostFunctions hfs(env);
-
-        {
-            // f32 set constant, opcode disabled exception
-            auto const re = runEscrowWasm(disabledFloatWasm, hfs, 1'000'000, funcName, {});
-            if (BEAST_EXPECT(!re.has_value()))
-            {
-                BEAST_EXPECT(re.error().ter == tecFAILED_PROCESSING);
-            }
-        }
-
-        {
-            // f32 add, can't create module exception
-            disabledFloatWasm[0x11e] = 0x92;
-            auto const re = runEscrowWasm(disabledFloatWasm, hfs, 1'000'000, funcName, {});
-            if (BEAST_EXPECT(!re.has_value()))
-            {
-                BEAST_EXPECT(re.error().ter == tecFAILED_PROCESSING);
-            }
-        }
-    }
-
-    void
-    testWasmMemory()
-    {
-        testcase("Wasm additional memory limit tests");
-        BEAST_EXPECT(finishFunctionReturns(kMemoryPointerAtLimitHex, 1));
-        BEAST_EXPECT(!runFinish(kMemoryPointerOverLimitHex).has_value());
-        BEAST_EXPECT(!runFinish(kMemoryOffsetOverLimitHex).has_value());
-        BEAST_EXPECT(!runFinish(kMemoryEndOfWordOverLimitHex).has_value());
-        BEAST_EXPECT(finishFunctionReturns(kMemoryGrow0To1PageHex, 1));
-        BEAST_EXPECT(finishFunctionReturns(kMemoryGrow1To0PageHex, -1));
-        BEAST_EXPECT(finishFunctionReturns(kMemoryLastByteOf8MbHex, 1));
-        BEAST_EXPECT(finishFunctionReturns(kMemoryGrow1MoreThan8MbHex, -1));
-        BEAST_EXPECT(finishFunctionReturns(kMemoryGrow0MoreThan8MbHex, 1));
-        BEAST_EXPECT(!runFinish(kMemoryInit1MoreThan8MbHex).has_value());
-        BEAST_EXPECT(!runFinish(kMemoryNegativeAddressHex).has_value());
-    }
-
-    void
-    testWasmTable()
-    {
-        testcase("Wasm table limit tests");
-        BEAST_EXPECT(finishFunctionReturns(kTable64ElementsHex, 1));
-        BEAST_EXPECT(!runFinish(kTable65ElementsHex).has_value());
-        BEAST_EXPECT(!runFinish(kTable2TablesHex).has_value());
-        BEAST_EXPECT(finishFunctionReturns(kTable0ElementsHex, 1));
-        BEAST_EXPECT(!runFinish(kTableUintMaxHex).has_value());
-    }
-
-    void
-    testWasmProposal()
-    {
-        testcase("Wasm disabled proposal tests");
-        BEAST_EXPECT(!runFinish(kProposalMutableGlobalHex).has_value());
-        BEAST_EXPECT(!runFinish(kProposalGcStructNewHex).has_value());
-        BEAST_EXPECT(!runFinish(kProposalMultiValueHex).has_value());
-        BEAST_EXPECT(!runFinish(kProposalSignExtHex).has_value());
-        BEAST_EXPECT(!runFinish(kProposalFloatToIntHex).has_value());
-        BEAST_EXPECT(!runFinish(kProposalBulkMemoryHex).has_value());
-        BEAST_EXPECT(!runFinish(kProposalRefTypesHex).has_value());
-        BEAST_EXPECT(!runFinish(kProposalTailCallHex).has_value());
-        BEAST_EXPECT(!runFinish(kProposalExtendedConstHex).has_value());
-        BEAST_EXPECT(!runFinish(kProposalMultiMemoryHex).has_value());
-        BEAST_EXPECT(!runFinish(kProposalCustomPageSizesHex).has_value());
-        BEAST_EXPECT(!runFinish(kProposalMemory64Hex).has_value());
-        BEAST_EXPECT(!runFinish(kProposalWideArithmeticHex).has_value());
-    }
-
-    void
-    testWasmTrap()
-    {
-        testcase("Wasm trap tests");
-        BEAST_EXPECT(!runFinish(kTrapDivideBy0Hex).has_value());
-        BEAST_EXPECT(!runFinish(kTrapIntOverflowHex).has_value());
-        BEAST_EXPECT(!runFinish(kTrapUnreachableHex).has_value());
-        BEAST_EXPECT(!runFinish(kTrapNullCallHex).has_value());
-        BEAST_EXPECT(!runFinish(kTrapFuncSigMismatchHex).has_value());
-    }
-
-    void
-    testWasmWasi()
-    {
-        testcase("Wasm Wasi tests");
-        BEAST_EXPECT(!runFinish(kWasiGetTimeHex).has_value());
-        BEAST_EXPECT(!runFinish(kWasiPrintHex).has_value());
-    }
-
-    void
-    testWasmSectionCorruption()
-    {
-        testcase("Wasm Section Corruption tests");
-        BEAST_EXPECT(!runFinish(kBadMagicNumberHex).has_value());
-        BEAST_EXPECT(!runFinish(kBadVersionNumberHex).has_value());
-        BEAST_EXPECT(!runFinish(kLyingHeaderHex).has_value());
-        BEAST_EXPECT(!runFinish(kNeverEndingNumberHex).has_value());
-        BEAST_EXPECT(!runFinish(kVectorLieHex).has_value());
-        BEAST_EXPECT(!runFinish(kSectionOrderingHex).has_value());
-        BEAST_EXPECT(!runFinish(kGhostPayloadHex).has_value());
-        BEAST_EXPECT(!runFinish(kJunkAfterSectionHex).has_value());
-        BEAST_EXPECT(!runFinish(kInvalidSectionIdHex).has_value());
-        BEAST_EXPECT(!runFinish(kLocalVariableBombHex).has_value());
-    }
-
-    void
-    testStartFunctionLoop()
-    {
-        testcase("infinite loop in start function");
-
-        using namespace test::jtx;
-        Env env(*this);
-
-        auto const startLoopWasm = hexToBytes(kStartLoopHex);
-        TestLedgerDataProvider hfs(env);
-        ImportVec const imports;
-
-        auto& engine = WasmEngine::instance();
-        auto checkRes =
-            engine.check(startLoopWasm, hfs, escrowFunctionName, {}, imports, env.journal);
-        BEAST_EXPECTS(checkRes == tesSUCCESS, transToken(checkRes));
-
-        auto result =
-            engine.run(startLoopWasm, hfs, 1'000'000, escrowFunctionName, {}, imports, env.journal);
-        auto resultTer = result.error().ter;
-        BEAST_EXPECTS(resultTer == tecFAILED_PROCESSING, transToken(resultTer));
-    }
-
-    void
-    testBadAlign()
-    {
-        testcase("Wasm Bad Align");
-
-        // bad_align.c
-        auto const badAlignWasm = hexToBytes(kBadAlignWasmHex);
-
-        using namespace test::jtx;
-
-        Env env{*this};
-        TestHostFunctions hfs(env);
-        auto imports = createWasmImport(hfs);
-
-        {  // Calls float_from_uint with bad alignment.
-           // Can be checked through codecov
-            auto& engine = WasmEngine::instance();
-
-            auto re = engine.run(badAlignWasm, hfs, 1'000'000, "test", {}, imports, env.journal);
-            if (BEAST_EXPECTS(re, transToken(re.error().ter)))
-            {
-                BEAST_EXPECTS(re->result == 0x47308594, std::to_string(re->result));
-            }
-        }
-
-        env.close();
-    }
-
-    void
-    testReturnType()
-    {
-        using namespace test::jtx;
-        Env env(*this);
-        TestHostFunctions hfs(env);
-
-        testcase("Wasm invalid return type");
-
-        // return int64.
-        {  // (module
-            //   (memory (export "memory") 1)
-            //   (func (export "finish") (result i64)
-            //     i64.const 0x100000000))
-            auto const wasmHex =
-                "0061736d010000000105016000017e030201000503010001"
-                "071302066d656d6f727902000666696e69736800000a0a01"
-                "08004280808080100b";
-            auto const wasm = hexToBytes(wasmHex);
-            auto const re = runEscrowWasm(wasm, hfs, 100'000, escrowFunctionName, {});
-            BEAST_EXPECT(!re);
-        }
-
-        // return void. wasmi return execution error
-        {  //(module
-           //  (type (;0;) (func))
-           //  (func (;0;) (type 0)
-           //   return)
-           //  (memory (;0;) 1)
-           //  (export "memory" (memory 0))
-           //  (export "finish" (func 0)))
-            auto const wasmHex =
-                "0061736d01000000010401600000030201000503010001071302066d656d6f"
-                "727902000666696e69736800000a050103000f0b";
-            auto const wasm = hexToBytes(wasmHex);
-            auto const re = runEscrowWasm(wasm, hfs, 100'000, escrowFunctionName, {});
-            BEAST_EXPECT(!re);
-        }
-
-        // return i32, i32. wasmi doesn't create module
-        {  //(module
-           //  (memory (export "memory") 1)
-           //  (func (export "finish") (result i32 i32)
-           //   i32.const 0x10000000
-           //   i32.const 0x100000FF))
-            auto const wasmHex =
-                "0061736d010000000106016000027f7f030201000503010001071302066d65"
-                "6d6f727902000666696e69736800000a10010e0041808080800141ff818080"
-                "010b";
-            auto const wasm = hexToBytes(wasmHex);
-            auto const re = runEscrowWasm(wasm, hfs, 100'000, escrowFunctionName, {});
-            BEAST_EXPECT(!re);
-        }
-    }
-
-    void
-    testParameterType()
-    {
-        using namespace test::jtx;
-        Env env(*this);
-        TestHostFunctions hfs(env);
-
-        testcase("Wasm invalid params");
-
-        // (module
-        //   (memory (export "memory") 1)
-        //   (func $test1 (export "test1") (param i32) (result i32)
-        //     i32.const 1000)
-        //   (func $test2 (export "test2") (param i32 i32) (result i32)
-        //     i32.const 1001))
-        auto const wasmHex =
-            "0061736d01000000010c0260017f017f60027f7f017f03030200010503010001071a03066d656d6f727902"
-            "00057465737431000005746573743200010a0d02050041e8070b050041e9070b";
-        auto const wasm = hexToBytes(wasmHex);
-
-        // good params, module is working properly
-        {
-            auto const re = runEscrowWasm(wasm, hfs, 100'000, "test2", wasmParams(2, 10));
-            BEAST_EXPECT(re && re->result == 1001 && re->cost == 37);
-        }
-
-        // no params
-        {
-            auto const re = runEscrowWasm(wasm, hfs, 100'000, "test1", {});
-            BEAST_EXPECT(!re);
-        }
-
-        // more params
-        {
-            auto const re = runEscrowWasm(wasm, hfs, 100'000, "test1", wasmParams(0, 1));
-            BEAST_EXPECT(!re);
-        }
-
-        // less params
-        {
-            auto const re = runEscrowWasm(wasm, hfs, 100'000, "test2", wasmParams(1));
-            BEAST_EXPECT(!re);
-        }
-
-        // invalid type
-        {
-            auto const re =
-                runEscrowWasm(wasm, hfs, 100'000, "test1", wasmParams(std::int64_t(15)));
-            BEAST_EXPECT(!re);
-        }
-    }
-
-    void
-    testSwapBytes()
-    {
-        testcase("Wasm swap bytes");
-
-        uint64_t const swapDataU64 = 0x123456789abcdeffull;
-        uint64_t const reverseSwapDataU64 = 0xffdebc9a78563412ull;
-        int64_t const swapDataI64 = 0x123456789abcdeffll;
-        int64_t const reverseSwapDataI64 = 0xffdebc9a78563412ll;
-
-        uint32_t const swapDataU32 = 0x12789aff;
-        uint32_t const reverseSwapDataU32 = 0xff9a7812;
-        int32_t const swapDataI32 = 0x12789aff;
-        int32_t const reverseSwapDataI32 = 0xff9a7812;
-
-        uint16_t const swapDataU16 = 0x12ff;
-        uint16_t const reverseSwapDataU16 = 0xff12;
-        int16_t const swapDataI16 = 0x12ff;
-        int16_t const reverseSwapDataI16 = 0xff12;
-
-        uint64_t b1 = swapDataU64;
-        int64_t b2 = swapDataI64;
-        b1 = adjustWasmEndianessHlp(b1);
-        b2 = adjustWasmEndianessHlp(b2);
-        BEAST_EXPECT(b1 == reverseSwapDataU64);
-        BEAST_EXPECT(b2 == reverseSwapDataI64);
-        b1 = adjustWasmEndianessHlp(b1);
-        b2 = adjustWasmEndianessHlp(b2);
-        BEAST_EXPECT(b1 == swapDataU64);
-        BEAST_EXPECT(b2 == swapDataI64);
-
-        uint32_t b3 = swapDataU32;
-        int32_t b4 = swapDataI32;
-        b3 = adjustWasmEndianessHlp(b3);
-        b4 = adjustWasmEndianessHlp(b4);
-        BEAST_EXPECT(b3 == reverseSwapDataU32);
-        BEAST_EXPECT(b4 == reverseSwapDataI32);
-        b3 = adjustWasmEndianessHlp(b3);
-        b4 = adjustWasmEndianessHlp(b4);
-        BEAST_EXPECT(b3 == swapDataU32);
-        BEAST_EXPECT(b4 == swapDataI32);
-
-        uint16_t b5 = swapDataU16;
-        int16_t b6 = swapDataI16;
-        b5 = adjustWasmEndianessHlp(b5);
-        b6 = adjustWasmEndianessHlp(b6);
-        BEAST_EXPECT(b5 == reverseSwapDataU16);
-        BEAST_EXPECT(b6 == reverseSwapDataI16);
-        b5 = adjustWasmEndianessHlp(b5);
-        b6 = adjustWasmEndianessHlp(b6);
-        BEAST_EXPECT(b5 == swapDataU16);
-        BEAST_EXPECT(b6 == swapDataI16);
-    }
-
-    void
-    testManyParams()
-    {
-        testcase("Wasm Many params");
-
-        auto const params1k = hexToBytes(kThousandParamsHex);
-        auto const params1k1 = hexToBytes(kThousand1ParamsHex);
-
-        using namespace test::jtx;
-
-        Env env{*this};
-        TestHostFunctions hfs(env);
-        auto imports = createWasmImport(hfs);
-
-        // add 1k parameter (max that wasmi support)
-        std::vector params;
-        params.reserve(1000);
-        for (int i = 0; i < 1000; ++i)
-            params.push_back({.type = WasmTypes::WtI32, .of = {.i32 = 2 * i}});
-
-        auto& engine = WasmEngine::instance();
-        {
-            auto re = engine.run(params1k, hfs, 1'000'000, "test", params, imports, env.journal);
-            BEAST_EXPECT(re && re->result == 999000);
-        }
-
-        // add 1 more parameter, module can't be created now
-        params.push_back({.type = WasmTypes::WtI32, .of = {.i32 = 2 * 1000}});
-        {
-            auto re = engine.run(params1k1, hfs, 1'000'000, "test", params, imports, env.journal);
-            BEAST_EXPECT(!re);
-        }
-
-        // function that create 10k local variables
-        auto const locals10k = hexToBytes(kLocals10kHex);
-        {
-            auto re = engine.run(
-                locals10k, hfs, 1'000'000, "test", wasmParams(0, 1), imports, env.journal);
-            BEAST_EXPECT(re && re->result == 890'489'442);
-        }
-
-        // module has 5k functions
-        auto const functions5k = hexToBytes(kFunctions5kHex);
-        {
-            auto re = engine.run(
-                functions5k, hfs, 1'000'000, "test0001", wasmParams(2, 3), imports, env.journal);
-            BEAST_EXPECT(re && re->result == 5);
-        }
-
-        env.close();
-    }
-
-    void
-    testOpcodes()
-    {
-        using namespace test::jtx;
-
-        unsigned const reserved = 64;
-        std::uint8_t const nop = 0x01;
-        std::array const codeMarker = {
-            nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop};
-        auto const opcReserved = hexToBytes(kOpcReservedHex);
-
-        Env env{*this};
-        auto& engine = WasmEngine::instance();
-
-        TestHostFunctions hfs(env);
-        auto imports = createWasmImport(hfs);
-        env.close();
-
-        {
-            auto run = [&](std::vector const& code,
-                           bool good = false,
-                           int64_t cost = -1,
-                           std::source_location const location = std::source_location::current()) {
-                auto const lineStr = " (" + std::to_string(location.line()) + ")";
-                auto re =
-                    engine.run(code, hfs, 1'000'000, "all_instructions", {}, imports, env.journal);
-                if (BEAST_EXPECTS(re.has_value() == good, transToken(re.error().ter) + lineStr) &&
-                    good)
-                    BEAST_EXPECTS(re->cost == cost, std::to_string(re->cost) + lineStr);
-            };
-
-            // 1 byte instruction
-            auto test = [&](std::uint8_t start,
-                            std::uint8_t finish,
-                            bool good = false,
-                            int64_t cost = -1,
-                            std::source_location const location = std::source_location::current()) {
-                auto const lineStr = " (" + std::to_string(location.line()) + ")";
-                auto code = opcReserved;
-                auto codeRange = std::ranges::search(code, codeMarker);
-                if (!BEAST_EXPECTS(!codeRange.empty(), lineStr))
-                    return;
-
-                auto it = codeRange.begin();
-                for (std::uint16_t i = start; i <= finish; ++i)
-                {
-                    *it = i;
-                    run(code, good, cost, location);
-                }
-            };
-
-            // 2 bytes instruction
-            auto test2 = [&](std::uint8_t major,
-                             std::uint16_t start,
-                             std::uint16_t finish,
-                             bool good = false,
-                             int64_t cost = -1,
-                             std::source_location const location =
-                                 std::source_location::current()) {
-                auto const lineStr = " (" + std::to_string(location.line()) + ")";
-                auto code = opcReserved;
-                auto codeRange = std::ranges::search(code, codeMarker);
-                if (!BEAST_EXPECTS(!codeRange.empty(), lineStr))
-                    return;
-
-                auto it = codeRange.begin();
-                *it++ = major;
-                for (std::uint16_t i = start; i <= finish; ++i)
-                {
-                    auto it2 = it;
-                    uleb128(it2, i);
-                    run(code, good, cost, location);
-                }
-            };
-
-            // multibytes instructions
-            auto testMB = [&](std::vector const& codeSnap,
-                              bool good = false,
-                              int64_t cost = -1,
-                              std::source_location const location =
-                                  std::source_location::current()) {
-                auto const lineStr = " (" + std::to_string(location.line()) + ")";
-                auto code = opcReserved;
-                auto codeRange = std::ranges::search(code, codeMarker);
-                if (!BEAST_EXPECTS(!codeRange.empty(), lineStr))
-                    return;
-
-                if (!BEAST_EXPECTS(codeSnap.size() < reserved, lineStr))
-                    return;
-                auto it = codeRange.begin();
-                for (auto x : codeSnap)
-                    *it++ = x;
-                run(code, good, cost, location);
-            };
-
-            // normal run
-            testcase("Wasm reserved opcodes main");
-            test(nop, nop, true, 534);
-
-            // reserved main
-            test(0x06, 0x0A);
-            test(0x12, 0x19);
-            test(0x25, 0x27);
-            test(0xC0, 0xFA);
-            test(0xFF, 0xFF);
-
-            // reserved gc, string
-            testcase("Wasm reserved opcodes gc");
-            test2(0xFB, 0x00, 0xBF);  // not supported by compiler
-
-            // reserved FC
-            testcase("Wasm reserved opcodes FC");
-            test2(0xFC, 0x00, 0x07);  // floats, disabled
-            test2(0xFC, 0x12, 0x1F);
-
-            // reserved SIMD
-            testcase("Wasm reserved opcodes SIMD");
-            test2(0xFD, 0x9A, 0x9A);
-            test2(0xFD, 0xA2, 0xA2);
-            test2(0xFD, 0xA5, 0xA6);
-            test2(0xFD, 0xAF, 0xB0);
-            test2(0xFD, 0xB2, 0xB4);
-            test2(0xFD, 0xB8, 0xB8);
-            test2(0xFD, 0xC2, 0xC2);
-            test2(0xFD, 0xC5, 0xC6);
-            test2(0xFD, 0xCF, 0xD0);
-            test2(0xFD, 0xD2, 0xD4);
-            test2(0xFD, 0xE2, 0xE2);
-            test2(0xFD, 0xEE, 0xEE);
-            test2(0xFD, 0x115, 0x12F);
-
-            testcase("Wasm opcodes THREADS");
-            test2(0xFE, 0x00, 0x4F);  // not supported by compiler
-
-            // FC mem instructions
-            testMB({0x41, 0x00, 0x41, 0x00, 0x41, 0x04, 0xFC, 0x08, 0x00, 0x00});  // memory.init
-            testMB({0xFC, 0x09, 0x00});                                            // data.drop
-            testMB({0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xFC, 0x0A, 0x00, 0x00});  // memory.copy
-            testMB({0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xFC, 0x0B, 0x00});        // memory.fill
-            testMB({0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xFC, 0x0C, 0x00, 0x00});  // table.init
-            testMB({0xFC, 0x0D, 0x00});                                            // elem.drop
-            testMB({0x41, 0x00, 0x41, 0x00, 0x41, 0x00, 0xFC, 0x0E, 0x00, 0x00});  // table.copy
-            testMB({0xD2, 0x00, 0x41, 0x00, 0xFC, 0x0F, 0x00, 0x1A});              // table.grow
-            testMB({0x1A, 0xFC, 0x10, 0x00, 0x1A});                                // table.size
-            testMB({0x41, 0x00, 0xD2, 0x00, 0x41, 0x00, 0xFC, 0x11, 0x00});        // table.fill
-
-            testcase("Wasm opcodes SIMD");
-            // clang-format off
-
-            // generated by auggie
-            // SIMD instructions
-            testMB({0x41, 0x00, 0xFD, 0x00, 0x04, 0x00, 0x1A});  // v128.load
-            testMB({0x41, 0x00, 0xFD, 0x01, 0x03, 0x00, 0x1A});  // v128.load8x8_s
-            testMB({0x41, 0x00, 0xFD, 0x02, 0x03, 0x00, 0x1A});  // v128.load8x8_u
-            testMB({0x41, 0x00, 0xFD, 0x03, 0x03, 0x00, 0x1A});  // v128.load16x4_s
-            testMB({0x41, 0x00, 0xFD, 0x04, 0x03, 0x00, 0x1A});  // v128.load16x4_u
-            testMB({0x41, 0x00, 0xFD, 0x05, 0x03, 0x00, 0x1A});  // v128.load32x2_s
-            testMB({0x41, 0x00, 0xFD, 0x06, 0x03, 0x00, 0x1A});  // v128.load32x2_u
-            testMB({0x41, 0x00, 0xFD, 0x07, 0x00, 0x00, 0x1A});  // v128.load8_splat
-            testMB({0x41, 0x00, 0xFD, 0x08, 0x01, 0x00, 0x1A});  // v128.load16_splat
-            testMB({0x41, 0x00, 0xFD, 0x09, 0x02, 0x00, 0x1A});  // v128.load32_splat
-            testMB({0x41, 0x00, 0xFD, 0x0A, 0x03, 0x00, 0x1A});  // v128.load64_splat
-            testMB({0x41, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0B, 0x04, 0x00});  // v128.store
-            testMB({0xFD, 0x0C, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00,
-                 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x1A});  // v128.const
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0D, 0x00, 0x01, 0x02, 0x03,
-                    0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x1A});  // i8x16.shuffle
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0E, 0x1A});  // i8x16.swizzle
-            testMB({0x41, 0x2A, 0xFD, 0x0F, 0x1A});                                                  // i8x16.splat
-            testMB({0x41, 0x2A, 0xFD, 0x10, 0x1A});                                                  // i16x8.splat
-            testMB({0x41, 0x2A, 0xFD, 0x11, 0x1A});                                                  // i32x4.splat
-            testMB({0x42, 0x2A, 0xFD, 0x12, 0x1A});                                                  // i64x2.splat
-
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x15, 0x00, 0x1A});  // i8x16.extract_lane_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x16, 0x00, 0x1A});  // i8x16.extract_lane_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x2A, 0xFD, 0x17, 0x00, 0x1A});  // i8x16.replace_lane
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x18, 0x00, 0x1A});  // i16x8.extract_lane_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x19, 0x00, 0x1A});  // i16x8.extract_lane_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x2A, 0xFD, 0x1A, 0x00, 0x1A});  // i16x8.replace_lane
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x1B, 0x00, 0x1A});  // i32x4.extract_lane
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x2A, 0xFD, 0x1C, 0x00, 0x1A});  // i32x4.replace_lane
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x1D, 0x00, 0x1A});  // i64x2.extract_lane
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x2A, 0xFD, 0x1E, 0x00, 0x1A});  // i64x2.replace_lane
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x1F, 0x00, 0x1A});  // f32x4.extract_lane
-            testMB(
-                {0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                 0x00, 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x80, 0x3F, 0xFD, 0x20, 0x00, 0x1A});  // f32x4.replace_lane
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x21, 0x00, 0x1A});  // f64x2.extract_lane
-            testMB(
-                {0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x3F, 0xFD, 0x22, 0x00, 0x1A});  // f64x2.replace_lane
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x23, 0x1A});  // i8x16.eq
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x24, 0x1A});  // i8x16.ne
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x25, 0x1A});  // i8x16.lt_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x26, 0x1A});  // i8x16.lt_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x27, 0x1A});  // i8x16.gt_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x28, 0x1A});  // i8x16.gt_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x29, 0x1A});  // i8x16.le_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x2A, 0x1A});  // i8x16.le_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x2B, 0x1A});  // i8x16.ge_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x2C, 0x1A});  // i8x16.ge_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x2D, 0x1A});  // i16x8.eq
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x2E, 0x1A});  // i16x8.ne
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x2F, 0x1A});  // i16x8.lt_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x30, 0x1A});  // i16x8.lt_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x31, 0x1A});  // i16x8.gt_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x32, 0x1A});  // i16x8.gt_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x33, 0x1A});  // i16x8.le_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x34, 0x1A});  // i16x8.le_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x35, 0x1A});  // i16x8.ge_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x36, 0x1A});  // i16x8.ge_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x37, 0x1A});  // i32x4.eq
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x38, 0x1A});  // i32x4.ne
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x39, 0x1A});  // i32x4.lt_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x3A, 0x1A});  // i32x4.lt_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x3B, 0x1A});  // i32x4.gt_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x3C, 0x1A});  // i32x4.gt_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x3D, 0x1A});  // i32x4.le_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x3E, 0x1A});  // i32x4.le_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x3F, 0x1A});  // i32x4.ge_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x40, 0x1A});  // i32x4.ge_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x4D, 0x1A});  // v128.not
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x4E, 0x1A});  // v128.and
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x4F, 0x1A});  // v128.andnot
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x50, 0x1A});  // v128.or
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x51, 0x1A});  // v128.xor
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x52, 0x1A});  // v128.bitselect
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x53, 0x1A});  // v128.any_true
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x60, 0x1A});  // i8x16.abs
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x61, 0x1A});  // i8x16.neg
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x62, 0x1A});  // i8x16.popcnt
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x63, 0x1A});  // i8x16.all_true
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x64, 0x1A});  // i8x16.bitmask
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x01, 0xFD, 0x6B, 0x1A});  // i8x16.shl
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x01, 0xFD, 0x6C, 0x1A});  // i8x16.shr_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x01, 0xFD, 0x6D, 0x1A});  // i8x16.shr_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x6E, 0x1A});  // i8x16.add
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x6F, 0x1A});  // i8x16.add_sat_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x70, 0x1A});  // i8x16.add_sat_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x71, 0x1A});  // i8x16.sub
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x72, 0x1A});  // i8x16.sub_sat_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x73, 0x1A});  // i8x16.sub_sat_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x76, 0x1A});  // i8x16.min_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x77, 0x1A});  // i8x16.min_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x78, 0x1A});  // i8x16.max_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x79, 0x1A});  // i8x16.max_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x7B, 0x1A});  // i8x16.avgr_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x80, 0x01, 0x1A});  // i16x8.abs
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x81, 0x01, 0x1A});  // i16x8.neg
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x83, 0x01, 0x1A});  // i16x8.all_true
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x84, 0x01, 0x1A});  // i16x8.bitmask
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x01, 0xFD, 0x8B, 0x01, 0x1A});  // i16x8.shl
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x01, 0xFD, 0x8C, 0x01, 0x1A});  // i16x8.shr_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x01, 0xFD, 0x8D, 0x01, 0x1A});  // i16x8.shr_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x8E, 0x01, 0x1A});  // i16x8.add
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x8F, 0x01, 0x1A});  // i16x8.add_sat_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x90, 0x01, 0x1A});  // i16x8.add_sat_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x91, 0x01, 0x1A});  // i16x8.sub
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x92, 0x01, 0x1A});  // i16x8.sub_sat_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x93, 0x01, 0x1A});  // i16x8.sub_sat_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x95, 0x01, 0x1A});  // i16x8.mul
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x96, 0x01, 0x1A});  // i16x8.min_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x97, 0x01, 0x1A});  // i16x8.min_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x98, 0x01, 0x1A});  // i16x8.max_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x99, 0x01, 0x1A});  // i16x8.max_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x9B, 0x01, 0x1A});  // i16x8.avgr_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xA0, 0x01, 0x1A});  // i32x4.abs
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xA1, 0x01, 0x1A});  // i32x4.neg
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xA3, 0x01, 0x1A});  // i32x4.all_true
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xA4, 0x01, 0x1A});  // i32x4.bitmask
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x01, 0xFD, 0xAB, 0x01, 0x1A});  // i32x4.shl
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x01, 0xFD, 0xAC, 0x01, 0x1A});  // i32x4.shr_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x01, 0xFD, 0xAD, 0x01, 0x1A});  // i32x4.shr_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xAE, 0x01, 0x1A});  // i32x4.add
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xB1, 0x01, 0x1A});  // i32x4.sub
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xB5, 0x01, 0x1A});  // i32x4.mul
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xB6, 0x01, 0x1A});  // i32x4.min_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xB7, 0x01, 0x1A});  // i32x4.min_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xB8, 0x01, 0x1A});  // i32x4.max_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xB9, 0x01, 0x1A});  // i32x4.max_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xBA, 0x01, 0x1A});  // i32x4.dot_i16x8_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xC0, 0x01, 0x1A});  // i64x2.abs
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xC1, 0x01, 0x1A});  // i64x2.neg
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xC3, 0x01, 0x1A});  // i64x2.all_true
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xC4, 0x01, 0x1A});  // i64x2.bitmask
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x01, 0xFD, 0xCB, 0x01, 0x1A});  // i64x2.shl
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x01, 0xFD, 0xCC, 0x01, 0x1A});  // i64x2.shr_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x01, 0xFD, 0xCD, 0x01, 0x1A});  // i64x2.shr_u
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xCE, 0x01, 0x1A});  // i64x2.add
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xD1, 0x01, 0x1A});  // i64x2.sub
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xD5, 0x01, 0x1A});  // i64x2.mul
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xD6, 0x01, 0x1A});  // i64x2.eq
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xD7, 0x01, 0x1A});  // i64x2.ne
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xD8, 0x01, 0x1A});  // i64x2.lt_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xD9, 0x01, 0x1A});  // i64x2.gt_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xDA, 0x01, 0x1A});  // i64x2.le_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xDB, 0x01, 0x1A});  // i64x2.ge_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xF8, 0x01, 0x1A});  // i32x4.trunc_sat_f32x4_s
-            testMB({0xFD, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
-                    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0xF9, 0x01, 0x1A});  // i32x4.trunc_sat_f32x4_u
-
-            // clang-format on
-        }
-    }
-
-    void
-    run() override
-    {
-        using namespace test::jtx;
-
-        testGetDataHelperFunctions();
-        testWasmLib();
-        testBadWasm();
-        testWasmLedgerSqn();
-        testImpExp();
-
-        testWasmFib();
-
-        testHFCost();
-        testEscrowWasmDN();
-        // TODO: re-enable once the float fixtures are regenerated (see above)
-        // testFloat();
-
-        testCodecovWasm();
-        // TODO: broken, fix after Rust re-arch
-        // testDisabledFloat();
-
-        testWasmMemory();
-        testWasmTable();
-        testWasmProposal();
-        testWasmTrap();
-        testWasmWasi();
-        testWasmSectionCorruption();
-
-        // TODO: broken, fix after Rust re-arch
-        // testStartFunctionLoop();
-        testBadAlign();
-        testReturnType();
-        testSwapBytes();
-        testManyParams();
-        testParameterType();
-
-        testOpcodes();
-    }
-};
-
-BEAST_DEFINE_TESTSUITE(Wasm, app, xrpl);
-
-}  // namespace xrpl::test
diff --git a/src/test/app/invariants/InvariantsAMM_test.cpp b/src/test/app/invariants/InvariantsAMM_test.cpp
new file mode 100644
index 0000000000..498c35c653
--- /dev/null
+++ b/src/test/app/invariants/InvariantsAMM_test.cpp
@@ -0,0 +1,249 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+class InvariantsAMM_test : public InvariantsBase
+{
+    FeatureBitset const all_{test::jtx::testableAmendments()};
+
+    void
+    testAMMDeleteInvariants(FeatureBitset features)
+    {
+        using namespace test::jtx;
+
+        bool const enforceAMMDelete = features[fixCleanup3_3_0];
+        testcase << "AMM delete invariants" + std::string(enforceAMMDelete ? " fix" : "");
+
+        Env env(*this, features);
+        Account const issuer{"issuer"};
+        Issue const lptIssue{Currency(0x4c50540000000000), issuer.id()};
+        STAmount const zeroLP{lptIssue, 0};
+        STAmount const nonZeroLP{lptIssue, 1};
+
+        auto const makeAMM = [](STAmount const& lptBalance) {
+            auto sleAMM = std::make_shared(keylet::amm(uint256(1)));
+            sleAMM->setFieldAmount(sfLPTokenBalance, lptBalance);
+            return sleAMM;
+        };
+
+        auto const checkInvariant = [&](TxType txType,
+                                        TER result,
+                                        std::optional const& deletedLPBalance,
+                                        bool expected,
+                                        std::string const& expectedLog) {
+            test::StreamSink sink{beast::Severity::Warning};
+            beast::Journal const jlog{sink};
+            ValidAMM invariant;
+
+            if (deletedLPBalance)
+                invariant.visitEntry(true, makeAMM(*deletedLPBalance), nullptr);
+
+            bool const actual = invariant.finalize(
+                STTx{txType, [](STObject&) {}}, result, XRPAmount{}, *env.current(), jlog);
+
+            BEAST_EXPECTS(actual == expected, "unexpected AMM delete invariant result");
+            auto const messages = sink.messages().str();
+            auto const expectedLogWhenEnforced = enforceAMMDelete ? expectedLog : "";
+            if (!expectedLogWhenEnforced.empty())
+            {
+                BEAST_EXPECTS(messages.contains(expectedLogWhenEnforced), expectedLogWhenEnforced);
+            }
+            else
+            {
+                BEAST_EXPECTS(messages.empty(), messages);
+            }
+        };
+
+        checkInvariant(
+            ttPAYMENT,
+            tesSUCCESS,
+            nonZeroLP,
+            !enforceAMMDelete,
+            "Invariant failed: AMM failed, unexpected AMM deletion by");
+        checkInvariant(
+            ttAMM_DELETE,
+            tesSUCCESS,
+            std::nullopt,
+            !enforceAMMDelete,
+            "Invariant failed: AMMDelete failed, AMM object remained on tesSUCCESS");
+        checkInvariant(
+            ttAMM_DELETE,
+            tesSUCCESS,
+            nonZeroLP,
+            !enforceAMMDelete,
+            "Invariant failed: AMMDelete failed, AMM object deleted with non-zero LP balance");
+        checkInvariant(
+            ttAMM_DELETE,
+            tecINCOMPLETE,
+            zeroLP,
+            !enforceAMMDelete,
+            "Invariant failed: AMMDelete failed, AMM object deleted when result is not tesSUCCESS");
+
+        checkInvariant(ttAMM_WITHDRAW, tesSUCCESS, nonZeroLP, true, "");
+        checkInvariant(ttAMM_CLAWBACK, tesSUCCESS, nonZeroLP, true, "");
+
+        checkInvariant(ttAMM_DELETE, tesSUCCESS, zeroLP, true, "");
+        checkInvariant(ttAMM_WITHDRAW, tesSUCCESS, zeroLP, true, "");
+        checkInvariant(ttAMM_CLAWBACK, tesSUCCESS, zeroLP, true, "");
+    }
+
+    void
+    testAMM()
+    {
+        testcase << "AMM";
+        using namespace jtx;
+
+        MPTID mptID{};
+        uint256 ammID{};
+        AccountID ammAccountID{};
+        Account const gw{"gw"};
+        Issue lptIssue{};
+        PrettyAsset poolAsset{xrpIssue()};
+
+        auto deleteAMMAccount = [&](ApplyContext& ac, bool) {
+            auto sle = ac.view().peek(keylet::account(ammAccountID));
+            if (!sle)
+                return false;
+            ac.view().erase(sle);
+            return true;
+        };
+
+        auto updateLPTokensBalance = [&](ApplyContext& ac, std::int64_t amount) {
+            auto sle = ac.view().peek(keylet::amm(ammID));
+            if (!sle)
+                return false;
+            sle->setFieldAmount(sfLPTokenBalance, STAmount{lptIssue, amount});
+            ac.view().update(sle);
+            return true;
+        };
+        auto updateLPTokensBadAmount = [&](ApplyContext& ac, bool) {
+            return updateLPTokensBalance(ac, -1);
+        };
+        auto updateLPTokensBadBalance = [&](ApplyContext& ac, bool) {
+            return updateLPTokensBalance(ac, 200'000'000);
+        };
+        auto updateAMM = [&](ApplyContext& ac, bool) { return updateLPTokensBalance(ac, 10); };
+
+        auto updateAMMPool = [&](ApplyContext& ac, bool isMPT) {
+            if (isMPT)
+            {
+                auto sle = ac.view().peek(keylet::mptoken(mptID, ammAccountID));
+                if (!sle)
+                    return false;
+                sle->setFieldU64(sfMPTAmount, 1);
+                ac.view().update(sle);
+                return true;
+            }
+            auto sle = ac.view().peek(keylet::account(ammAccountID));
+            if (!sle)
+                return false;
+            sle->setFieldAmount(sfBalance, XRP(1));
+            ac.view().update(sle);
+            return true;
+        };
+
+        auto test = [&](auto const txType,
+                        auto&& update,
+                        bool isMPT,
+                        TER error = tecINVARIANT_FAILED) {
+            doInvariantCheck(
+                {{"AMM"}},
+                [&](Account const&, Account const&, ApplyContext& ac) { return update(ac, isMPT); },
+                XRPAmount{},
+                STTx{txType, [&](STObject& tx) {}},
+                {tecINVARIANT_FAILED, error},
+                [&](Account const&, Account const&, Env& env) {
+                    env.fund(XRP(1'000), gw);
+                    poolAsset = [&]() -> PrettyAsset {
+                        if (isMPT)
+                        {
+                            MPT const mpt = MPTTester({.env = env, .issuer = gw});
+                            mptID = mpt.issuanceID;
+                            return mpt;
+                        }
+                        return gw["USD"];
+                    }();
+                    AMM const amm(env, gw, XRP(100), poolAsset(100));
+                    ammAccountID = amm.ammAccount();
+                    ammID = amm.ammID();
+                    lptIssue = amm.lptIssue();
+                    return true;
+                });
+        };
+
+        for (bool const isMPT : {false, true})
+        {
+            // Under fixCleanup3_4_0 the MPT balance invariants also fire on the
+            // second pass, so both IOU and MPT pools now escalate to tef.
+            auto const error = TER(tefINVARIANT_FAILED);
+            for (auto txType : {ttAMM_CREATE, ttAMM_DEPOSIT, ttAMM_CLAWBACK, ttAMM_WITHDRAW})
+            {
+                test(txType, deleteAMMAccount, isMPT, tefINVARIANT_FAILED);
+                test(txType, updateLPTokensBadAmount, isMPT);
+                test(txType, updateLPTokensBadBalance, isMPT);
+            }
+            for (auto txType : {ttAMM_BID, ttAMM_VOTE})
+            {
+                test(txType, updateAMMPool, isMPT, error);
+                test(txType, updateLPTokensBadAmount, isMPT);
+                test(txType, updateLPTokensBadBalance, isMPT);
+            }
+            for (auto txType : {ttAMM_DELETE, ttCHECK_CASH, ttOFFER_CREATE, ttPAYMENT})
+            {
+                test(txType, updateAMM, isMPT);
+            }
+        }
+    }
+
+    // Test the invariant overwrite fix for both pre- and post-amendment
+    // behavior. With the fix enabled, |= accumulates violations across
+    // entries so a later valid entry cannot clear an earlier violation.
+    // Without the fix, = assignment means the last-visited entry wins.
+
+    void
+    run() override
+    {
+        testAMMDeleteInvariants(all_);
+        testAMMDeleteInvariants(all_ - fixCleanup3_3_0);
+        testAMM();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(InvariantsAMM, app, xrpl);
+
+}  // namespace xrpl::test
diff --git a/src/test/app/invariants/InvariantsBase.cpp b/src/test/app/invariants/InvariantsBase.cpp
new file mode 100644
index 0000000000..650a21cb07
--- /dev/null
+++ b/src/test/app/invariants/InvariantsBase.cpp
@@ -0,0 +1,241 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+test::jtx::Env
+InvariantsBase::makeEnv(FeatureBitset features)
+{
+    return {*this, test::jtx::envconfig(), features, nullptr, beast::Severity::Disabled};
+}
+
+void
+InvariantsBase::doInvariantCheck(
+    std::vector const& expectLogs,
+    Precheck const& precheck,
+    XRPAmount fee,
+    STTx tx,
+    std::initializer_list ters,
+    Preclose const& preclose,
+    TxAccount setTxAccount,
+    std::source_location const& loc,
+    TER initialResult)
+{
+    doInvariantCheck(
+        makeEnv(test::jtx::testableAmendments()),
+        expectLogs,
+        precheck,
+        fee,
+        tx,
+        ters,
+        preclose,
+        setTxAccount,
+        loc,
+        initialResult);
+}
+
+void
+InvariantsBase::doInvariantCheck(
+    test::jtx::Env&& env,
+    std::vector const& expectLogs,
+    Precheck const& precheck,
+    XRPAmount fee,
+    STTx tx,
+    std::initializer_list ters,
+    Preclose const& preclose,
+    TxAccount setTxAccount,
+    std::source_location const& loc,
+    TER initialResult)
+{
+    using namespace test::jtx;
+
+    Account const a1{"A1"};
+    Account const a2{"A2"};
+    env.fund(XRP(1000), a1, a2);
+    if (preclose)
+        BEAST_EXPECT(preclose(a1, a2, env));
+    env.close();
+
+    if (setTxAccount != TxAccount::None)
+        tx.setAccountID(sfAccount, setTxAccount == TxAccount::A1 ? a1.id() : a2.id());
+
+    doInvariantCheck(
+        std::move(env), a1, a2, expectLogs, precheck, fee, tx, ters, loc, initialResult);
+}
+
+void
+InvariantsBase::doInvariantCheck(
+    // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
+    test::jtx::Env&& env,
+    test::jtx::Account const& a1,
+    test::jtx::Account const& a2,
+    std::vector const& expectLogs,
+    Precheck const& precheck,
+    XRPAmount fee,
+    STTx tx,
+    std::initializer_list ters,
+    std::source_location const& loc,
+    TER initialResult)
+{
+    using namespace test::jtx;
+
+    OpenView ov{*env.current()};
+    test::StreamSink sink{beast::Severity::Warning};
+    beast::Journal const jlog{sink};
+    ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+
+    // Invariants normally run in the Transaction's "apply" (operator()) context, and can always
+    // access global Rules.
+    CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+    BEAST_EXPECT(precheck(a1, a2, ac));
+
+    auto transactor = makeTransactor(ac);
+    if (!BEAST_EXPECT(transactor))
+        return;
+
+    // Invoke the check twice to cover the tec and tef cases. Both passes run
+    // against the same view -- production would discard it in between -- so
+    // the second sees the same violation and escalates tec -> tef. A
+    // {tec, tef} pair therefore means "enforced whatever the incoming
+    // result", not that the transaction ends in tef on ledger.
+    if (!BEAST_EXPECT(ters.size() == 2))
+        return;
+
+    TER terActual = initialResult;
+    for (TER const& terExpect : ters)
+    {
+        TER const terInput = terActual;
+        terActual = transactor->checkInvariants(terActual, fee, Transactor::InvariantScope::Full);
+        expect(
+            terExpect == terActual,
+            "expected: " + transToken(terExpect) + " got: " + transToken(terActual),
+            loc.file_name(),
+            loc.line());
+        auto const messages = sink.messages().str();
+
+        // checkInvariants returns its input unchanged unless something
+        // fires, so a changed result means an invariant fired, and a firing
+        // invariant must log.
+        if (terActual != terInput)
+        {
+            expect(
+                messages.starts_with("Invariant failed:") ||
+                    messages.starts_with("Transaction caused an exception"),
+                messages,
+                loc.file_name(),
+                loc.line());
+        }
+
+        // std::cerr << messages << '\n';
+        for (auto const& m : expectLogs)
+        {
+            expect(messages.contains(m), m, loc.file_name(), loc.line());
+        }
+    }
+}
+
+Keylet
+InvariantsBase::createLoanBroker(
+    jtx::Account const& a,
+    jtx::Env& env,
+    jtx::PrettyAsset const& asset)
+{
+    using namespace jtx;
+
+    // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
+    // accepts closed-ended vaults. Build one with a comfortable
+    // subscription window; LoanBrokerSet itself is not phase-gated,
+    // so leaving the vault in the Subscription phase is fine here.
+    uint256 vaultID;
+    Vault const vault{env};
+    auto [tx, vKeylet, _] = vault.createClosedEnded(
+        {.owner = a,
+         .asset = asset,
+         .subscriptionOffset = std::chrono::seconds{60},
+         .investmentWindow = std::chrono::seconds{kMinInvestmentPeriod + 1'000'000u}});
+    env(tx);
+    BEAST_EXPECT(env.le(vKeylet));
+
+    vaultID = vKeylet.key;
+
+    // Create Loan Broker
+    using namespace loan_broker;
+
+    auto const loanBrokerKeylet = keylet::loanBroker(a.id(), SeqProxy::rawSequence(env.seq(a)));
+    // Create a Loan Broker with all default values.
+    env(set(a, vaultID), Fee(kIncrement));
+
+    return loanBrokerKeylet;
+}
+
+SLE::pointer
+InvariantsBase::makeLoanSle(
+    uint256 const& loanBrokerID,
+    std::uint32_t loanSeq,
+    AccountID const& borrower)
+{
+    auto sleLoan =
+        std::make_shared(keylet::loan(loanBrokerID, SeqProxy::rawSequence(loanSeq)));
+    // SoeRequired fields.
+    sleLoan->at(sfLoanBrokerID) = loanBrokerID;
+    sleLoan->at(sfLoanSequence) = loanSeq;
+    sleLoan->at(sfBorrower) = borrower;
+    sleLoan->at(sfStartDate) = 0u;
+    sleLoan->at(sfPaymentInterval) = 1u;
+    sleLoan->at(sfPeriodicPayment) = Number(1);
+    // SoeDefault fields, materialized so that an invariant reading them through
+    // at() does not throw on this hand-built entry.
+    sleLoan->at(sfLoanServiceFee) = Number(0);
+    sleLoan->at(sfLatePaymentFee) = Number(0);
+    sleLoan->at(sfClosePaymentFee) = Number(0);
+    sleLoan->at(sfPrincipalOutstanding) = Number(0);
+    sleLoan->at(sfTotalValueOutstanding) = Number(0);
+    sleLoan->at(sfManagementFeeOutstanding) = Number(0);
+    sleLoan->setFieldU32(sfPaymentRemaining, 0);
+    sleLoan->makeFieldPresent(sfOwnerNode);
+    sleLoan->makeFieldPresent(sfLoanBrokerNode);
+    return sleLoan;
+}
+
+}  // namespace xrpl::test
diff --git a/src/test/app/invariants/InvariantsBase.h b/src/test/app/invariants/InvariantsBase.h
new file mode 100644
index 0000000000..6b4327eb78
--- /dev/null
+++ b/src/test/app/invariants/InvariantsBase.h
@@ -0,0 +1,134 @@
+#pragma once
+
+#include 
+#include 
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class Transactor;
+
+// Test-only factory — not part of the public API.
+// The returned Transactor holds a raw reference to ctx; the caller must ensure
+// the ApplyContext outlives the Transactor. Implemented in applySteps.cpp
+std::unique_ptr
+makeTransactor(ApplyContext& ctx);
+
+}  // namespace xrpl
+
+namespace xrpl::test {
+
+class InvariantsBase : public beast::unit_test::Suite
+{
+protected:
+    // The optional Preclose function is used to process additional transactions
+    // on the ledger after creating two accounts, but before closing it, and
+    // before the Precheck function. These should only be valid functions, and
+    // not direct manipulations. Preclose is not commonly used.
+    using Preclose = std::function<
+        bool(test::jtx::Account const& a, test::jtx::Account const& b, test::jtx::Env& env)>;
+
+    // this is common setup/method for running a failing invariant check. The
+    // precheck function is used to manipulate the ApplyContext with view
+    // changes that will cause the check to fail.
+    using Precheck = std::function<
+        bool(test::jtx::Account const& a, test::jtx::Account const& b, ApplyContext& ac)>;
+
+    enum class TxAccount : int { None = 0, A1, A2 };
+
+    test::jtx::Env
+    makeEnv(FeatureBitset features);
+
+    /**
+     * Run a specific test case to put the ledger into a state that will be
+     * detected by an invariant. Simulates the actions of a transaction that
+     * would violate an invariant.
+     *
+     * @param expectLogs One or more messages related to the failing invariant
+     *  that should be in the log output
+     * @param precheck See "Precheck" above
+     * @param fee If provided, the fee amount paid by the simulated transaction.
+     * @param tx A mock transaction that took the actions to trigger the
+     *  invariant. In most cases, only the type matters.
+     * @param ters The TER results expected on the two passes of the invariant
+     *  checker.
+     * @param preclose See "Preclose" above. Note that @preclose runs *before*
+     *  @precheck, but is the last parameter for historical reasons
+     * @param setTxAccount optionally set to add sfAccount to tx (either A1 or A2)
+     */
+    void
+    doInvariantCheck(
+        std::vector const& expectLogs,
+        Precheck const& precheck,
+        XRPAmount fee = XRPAmount{},
+        STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}},
+        std::initializer_list ters = {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+        Preclose const& preclose = {},
+        TxAccount setTxAccount = TxAccount::None,
+        std::source_location const& loc = std::source_location::current(),
+        // Result fed to the invariant checker on the first pass. Set it to a
+        // tec to exercise result-dependent invariants; the harness runs no
+        // transactor, so one never arises on its own.
+        TER initialResult = tesSUCCESS);
+
+    void
+    doInvariantCheck(
+        test::jtx::Env&& env,
+        std::vector const& expectLogs,
+        Precheck const& precheck,
+        XRPAmount fee = XRPAmount{},
+        STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}},
+        std::initializer_list ters = {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+        Preclose const& preclose = {},
+        TxAccount setTxAccount = TxAccount::None,
+        std::source_location const& loc = std::source_location::current(),
+        TER initialResult = tesSUCCESS);
+
+    void
+    doInvariantCheck(
+        // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved)
+        test::jtx::Env&& env,
+        test::jtx::Account const& a1,
+        test::jtx::Account const& a2,
+        std::vector const& expectLogs,
+        Precheck const& precheck,
+        XRPAmount fee = XRPAmount{},
+        STTx tx = STTx{ttACCOUNT_SET, [](STObject&) {}},
+        std::initializer_list ters = {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+        std::source_location const& loc = std::source_location::current(),
+        TER initialResult = tesSUCCESS);
+
+    Keylet
+    createLoanBroker(jtx::Account const& a, jtx::Env& env, jtx::PrettyAsset const& asset);
+
+    // Build an ltLOAN SLE with every SoeRequired field explicitly set and
+    // every SoeDefault field the invariants read via `at()` materialized, so
+    // rawInsert-based tests don't accidentally trip an unrelated invariant
+    // or throw from a missing SoeDefault field.
+    static SLE::pointer
+    makeLoanSle(uint256 const& loanBrokerID, std::uint32_t loanSeq, AccountID const& borrower);
+};
+
+}  // namespace xrpl::test
diff --git a/src/test/app/invariants/InvariantsEscrowNFT_test.cpp b/src/test/app/invariants/InvariantsEscrowNFT_test.cpp
new file mode 100644
index 0000000000..f0afa2377c
--- /dev/null
+++ b/src/test/app/invariants/InvariantsEscrowNFT_test.cpp
@@ -0,0 +1,352 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+class InvariantsEscrowNFT_test : public InvariantsBase
+{
+    void
+    testNoZeroEscrow()
+    {
+        using namespace test::jtx;
+        testcase << "no zero escrow";
+
+        doInvariantCheck(
+            {{"XRP net change of -1000000 doesn't match fee 0"},
+             {"escrow specifies invalid amount"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // escrow with negative amount
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                auto sleNew = std::make_shared(
+                    keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
+                sleNew->setFieldAmount(sfAmount, XRP(-1));
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"XRP net change was positive: 100000000000000001"},
+             {"escrow specifies invalid amount"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // escrow with too-large amount
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                auto sleNew = std::make_shared(
+                    keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
+                // Use `drops(1)` to bypass a call to STAmount::canonicalize
+                // with an invalid value
+                sleNew->setFieldAmount(sfAmount, kInitialXrp + drops(1));
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        // IOU < 0
+        doInvariantCheck(
+            {{"escrow specifies invalid amount"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // escrow with too-little iou
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                auto sleNew = std::make_shared(
+                    keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
+
+                Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
+                STAmount const amt(usd, -1);
+                sleNew->setFieldAmount(sfAmount, amt);
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        // IOU bad currency
+        doInvariantCheck(
+            {{"escrow specifies invalid amount"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // escrow with bad iou currency
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                auto sleNew = std::make_shared(
+                    keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
+
+                Issue const bad{badCurrency(), AccountID(0x4985601)};
+                STAmount const amt(bad, 1);
+                sleNew->setFieldAmount(sfAmount, amt);
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        // MPT < 0
+        doInvariantCheck(
+            {{"escrow specifies invalid amount"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // escrow with too-little mpt
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                auto sleNew = std::make_shared(
+                    keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
+
+                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
+                STAmount const amt(mpt, -1);
+                sleNew->setFieldAmount(sfAmount, amt);
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        // MPT OutstandingAmount < 0
+        doInvariantCheck(
+            {{"escrow specifies invalid amount"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // mptissuance outstanding is negative
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+
+                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
+                sleNew->setFieldU64(sfOutstandingAmount, std::numeric_limits::max());
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        // MPT LockedAmount < 0
+        doInvariantCheck(
+            {{"escrow specifies invalid amount"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // mptissuance locked is less than locked
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+
+                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
+                sleNew->setFieldU64(sfLockedAmount, std::numeric_limits::max());
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        // MPT OutstandingAmount < LockedAmount
+        doInvariantCheck(
+            {{"escrow specifies invalid amount"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // mptissuance outstanding is less than locked
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+
+                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
+                sleNew->setFieldU64(sfOutstandingAmount, 1);
+                sleNew->setFieldU64(sfLockedAmount, 10);
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        // MPT MPTAmount < 0
+        doInvariantCheck(
+            {{"escrow specifies invalid amount"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // mptoken amount is negative
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+
+                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
+                auto sleNew = std::make_shared(keylet::mptoken(mpt.getMptID(), a1));
+                sleNew->setFieldU64(sfMPTAmount, std::numeric_limits::max());
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        // MPT LockedAmount < 0
+        doInvariantCheck(
+            {{"escrow specifies invalid amount"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // mptoken locked amount is negative
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+
+                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
+                auto sleNew = std::make_shared(keylet::mptoken(mpt.getMptID(), a1));
+                sleNew->setFieldU64(sfLockedAmount, std::numeric_limits::max());
+                ac.view().insert(sleNew);
+                return true;
+            });
+    }
+
+    void
+    testNFTokenPageInvariants()
+    {
+        using namespace test::jtx;
+        testcase << "NFTokenPage";
+
+        // lambda that returns an STArray of NFTokenIDs.
+        uint256 const firstNFTID(
+            "0000000000000000000000000000000000000001FFFFFFFFFFFFFFFF00000000");
+        auto makeNFTokenIDs = [&firstNFTID](unsigned int nftCount) {
+            SOTemplate const* nfTokenTemplate =
+                InnerObjectFormats::getInstance().findSOTemplateBySField(sfNFToken);
+
+            uint256 nftID(firstNFTID);
+            STArray ret;
+            for (int i = 0; i < nftCount; ++i)
+            {
+                STObject newNFToken(*nfTokenTemplate, sfNFToken, [&nftID](STObject& object) {
+                    object.setFieldH256(sfNFTokenID, nftID);
+                });
+                ret.pushBack(std::move(newNFToken));
+                ++nftID;
+            }
+            return ret;
+        };
+
+        doInvariantCheck(
+            {{"NFT page has invalid size"}},
+            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
+                nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(0));
+
+                ac.view().insert(nftPage);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"NFT page has invalid size"}},
+            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
+                nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(33));
+
+                ac.view().insert(nftPage);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"NFTs on page are not sorted"}},
+            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
+                STArray nfTokens = makeNFTokenIDs(2);
+                std::iter_swap(nfTokens.begin(), nfTokens.begin() + 1);
+
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
+                nftPage->setFieldArray(sfNFTokens, nfTokens);
+
+                ac.view().insert(nftPage);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"NFT contains empty URI"}},
+            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
+                STArray nfTokens = makeNFTokenIDs(1);
+                nfTokens[0].setFieldVL(sfURI, Blob{});
+
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
+                nftPage->setFieldArray(sfNFTokens, nfTokens);
+
+                ac.view().insert(nftPage);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"NFT page is improperly linked"}},
+            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
+                nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
+                nftPage->setFieldH256(sfPreviousPageMin, keylet::nftokenPageMax(a1).key);
+
+                ac.view().insert(nftPage);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"NFT page is improperly linked"}},
+            [&makeNFTokenIDs](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
+                nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
+                nftPage->setFieldH256(sfPreviousPageMin, keylet::nftokenPageMin(a2).key);
+
+                ac.view().insert(nftPage);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"NFT page is improperly linked"}},
+            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
+                auto nftPage = std::make_shared(keylet::nftokenPageMax(a1));
+                nftPage->setFieldArray(sfNFTokens, makeNFTokenIDs(1));
+                nftPage->setFieldH256(sfNextPageMin, nftPage->key());
+
+                ac.view().insert(nftPage);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"NFT page is improperly linked"}},
+            [&makeNFTokenIDs](Account const& a1, Account const& a2, ApplyContext& ac) {
+                STArray nfTokens = makeNFTokenIDs(1);
+                auto nftPage = std::make_shared(keylet::nftokenPage(
+                    keylet::nftokenPageMax(a1), ++(nfTokens[0].getFieldH256(sfNFTokenID))));
+                nftPage->setFieldArray(sfNFTokens, nfTokens);
+                nftPage->setFieldH256(sfNextPageMin, keylet::nftokenPageMax(a2).key);
+
+                ac.view().insert(nftPage);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"NFT found in incorrect page"}},
+            [&makeNFTokenIDs](Account const& a1, Account const&, ApplyContext& ac) {
+                STArray nfTokens = makeNFTokenIDs(2);
+                auto nftPage = std::make_shared(keylet::nftokenPage(
+                    keylet::nftokenPageMax(a1), (nfTokens[1].getFieldH256(sfNFTokenID))));
+                nftPage->setFieldArray(sfNFTokens, nfTokens);
+
+                ac.view().insert(nftPage);
+                return true;
+            });
+    }
+
+    void
+    run() override
+    {
+        testNoZeroEscrow();
+        testNFTokenPageInvariants();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(InvariantsEscrowNFT, app, xrpl);
+
+}  // namespace xrpl::test
diff --git a/src/test/app/invariants/InvariantsMPT_test.cpp b/src/test/app/invariants/InvariantsMPT_test.cpp
new file mode 100644
index 0000000000..4692463baa
--- /dev/null
+++ b/src/test/app/invariants/InvariantsMPT_test.cpp
@@ -0,0 +1,1577 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+class InvariantsMPT_test : public InvariantsBase
+{
+    FeatureBitset const all_{test::jtx::testableAmendments()};
+
+    void
+    testMPT()
+    {
+        using namespace test::jtx;
+        testcase << "MPT";
+
+        MPTIssue const nonCanonicalMPTIssue{makeMptID(1, AccountID(0x4985601))};
+        auto const nonCanonicalMPTAmount = [&](SField const& field) {
+            return STAmount{
+                field,
+                nonCanonicalMPTIssue,
+                kMaxMpTokenAmount + std::uint64_t{1},
+                0,
+                false,
+                STAmount::Unchecked{}};
+        };
+        auto const negativeMPTAmount = [&](SField const& field) {
+            return STAmount{field, nonCanonicalMPTIssue, 2, 0, true, STAmount::Unchecked{}};
+        };
+        auto const nonCanonicalMPTPayment = [&]() {
+            return STTx{ttPAYMENT, [&](STObject& tx) {
+                            tx.setFieldAmount(sfAmount, nonCanonicalMPTAmount(sfAmount));
+                        }};
+        };
+
+        doInvariantCheck(
+            makeEnv(all_ - fixCleanup3_2_0),
+            {},
+            [](Account const&, Account const&, ApplyContext&) { return true; },
+            XRPAmount{},
+            nonCanonicalMPTPayment(),
+            {tesSUCCESS, tesSUCCESS});
+
+        doInvariantCheck(
+            {{"ledger entry contains non-canonical MPT or XRP amount"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+
+                auto sleNew = std::make_shared(
+                    keylet::check(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
+                sleNew->setAccountID(sfAccount, a1.id());
+                sleNew->setAccountID(sfDestination, a2.id());
+                sleNew->setFieldAmount(sfSendMax, nonCanonicalMPTAmount(sfSendMax));
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"ledger entry contains non-canonical MPT or XRP amount"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+
+                auto sleNew = std::make_shared(
+                    keylet::check(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
+                sleNew->setAccountID(sfAccount, a1.id());
+                sleNew->setAccountID(sfDestination, a2.id());
+                sleNew->setFieldAmount(sfSendMax, negativeMPTAmount(sfSendMax));
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        // MPT OutstandingAmount > MaximumAmount
+        doInvariantCheck(
+            {{"OutstandingAmount overflow"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // mptissuance outstanding is negative
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+
+                MPTIssue const mpt{makeMptID(sle->getFieldU32(sfSequence), a1)};
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
+                sleNew->setFieldU64(sfOutstandingAmount, 110);
+                sleNew->setFieldU64(sfMaximumAmount, 100);
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        // MPTToken amount doesn't add up to OutstandingAmount
+        doInvariantCheck(
+            {{"invalid OutstandingAmount balance"}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                // mptissuance outstanding is negative
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+
+                MPTIssue const mpt{makeMptID(sle->getFieldU32(sfSequence), a1)};
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
+                sleNew->setFieldU64(sfOutstandingAmount, 100);
+                sleNew->setFieldU64(sfMaximumAmount, 100);
+                ac.view().insert(sleNew);
+
+                sleNew = std::make_shared(keylet::mptoken(mpt.getMptID(), a2));
+                sleNew->setFieldU64(sfMPTAmount, 90);
+                ac.view().insert(sleNew);
+
+                return true;
+            });
+
+        // Overflow/Invalid balance on payment
+        auto testPayment = [&](std::string const& log, auto&& update) {
+            MPTID id;
+            doInvariantCheck(
+                {{log}},
+                [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    return update(id, ac, a1);
+                },
+                XRPAmount{},
+                STTx{ttPAYMENT, [](STObject& tx) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                [&](Account const& a1, Account const& a2, Env& env) {
+                    Account const gw("gw");
+                    env.fund(XRP(1'000), gw);
+                    MPTTester const mpt(
+                        {.env = env, .issuer = gw, .holders = {a1}, .pay = 100, .maxAmt = 100});
+                    id = mpt.issuanceID();
+                    return true;
+                });
+        };
+        testPayment(
+            "invalid OutstandingAmount balance",
+            [&](MPTID const& id, ApplyContext& ac, Account const& a1) {
+                auto sle = ac.view().peek(keylet::mptoken(id, a1));
+                if (!sle)
+                    return false;
+                sle->setFieldU64(sfMPTAmount, 101);
+                ac.view().update(sle);
+                return true;
+            });
+        testPayment(
+            "OutstandingAmount overflow", [&](MPTID const& id, ApplyContext& ac, Account const&) {
+                auto sle = ac.view().peek(keylet::mptokenIssuance(id));
+                if (!sle)
+                    return false;
+                sle->setFieldU64(sfOutstandingAmount, 101);
+                ac.view().update(sle);
+                return true;
+            });
+
+        // The on-failure MPT checks (OutstandingAmount balance / transfer) apply
+        // to every non-tesSUCCESS result, with no per-result exemption: on a tec
+        // the transactor discards the view and re-applies only offer, trust
+        // line, NFT offer and credential deletions, so an MPT change reaching
+        // the invariant is a bug whatever the code. Seeded via initialResult.
+        {
+            MPTID id;
+            // preclose: gw issues an MPT held by A1 and A2.
+            auto const setup = [&](Account const& a1, Account const& a2, Env& env) {
+                Account const gw("gw");
+                env.fund(XRP(1'000), gw);
+                MPTTester const mpt(
+                    {.env = env, .issuer = gw, .holders = {a1, a2}, .pay = 50, .maxAmt = 1'000});
+                id = mpt.issuanceID();
+                return true;
+            };
+
+            // Consistent mint: OutstandingAmount and A1's balance both grow by
+            // 10, so conservation holds and only the on-failure check fires.
+            Precheck const mint = [&](Account const& a1, Account const&, ApplyContext& ac) {
+                auto sleIss = ac.view().peek(keylet::mptokenIssuance(id));
+                auto sleTok = ac.view().peek(keylet::mptoken(id, a1.id()));
+                if (!sleIss || !sleTok)
+                    return false;
+                (*sleIss)[sfOutstandingAmount] = (*sleIss)[sfOutstandingAmount] + 10;
+                (*sleTok)[sfMPTAmount] = (*sleTok)[sfMPTAmount] + 10;
+                ac.view().update(sleIss);
+                ac.view().update(sleTok);
+                return true;
+            };
+
+            // Holder-to-holder transfer (A1 -> A2 by 10). OutstandingAmount is
+            // unchanged, and CanTransfer keeps the ordinary transfer check
+            // quiet, so only the on-failure check fires.
+            Precheck const transfer = [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleIss = ac.view().peek(keylet::mptokenIssuance(id));
+                auto sleA = ac.view().peek(keylet::mptoken(id, a1.id()));
+                auto sleB = ac.view().peek(keylet::mptoken(id, a2.id()));
+                if (!sleIss || !sleA || !sleB)
+                    return false;
+                (*sleIss)[sfFlags] = (*sleIss)[sfFlags] | lsfMPTCanTransfer;
+                (*sleA)[sfMPTAmount] = (*sleA)[sfMPTAmount] - 10;
+                (*sleB)[sfMPTAmount] = (*sleB)[sfMPTAmount] + 10;
+                ac.view().update(sleIss);
+                ac.view().update(sleA);
+                ac.view().update(sleB);
+                return true;
+            };
+
+            STTx const payment{ttPAYMENT, [](STObject&) {}};
+
+            // Negative controls: nothing fires on tesSUCCESS. Without these, the
+            // cases below would still pass if the result guard were dropped.
+            doInvariantCheck({}, mint, XRPAmount{}, payment, {tesSUCCESS, tesSUCCESS}, setup);
+            doInvariantCheck({}, transfer, XRPAmount{}, payment, {tesSUCCESS, tesSUCCESS}, setup);
+
+            // tecKILLED and tecINCOMPLETE are not special: an MPT change paired
+            // with either fires, as with any other failure.
+            doInvariantCheck(
+                {{"OutstandingAmount balance changed on failure"}},
+                mint,
+                XRPAmount{},
+                payment,
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                setup,
+                TxAccount::None,
+                std::source_location::current(),
+                tecKILLED);
+            doInvariantCheck(
+                {{"OutstandingAmount balance changed on failure"}},
+                mint,
+                XRPAmount{},
+                payment,
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                setup,
+                TxAccount::None,
+                std::source_location::current(),
+                tecINCOMPLETE);
+            doInvariantCheck(
+                {{"MPToken balance changed on failure"}},
+                transfer,
+                XRPAmount{},
+                payment,
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                setup,
+                TxAccount::None,
+                std::source_location::current(),
+                tecKILLED);
+            doInvariantCheck(
+                {{"MPToken balance changed on failure"}},
+                transfer,
+                XRPAmount{},
+                payment,
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                setup,
+                TxAccount::None,
+                std::source_location::current(),
+                tecINCOMPLETE);
+            // The same change under a third failure result: the check keys off
+            // "not tesSUCCESS", nothing finer.
+            doInvariantCheck(
+                {{"OutstandingAmount balance changed on failure"}},
+                mint,
+                XRPAmount{},
+                payment,
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                setup,
+                TxAccount::None,
+                std::source_location::current(),
+                tecEXPIRED);
+            doInvariantCheck(
+                {{"MPToken balance changed on failure"}},
+                transfer,
+                XRPAmount{},
+                payment,
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                setup,
+                TxAccount::None,
+                std::source_location::current(),
+                tecEXPIRED);
+
+            // A lock moves value within one holder, so it is not a two-sided
+            // transfer and the `senders || receivers` form is what catches it.
+            // OutstandingAmount and the holder total are unchanged, so the
+            // balance check stays quiet.
+            Precheck const lock = [&](Account const& a1, Account const&, ApplyContext& ac) {
+                auto sleTok = ac.view().peek(keylet::mptoken(id, a1.id()));
+                if (!sleTok || (*sleTok)[sfMPTAmount] < 10)
+                    return false;
+                // A fresh MPToken has no locked amount, so set it directly.
+                (*sleTok)[sfMPTAmount] = (*sleTok)[sfMPTAmount] - 10;
+                sleTok->setFieldU64(sfLockedAmount, 10);
+                ac.view().update(sleTok);
+                return true;
+            };
+            // Negative control: a lock is legitimate on tesSUCCESS.
+            doInvariantCheck({}, lock, XRPAmount{}, payment, {tesSUCCESS, tesSUCCESS}, setup);
+            doInvariantCheck(
+                {{"MPToken balance changed on failure"}},
+                lock,
+                XRPAmount{},
+                payment,
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                setup,
+                TxAccount::None,
+                std::source_location::current(),
+                tecKILLED);
+            // The lock is caught under any failure result.
+            doInvariantCheck(
+                {{"MPToken balance changed on failure"}},
+                lock,
+                XRPAmount{},
+                payment,
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                setup,
+                TxAccount::None,
+                std::source_location::current(),
+                tecEXPIRED);
+
+            // A deleted MPToken has no amtAfter, so the sender/receiver counts
+            // skip it and only the deletedAuthorized_ term can catch it. That
+            // needs holders authorized but never paid, so the MPToken can be
+            // erased with a zero balance and OutstandingAmount untouched --
+            // otherwise the holder would register as a sender instead.
+            MPTID emptyId;
+            auto const setupEmpty = [&](Account const& a1, Account const& a2, Env& env) {
+                Account const gw("gw");
+                env.fund(XRP(1'000), gw);
+                MPTTester const mpt({.env = env, .issuer = gw, .holders = {a1, a2}, .maxAmt = 100});
+                emptyId = mpt.issuanceID();
+                return true;
+            };
+            Precheck const eraseToken = [&](Account const& a1, Account const&, ApplyContext& ac) {
+                auto sleTok = ac.view().peek(keylet::mptoken(emptyId, a1.id()));
+                if (!sleTok || (*sleTok)[sfMPTAmount] != 0)
+                    return false;
+                ac.view().erase(sleTok);
+                return true;
+            };
+            // ValidMPTIssuance also reports the deletion, so assert on
+            // ValidMPTTransfer's message, which only the new check can produce.
+            doInvariantCheck(
+                {{"MPToken deleted on failure"}},
+                eraseToken,
+                XRPAmount{},
+                payment,
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                setupEmpty,
+                TxAccount::None,
+                std::source_location::current(),
+                tecEXPIRED);
+        }
+
+        // Invalid IOU clawback delta must fail once MPTokensV2 enforces before/after validation.
+        {
+            Env env(*this, all_);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            auto const usd = issuer["USD"];
+            env.trust(usd(100), holder);
+            env(pay(issuer, holder, usd(100)));
+            env.close();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {{"Invariant failed: trustline clawback balance change is invalid"}},
+                [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
+                    auto sle =
+                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
+                    if (!sle)
+                        return false;
+
+                    STAmount balance{Issue{usd.currency, issuer.id()}, 80};
+                    if (holder.id() > issuer.id())
+                        balance.negate();
+                    sle->setFieldAmount(sfBalance, balance);
+                    ac.view().update(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
+                    }},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+        }
+
+        // Full IOU clawback may delete the trustline; missing after-SLE represents zero balance.
+        {
+            Env env(*this, all_);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            auto const usd = issuer["USD"];
+            env.trust(usd(100), holder);
+            env(pay(issuer, holder, usd(100)));
+            env.close();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {},
+                [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
+                    auto const sle =
+                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
+                    if (!sle)
+                        return false;
+
+                    ac.view().erase(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 100};
+                    }},
+                {tesSUCCESS, tesSUCCESS});
+        }
+
+        // Pre-MPTokensV2 invalid IOU clawback delta logs but remains non-enforcing.
+        {
+            Env env(*this, all_ - featureMPTokensV2);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            auto const usd = issuer["USD"];
+            env.trust(usd(100), holder);
+            env(pay(issuer, holder, usd(100)));
+            env.close();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {{"Invariant failed: trustline clawback balance change is invalid"}},
+                [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
+                    auto sle =
+                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
+                    if (!sle)
+                        return false;
+
+                    STAmount balance{Issue{usd.currency, issuer.id()}, 80};
+                    if (holder.id() > issuer.id())
+                        balance.negate();
+                    sle->setFieldAmount(sfBalance, balance);
+                    ac.view().update(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
+                    }},
+                {tesSUCCESS, tesSUCCESS});
+        }
+
+        // Invalid MPT clawback delta must fail when raw MPToken debit mismatches sfAmount.
+        {
+            Env env(*this, all_);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            MPTTester const mpt(
+                {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
+            auto const id = mpt.issuanceID();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {{"Invariant failed: MPT clawback balance change is invalid"}},
+                [id](Account const& holder, Account const&, ApplyContext& ac) {
+                    auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
+                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
+                    if (!sleToken || !sleIssuance)
+                        return false;
+
+                    sleToken->setFieldU64(sfMPTAmount, 80);
+                    sleIssuance->setFieldU64(sfOutstandingAmount, 80);
+                    ac.view().update(sleToken);
+                    ac.view().update(sleIssuance);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfHolder] = holder.id();
+                        tx[sfAmount] = STAmount{MPTIssue{id}, 10};
+                    }},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+        }
+
+        // A clawback that mutates both IOU and MPT entries must fail under MPTokensV2.
+        {
+            Env env(*this, all_);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            auto const usd = issuer["USD"];
+            env.trust(usd(100), holder);
+            env(pay(issuer, holder, usd(100)));
+            MPTTester const mpt(
+                {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
+            auto const id = mpt.issuanceID();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {{"Invariant failed: trustline and MPToken both changed"}},
+                [issuer, usd, id](Account const& holder, Account const&, ApplyContext& ac) {
+                    auto const sleLine =
+                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
+                    auto const sleToken = ac.view().peek(keylet::mptoken(id, holder.id()));
+                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
+                    if (!sleLine || !sleToken || !sleIssuance)
+                        return false;
+
+                    STAmount balance{Issue{usd.currency, issuer.id()}, 90};
+                    if (holder.id() > issuer.id())
+                        balance.negate();
+                    sleLine->setFieldAmount(sfBalance, balance);
+                    sleToken->setFieldU64(sfMPTAmount, 90);
+                    sleIssuance->setFieldU64(sfOutstandingAmount, 90);
+                    ac.view().update(sleLine);
+                    ac.view().update(sleToken);
+                    ac.view().update(sleIssuance);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfHolder] = holder.id();
+                        tx[sfAmount] = STAmount{MPTIssue{id}, 10};
+                    }},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+        }
+
+        // Clawback that modifies a trustline other than the one implied by the
+        // tx amount: clawbackTrustLineBalanceInHolderTerms returns nullopt for
+        // the mismatched line.
+        {
+            Env env(*this, all_);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            auto const usd = issuer["USD"];
+            auto const eur = issuer["EUR"];
+            env.trust(eur(100), holder);
+            env(pay(issuer, holder, eur(100)));
+            env.close();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {{"Invariant failed: trustline clawback changed the wrong line"}},
+                [issuer, eur](Account const& holder, Account const&, ApplyContext& ac) {
+                    auto sle =
+                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), eur.currency));
+                    if (!sle)
+                        return false;
+                    STAmount balance{Issue{eur.currency, issuer.id()}, 90};
+                    if (holder.id() > issuer.id())
+                        balance.negate();
+                    sle->setFieldAmount(sfBalance, balance);
+                    ac.view().update(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
+                    }},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+        }
+
+        // Clawback leaving the holder's balance negative.
+        {
+            Env env(*this, all_);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            auto const usd = issuer["USD"];
+            env.trust(usd(100), holder);
+            env(pay(issuer, holder, usd(100)));
+            env.close();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {{"Invariant failed: trustline or MPT balance is negative"}},
+                [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
+                    auto sle =
+                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
+                    if (!sle)
+                        return false;
+                    // Make the holder's balance negative from their perspective.
+                    STAmount balance{Issue{usd.currency, issuer.id()}, 80};
+                    if (holder.id() < issuer.id())
+                        balance.negate();
+                    sle->setFieldAmount(sfBalance, balance);
+                    ac.view().update(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
+                    }},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+        }
+
+        // IOU-amount clawback while only an MPToken changed: no trustline was
+        // recorded, so iou_.before is empty.
+        {
+            Env env(*this, all_);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            auto const usd = issuer["USD"];
+            MPTTester const mpt(
+                {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
+            auto const id = mpt.issuanceID();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {{"Invariant failed: trustline clawback changed the wrong line"}},
+                [id](Account const& holder, Account const&, ApplyContext& ac) {
+                    auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
+                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
+                    if (!sleToken || !sleIssuance)
+                        return false;
+                    sleToken->setFieldU64(sfMPTAmount, 90);
+                    sleIssuance->setFieldU64(sfOutstandingAmount, 90);
+                    ac.view().update(sleToken);
+                    ac.view().update(sleIssuance);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 10};
+                    }},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+        }
+
+        // Valid trustline change but a zero clawback amount.
+        {
+            Env env(*this, all_);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            auto const usd = issuer["USD"];
+            env.trust(usd(100), holder);
+            env(pay(issuer, holder, usd(100)));
+            env.close();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {{"Invariant failed: trustline clawback amount is invalid"}},
+                [issuer, usd](Account const& holder, Account const&, ApplyContext& ac) {
+                    auto sle =
+                        ac.view().peek(keylet::trustLine(holder.id(), issuer.id(), usd.currency));
+                    if (!sle)
+                        return false;
+                    STAmount balance{Issue{usd.currency, issuer.id()}, 90};
+                    if (holder.id() > issuer.id())
+                        balance.negate();
+                    sle->setFieldAmount(sfBalance, balance);
+                    ac.view().update(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfAmount] = STAmount{Issue{usd.currency, holder.id()}, 0};
+                    }},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+        }
+
+        // MPT clawback tx missing the Holder field.
+        {
+            Env env(*this, all_);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            MPTTester const mpt(
+                {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
+            auto const id = mpt.issuanceID();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {{"Invariant failed: MPT clawback missing holder"}},
+                [id](Account const& holder, Account const&, ApplyContext& ac) {
+                    auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
+                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
+                    if (!sleToken || !sleIssuance)
+                        return false;
+                    sleToken->setFieldU64(sfMPTAmount, 90);
+                    sleIssuance->setFieldU64(sfOutstandingAmount, 90);
+                    ac.view().update(sleToken);
+                    ac.view().update(sleIssuance);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfAmount] = STAmount{MPTIssue{id}, 10};
+                    }},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+        }
+
+        // MPT clawback where the holder's MPToken was deleted (after is empty).
+        {
+            Env env(*this, all_);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            MPTTester const mpt(
+                {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
+            auto const id = mpt.issuanceID();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {{"Invariant failed: MPT clawback token is missing"}},
+                [id](Account const& holder, Account const&, ApplyContext& ac) {
+                    auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
+                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
+                    if (!sleToken || !sleIssuance)
+                        return false;
+                    // Keep the issuance consistent after removing the token.
+                    sleIssuance->setFieldU64(sfOutstandingAmount, 0);
+                    ac.view().update(sleIssuance);
+                    ac.view().erase(sleToken);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfHolder] = holder.id();
+                        tx[sfAmount] = STAmount{MPTIssue{id}, 10};
+                    }},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+        }
+
+        // MPT clawback that changed a different holder's MPToken.
+        {
+            Env env(*this, all_);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            MPTTester const mpt(
+                {.env = env,
+                 .issuer = issuer,
+                 .holders = {holder, other},
+                 .pay = 100,
+                 .maxAmt = 200});
+            auto const id = mpt.issuanceID();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {{"Invariant failed: MPT clawback changed the wrong token"}},
+                [id](Account const&, Account const& other, ApplyContext& ac) {
+                    auto const sleToken = ac.view().peek(keylet::mptoken(id, other));
+                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
+                    if (!sleToken || !sleIssuance)
+                        return false;
+                    sleToken->setFieldU64(sfMPTAmount, 90);
+                    sleIssuance->setFieldU64(sfOutstandingAmount, 190);
+                    ac.view().update(sleToken);
+                    ac.view().update(sleIssuance);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfHolder] = holder.id();
+                        tx[sfAmount] = STAmount{MPTIssue{id}, 10};
+                    }},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+        }
+
+        // Valid MPToken change but a zero MPT clawback amount.
+        {
+            Env env(*this, all_);
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            Account const other{"other"};
+            env.fund(XRP(1'000), issuer, holder, other);
+            MPTTester const mpt(
+                {.env = env, .issuer = issuer, .holders = {holder}, .pay = 100, .maxAmt = 100});
+            auto const id = mpt.issuanceID();
+
+            doInvariantCheck(
+                std::move(env),
+                holder,
+                other,
+                {{"Invariant failed: MPT clawback amount is invalid"}},
+                [id](Account const& holder, Account const&, ApplyContext& ac) {
+                    auto const sleToken = ac.view().peek(keylet::mptoken(id, holder));
+                    auto const sleIssuance = ac.view().peek(keylet::mptokenIssuance(id));
+                    if (!sleToken || !sleIssuance)
+                        return false;
+                    sleToken->setFieldU64(sfMPTAmount, 90);
+                    sleIssuance->setFieldU64(sfOutstandingAmount, 90);
+                    ac.view().update(sleToken);
+                    ac.view().update(sleIssuance);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttCLAWBACK,
+                    [&](STObject& tx) {
+                        tx[sfAccount] = issuer.id();
+                        tx[sfHolder] = holder.id();
+                        tx[sfAmount] = STAmount{MPTIssue{id}, 0};
+                    }},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+        }
+
+        // More MPTokens created than expected
+        std::array, 4> const tests = {
+            std::make_pair(ttAMM_WITHDRAW, 2),
+            std::make_pair(ttAMM_CLAWBACK, 2),
+            std::make_pair(ttAMM_CREATE, 3),
+            std::make_pair(ttCHECK_CASH, 2)};
+        for (auto const& [tx, nTokens] : tests)
+        {
+            doInvariantCheck(
+                {{std::string("MPToken created for the MPT issuer")}},
+                [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    auto const sle = ac.view().peek(keylet::account(a1.id()));
+                    if (!sle)
+                        return false;
+
+                    auto seq = sle->getFieldU32(sfSequence);
+                    for (int i = 0; i < nTokens; ++i)
+                    {
+                        MPTIssue const mpt{makeMptID(seq + i, a1)};
+                        auto sleNew =
+                            std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
+                        ac.view().insert(sleNew);
+
+                        sleNew = std::make_shared(keylet::mptoken(mpt.getMptID(), a2));
+                        ac.view().insert(sleNew);
+                    }
+
+                    return true;
+                },
+                XRPAmount{},
+                STTx{tx, [](STObject& tx) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+        }
+
+        // More MPTokens deleted than expected
+        for (auto const& tx : {ttAMM_WITHDRAW, ttAMM_CLAWBACK})
+        {
+            MPTID id;
+            Account const a3("A3");
+            doInvariantCheck(
+                {{"MPT authorize  succeeded but created/deleted bad number of mptokens"}},
+                [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    for (auto const& a : {a1, a2, a3})
+                    {
+                        auto sle = ac.view().peek(keylet::mptoken(id, a));
+                        if (!sle)
+                            return false;
+                        ac.view().erase(sle);
+                    }
+                    return true;
+                },
+                XRPAmount{},
+                STTx{tx, [](STObject& tx) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                [&](Account const& a1, Account const& a2, Env& env) {
+                    Account const gw("gw");
+                    env.fund(XRP(1'000), gw, a3);
+                    MPTTester const mpt({.env = env, .issuer = gw, .holders = {a1, a2, a3}});
+                    id = mpt.issuanceID();
+                    return true;
+                });
+        }
+
+        // sfReferenceHolding can only be set on creation by VaultCreate. A
+        // non-VaultCreate transaction that creates an MPTokenIssuance with
+        // sfReferenceHolding present must trip the invariant.
+        doInvariantCheck(
+            {{"sfReferenceHolding set on a new MPTokenIssuance by a "
+              "non-VaultCreate transaction"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                auto const sleAcct = ac.view().peek(keylet::account(a1.id()));
+                if (!sleAcct)
+                    return false;
+                MPTIssue const mpt{makeMptID(sleAcct->getFieldU32(sfSequence), a1)};
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
+                sleNew->setFieldH256(sfReferenceHolding, uint256{1});
+                ac.view().insert(sleNew);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_SET, [](STObject&) {}});
+
+        // sfReferenceHolding is immutable: changing the field on an
+        // existing MPTokenIssuance must trip the invariant. Set up a real
+        // vault via preclose (so the share issuance carries
+        // sfReferenceHolding), then mutate it in precheck to produce a
+        // before/after pair.
+        {
+            uint256 vaultKey;
+            doInvariantCheck(
+                {{"sfReferenceHolding was modified on an existing "
+                  "MPTokenIssuance"}},
+                [&](Account const&, Account const&, ApplyContext& ac) {
+                    auto const sleVault = ac.view().peek(keylet::vault(vaultKey));
+                    if (!sleVault)
+                        return false;
+                    auto sleIssuance =
+                        ac.view().peek(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
+                    if (!sleIssuance)
+                        return false;
+                    sleIssuance->setFieldH256(sfReferenceHolding, uint256{2});
+                    ac.view().update(sleIssuance);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttACCOUNT_SET, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                [&](Account const& a1, Account const&, Env& env) {
+                    Account const issuer{"issuer"};
+                    env.fund(XRP(10'000), issuer);
+                    env.close();
+                    MPTTester mptt{env, issuer, kMptInitNoFund};
+                    mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
+                    PrettyAsset const asset = mptt.issuanceID();
+                    mptt.authorize({.account = a1});
+                    env.close();
+
+                    Vault const vault{env};
+                    auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
+                    env(tx);
+                    env.close();
+                    vaultKey = keylet.key;
+                    return true;
+                });
+        }
+
+        // A vault pseudo-account's MPToken cannot be deleted by anything
+        // other than a VaultDelete transaction. Set up a vault, then have
+        // an arbitrary tx erase the pseudo's MPToken in precheck.
+        {
+            uint256 vaultKey;
+            doInvariantCheck(
+                {{"vault pseudo-account holding deleted by a "
+                  "non-VaultDelete transaction"}},
+                [&](Account const&, Account const&, ApplyContext& ac) {
+                    auto const sleVault = ac.view().peek(keylet::vault(vaultKey));
+                    if (!sleVault)
+                        return false;
+                    auto const sleIssuance =
+                        ac.view().peek(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
+                    if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
+                        return false;
+                    auto sleHolding = ac.view().peek(
+                        keylet::unchecked(sleIssuance->getFieldH256(sfReferenceHolding)));
+                    if (!sleHolding)
+                        return false;
+                    ac.view().erase(sleHolding);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttACCOUNT_SET, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                [&](Account const& a1, Account const&, Env& env) {
+                    Account const issuer{"issuer"};
+                    env.fund(XRP(10'000), issuer);
+                    env.close();
+                    MPTTester mptt{env, issuer, kMptInitNoFund};
+                    mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
+                    PrettyAsset const asset = mptt.issuanceID();
+                    mptt.authorize({.account = a1});
+                    env.close();
+
+                    Vault const vault{env};
+                    auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
+                    env(tx);
+                    env.close();
+                    vaultKey = keylet.key;
+                    return true;
+                });
+        }
+
+        // Invalid transfer
+        std::array, 3> const invalidTransferTests = {
+            std::make_pair(ttAMM_WITHDRAW, false),
+            std::make_pair(ttPAYMENT, false),
+            std::make_pair(ttPAYMENT, true)};
+        // The two amendments that gate enforcement, in all four combinations.
+        FeatureBitset const gatesEnabled{featureMPTokensV2, fixCleanup3_4_0};
+        for (auto const gates :
+             {gatesEnabled,
+              gatesEnabled - featureMPTokensV2,
+              gatesEnabled - fixCleanup3_4_0,
+              FeatureBitset{}})
+        {
+            for (auto const& [tx, crossCurrencyPayment] : invalidTransferTests)
+            {
+                for (auto const flag :
+                     {static_cast(lsfMPTLocked),
+                      ~lsfMPTCanTransfer,
+                      ~lsfMPTCanTrade,
+                      0u})
+                {
+                    MPTID id{};
+                    auto const isSuccess = !gates.any() || flag == 0 ||
+                        (tx == ttPAYMENT && !crossCurrencyPayment && (flag == ~lsfMPTCanTrade)) ||
+                        (tx == ttAMM_WITHDRAW &&
+                         (flag == ~lsfMPTCanTrade || flag == ~lsfMPTCanTransfer));
+                    std::pair const error = isSuccess
+                        ? std::make_pair(TER(tesSUCCESS), TER(tesSUCCESS))
+                        : std::make_pair(TER(tecINVARIANT_FAILED), TER(tefINVARIANT_FAILED));
+                    doInvariantCheck(
+                        {{isSuccess ? "" : "invalid MPToken transfer between holders"}},
+                        [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                            auto update = [&](AccountID const& a, std::uint64_t v) {
+                                auto sle = ac.view().peek(keylet::mptoken(id, a));
+                                if (!sle)
+                                    return false;
+                                sle->at(sfMPTAmount) = v;
+                                ac.view().update(sle);
+                                return true;
+                            };
+                            auto issuanceSle = ac.view().peek(keylet::mptokenIssuance(id));
+                            if (!issuanceSle)
+                                return false;
+                            auto const flags = issuanceSle->at(sfFlags);
+                            if (flag == lsfMPTLocked)
+                            {
+                                issuanceSle->at(sfFlags) = flags | lsfMPTLocked;
+                            }
+                            else if (flag != 0u)
+                            {
+                                issuanceSle->at(sfFlags) = flags & flag;
+                            }
+                            issuanceSle->at(sfOutstandingAmount) = 200;
+                            ac.view().update(issuanceSle);
+                            return update(a1, 101) && update(a2, 99);
+                        },
+                        XRPAmount{},
+                        STTx{
+                            tx,
+                            [&](STObject& tx) {
+                                if (crossCurrencyPayment)
+                                {
+                                    tx.setFieldAmount(
+                                        sfSendMax, STAmount(MPTAmount{100}, MPTIssue{id}));
+                                }
+                            }},
+                        {error.first, error.second},
+                        [&](Account const& a1, Account const& a2, Env& env) {
+                            Account const gw("gw");
+                            env.fund(XRP(1'000), gw);
+                            MPTTester const usd(
+                                {.env = env, .issuer = gw, .holders = {a1, a2}, .pay = 100});
+                            id = usd.issuanceID();
+                            // Either gate enforces, so both must be off to stay
+                            // advisory. Disable after setting up the MPT; the
+                            // next env.close() is what makes it take effect.
+                            if (!gates[featureMPTokensV2])
+                                env.disableFeature(featureMPTokensV2);
+                            if (!gates[fixCleanup3_4_0])
+                                env.disableFeature(fixCleanup3_4_0);
+                            return true;
+                        });
+                }
+            }
+        }
+
+        // An orphan has a zero balance, so only deletion is legitimate (see
+        // "Skipping Deleted MPTs" in testConfidentialMPTTransfer).
+        {
+            MPTID orphanID;
+            auto const setupOrphan = [&](Account const& a1, Account const& a2, Env& env) {
+                MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
+                mpt.create({.flags = tfMPTCanTransfer});
+                orphanID = mpt.issuanceID();
+                // A2 is authorized but never paid, so its balance is zero and
+                // the issuance can be destroyed while its MPToken lives on.
+                mpt.authorize({.account = a2});
+                mpt.destroy();
+                return true;
+            };
+            // ValidMPTBalanceChanges also reports this, so assert on the
+            // orphan message, which only the missing-issuance branch produces.
+            doInvariantCheck(
+                {{"orphaned MPToken balance changed"}},
+                [&](Account const&, Account const& a2, ApplyContext& ac) {
+                    auto sleTok = ac.view().peek(keylet::mptoken(orphanID, a2.id()));
+                    if (!sleTok || (*sleTok)[sfMPTAmount] != 0)
+                        return false;
+                    (*sleTok)[sfMPTAmount] = (*sleTok)[sfMPTAmount] + 10;
+                    ac.view().update(sleTok);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                setupOrphan);
+            // Negative control: erasing the orphan is how it gets cleaned up.
+            doInvariantCheck(
+                {},
+                [&](Account const&, Account const& a2, ApplyContext& ac) {
+                    auto sleTok = ac.view().peek(keylet::mptoken(orphanID, a2.id()));
+                    if (!sleTok)
+                        return false;
+                    ac.view().erase(sleTok);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+                {tesSUCCESS, tesSUCCESS},
+                setupOrphan);
+            // The same erase on a failure. The orphan branch continues, so only
+            // the pre-loop deletion check can report this one.
+            doInvariantCheck(
+                {{"MPToken deleted on failure"}},
+                [&](Account const&, Account const& a2, ApplyContext& ac) {
+                    auto sleTok = ac.view().peek(keylet::mptoken(orphanID, a2.id()));
+                    if (!sleTok)
+                        return false;
+                    ac.view().erase(sleTok);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                setupOrphan,
+                TxAccount::None,
+                std::source_location::current(),
+                tecEXPIRED);
+        }
+
+        // Vault-share freeze invariant: isVaultPseudoAccountFrozen descends
+        // through sfReferenceHolding to test the vault's underlying asset for
+        // each changed holder.
+        {
+            Account const gw{"gw"};
+            MPTID shareID{};
+
+            // Vault setup: a1 and a2 both deposit IOU and hold vault shares.
+            auto const setupVault = [&](Account const& a1,
+                                        Account const& a2,
+                                        Env& env) -> std::tuple {
+                env.fund(XRP(1'000), gw);
+                env.trust(gw["IOU"](10'000), a1);
+                env.trust(gw["IOU"](10'000), a2);
+                env.close();
+                env(pay(gw, a1, gw["IOU"](500)));
+                env(pay(gw, a2, gw["IOU"](500)));
+                env.close();
+
+                Vault const vault{env};
+                auto [createTx, vaultKeylet] = vault.create({.owner = a1, .asset = gw["IOU"]});
+                env(createTx);
+                env.close();
+                env(vault.deposit(
+                    {.depositor = a1, .id = vaultKeylet.key, .amount = gw["IOU"](100)}));
+                env(vault.deposit(
+                    {.depositor = a2, .id = vaultKeylet.key, .amount = gw["IOU"](100)}));
+                env.close();
+
+                return {env.le(vaultKeylet)->at(sfShareMPTID), env.le(vaultKeylet)->at(sfAccount)};
+            };
+
+            // Simulate a vault-share transfer: a1 sends 10 shares to a2.
+            auto const precheck =
+                [&](Account const& a1, Account const& a2, ApplyContext& ac) -> bool {
+                auto sle1 = ac.view().peek(keylet::mptoken(shareID, a1.id()));
+                auto sle2 = ac.view().peek(keylet::mptoken(shareID, a2.id()));
+                if (!sle1 || !sle2)
+                    return false;
+                (*sle1)[sfMPTAmount] -= 10;
+                (*sle2)[sfMPTAmount] += 10;
+                ac.view().update(sle1);
+                ac.view().update(sle2);
+                return true;
+            };
+
+            // Case: vault pseudo-account's IOU trustline is frozen.
+            {
+                auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
+                    auto [sid, vid] = setupVault(a1, a2, env);
+                    shareID = sid;
+                    env(trust(gw, gw["IOU"](0), Account{"vaultPseudo", vid}, tfSetFreeze));
+                    env.close();
+                    return true;
+                };
+
+                doInvariantCheck(
+                    Env{*this, all_},
+                    {{"invalid MPToken transfer between holders"}},
+                    precheck,
+                    XRPAmount{},
+                    STTx{ttPAYMENT, [](STObject&) {}},
+                    {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                    preclose);
+            }
+
+            // Case: receiver's (a2's) IOU trustline is frozen.
+            {
+                auto const preclose = [&](Account const& a1, Account const& a2, Env& env) -> bool {
+                    auto [sid, vid] = setupVault(a1, a2, env);
+                    shareID = sid;
+                    env(trust(gw, gw["IOU"](0), a2, tfSetFreeze));
+                    env.close();
+                    return true;
+                };
+
+                doInvariantCheck(
+                    Env{*this, all_},
+                    {{"invalid MPToken transfer between holders"}},
+                    precheck,
+                    XRPAmount{},
+                    STTx{ttPAYMENT, [](STObject&) {}},
+                    {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                    preclose);
+            }
+        }
+    }
+
+    void
+    testConfidentialMPTTransfer()
+    {
+        using namespace test::jtx;
+        testcase << "ValidConfidentialMPToken";
+
+        MPTID mptID;
+
+        // Generate an MPT with privacy, issue 100 tokens to A2.
+        // Perform a confidential conversion to populate encrypted state.
+        auto const precloseConfidential =
+            [&mptID](Account const& a1, Account const& a2, Env& env) -> bool {
+            MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
+            mpt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
+            mptID = mpt.issuanceID();
+
+            mpt.authorize({.account = a2});
+            mpt.pay(a1, a2, 100);
+
+            mpt.generateKeyPair(a1);
+            mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)});
+
+            mpt.generateKeyPair(a2);
+            mpt.convert({
+                .account = a2,
+                .amt = 100,
+                .holderPubKey = mpt.getPubKey(a2),
+            });
+            return true;
+        };
+
+        // badDelete
+        doInvariantCheck(
+            {"MPToken deleted with encrypted fields while COA > 0"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                // Force an erase of the object while the COA remains 100
+                ac.view().erase(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseConfidential);
+
+        // badConsistency
+        doInvariantCheck(
+            {"MPToken encrypted field existence inconsistency"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                // Remove one of the required encrypted fields to create a mismatch
+                sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
+                ac.view().update(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        doInvariantCheck(
+            {"MPToken encrypted field existence inconsistency"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
+                sleToken->makeFieldAbsent(sfConfidentialBalanceInbox);
+                sleToken->makeFieldAbsent(sfConfidentialBalanceSpending);
+                sleToken->setFieldVL(sfAuditorEncryptedBalance, Blob{0x00});
+                ac.view().update(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        // requiresPrivacyFlag
+        auto const precloseNoPrivacy = [&mptID](
+                                           Account const& a1, Account const& a2, Env& env) -> bool {
+            MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
+            // completely omitted the tfMPTCanHoldConfidentialBalance flag here.
+            mpt.create({.flags = tfMPTCanTransfer});
+            mptID = mpt.issuanceID();
+            mpt.authorize({.account = a2});
+            mpt.pay(a1, a2, 100);
+            return true;
+        };
+
+        doInvariantCheck(
+            {"MPToken has encrypted fields but Issuance does not have "
+             "lsfMPTCanHoldConfidentialBalance "
+             "set"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                // Inject all three encrypted fields consistently (inbox+spending+issuer must be
+                // in sync or badConsistency fires first and masks requiresPrivacyFlag).
+                sleToken->setFieldVL(sfConfidentialBalanceInbox, Blob{0x00});
+                sleToken->setFieldVL(sfConfidentialBalanceSpending, Blob{0x00});
+                sleToken->setFieldVL(sfIssuerEncryptedBalance, Blob{0x00});
+                ac.view().update(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseNoPrivacy);
+
+        // badCOA
+        doInvariantCheck(
+            {"Confidential outstanding amount exceeds total outstanding amount"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
+                if (!sleIssuance)
+                    return false;
+                // Total outstanding is natively 100; bloat the COA over 100
+                sleIssuance->setFieldU64(sfConfidentialOutstandingAmount, 200);
+                ac.view().update(sleIssuance);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_ISSUANCE_SET, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        // Conservation Violation
+        doInvariantCheck(
+            {"Token conservation violation for MPT"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
+                if (!sleIssuance)
+                    return false;
+
+                sleIssuance->setFieldU64(
+                    sfConfidentialOutstandingAmount,
+                    sleIssuance->getFieldU64(sfConfidentialOutstandingAmount) - 10);
+                ac.view().update(sleIssuance);
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        // Send/MergeInbox must not change OutstandingAmount (coaDelta == 0)
+        doInvariantCheck(
+            {"Invariant failed: OutstandingAmount changed "
+             "by confidential transaction that should not "
+             "modify it for MPT"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleIssuance = ac.view().peek(keylet::mptokenIssuance(mptID));
+                if (!sleIssuance)
+                    return false;
+                sleIssuance->setFieldU64(
+                    sfOutstandingAmount, sleIssuance->getFieldU64(sfOutstandingAmount) + 1);
+                ac.view().update(sleIssuance);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        // Send/MergeInbox and zero-COA-delta confidential transactions must not
+        // change public holder MPTAmount.
+        doInvariantCheck(
+            {"Invariant failed: MPTAmount changed by confidential "
+             "transaction that should not modify this field."},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                sleToken->setFieldU64(sfMPTAmount, sleToken->getFieldU64(sfMPTAmount) + 1);
+                ac.view().update(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttCONFIDENTIAL_MPT_SEND, [](STObject&) {}},
+            // Second pass is tef: the bumped MPTAmount also trips
+            // ValidMPTTransfer's on-failure check, which escalates the tec.
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseConfidential);
+
+        // badVersion
+        doInvariantCheck(
+            {"MPToken sfConfidentialBalanceVersion not updated when sfConfidentialBalanceSpending "
+             "changed"},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                Blob const kChangedConfidentialSpending = {0xBA, 0xDD};
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                sleToken->setFieldVL(sfConfidentialBalanceSpending, kChangedConfidentialSpending);
+
+                // DO NOT update sfConfidentialBalanceVersion
+                ac.view().update(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseConfidential);
+
+        // Skipping Deleted MPTs (Issuance deleted)
+        auto const precloseOrphan = [&mptID](
+                                        Account const& a1, Account const& a2, Env& env) -> bool {
+            MPTTester mpt(env, a1, {.holders = {a2}, .fund = false});
+            mpt.create({.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance});
+            mptID = mpt.issuanceID();
+            mpt.authorize({.account = a2});
+
+            // Generate privacy keys and convert 0 amount so Bob has the encrypted fields
+            mpt.generateKeyPair(a1);
+            mpt.set({.account = a1, .issuerPubKey = mpt.getPubKey(a1)});
+            mpt.generateKeyPair(a2);
+            mpt.convert({
+                .account = a2,
+                .amt = 0,
+                .holderPubKey = mpt.getPubKey(a2),
+            });
+
+            // Immediately destroy the issuance. A2's empty, encrypted token object lives on.
+            mpt.destroy();
+            return true;
+        };
+
+        doInvariantCheck(
+            {},
+            [&mptID](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleToken = ac.view().peek(keylet::mptoken(mptID, a2.id()));
+                if (!sleToken)
+                    return false;
+                // Safely able to erase the deleted token.
+                ac.view().erase(sleToken);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
+            {tesSUCCESS, tesSUCCESS},
+            precloseOrphan);
+    }
+
+public:
+    void
+    run() override
+    {
+        testConfidentialMPTTransfer();
+        testMPT();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(InvariantsMPT, app, xrpl);
+
+}  // namespace xrpl::test
diff --git a/src/test/app/invariants/InvariantsMisc_test.cpp b/src/test/app/invariants/InvariantsMisc_test.cpp
new file mode 100644
index 0000000000..a0084ac530
--- /dev/null
+++ b/src/test/app/invariants/InvariantsMisc_test.cpp
@@ -0,0 +1,1585 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+class InvariantsMisc_test : public InvariantsBase
+{
+    FeatureBitset const all_{test::jtx::testableAmendments()};
+
+    void
+    testXRPNotCreated()
+    {
+        using namespace test::jtx;
+        testcase << "XRP created";
+        doInvariantCheck(
+            {{"XRP net change was positive: 500"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // put a single account in the view and "manufacture" some XRP
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                auto amt = sle->getFieldAmount(sfBalance);
+                sle->setFieldAmount(sfBalance, amt + STAmount{500});
+                ac.view().update(sle);
+                return true;
+            });
+    }
+
+    void
+    testAccountRootsNotRemoved()
+    {
+        using namespace test::jtx;
+        testcase << "account root removed";
+
+        // An account was deleted, but not by an AccountDelete transaction.
+        doInvariantCheck(
+            {{"an account root was deleted"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // remove an account from the view
+                auto sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                // Clear the balance so the "account deletion left behind a
+                // non-zero balance" check doesn't trip earlier than the desired
+                // check.
+                sle->at(sfBalance) = beast::kZero;
+                ac.view().erase(sle);
+                return true;
+            });
+
+        // Successful AccountDelete transaction that didn't delete an account.
+        //
+        // Note that this is a case where a second invocation of the invariant
+        // checker returns a tecINVARIANT_FAILED, not a tefINVARIANT_FAILED.
+        // After a discussion with the team, we believe that's okay.
+        doInvariantCheck(
+            {{"account deletion succeeded without deleting an account"}},
+            [](Account const&, Account const&, ApplyContext& ac) { return true; },
+            XRPAmount{},
+            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
+
+        // Successful AccountDelete that deleted more than one account.
+        doInvariantCheck(
+            {{"account deletion succeeded but deleted multiple accounts"}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                // remove two accounts from the view
+                auto sleA1 = ac.view().peek(keylet::account(a1.id()));
+                auto sleA2 = ac.view().peek(keylet::account(a2.id()));
+                if (!sleA1 || !sleA2)
+                    return false;
+                // Clear the balance so the "account deletion left behind a
+                // non-zero balance" check doesn't trip earlier than the desired
+                // check.
+                sleA1->at(sfBalance) = beast::kZero;
+                sleA2->at(sfBalance) = beast::kZero;
+                ac.view().erase(sleA1);
+                ac.view().erase(sleA2);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
+    }
+
+    void
+    testAccountRootsDeletedClean()
+    {
+        using namespace test::jtx;
+        testcase << "account root deletion left artifact";
+
+        doInvariantCheck(
+            {{"account deletion left behind a non-zero balance"}},
+            // NOLINTNEXTLINE(readability-identifier-naming)
+            [&](Account const& A1, Account const& A2, ApplyContext& ac) {
+                // A1 has a balance. Delete A1
+                auto const a1 = A1.id();
+                auto const sleA1 = ac.view().peek(keylet::account(a1));
+                if (!sleA1)
+                    return false;
+                if (!BEAST_EXPECT(*sleA1->at(sfBalance) != beast::kZero))
+                    return false;
+
+                ac.view().erase(sleA1);
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
+
+        doInvariantCheck(
+            {{"account deletion left behind a non-zero owner count"}},
+            // NOLINTNEXTLINE(readability-identifier-naming)
+            [&](Account const& A1, Account const& A2, ApplyContext& ac) {
+                // Increment A1's owner count, then delete A1
+                auto const a1 = A1.id();
+                auto const sleA1 = ac.view().peek(keylet::account(a1));
+                if (!sleA1)
+                    return false;
+                // Clear the balance so the "account deletion left behind a
+                // non-zero balance" check doesn't trip earlier than the desired
+                // check.
+                sleA1->at(sfBalance) = beast::kZero;
+                BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0);
+                increaseOwnerCount(ac.view(), sleA1, {}, 1, ac.journal);
+
+                ac.view().erase(sleA1);
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
+
+        doInvariantCheck(
+            {{"account deletion left behind a sponsorship field"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
+                if (!sleA1)
+                    return false;
+                sleA1->at(sfBalance) = beast::kZero;
+                sleA1->setFieldU32(sfSponsoredOwnerCount, 1);
+
+                ac.view().erase(sleA1);
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
+
+        doInvariantCheck(
+            {{"account deletion left behind a sponsorship field"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
+                if (!sleA1)
+                    return false;
+                sleA1->at(sfBalance) = beast::kZero;
+                sleA1->setFieldU32(sfSponsoringOwnerCount, 1);
+
+                ac.view().erase(sleA1);
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
+
+        doInvariantCheck(
+            {{"account deletion left behind a sponsorship field"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const a1Id = a1.id();
+                auto const sleA1 = ac.view().peek(keylet::account(a1Id));
+                if (!sleA1)
+                    return false;
+                sleA1->at(sfBalance) = beast::kZero;
+                sleA1->setFieldU32(sfSponsoringAccountCount, 1);
+
+                ac.view().erase(sleA1);
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
+
+        doInvariantCheck(
+            {{"account deletion left behind a sponsorship field"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
+                if (!sleA1)
+                    return false;
+                sleA1->at(sfBalance) = beast::kZero;
+                sleA1->setAccountID(sfSponsor, a2.id());
+
+                ac.view().erase(sleA1);
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
+
+        doInvariantCheck(
+            Env{*this, FeatureBitset{featureSponsor}},
+            {{"account deletion left behind a sponsorship field"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sleA1 = ac.view().peek(keylet::account(a1.id()));
+                if (!sleA1)
+                    return false;
+                sleA1->at(sfBalance) = beast::kZero;
+                sleA1->setAccountID(sfSponsor, a2.id());
+
+                ac.view().erase(sleA1);
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
+
+        for (auto const& [keyletfunc, type, includeInTests] : kDirectAccountKeylets)
+        {
+            if (!includeInTests)
+                continue;
+
+            using namespace std::string_literals;
+
+            doInvariantCheck(
+                {{"account deletion left behind a "s + type.cStr() + " object"}},
+                // NOLINTNEXTLINE(readability-identifier-naming)
+                [&](Account const& A1, Account const& A2, ApplyContext& ac) {
+                    // Add an object to the ledger for account A1, then delete
+                    // A1
+                    auto const a1 = A1.id();
+                    auto sleA1 = ac.view().peek(keylet::account(a1));
+                    if (!sleA1)
+                        return false;
+
+                    auto const key = std::invoke(keyletfunc, a1);
+                    auto const newSLE = std::make_shared(key);
+                    ac.view().insert(newSLE);
+                    // Clear the balance so the "account deletion left behind a
+                    // non-zero balance" check doesn't trip earlier than the
+                    // desired check.
+                    sleA1->at(sfBalance) = beast::kZero;
+                    ac.view().erase(sleA1);
+
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
+        }
+
+        // NFT special case
+        doInvariantCheck(
+            {{"account deletion left behind a NFTokenPage object"}},
+            [&](Account const& a1, Account const&, ApplyContext& ac) {
+                // remove an account from the view
+                auto sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                // Clear the balance so the "account deletion left behind a
+                // non-zero balance" check doesn't trip earlier than the desired
+                // check.
+                sle->at(sfBalance) = beast::kZero;
+                sle->at(sfOwnerCount) = 0;
+                ac.view().erase(sle);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_DELETE, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            [&](Account const& a1, Account const&, Env& env) {
+                // Preclose callback to mint the NFT which will be deleted in
+                // the Precheck callback above.
+                env(token::mint(a1));
+
+                return true;
+            });
+
+        // AMM special cases
+        AccountID ammAcctID;
+        uint256 ammKey;
+        Issue ammIssue;
+        doInvariantCheck(
+            {{"account deletion left behind a DirectoryNode object"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                // Delete the AMM account without cleaning up the directory or
+                // deleting the AMM object
+                auto sle = ac.view().peek(keylet::account(ammAcctID));
+                if (!sle)
+                    return false;
+
+                BEAST_EXPECT(sle->at(~sfAMMID));
+                BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
+
+                // Clear the balance so the "account deletion left behind a
+                // non-zero balance" check doesn't trip earlier than the desired
+                // check.
+                sle->at(sfBalance) = beast::kZero;
+                sle->at(sfOwnerCount) = 0;
+                ac.view().erase(sle);
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttAMM_WITHDRAW, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                // Preclose callback to create the AMM which will be partially
+                // deleted in the Precheck callback above.
+                AMM const amm(env, a1, XRP(100), a1["USD"](50));
+                ammAcctID = amm.ammAccount();
+                ammKey = amm.ammID();
+                ammIssue = amm.lptIssue();
+                return true;
+            });
+        doInvariantCheck(
+            {{"account deletion left behind a AMM object"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                // Delete all the AMM's trust lines, remove the AMM from the AMM
+                // account's directory (this deletes the directory), and delete
+                // the AMM account. Do not delete the AMM object.
+                auto sle = ac.view().peek(keylet::account(ammAcctID));
+                if (!sle)
+                    return false;
+
+                BEAST_EXPECT(sle->at(~sfAMMID));
+                BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
+
+                for (auto const& trustKeylet :
+                     {keylet::trustLine(ammAcctID, a1["USD"]), keylet::trustLine(a1, ammIssue)})
+                {
+                    auto const line = ac.view().peek(trustKeylet);
+                    if (!line)
+                    {
+                        return false;
+                    }
+
+                    STAmount const lowLimit = line->at(sfLowLimit);
+                    STAmount const highLimit = line->at(sfHighLimit);
+                    BEAST_EXPECT(
+                        trustDelete(
+                            ac.view(),
+                            line,
+                            lowLimit.getIssuer(),
+                            highLimit.getIssuer(),
+                            ac.journal) == tesSUCCESS);
+                }
+
+                auto const ammSle = ac.view().peek(keylet::amm(ammKey));
+                if (!BEAST_EXPECT(ammSle))
+                    return false;
+                auto const ownerDirKeylet = keylet::ownerDir(ammAcctID);
+
+                BEAST_EXPECT(
+                    ac.view().dirRemove(ownerDirKeylet, ammSle->at(sfOwnerNode), ammKey, false));
+                BEAST_EXPECT(
+                    !ac.view().exists(ownerDirKeylet) || ac.view().emptyDirDelete(ownerDirKeylet));
+
+                // Clear the balance so the "account deletion left behind a
+                // non-zero balance" check doesn't trip earlier than the desired
+                // check.
+                sle->at(sfBalance) = beast::kZero;
+                sle->at(sfOwnerCount) = 0;
+                ac.view().erase(sle);
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttAMM_WITHDRAW, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                // Preclose callback to create the AMM which will be partially
+                // deleted in the Precheck callback above.
+                AMM const amm(env, a1, XRP(100), a1["USD"](50));
+                ammAcctID = amm.ammAccount();
+                ammKey = amm.ammID();
+                ammIssue = amm.lptIssue();
+                return true;
+            });
+    }
+
+    void
+    testTypesMatch()
+    {
+        using namespace test::jtx;
+        testcase << "ledger entry types don't match";
+        doInvariantCheck(
+            {{"ledger entry type mismatch"}, {"XRP net change of -1000000000 doesn't match fee 0"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // replace an entry in the table with an SLE of a different type
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                auto const sleNew = std::make_shared(ltTICKET, sle->key());
+                ac.rawView().rawReplace(sleNew);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"invalid ledger entry type added"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                // add an entry in the table with an SLE of an invalid type
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+
+                // make a dummy escrow ledger entry, then change the type to an
+                // unsupported value so that the valid type invariant check
+                // will fail.
+                auto const sleNew = std::make_shared(
+                    keylet::escrow(a1, SeqProxy::rawSequence((*sle)[sfSequence] + 2)));
+
+                // We don't use ltNICKNAME directly since it's marked deprecated
+                // to prevent accidental use elsewhere.
+                sleNew->type_ = static_cast('n');
+                ac.view().insert(sleNew);
+                return true;
+            });
+    }
+
+    void
+    testXRPBalanceCheck()
+    {
+        using namespace test::jtx;
+        testcase << "XRP balance checks";
+
+        doInvariantCheck(
+            {{"Cannot return non-native STAmount as XRPAmount"}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                // non-native balance
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                STAmount const nonNative(a2["USD"](51));
+                sle->setFieldAmount(sfBalance, nonNative);
+                ac.view().update(sle);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"incorrect account XRP balance"}, {"XRP net change was positive: 99999999000000001"}},
+            [this](Account const& a1, Account const&, ApplyContext& ac) {
+                // balance exceeds genesis amount
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                // Use `drops(1)` to bypass a call to STAmount::canonicalize
+                // with an invalid value
+                sle->setFieldAmount(sfBalance, kInitialXrp + drops(1));
+                BEAST_EXPECT(!sle->getFieldAmount(sfBalance).negative());
+                ac.view().update(sle);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"incorrect account XRP balance"},
+             {"XRP net change of -1000000001 doesn't match fee 0"}},
+            [this](Account const& a1, Account const&, ApplyContext& ac) {
+                // balance is negative
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                sle->setFieldAmount(sfBalance, STAmount{1, true});
+                BEAST_EXPECT(sle->getFieldAmount(sfBalance).negative());
+                ac.view().update(sle);
+                return true;
+            });
+    }
+
+    void
+    testTransactionFeeCheck()
+    {
+        using namespace test::jtx;
+        using namespace std::string_literals;
+        testcase << "Transaction fee checks";
+
+        doInvariantCheck(
+            {{"fee paid was negative: -1"}, {"XRP net change of 0 doesn't match fee -1"}},
+            [](Account const&, Account const&, ApplyContext&) { return true; },
+            XRPAmount{-1});
+
+        doInvariantCheck(
+            {{"fee paid exceeds system limit: "s + to_string(kInitialXrp)},
+             {"XRP net change of 0 doesn't match fee "s + to_string(kInitialXrp)}},
+            [](Account const&, Account const&, ApplyContext&) { return true; },
+            XRPAmount{kInitialXrp});
+
+        doInvariantCheck(
+            {{"fee paid is 20 exceeds fee specified in transaction."},
+             {"XRP net change of 0 doesn't match fee 20"}},
+            [](Account const&, Account const&, ApplyContext&) { return true; },
+            XRPAmount{20},
+            STTx{ttACCOUNT_SET, [](STObject& tx) { tx.setFieldAmount(sfFee, XRPAmount{10}); }});
+    }
+
+    void
+    testNoBadOffers()
+    {
+        using namespace test::jtx;
+        testcase << "no bad offers";
+
+        doInvariantCheck(
+            {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
+                // offer with negative takerpays
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                auto sleNew = std::make_shared(
+                    keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
+                sleNew->setAccountID(sfAccount, a1.id());
+                sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
+                sleNew->setFieldAmount(sfTakerPays, XRP(-1));
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
+                // offer with negative takergets
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                auto sleNew = std::make_shared(
+                    keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
+                sleNew->setAccountID(sfAccount, a1.id());
+                sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
+                sleNew->setFieldAmount(sfTakerPays, a1["USD"](10));
+                sleNew->setFieldAmount(sfTakerGets, XRP(-1));
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"offer with a bad amount"}}, [](Account const& a1, Account const&, ApplyContext& ac) {
+                // offer XRP to XRP
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                auto sleNew = std::make_shared(
+                    keylet::offer(a1.id(), SeqProxy::rawSequence((*sle)[sfSequence])));
+                sleNew->setAccountID(sfAccount, a1.id());
+                sleNew->setFieldU32(sfSequence, (*sle)[sfSequence]);
+                sleNew->setFieldAmount(sfTakerPays, XRP(10));
+                sleNew->setFieldAmount(sfTakerGets, XRP(11));
+                ac.view().insert(sleNew);
+                return true;
+            });
+    }
+
+    void
+    testValidNewAccountRoot()
+    {
+        using namespace test::jtx;
+        testcase << "valid new account root";
+
+        doInvariantCheck(
+            {{"account root created illegally"}},
+            [](Account const&, Account const&, ApplyContext& ac) {
+                // Insert a new account root created by a non-payment into
+                // the view.
+                Account const a3{"A3"};
+                Keylet const acctKeylet = keylet::account(a3);
+                auto const sleNew = std::make_shared(acctKeylet);
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"multiple accounts created in a single transaction"}},
+            [](Account const&, Account const&, ApplyContext& ac) {
+                // Insert two new account roots into the view.
+                {
+                    Account const a3{"A3"};
+                    Keylet const acctKeylet = keylet::account(a3);
+                    auto const sleA3 = std::make_shared(acctKeylet);
+                    ac.view().insert(sleA3);
+                }
+                {
+                    Account const a4{"A4"};
+                    Keylet const acctKeylet = keylet::account(a4);
+                    auto const sleA4 = std::make_shared(acctKeylet);
+                    ac.view().insert(sleA4);
+                }
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"account created with wrong starting sequence number"}},
+            [](Account const&, Account const&, ApplyContext& ac) {
+                // Insert a new account root with the wrong starting sequence.
+                Account const a3{"A3"};
+                Keylet const acctKeylet = keylet::account(a3);
+                auto const sleNew = std::make_shared(acctKeylet);
+                sleNew->setFieldU32(sfSequence, ac.view().seq() + 1);
+                ac.view().insert(sleNew);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPAYMENT, [](STObject& tx) {}});
+
+        doInvariantCheck(
+            {{"pseudo-account created by a wrong transaction type"}},
+            [](Account const&, Account const&, ApplyContext& ac) {
+                Account const a3{"A3"};
+                Keylet const acctKeylet = keylet::account(a3);
+                auto const sleNew = std::make_shared(acctKeylet);
+                sleNew->setFieldU32(sfSequence, 0);
+                sleNew->setFieldH256(sfAMMID, uint256(1));
+                sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple);
+                ac.view().insert(sleNew);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPAYMENT, [](STObject& tx) {}});
+
+        doInvariantCheck(
+            {{"account created with wrong starting sequence number"}},
+            [](Account const&, Account const&, ApplyContext& ac) {
+                Account const a3{"A3"};
+                Keylet const acctKeylet = keylet::account(a3);
+                auto const sleNew = std::make_shared(acctKeylet);
+                sleNew->setFieldU32(sfSequence, ac.view().seq());
+                sleNew->setFieldH256(sfAMMID, uint256(1));
+                sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
+                ac.view().insert(sleNew);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttAMM_CREATE, [](STObject& tx) {}});
+
+        doInvariantCheck(
+            {{"pseudo-account created with wrong flags"}},
+            [](Account const&, Account const&, ApplyContext& ac) {
+                Account const a3{"A3"};
+                Keylet const acctKeylet = keylet::account(a3);
+                auto const sleNew = std::make_shared(acctKeylet);
+                sleNew->setFieldU32(sfSequence, 0);
+                sleNew->setFieldH256(sfAMMID, uint256(1));
+                sleNew->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple);
+                ac.view().insert(sleNew);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject& tx) {}});
+
+        doInvariantCheck(
+            {{"pseudo-account created with wrong flags"}},
+            [](Account const&, Account const&, ApplyContext& ac) {
+                Account const a3{"A3"};
+                Keylet const acctKeylet = keylet::account(a3);
+                auto const sleNew = std::make_shared(acctKeylet);
+                sleNew->setFieldU32(sfSequence, 0);
+                sleNew->setFieldH256(sfAMMID, uint256(1));
+                sleNew->setFieldU32(
+                    sfFlags,
+                    lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth | lsfRequireDestTag);
+                ac.view().insert(sleNew);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttAMM_CREATE, [](STObject& tx) {}});
+    }
+
+    void
+    testNoModifiedUnmodifiableFields()
+    {
+        testcase("no modified unmodifiable fields");
+        using namespace jtx;
+
+        // Initialize with a placeholder value because there's no default ctor
+        Keylet loanBrokerKeylet = keylet::amendments();
+        Preclose const createLoanBroker = [&, this](Account const& a, Account const& b, Env& env) {
+            PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+
+            loanBrokerKeylet = this->createLoanBroker(a, env, xrpAsset);
+            return BEAST_EXPECT(env.le(loanBrokerKeylet));
+        };
+
+        {
+            auto const mods = std::to_array>({
+                [](SLE::pointer& sle) { sle->at(sfSequence) += 1; },
+                [](SLE::pointer& sle) { sle->at(sfOwnerNode) += 1; },
+                [](SLE::pointer& sle) { sle->at(sfVaultNode) += 1; },
+                [](SLE::pointer& sle) { sle->at(sfVaultID) = uint256(1u); },
+                [](SLE::pointer& sle) { sle->at(sfAccount) = sle->at(sfOwner); },
+                [](SLE::pointer& sle) { sle->at(sfOwner) = sle->at(sfAccount); },
+                [](SLE::pointer& sle) { sle->at(sfManagementFeeRate) += 1; },
+                [](SLE::pointer& sle) { sle->at(sfCoverRateMinimum) += 1; },
+                [](SLE::pointer& sle) { sle->at(sfCoverRateLiquidation) += 1; },
+                [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
+                [](SLE::pointer& sle) { sle->at(sfLedgerIndex) = sle->at(sfVaultID).value(); },
+            });
+
+            for (auto const& mod : mods)
+            {
+                doInvariantCheck(
+                    {{"changed an unchangeable field"}},
+                    [&](Account const& a1, Account const&, ApplyContext& ac) {
+                        auto sle = ac.view().peek(loanBrokerKeylet);
+                        if (!sle)
+                            return false;
+                        mod(sle);
+                        ac.view().update(sle);
+                        return true;
+                    },
+                    XRPAmount{},
+                    STTx{ttACCOUNT_SET, [](STObject& tx) {}},
+                    {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                    createLoanBroker);
+            }
+        }
+
+        // Loan flag immutability lives in NoModifiedUnmodifiableFields's
+        // ltLOAN case: lsfLoanOverpayment must never toggle in either
+        // direction, and lsfLoanDefault (gated on featureLendingProtocolV1_1)
+        // may only transition from unset to set. Each case needs a loan that
+        // already exists in the base ledger, so that the apply-view modification
+        // is seen as a before/after change rather than an insertion.
+        {
+            struct Case
+            {
+                std::uint32_t before;
+                std::uint32_t after;
+                std::string expected;
+            };
+            auto const cases = std::to_array({
+                {.before = lsfLoanOverpayment,
+                 .after = 0,
+                 .expected = "lsfLoanOverpayment flag toggled on immutable ledger entry"},
+                {.before = 0,
+                 .after = lsfLoanOverpayment,
+                 .expected = "lsfLoanOverpayment flag toggled on immutable ledger entry"},
+                {.before = lsfLoanDefault,
+                 .after = 0,
+                 .expected = "lsfLoanDefault flag cleared on immutable ledger entry"},
+            });
+
+            for (auto const& c : cases)
+            {
+                Env env{*this, all_};
+                Account const a1{"A1"};
+                env.fund(XRP(1000), a1);
+                env.close();
+
+                OpenView ov{*env.current()};
+
+                auto const brokerKeylet =
+                    keylet::loanBroker(a1.id(), SeqProxy::rawSequence(ov.seq()));
+                auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
+                {
+                    auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a1.id());
+                    sleLoan->at(sfPrincipalOutstanding) = Number(100);
+                    sleLoan->at(sfTotalValueOutstanding) = Number(150);
+                    sleLoan->setFieldU32(sfPaymentRemaining, 1);
+                    sleLoan->setFieldU32(sfFlags, c.before);
+                    ov.rawInsert(sleLoan);
+                }
+
+                STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
+                test::StreamSink sink{beast::Severity::Warning};
+                beast::Journal const jlog{sink};
+                ApplyContext ac{
+                    env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+                CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+                auto sleLoan = ac.view().peek(loanKeylet);
+                if (!BEAST_EXPECT(sleLoan))
+                    continue;
+                sleLoan->setFieldU32(sfFlags, c.after);
+                ac.view().update(sleLoan);
+
+                auto transactor = makeTransactor(ac);
+                if (!BEAST_EXPECT(transactor))
+                    continue;
+                TER const result = transactor->checkInvariants(
+                    tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
+                BEAST_EXPECT(result == tecINVARIANT_FAILED);
+                BEAST_EXPECT(sink.messages().str().contains(c.expected));
+            }
+        }
+
+        // Pre-featureLendingProtocolV1_1 sibling of the lsfLoanOverpayment
+        // cases above: the same set-once immutability was originally enforced
+        // by ValidLoan::finalize, so with V1_1 disabled toggling the flag
+        // must trip that legacy check instead. lsfLoanDefault immutability
+        // did not exist pre-V1_1 and is not tested here.
+        {
+            auto const cases = std::to_array>({
+                {lsfLoanOverpayment, 0},
+                {0, lsfLoanOverpayment},
+            });
+
+            for (auto const& [before, after] : cases)
+            {
+                Env env{*this, all_ - featureLendingProtocolV1_1};
+                Account const a1{"A1"};
+                env.fund(XRP(1000), a1);
+                env.close();
+
+                OpenView ov{*env.current()};
+
+                auto const brokerKeylet =
+                    keylet::loanBroker(a1.id(), SeqProxy::rawSequence(ov.seq()));
+                auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
+                {
+                    auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a1.id());
+                    sleLoan->at(sfPrincipalOutstanding) = Number(100);
+                    sleLoan->at(sfTotalValueOutstanding) = Number(150);
+                    sleLoan->setFieldU32(sfPaymentRemaining, 1);
+                    sleLoan->setFieldU32(sfFlags, before);
+                    ov.rawInsert(sleLoan);
+                }
+
+                STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
+                test::StreamSink sink{beast::Severity::Warning};
+                beast::Journal const jlog{sink};
+                ApplyContext ac{
+                    env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+                CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+                auto sleLoan = ac.view().peek(loanKeylet);
+                if (!BEAST_EXPECT(sleLoan))
+                    continue;
+                sleLoan->setFieldU32(sfFlags, after);
+                ac.view().update(sleLoan);
+
+                auto transactor = makeTransactor(ac);
+                if (!BEAST_EXPECT(transactor))
+                    continue;
+                TER const result = transactor->checkInvariants(
+                    tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
+                BEAST_EXPECT(result == tecINVARIANT_FAILED);
+                BEAST_EXPECT(sink.messages().str().contains("Loan Overpayment flag changed"));
+            }
+        }
+
+        // Under featureLendingProtocolV1_1, ValidLoan::finalize requires
+        // interest due (total value minus principal and management fee) to be
+        // non-negative after each value is rounded to sfLoanScale. Test zero,
+        // each way to produce a one-unit deficit, and a two-unit deficit. At
+        // scale 0, an XRP-backed broker rejects any deficit, while an
+        // IOU-backed one permits one unit of rounding tolerance.
+        {
+            struct Case
+            {
+                Number totalValue;
+                Number principal;
+                Number managementFee;
+                bool expectFireIntegral;
+                bool expectFireTolerant;
+            };
+            // The first case sits exactly at the boundary, the middle three
+            // perturb one component so that interest due is -1, which is within
+            // the tolerance, and the last overshoots it at -2.
+            auto const cases = std::to_array({
+                {.totalValue = Number(100),
+                 .principal = Number(100),
+                 .managementFee = Number(0),
+                 .expectFireIntegral = false,
+                 .expectFireTolerant = false},
+                {.totalValue = Number(99),
+                 .principal = Number(100),
+                 .managementFee = Number(0),
+                 .expectFireIntegral = true,
+                 .expectFireTolerant = false},
+                {.totalValue = Number(100),
+                 .principal = Number(101),
+                 .managementFee = Number(0),
+                 .expectFireIntegral = true,
+                 .expectFireTolerant = false},
+                {.totalValue = Number(100),
+                 .principal = Number(100),
+                 .managementFee = Number(1),
+                 .expectFireIntegral = true,
+                 .expectFireTolerant = false},
+                {.totalValue = Number(98),
+                 .principal = Number(100),
+                 .managementFee = Number(0),
+                 .expectFireIntegral = true,
+                 .expectFireTolerant = true},
+            });
+
+            for (bool const integralAsset : {true, false})
+            {
+                for (auto const& c : cases)
+                {
+                    Env env{*this, all_};
+                    Account const a1{"A1"};
+                    Account const issuer{"issuer"};
+                    env.fund(XRP(1000), a1, issuer);
+                    env.close();
+
+                    // The check reads the broker's vault asset to decide
+                    // whether the rounding tolerance applies, so both
+                    // branches need a real broker over the relevant asset.
+                    auto const asset = [&]() -> PrettyAsset {
+                        if (integralAsset)
+                            return PrettyAsset{xrpIssue(), 1'000'000};
+                        PrettyAsset const iouAsset = issuer["IOU"];
+                        env(trust(a1, iouAsset(1000)));
+                        env(pay(issuer, a1, iouAsset(1000)));
+                        env.close();
+                        return iouAsset;
+                    }();
+
+                    auto const brokerKeylet = this->createLoanBroker(a1, env, asset);
+                    if (!BEAST_EXPECT(env.le(brokerKeylet)))
+                        continue;
+                    env.close();
+
+                    OpenView ov{*env.current()};
+
+                    auto const loanKeylet =
+                        keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
+                    // Seed a loan whose interest due sits at the boundary. The
+                    // apply-view update below moves it.
+                    {
+                        auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a1.id());
+                        sleLoan->at(sfPrincipalOutstanding) = Number(100);
+                        sleLoan->at(sfTotalValueOutstanding) = Number(100);
+                        sleLoan->at(sfManagementFeeOutstanding) = Number(0);
+                        sleLoan->at(sfLoanScale) = 0;
+                        sleLoan->setFieldU32(sfPaymentRemaining, 1);
+                        ov.rawInsert(sleLoan);
+                    }
+
+                    STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
+                    test::StreamSink sink{beast::Severity::Warning};
+                    beast::Journal const jlog{sink};
+                    ApplyContext ac{
+                        env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+                    CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+                    auto sleLoan = ac.view().peek(loanKeylet);
+                    if (!BEAST_EXPECT(sleLoan))
+                        continue;
+                    sleLoan->at(sfTotalValueOutstanding) = c.totalValue;
+                    sleLoan->at(sfPrincipalOutstanding) = c.principal;
+                    sleLoan->at(sfManagementFeeOutstanding) = c.managementFee;
+                    ac.view().update(sleLoan);
+
+                    auto transactor = makeTransactor(ac);
+                    if (!BEAST_EXPECT(transactor))
+                        continue;
+                    TER const result = transactor->checkInvariants(
+                        tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
+                    auto const messages = sink.messages().str();
+                    if (integralAsset ? c.expectFireIntegral : c.expectFireTolerant)
+                    {
+                        BEAST_EXPECT(result == tecINVARIANT_FAILED);
+                        BEAST_EXPECT(messages.contains("Loan interest due is negative"));
+                    }
+                    else
+                    {
+                        // Other invariants may still fire on this raw-inserted
+                        // loan, so only assert the specific message is absent.
+                        BEAST_EXPECT(!messages.contains("Loan interest due is negative"));
+                    }
+                }
+            }
+        }
+
+        // VaultKind, SubscriptionDate and RedemptionDate are immutable once set at creation.
+        // Enforced by NoModifiedUnmodifiableFields on ltVAULT via kFieldChanged.
+        Keylet closedEndedVaultKeylet = keylet::amendments();
+        Preclose const createClosedEndedVault = [&, this](
+                                                    Account const& a, Account const&, Env& env) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto const red = sub + kMinInvestmentPeriod + 1'000'000;
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create(
+                {.owner = a,
+                 .asset = xrpIssue(),
+                 .vaultKind = std::to_underlying(VaultKind::ClosedEnded),
+                 .subscriptionDate = sub,
+                 .redemptionDate = red});
+            env(tx);
+            closedEndedVaultKeylet = keylet;
+            return BEAST_EXPECT(env.le(closedEndedVaultKeylet));
+        };
+
+        {
+            // Each mutation must keep the vault otherwise valid so that only the immutability check
+            // fires. Shifting both dates by the same offset preserves the gap; bumping sfVaultKind
+            // stays within the recognised range.
+            auto const mods = std::to_array>({
+                [](SLE::pointer& sle) { sle->at(sfVaultKind) += 1; },
+                [](SLE::pointer& sle) { sle->at(sfSubscriptionDate) += 1; },
+                [](SLE::pointer& sle) { sle->at(sfRedemptionDate) += 1; },
+            });
+
+            for (auto const& mod : mods)
+            {
+                doInvariantCheck(
+                    {{"changed an unchangeable field"}},
+                    [&](Account const&, Account const&, ApplyContext& ac) {
+                        auto sle = ac.view().peek(closedEndedVaultKeylet);
+                        if (!sle)
+                            return false;
+                        mod(sle);
+                        ac.view().update(sle);
+                        return true;
+                    },
+                    XRPAmount{},
+                    STTx{ttACCOUNT_SET, [](STObject&) {}},
+                    {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                    createClosedEndedVault);
+            }
+        }
+
+        {
+            auto const mods = std::to_array>({
+                [](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
+                [](SLE::pointer& sle) { sle->at(sfLedgerIndex) = uint256(1u); },
+            });
+
+            for (auto const& mod : mods)
+            {
+                doInvariantCheck(
+                    {{"changed an unchangeable field"}},
+                    [&](Account const& a1, Account const&, ApplyContext& ac) {
+                        auto sle = ac.view().peek(keylet::account(a1.id()));
+                        if (!sle)
+                            return false;
+                        mod(sle);
+                        ac.view().update(sle);
+                        return true;
+                    });
+            }
+        }
+    }
+
+    void
+    testInvariantOverwrite(FeatureBitset features)
+    {
+        using namespace test::jtx;
+        bool const fixEnabled = features[fixCleanup3_1_3];
+        std::initializer_list const failTers = {tecINVARIANT_FAILED, tefINVARIANT_FAILED};
+        std::initializer_list const passTers = {tesSUCCESS, tesSUCCESS};
+
+        // Insert two trust line SLEs in hash-sorted order, with the "bad"
+        // entry at the lower-sorting key so it is visited first by
+        // ApplyStateTable::visit(). The configurer callables receive the
+        // SLE and the Issue corresponding to that side's keylet currency.
+        auto const insertOrderedTrustLinePair = [](ApplyContext& ac,
+                                                   Account const& a1,
+                                                   Account const& a2,
+                                                   Account const& a3,
+                                                   auto const& badConfig,
+                                                   auto const& goodConfig) {
+            char const* const c1 = "USD";
+            char const* const c2 = "EUR";
+            auto const k1 = keylet::trustLine(a1, a2, a1[c1].currency);
+            auto const k2 = keylet::trustLine(a1, a3, a1[c2].currency);
+
+            bool const k1First = k1.key < k2.key;
+            auto const& badKey = k1First ? k1 : k2;
+            auto const& goodKey = k1First ? k2 : k1;
+            Issue const badIss{k1First ? a1[c1].currency : a1[c2].currency, a1.id()};
+            Issue const goodIss{k1First ? a1[c2].currency : a1[c1].currency, a1.id()};
+
+            auto const sleBad = std::make_shared(badKey);
+            badConfig(*sleBad, badIss);
+            ac.view().insert(sleBad);
+
+            auto const sleGood = std::make_shared(goodKey);
+            goodConfig(*sleGood, goodIss);
+            ac.view().insert(sleGood);
+        };
+
+        // Regression: bad XRP trust line followed by a valid trust line.
+        // With the fix, the invariant catches the violation. Without it,
+        // the valid entry overwrites the flag to false. The keylet
+        // currencies are non-XRP (the invariant inspects sfLowLimit /
+        // sfHighLimit issue, not the keylet currency).
+        testcase << "overwrite: NoXRPTrustLines" + std::string(fixEnabled ? " fix" : "");
+        doInvariantCheck(
+            makeEnv(features),
+            fixEnabled ? std::vector{{"an XRP trust line was created"}}
+                       : std::vector{},
+            [&insertOrderedTrustLinePair](Account const& a1, Account const& a2, ApplyContext& ac) {
+                Account const a3{"A3"};
+                insertOrderedTrustLinePair(
+                    ac,
+                    a1,
+                    a2,
+                    a3,
+                    [](SLE& sle, Issue const& iss) {
+                        // sfLowLimit has xrpIssue, making isXrp = true
+                        sle.setFieldAmount(sfLowLimit, STAmount{xrpIssue(), 0});
+                        sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
+                    },
+                    [](SLE& sle, Issue const& iss) {
+                        sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
+                        sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
+                    });
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_SET, [](STObject&) {}},
+            fixEnabled ? failTers : passTers);
+
+        // Regression: bad deep-freeze trust line followed by a valid one.
+        testcase << "overwrite: NoDeepFreeze" + std::string(fixEnabled ? " fix" : "");
+        doInvariantCheck(
+            makeEnv(features),
+            fixEnabled ? std::vector{{"a trust line with deep freeze flag without "
+                                                   "normal freeze was created"}}
+                       : std::vector{},
+            [&insertOrderedTrustLinePair](Account const& a1, Account const& a2, ApplyContext& ac) {
+                Account const a3{"A3"};
+                insertOrderedTrustLinePair(
+                    ac,
+                    a1,
+                    a2,
+                    a3,
+                    [](SLE& sle, Issue const& iss) {
+                        sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
+                        sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
+                        sle.setFieldU32(sfFlags, lsfLowDeepFreeze);
+                    },
+                    [](SLE& sle, Issue const& iss) {
+                        sle.setFieldAmount(sfLowLimit, STAmount{iss, 0});
+                        sle.setFieldAmount(sfHighLimit, STAmount{iss, 0});
+                        sle.setFieldU32(sfFlags, 0u);
+                    });
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_SET, [](STObject&) {}},
+            fixEnabled ? failTers : passTers);
+
+        // Regression: MPT OutstandingAmount exceeds max, but locked <=
+        // outstanding. Plain assignment would overwrite bad_ = true.
+        // With the fix, NoZeroEscrow catches it.
+        // Without the fix, NoZeroEscrow passes but ValidMPTIssuance
+        // still fires ("a MPT issuance was created").
+        testcase << "overwrite: NoZeroEscrow MPT" + std::string(fixEnabled ? " fix" : "");
+        doInvariantCheck(
+            makeEnv(features),
+            fixEnabled ? std::vector{{"escrow specifies invalid amount"}}
+                       : std::vector{{"a MPT issuance was created"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                auto const sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+
+                MPTIssue const mpt{makeMptID(1, AccountID(0x4985601))};
+                auto sleNew = std::make_shared(keylet::mptokenIssuance(mpt.getMptID()));
+                // outstanding exceeds kMaxMpTokenAmount -> checkAmount sets bad_
+                sleNew->setFieldU64(sfOutstandingAmount, kMaxMpTokenAmount + 1);
+                // locked is valid and <= outstanding -> must NOT clear bad_
+                sleNew->setFieldU64(sfLockedAmount, 10);
+                ac.view().insert(sleNew);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttACCOUNT_SET, [](STObject&) {}},
+            failTers);
+    }
+
+    void
+    testSponsorship()
+    {
+        using namespace test::jtx;
+        using namespace std::string_literals;
+        testcase("Sponsorship");
+        {
+            auto const expectMessage =
+                "SponsoredOwnerCount does not equal SponsoringOwnerCount delta.";
+
+            doInvariantCheck(
+                {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    auto const sle = ac.view().peek(keylet::account(a1.id()));
+                    if (!sle)
+                        return false;
+                    sle->setFieldU32(sfSponsoredOwnerCount, 1);
+                    ac.view().update(sle);
+                    return true;
+                });
+
+            doInvariantCheck(
+                {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    auto const sle = ac.view().peek(keylet::account(a1.id()));
+                    if (!sle)
+                        return false;
+                    sle->setFieldU32(sfSponsoringOwnerCount, 1);
+                    ac.view().update(sle);
+                    return true;
+                });
+        }
+
+        {
+            auto const expectMessage =
+                "OwnerCount must be greater than or equal to SponsoredOwnerCount.";
+
+            doInvariantCheck(
+                {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    auto const sle = ac.view().peek(keylet::account(a1.id()));
+                    if (!sle)
+                        return false;
+                    sle->setFieldU32(sfOwnerCount, 0);
+                    sle->setFieldU32(sfSponsoredOwnerCount, 1);
+                    ac.view().update(sle);
+
+                    auto const sle2 = ac.view().peek(keylet::account(a2.id()));
+                    if (!sle2)
+                        return false;
+                    sle2->setFieldU32(sfSponsoringOwnerCount, 1);
+                    ac.view().update(sle2);
+                    return true;
+                });
+        }
+
+        {
+            auto const expectMessage =
+                "SponsoredObjectOwnerCount does not equal SponsoredOwnerCount delta.";
+            uint256 checkID;
+
+            doInvariantCheck(
+                {{expectMessage}},
+                [&](Account const&, Account const& a2, ApplyContext& ac) {
+                    auto const check = ac.view().peek(keylet::check(checkID));
+                    if (!check)
+                        return false;
+                    check->setAccountID(sfSponsor, a2.id());
+                    ac.view().update(check);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttACCOUNT_SET, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                [&checkID](Account const& a1, Account const& a2, Env& env) {
+                    checkID = keylet::check(a1.id(), SeqProxy::rawSequence(env.seq(a1))).key;
+                    env(check::create(a1, a2, XRP(1)));
+                    return true;
+                });
+        }
+
+        {
+            auto const expectMessage =
+                "Invariant failed: Net delta of SponsoringAccountCount does "
+                "not match net delta of sfSponsor presence.";
+
+            doInvariantCheck(
+                {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    auto const sle = ac.view().peek(keylet::account(a1.id()));
+                    if (!sle)
+                        return false;
+                    sle->setFieldU32(sfSponsoringAccountCount, 1);
+                    ac.view().update(sle);
+                    return true;
+                });
+
+            doInvariantCheck(
+                {{expectMessage}}, [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    auto const sle = ac.view().peek(keylet::account(a1.id()));
+                    if (!sle)
+                        return false;
+                    sle->setAccountID(sfSponsor, a2.id());
+                    ac.view().update(sle);
+                    return true;
+                });
+        }
+    }
+
+    void
+    testObjectHasPseudoAccount()
+    {
+        testcase << "object has pseudo-account";
+        using namespace jtx;
+
+        auto const amendments = all_ | fixCleanup3_3_0;
+
+        // Vault: object deleted without its pseudo-account
+        {
+            Keylet vaultKeylet = keylet::amendments();
+            doInvariantCheck(
+                Env{*this, amendments},
+                {{"deleted Vault without deleting its pseudo-account"}},
+                [&vaultKeylet](Account const&, Account const&, ApplyContext& ac) {
+                    auto sle = ac.view().peek(vaultKeylet);
+                    if (!sle)
+                        return false;
+                    ac.view().erase(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttVAULT_DELETE, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                [&vaultKeylet](Account const& a1, Account const&, Env& env) {
+                    Vault const vault{env};
+                    auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
+                    env(tx);
+                    vaultKeylet = keylet;
+                    return true;
+                });
+        }
+
+        // AMM: object deleted without its pseudo-account
+        {
+            uint256 ammID{};
+            Account const gw{"gw"};
+            doInvariantCheck(
+                Env{*this, amendments},
+                {{"deleted AMM without deleting its pseudo-account"}},
+                [&ammID](Account const&, Account const&, ApplyContext& ac) {
+                    auto sle = ac.view().peek(keylet::amm(ammID));
+                    if (!sle)
+                        return false;
+                    ac.view().erase(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttAMM_DELETE, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                [&ammID, &gw](Account const&, Account const&, Env& env) {
+                    env.fund(XRP(1'000), gw);
+                    AMM const amm(env, gw, XRP(100), gw["USD"](100));
+                    ammID = amm.ammID();
+                    return true;
+                });
+        }
+
+        // LoanBroker: object deleted without its pseudo-account
+        {
+            Keylet loanBrokerKeylet = keylet::amendments();
+            doInvariantCheck(
+                Env{*this, amendments},
+                {{"deleted LoanBroker without deleting its pseudo-account"}},
+                [&loanBrokerKeylet](Account const&, Account const&, ApplyContext& ac) {
+                    auto sle = ac.view().peek(loanBrokerKeylet);
+                    if (!sle)
+                        return false;
+                    ac.view().erase(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttLOAN_BROKER_DELETE, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                [&loanBrokerKeylet, this](Account const& a1, Account const&, Env& env) {
+                    PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+                    loanBrokerKeylet = this->createLoanBroker(a1, env, xrpAsset);
+                    return BEAST_EXPECT(env.le(loanBrokerKeylet));
+                });
+        }
+
+        // Deleted object missing sfAccount field (defensive check).
+        // Manually construct the view to place a vault SLE without
+        // sfAccount into the base ledger, then erase it.
+        {
+            Env env{*this, amendments};
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env.fund(XRP(1000), a1, a2);
+            env.close();
+
+            OpenView ov{*env.current()};
+
+            auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ov.seq()));
+            auto sleVault = std::make_shared(vaultKeylet);
+            sleVault->makeFieldAbsent(sfAccount);
+            ov.rawInsert(sleVault);
+
+            STTx const tx{ttVAULT_DELETE, [](STObject&) {}};
+            test::StreamSink sink{beast::Severity::Warning};
+            beast::Journal const jlog{sink};
+            ApplyContext ac{
+                env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+            CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+            auto sle = ac.view().peek(vaultKeylet);
+            if (!BEAST_EXPECT(sle))
+                return;
+            ac.view().erase(sle);
+
+            auto transactor = makeTransactor(ac);
+            if (!BEAST_EXPECT(transactor))
+                return;
+            TER const result = transactor->checkInvariants(
+                tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
+            BEAST_EXPECT(result == tecINVARIANT_FAILED);
+            BEAST_EXPECT(sink.messages().str().contains("is missing pseudo-account field"));
+        }
+    }
+
+    void
+    testTxCheckException()
+    {
+        testcase << "txCheck exception";
+        using namespace jtx;
+
+        // A TxInvariantCheck that throws from the requested hook, so we can
+        // exercise checkInvariantsHelper's catch block via the
+        // transaction-specific layer (as opposed to the protocol layer,
+        // which testObjectHasPseudoAccount's last case already covers via a
+        // real Transactor's finalizeInvariants).
+        enum class ThrowFrom { VisitEntry, Finalize };
+
+        struct ThrowingTxInvariantCheck : TxInvariantCheck
+        {
+            ThrowFrom const throwFrom;
+
+            explicit ThrowingTxInvariantCheck(ThrowFrom throwFrom) : throwFrom(throwFrom)
+            {
+            }
+
+            void
+            visitEntry(bool, SLE::const_ref, SLE::const_ref) override
+            {
+                if (throwFrom == ThrowFrom::VisitEntry)
+                    throw std::runtime_error("test-injected visitEntry exception");
+            }
+
+            [[nodiscard]] bool
+            finalize(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) override
+            {
+                if (throwFrom == ThrowFrom::Finalize)
+                    throw std::runtime_error("test-injected finalize exception");
+                return true;
+            }
+        };
+
+        for (auto const throwFrom : {ThrowFrom::VisitEntry, ThrowFrom::Finalize})
+        {
+            Env env{*this};
+            Account const alice{"alice"};
+            env.fund(XRP(1000), alice);
+            env.close();
+
+            OpenView ov{*env.current()};
+            STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
+            test::StreamSink sink{beast::Severity::Warning};
+            beast::Journal const jlog{sink};
+            ApplyContext ac{
+                env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+            CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+            // visitEntry only runs for entries the transaction touched, so
+            // make a modification for the traversal to report.
+            auto sle = ac.view().peek(keylet::account(alice.id()));
+            if (!BEAST_EXPECT(sle))
+                return;
+            sle->at(sfSequence) = sle->at(sfSequence) + 1;
+            ac.view().update(sle);
+
+            ThrowingTxInvariantCheck throwing{throwFrom};
+            TER terActual = tesSUCCESS;
+            for (TER const& terExpect : {TER(tecINVARIANT_FAILED), TER(tefINVARIANT_FAILED)})
+            {
+                terActual = checkInvariants(ac, terActual, XRPAmount{}, throwing);
+                BEAST_EXPECT(terExpect == terActual);
+                BEAST_EXPECT(sink.messages().str().contains(
+                    "Transaction caused an exception during invariant checks"));
+            }
+        }
+    }
+
+    void
+    testTxCheckFinalizeFalse()
+    {
+        testcase << "txCheck finalize returns false";
+        using namespace jtx;
+
+        // A TxInvariantCheck whose finalize returns false, so we can exercise
+        // the "Transaction has failed one or more transaction invariants"
+        // log path in checkInvariantsHelper independently of any real
+        // transactor. This is the transaction-layer analogue of the
+        // protocol-layer coverage in testObjectHasPseudoAccount / others.
+        struct FailingTxInvariantCheck : TxInvariantCheck
+        {
+            void
+            visitEntry(bool, SLE::const_ref, SLE::const_ref) override
+            {
+            }
+
+            [[nodiscard]] bool
+            finalize(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&) override
+            {
+                return false;
+            }
+        };
+
+        Env env{*this};
+        Account const alice{"alice"};
+        env.fund(XRP(1000), alice);
+        env.close();
+
+        OpenView ov{*env.current()};
+        STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
+        test::StreamSink sink{beast::Severity::Warning};
+        beast::Journal const jlog{sink};
+        ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+        CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+        FailingTxInvariantCheck failing;
+        TER terActual = tesSUCCESS;
+        for (TER const& terExpect : {TER(tecINVARIANT_FAILED), TER(tefINVARIANT_FAILED)})
+        {
+            terActual = checkInvariants(ac, terActual, XRPAmount{}, failing);
+            BEAST_EXPECT(terExpect == terActual);
+            BEAST_EXPECT(sink.messages().str().contains(
+                "Transaction has failed one or more transaction invariants"));
+            // The protocol-layer log must not appear: only the tx-layer
+            // finalize failed here.
+            BEAST_EXPECT(!sink.messages().str().contains(
+                "Transaction has failed one or more global invariants"));
+        }
+    }
+
+    void
+    run() override
+    {
+        testXRPNotCreated();
+        testAccountRootsNotRemoved();
+        testAccountRootsDeletedClean();
+        testTypesMatch();
+        testXRPBalanceCheck();
+        testTransactionFeeCheck();
+        testNoBadOffers();
+        testValidNewAccountRoot();
+        testNoModifiedUnmodifiableFields();
+        testInvariantOverwrite(all_);
+        testInvariantOverwrite(all_ - fixCleanup3_1_3);
+        testObjectHasPseudoAccount();
+        testSponsorship();
+        testTxCheckException();
+        testTxCheckFinalizeFalse();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(InvariantsMisc, app, xrpl);
+
+}  // namespace xrpl::test
diff --git a/src/test/app/invariants/InvariantsPermissioned_test.cpp b/src/test/app/invariants/InvariantsPermissioned_test.cpp
new file mode 100644
index 0000000000..87349fb9e1
--- /dev/null
+++ b/src/test/app/invariants/InvariantsPermissioned_test.cpp
@@ -0,0 +1,957 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+class InvariantsPermissioned_test : public InvariantsBase
+{
+    FeatureBitset const all_{test::jtx::testableAmendments()};
+
+    void
+    testPermissionedDomainInvariants(FeatureBitset features)
+    {
+        using namespace test::jtx;
+
+        bool const fixEnabled = features[fixCleanup3_1_3];
+        std::initializer_list const badTers = {tecINVARIANT_FAILED, tecINVARIANT_FAILED};
+        std::initializer_list const failTers = {tecINVARIANT_FAILED, tefINVARIANT_FAILED};
+
+        testcase << "PermissionedDomain" + std::string(fixEnabled ? " fix" : "");
+
+        doInvariantCheck(
+            makeEnv(features),
+            {{"permissioned domain with no rules."}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                return createPermissionedDomain(ac, a1, a2, 0).get();
+            },
+            XRPAmount{},
+            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
+            fixEnabled ? failTers : badTers);
+
+        testcase << "PermissionedDomain 2";
+
+        static constexpr auto kTooBig = kMaxPermissionedDomainCredentialsArraySize + 1;
+        doInvariantCheck(
+            makeEnv(features),
+            {{"permissioned domain bad credentials size " + std::to_string(kTooBig)}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                return !!createPermissionedDomain(ac, a1, a2, kTooBig);
+            },
+            XRPAmount{},
+            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
+            fixEnabled ? failTers : badTers);
+
+        testcase << "PermissionedDomain 3";
+        doInvariantCheck(
+            makeEnv(features),
+            {{"permissioned domain credentials aren't sorted"}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto slePd = createPermissionedDomain(ac, a1, a2, 0);
+
+                STArray credentials(sfAcceptedCredentials, 2);
+                for (std::size_t n = 0; n < 2; ++n)
+                {
+                    auto cred = STObject::makeInnerObject(sfCredential);
+                    cred.setAccountID(sfIssuer, a2);
+                    auto credType = std::string("cred_type") + std::to_string(9 - n);
+                    cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
+                    credentials.pushBack(std::move(cred));
+                }
+                slePd->setFieldArray(sfAcceptedCredentials, credentials);
+                ac.view().update(slePd);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
+            fixEnabled ? failTers : badTers);
+
+        testcase << "PermissionedDomain 4";
+        doInvariantCheck(
+            makeEnv(features),
+            {{"permissioned domain credentials aren't unique"}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto slePd = createPermissionedDomain(ac, a1, a2, 0);
+
+                STArray credentials(sfAcceptedCredentials, 2);
+                for (std::size_t n = 0; n < 2; ++n)
+                {
+                    auto cred = STObject::makeInnerObject(sfCredential);
+                    cred.setAccountID(sfIssuer, a2);
+                    cred.setFieldVL(sfCredentialType, Slice("cred_type", 9));
+                    credentials.pushBack(std::move(cred));
+                }
+                slePd->setFieldArray(sfAcceptedCredentials, credentials);
+                ac.view().update(slePd);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
+            fixEnabled ? failTers : badTers);
+
+        testcase << "PermissionedDomain Set 1";
+        doInvariantCheck(
+            makeEnv(features),
+            {{"permissioned domain with no rules."}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                // create PD
+                auto slePd = createPermissionedDomain(ac, a1, a2);
+
+                // update PD with empty rules
+                {
+                    STArray const credentials(sfAcceptedCredentials, 2);
+                    slePd->setFieldArray(sfAcceptedCredentials, credentials);
+                    ac.view().update(slePd);
+                }
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
+            fixEnabled ? failTers : badTers);
+
+        testcase << "PermissionedDomain Set 2";
+        doInvariantCheck(
+            makeEnv(features),
+            {{"permissioned domain bad credentials size " + std::to_string(kTooBig)}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                // create PD
+                auto slePd = createPermissionedDomain(ac, a1, a2);
+
+                // update PD
+                {
+                    STArray credentials(sfAcceptedCredentials, kTooBig);
+
+                    for (std::size_t n = 0; n < kTooBig; ++n)
+                    {
+                        auto cred = STObject::makeInnerObject(sfCredential);
+                        cred.setAccountID(sfIssuer, a2);
+                        auto credType = "cred_type2" + std::to_string(n);
+                        cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
+                        credentials.pushBack(std::move(cred));
+                    }
+
+                    slePd->setFieldArray(sfAcceptedCredentials, credentials);
+                    ac.view().update(slePd);
+                }
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
+            fixEnabled ? failTers : badTers);
+
+        testcase << "PermissionedDomain Set 3";
+        doInvariantCheck(
+            makeEnv(features),
+            {{"permissioned domain credentials aren't sorted"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                // create PD
+                auto slePd = createPermissionedDomain(ac, a1, a2);
+
+                // update PD
+                {
+                    STArray credentials(sfAcceptedCredentials, 2);
+                    for (std::size_t n = 0; n < 2; ++n)
+                    {
+                        auto cred = STObject::makeInnerObject(sfCredential);
+                        cred.setAccountID(sfIssuer, a2);
+                        auto credType = std::string("cred_type2") + std::to_string(9 - n);
+                        cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
+                        credentials.pushBack(std::move(cred));
+                    }
+
+                    slePd->setFieldArray(sfAcceptedCredentials, credentials);
+                    ac.view().update(slePd);
+                }
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
+            fixEnabled ? failTers : badTers);
+
+        testcase << "PermissionedDomain Set 4";
+        doInvariantCheck(
+            makeEnv(features),
+            {{"permissioned domain credentials aren't unique"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                // create PD
+                auto slePd = createPermissionedDomain(ac, a1, a2);
+
+                // update PD
+                {
+                    STArray credentials(sfAcceptedCredentials, 2);
+                    for (std::size_t n = 0; n < 2; ++n)
+                    {
+                        auto cred = STObject::makeInnerObject(sfCredential);
+                        cred.setAccountID(sfIssuer, a2);
+                        cred.setFieldVL(sfCredentialType, Slice("cred_type", 9));
+                        credentials.pushBack(std::move(cred));
+                    }
+                    slePd->setFieldArray(sfAcceptedCredentials, credentials);
+                    ac.view().update(slePd);
+                }
+
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
+            fixEnabled ? failTers : badTers);
+
+        std::initializer_list const goodTers = {tesSUCCESS, tesSUCCESS};
+
+        std::vector const badMoreThan1{
+            {"transaction affected more than 1 permissioned domain entry."}};
+        std::vector const emptyV;
+        std::vector const badNoDomains{{"no domain objects affected by"}};
+        std::vector const badNotDeleted{
+            {"domain object modified, but not deleted by "}};
+        std::vector const badDeleted{{"domain object deleted by"}};
+        std::vector const badTx{
+            {"domain object(s) affected by an unauthorized transaction."}};
+
+        {
+            testcase << "PermissionedDomain set 2 domains ";
+            doInvariantCheck(
+                makeEnv(features),
+                fixEnabled ? badMoreThan1 : emptyV,
+                [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    createPermissionedDomain(ac, a1, a2);
+                    createPermissionedDomain(ac, a1, a2, 2, 11);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
+                fixEnabled ? failTers : goodTers);
+        }
+
+        {
+            testcase << "PermissionedDomain del 2 domains";
+
+            Env env1(*this, features);
+
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env1.fund(XRP(1000), a1, a2);
+            env1.close();
+
+            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
+            [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
+            env1.close();
+
+            doInvariantCheck(
+                std::move(env1),
+                a1,
+                a2,
+                fixEnabled ? badMoreThan1 : emptyV,
+                [&pd1, &pd2](Account const&, Account const&, ApplyContext& ac) {
+                    auto sle1 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd1});
+                    auto sle2 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd2});
+                    ac.view().erase(sle1);
+                    ac.view().erase(sle2);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
+                fixEnabled ? failTers : goodTers);
+        }
+
+        {
+            testcase << "PermissionedDomain set 0 domains ";
+            doInvariantCheck(
+                makeEnv(features),
+                fixEnabled ? badNoDomains : emptyV,
+                [](Account const&, Account const&, ApplyContext&) { return true; },
+                XRPAmount{},
+                STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
+                fixEnabled ? badTers : goodTers);
+        }
+
+        {
+            testcase << "PermissionedDomain del 0 domains";
+
+            Env env1(*this, features);
+
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env1.fund(XRP(1000), a1, a2);
+            env1.close();
+
+            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
+            [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
+            env1.close();
+
+            doInvariantCheck(
+                std::move(env1),
+                a1,
+                a2,
+                fixEnabled ? badNoDomains : emptyV,
+                [](Account const&, Account const&, ApplyContext&) { return true; },
+                XRPAmount{},
+                STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
+                fixEnabled ? badTers : goodTers);
+        }
+
+        {
+            testcase << "PermissionedDomain set, delete domain";
+
+            Env env1(*this, features);
+
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env1.fund(XRP(1000), a1, a2);
+            env1.close();
+
+            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
+            env1.close();
+
+            doInvariantCheck(
+                std::move(env1),
+                a1,
+                a2,
+                fixEnabled ? badDeleted : emptyV,
+                [&pd1](Account const&, Account const&, ApplyContext& ac) {
+                    auto sle1 = ac.view().peek({ltPERMISSIONED_DOMAIN, pd1});
+                    ac.view().erase(sle1);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject&) {}},
+                fixEnabled ? failTers : goodTers);
+        }
+
+        {
+            testcase << "PermissionedDomain del, create domain ";
+            doInvariantCheck(
+                makeEnv(features),
+                fixEnabled ? badNotDeleted : emptyV,
+                [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    createPermissionedDomain(ac, a1, a2);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttPERMISSIONED_DOMAIN_DELETE, [](STObject&) {}},
+                fixEnabled ? failTers : goodTers);
+        }
+
+        {
+            testcase << "PermissionedDomain invalid tx";
+
+            doInvariantCheck(
+                fixEnabled ? badTx : emptyV,
+                [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    createPermissionedDomain(ac, a1, a2);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttPAYMENT, [](STObject&) {}},
+                failTers);
+        }
+    }
+
+    void
+    testPermissionedDEX(FeatureBitset features)
+    {
+        using namespace test::jtx;
+
+        bool const fixEnabled = features[fixCleanup3_1_3];
+
+        testcase << "PermissionedDEX" + std::string(fixEnabled ? " fix" : "");
+
+        doInvariantCheck(
+            makeEnv(features),
+            {{"domain doesn't exist"}},
+            [](Account const& a1, Account const&, ApplyContext& ac) {
+                Keylet const offerKey = keylet::offer(a1.id(), SeqProxy::rawSequence(10));
+                auto sleOffer = std::make_shared(offerKey);
+                sleOffer->setAccountID(sfAccount, a1);
+                sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
+                sleOffer->setFieldAmount(sfTakerGets, XRP(1));
+                ac.view().insert(sleOffer);
+                return true;
+            },
+            XRPAmount{},
+            STTx{
+                ttOFFER_CREATE,
+                [](STObject& tx) {
+                    tx.setFieldH256(
+                        sfDomainID,
+                        uint256{"F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E33"
+                                "70F3649CE134E5"});
+                    Account const a1{"A1"};
+                    tx.setFieldAmount(sfTakerPays, a1["USD"](10));
+                    tx.setFieldAmount(sfTakerGets, XRP(1));
+                }},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
+
+        // missing domain ID in offer object
+        doInvariantCheck(
+            makeEnv(features),
+            {{"hybrid offer is malformed"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
+                auto sleOffer = std::make_shared(offerKey);
+                sleOffer->setAccountID(sfAccount, a2);
+                sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
+                sleOffer->setFieldAmount(sfTakerGets, XRP(1));
+                sleOffer->setFlag(lsfHybrid);
+
+                STArray bookArr;
+                bookArr.pushBack(STObject::makeInnerObject(sfBook));
+                sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
+                ac.view().insert(sleOffer);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttOFFER_CREATE, [&](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
+
+        // more than one entry in sfAdditionalBooks
+        {
+            Env env1(*this, features);
+
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env1.fund(XRP(1000), a1, a2);
+            env1.close();
+
+            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
+            env1.close();
+
+            doInvariantCheck(
+                std::move(env1),
+                a1,
+                a2,
+                {{"hybrid offer is malformed"}},
+                [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
+                    auto sleOffer = std::make_shared(offerKey);
+                    sleOffer->setAccountID(sfAccount, a2);
+                    sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
+                    sleOffer->setFieldAmount(sfTakerGets, XRP(1));
+                    sleOffer->setFlag(lsfHybrid);
+                    sleOffer->setFieldH256(sfDomainID, pd1);
+
+                    STArray bookArr;
+                    bookArr.pushBack(STObject::makeInnerObject(sfBook));
+                    bookArr.pushBack(STObject::makeInnerObject(sfBook));
+                    sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
+                    ac.view().insert(sleOffer);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttOFFER_CREATE, [&](STObject&) {}},
+                {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
+        }
+
+        // empty sfAdditionalBooks (size 0)
+        {
+            Env env1(*this, features);
+
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env1.fund(XRP(1000), a1, a2);
+            env1.close();
+
+            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
+            env1.close();
+
+            doInvariantCheck(
+                std::move(env1),
+                a1,
+                a2,
+                fixEnabled ? std::vector{{"hybrid offer is malformed"}}
+                           : std::vector{},
+                [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
+                    auto sleOffer = std::make_shared(offerKey);
+                    sleOffer->setAccountID(sfAccount, a2);
+                    sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
+                    sleOffer->setFieldAmount(sfTakerGets, XRP(1));
+                    sleOffer->setFlag(lsfHybrid);
+                    sleOffer->setFieldH256(sfDomainID, pd1);
+
+                    STArray const bookArr;  // empty array, size 0
+                    sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
+                    ac.view().insert(sleOffer);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttOFFER_CREATE, [&](STObject&) {}},
+                fixEnabled ? std::initializer_list{tecINVARIANT_FAILED, tecINVARIANT_FAILED}
+                           : std::initializer_list{tesSUCCESS, tesSUCCESS});
+        }
+
+        // hybrid offer missing sfAdditionalBooks
+        {
+            Env env1(*this, features);
+
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env1.fund(XRP(1000), a1, a2);
+            env1.close();
+
+            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
+            env1.close();
+
+            doInvariantCheck(
+                std::move(env1),
+                a1,
+                a2,
+                {{"hybrid offer is malformed"}},
+                [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
+                    auto sleOffer = std::make_shared(offerKey);
+                    sleOffer->setAccountID(sfAccount, a2);
+                    sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
+                    sleOffer->setFieldAmount(sfTakerGets, XRP(1));
+                    sleOffer->setFlag(lsfHybrid);
+                    sleOffer->setFieldH256(sfDomainID, pd1);
+                    ac.view().insert(sleOffer);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttOFFER_CREATE, [&](STObject&) {}},
+                {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
+        }
+
+        {
+            Env env1(*this, features);
+
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env1.fund(XRP(1000), a1, a2);
+            env1.close();
+
+            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
+            [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env1, a1, a2);
+            env1.close();
+
+            doInvariantCheck(
+                std::move(env1),
+                a1,
+                a2,
+                {{"transaction consumed wrong domains"}},
+                [&pd1](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
+                    auto sleOffer = std::make_shared(offerKey);
+                    sleOffer->setAccountID(sfAccount, a2);
+                    sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
+                    sleOffer->setFieldAmount(sfTakerGets, XRP(1));
+                    sleOffer->setFieldH256(sfDomainID, pd1);
+                    ac.view().insert(sleOffer);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttOFFER_CREATE,
+                    [&pd2, &a1](STObject& tx) {
+                        tx.setFieldH256(sfDomainID, pd2);
+                        tx.setFieldAmount(sfTakerPays, a1["USD"](10));
+                        tx.setFieldAmount(sfTakerGets, XRP(1));
+                    }},
+                {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
+        }
+
+        {
+            Env env1(*this, features);
+
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env1.fund(XRP(1000), a1, a2);
+            env1.close();
+
+            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, a1, a2);
+            env1.close();
+
+            doInvariantCheck(
+                std::move(env1),
+                a1,
+                a2,
+                {{"domain transaction affected regular offers"}},
+                [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    Keylet const offerKey = keylet::offer(a2.id(), SeqProxy::rawSequence(10));
+                    auto sleOffer = std::make_shared(offerKey);
+                    sleOffer->setAccountID(sfAccount, a2);
+                    sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
+                    sleOffer->setFieldAmount(sfTakerGets, XRP(1));
+                    ac.view().insert(sleOffer);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{
+                    ttOFFER_CREATE,
+                    [&](STObject& tx) {
+                        Account const a1{"A1"};
+                        tx.setFieldH256(sfDomainID, pd1);
+                        tx.setFieldAmount(sfTakerPays, a1["USD"](10));
+                        tx.setFieldAmount(sfTakerGets, XRP(1));
+                    }},
+                {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
+        }
+    }
+
+    void
+    testPermissionedDEXDeletedOfferFallback()
+    {
+        using namespace test::jtx;
+
+        testcase << "PermissionedDEX null after";
+
+        // Tx is OfferCreate on pd2. Tracking pd1 fails the invariant iff that
+        // domain lands in the set finalize consults. after == null is never
+        // tracked (pre-340: after-only; post-340: early return) — same result,
+        // both sides are coverage/regression that we do not fall back to before.
+        auto const check = [this](
+                               FeatureBitset features,
+                               bool const afterIsNull,
+                               bool const isDelete,
+                               bool const expectInvariantFailure) {
+            Env env(*this, features);
+
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env.fund(XRP(1000), a1, a2);
+            env.close();
+
+            [[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env, a1, a2);
+            [[maybe_unused]] auto [seq2, pd2] = createPermissionedDomainEnv(env, a1, a2);
+            env.close();
+
+            auto sleOffer =
+                std::make_shared(keylet::offer(a2.id(), SeqProxy::rawSequence(10)));
+            sleOffer->setAccountID(sfAccount, a2);
+            sleOffer->setFieldAmount(sfTakerPays, a1["USD"](10));
+            sleOffer->setFieldAmount(sfTakerGets, XRP(1));
+            sleOffer->setFieldH256(sfDomainID, pd1);
+
+            CurrentTransactionRulesGuard const rulesGuard(env.current()->rules());
+
+            ValidPermissionedDEX invariant;
+            if (afterIsNull)
+            {
+                // Defensive path: after is null. Must not fall back to before.
+                invariant.visitEntry(isDelete, sleOffer, nullptr);
+            }
+            else
+            {
+                // Normal / real-erase path: after is the offer on pd1.
+                invariant.visitEntry(isDelete, nullptr, sleOffer);
+            }
+
+            STTx const tx{ttOFFER_CREATE, [&pd2, &a1](STObject& tx) {
+                              tx.setFieldH256(sfDomainID, pd2);
+                              tx.setFieldAmount(sfTakerPays, a1["USD"](10));
+                              tx.setFieldAmount(sfTakerGets, XRP(1));
+                          }};
+
+            test::StreamSink sink{beast::Severity::Warning};
+            beast::Journal const jlog{sink};
+            bool const passed =
+                invariant.finalize(tx, tesSUCCESS, XRPAmount{}, *env.current(), jlog);
+            BEAST_EXPECT(passed != expectInvariantFailure);
+            if (expectInvariantFailure)
+            {
+                BEAST_EXPECT(sink.messages().str().contains("transaction consumed wrong domains"));
+            }
+            else
+            {
+                BEAST_EXPECT(sink.messages().str().empty());
+            }
+        };
+
+        auto const pre = all_ - fixCleanup3_4_0;
+        auto const post = all_;
+
+        // after == null: not tracked
+        check(pre, true, true, false);
+        check(post, true, true, false);
+
+        // after == offer on pd1
+        // pre-340: domainsOld_ (delete still inserted) → fail
+        check(pre, false, true, true);
+        // post-340: isDelete → only domainsOld_ → pass; !isDelete → domains_ → fail
+        check(post, false, true, false);
+        check(post, false, false, true);
+    }
+
+    void
+    testBookDirectoryExchangeRate()
+    {
+        using namespace test::jtx;
+        testcase << "book directory exchange rate";
+
+        auto const getBookRootKey = [](Account const& account, std::uint64_t quality) {
+            Book const book{xrpIssue(), account["USD"], std::nullopt};
+            return keylet::quality(keylet::book(book), quality);
+        };
+
+        // Root book-directory pages carry exchange-rate metadata that must
+        // match the quality encoded in the directory key.
+        auto const makeRootPage = [](Keylet const& dir, std::uint64_t exchangeRate) {
+            auto sleDir = std::make_shared(dir);
+            sleDir->setFieldH256(sfRootIndex, dir.key);
+            STVector256 indexes;
+            indexes.pushBack(uint256{1});
+            sleDir->setFieldV256(sfIndexes, indexes);
+            sleDir->setFieldU64(sfExchangeRate, exchangeRate);
+            return sleDir;
+        };
+
+        // Child pages do not carry quality metadata; they only point back to
+        // the root directory.
+        auto const makeChildPage = [](Keylet const& rootDir) {
+            auto sleDir = std::make_shared(keylet::page(rootDir, 1));
+            sleDir->setFieldH256(sfRootIndex, rootDir.key);
+            STVector256 indexes;
+            indexes.pushBack(uint256{2});
+            sleDir->setFieldV256(sfIndexes, indexes);
+            return sleDir;
+        };
+
+        auto const makeOfferCreateTx = [] {
+            return STTx{ttOFFER_CREATE, [](STObject& tx) {
+                            Account const account{"A1"};
+                            tx.setFieldAmount(sfTakerPays, XRP(1));
+                            tx.setFieldAmount(sfTakerGets, account["USD"](1));
+                        }};
+        };
+        std::initializer_list const failTers = {tecINVARIANT_FAILED, tefINVARIANT_FAILED};
+
+        // Creating a root book directory with mismatched exchange-rate
+        // metadata violates the invariant.
+        doInvariantCheck(
+            {{"book directory exchange rate does not match directory quality"}},
+            [&](Account const& a1, Account const&, ApplyContext& ac) {
+                auto const directoryQuality = STAmount::kURateOne;
+                auto const dir = getBookRootKey(a1, directoryQuality);
+                ac.view().insert(makeRootPage(dir, directoryQuality + 1));
+                return true;
+            },
+            XRPAmount{},
+            makeOfferCreateTx(),
+            failTers);
+
+        // A new child page must point to an existing root page.
+        doInvariantCheck(
+            {{"book directory root missing"}},
+            [&](Account const& a1, Account const&, ApplyContext& ac) {
+                auto const directoryQuality = STAmount::kURateOne;
+                auto const rootDir = getBookRootKey(a1, directoryQuality);
+                // Insert only the child page.  It points at rootDir, but the
+                // corresponding root page is intentionally missing.
+                ac.view().insert(makeChildPage(rootDir));
+                return true;
+            },
+            XRPAmount{},
+            makeOfferCreateTx(),
+            failTers);
+
+        // Legacy bad-root tolerance:
+        // - The view contains a pre-existing root page with bad sfExchangeRate
+        //   metadata.
+        // - The simulated transaction only creates a child page pointing to
+        //   that root.
+        // - The invariant must pass because this transaction did not create
+        //   the bad root, only adding a child page.
+        {
+            Env env{*this, all_};
+            Account const a1{"A1"};
+            env.fund(XRP(1000), a1);
+            env.close();
+
+            OpenView view{*env.current()};
+            auto const directoryQuality = STAmount::kURateOne;
+            auto const rootDir = getBookRootKey(a1, directoryQuality);
+            view.rawInsert(makeRootPage(rootDir, directoryQuality + 1));
+
+            ValidBookDirectory invariant;
+            invariant.visitEntry(false, nullptr, makeChildPage(rootDir));
+
+            test::StreamSink sink{beast::Severity::Warning};
+            beast::Journal const jlog{sink};
+            BEAST_EXPECT(
+                invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
+        }
+
+        // A bad root is rejected when added, ignored when a legacy bad root is
+        // modified without changing sfRootIndex or deleted, and checked when a
+        // modified directory changes sfRootIndex.
+        {
+            Env env{*this, all_};
+            Account const a1{"A1"};
+            env.fund(XRP(1000), a1);
+            env.close();
+
+            OpenView view{*env.current()};
+            auto const directoryQuality = STAmount::kURateOne;
+            auto const rootDir = getBookRootKey(a1, directoryQuality);
+            auto const missingRootDir = getBookRootKey(a1, directoryQuality + 1);
+            auto const badRoot = makeRootPage(rootDir, directoryQuality + 1);
+            view.rawInsert(badRoot);
+
+            test::StreamSink sink{beast::Severity::Warning};
+            beast::Journal const jlog{sink};
+
+            {
+                // add
+                ValidBookDirectory invariant;
+                invariant.visitEntry(false, nullptr, badRoot);
+
+                BEAST_EXPECT(
+                    !invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
+            }
+            {
+                // modify (without changing the sfRootIndex)
+                ValidBookDirectory invariant;
+                invariant.visitEntry(false, badRoot, badRoot);
+
+                BEAST_EXPECT(
+                    invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
+            }
+            {
+                // modify (changing sfRootIndex to a missing root)
+                auto const childBefore = makeChildPage(rootDir);
+                auto const childAfter = std::make_shared(*childBefore, childBefore->key());
+                childAfter->setFieldH256(sfRootIndex, missingRootDir.key);
+
+                ValidBookDirectory invariant;
+                invariant.visitEntry(false, childBefore, childAfter);
+
+                test::StreamSink missingRootSink{beast::Severity::Warning};
+                beast::Journal const missingRootJlog{missingRootSink};
+                BEAST_EXPECT(!invariant.finalize(
+                    makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, missingRootJlog));
+                BEAST_EXPECT(
+                    missingRootSink.messages().str().contains("book directory root missing"));
+            }
+            {
+                // delete
+                view.rawErase(badRoot);
+                BEAST_EXPECT(!view.exists(rootDir));
+
+                ValidBookDirectory invariant;
+                invariant.visitEntry(true, badRoot, badRoot);
+                BEAST_EXPECT(
+                    invariant.finalize(makeOfferCreateTx(), tesSUCCESS, XRPAmount{}, view, jlog));
+            }
+        }
+    }
+
+    static SLE::pointer
+    createPermissionedDomain(
+        ApplyContext& ac,
+        test::jtx::Account const& a1,
+        test::jtx::Account const& a2,
+        std::uint32_t numCreds = 2,
+        std::uint32_t seq = 10)
+    {
+        Keylet const pdKeylet = keylet::permissionedDomain(a1.id(), SeqProxy::rawSequence(seq));
+        auto sle = std::make_shared(pdKeylet);
+
+        sle->setAccountID(sfOwner, a1);
+        sle->setFieldU32(sfSequence, seq);
+
+        if (numCreds != 0u)
+        {
+            // This array is sorted naturally, but if you are going to change
+            // this behavior, don't forget to use credentials::makeSorted
+            STArray credentials(sfAcceptedCredentials, numCreds);
+            for (std::size_t n = 0; n < numCreds; ++n)
+            {
+                auto cred = STObject::makeInnerObject(sfCredential);
+                cred.setAccountID(sfIssuer, a2);
+                auto credType = "cred_type" + std::to_string(n);
+                cred.setFieldVL(sfCredentialType, Slice(credType.c_str(), credType.size()));
+                credentials.pushBack(std::move(cred));
+            }
+            sle->setFieldArray(sfAcceptedCredentials, credentials);
+        }
+
+        ac.view().insert(sle);
+        return sle;
+    }
+
+    static std::pair
+    createPermissionedDomainEnv(
+        test::jtx::Env& env,
+        test::jtx::Account const& a1,
+        test::jtx::Account const& a2,
+        std::uint32_t numCreds = 2)
+    {
+        using namespace test::jtx;
+
+        pdomain::Credentials credentials;
+
+        for (std::size_t n = 0; n < numCreds; ++n)
+        {
+            auto credType = "cred_type" + std::to_string(n);
+            credentials.push_back({.issuer = a2, .credType = credType});
+        }
+
+        std::uint32_t const seq = env.seq(a1);
+        env(pdomain::setTx(a1, credentials));
+        uint256 const key = pdomain::getNewDomain(env.meta());
+
+        return {seq, key};
+    }
+
+    void
+    run() override
+    {
+        testPermissionedDomainInvariants(all_);
+        testPermissionedDomainInvariants(all_ - fixCleanup3_1_3);
+        testPermissionedDEX(all_);
+        testPermissionedDEX(all_ - fixCleanup3_1_3);
+        testPermissionedDEXDeletedOfferFallback();
+        testBookDirectoryExchangeRate();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(InvariantsPermissioned, app, xrpl);
+
+}  // namespace xrpl::test
diff --git a/src/test/app/invariants/InvariantsPseudoAccount_test.cpp b/src/test/app/invariants/InvariantsPseudoAccount_test.cpp
new file mode 100644
index 0000000000..6c8a710bef
--- /dev/null
+++ b/src/test/app/invariants/InvariantsPseudoAccount_test.cpp
@@ -0,0 +1,744 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+class InvariantsPseudoAccount_test : public InvariantsBase
+{
+    FeatureBitset const all_{test::jtx::testableAmendments()};
+
+    void
+    testValidPseudoAccounts()
+    {
+        testcase << "valid pseudo accounts";
+
+        using namespace jtx;
+
+        AccountID pseudoAccountID;
+        Preclose const createPseudo = [&, this](Account const& a, Account const& b, Env& env) {
+            PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+
+            // Create vault
+            Vault const vault{env};
+            auto [tx, vKeylet] = vault.create({.owner = a, .asset = xrpAsset});
+            env(tx);
+            env.close();
+            if (auto const vSle = env.le(vKeylet); BEAST_EXPECT(vSle))
+            {
+                pseudoAccountID = vSle->at(sfAccount);
+            }
+
+            return BEAST_EXPECT(env.le(keylet::account(pseudoAccountID)));
+        };
+
+        /* Cases to check
+            "pseudo-account has 0 pseudo-account fields set"
+            "pseudo-account has 2 pseudo-account fields set"
+            "pseudo-account sequence changed"
+            "pseudo-account flags are not set"
+            "pseudo-account has a regular key"
+            "pseudo-account has a sponsorship field"
+        */
+        struct Mod
+        {
+            std::string expectedFailure;
+            std::function func;
+        };
+        auto const mods = std::to_array({
+            {
+                .expectedFailure = "pseudo-account has 0 pseudo-account fields set",
+                .func =
+                    [this](SLE::pointer& sle) {
+                        BEAST_EXPECT(sle->at(~sfVaultID));
+                        sle->at(~sfVaultID) = std::nullopt;
+                    },
+            },
+            {
+                .expectedFailure = "pseudo-account sequence changed",
+                .func = [](SLE::pointer& sle) { sle->at(sfSequence) = 12345; },
+            },
+            {
+                .expectedFailure = "pseudo-account flags are not set",
+                .func = [](SLE::pointer& sle) { sle->at(sfFlags) = lsfNoFreeze; },
+            },
+            {
+                .expectedFailure = "pseudo-account has a regular key",
+                .func = [](SLE::pointer& sle) { sle->at(sfRegularKey) = Account("regular").id(); },
+            },
+            {
+                .expectedFailure = "pseudo-account has a sponsorship field",
+                .func = [](SLE::pointer& sle) { sle->at(sfSponsoredOwnerCount) = 1; },
+            },
+            {
+                .expectedFailure = "pseudo-account has a sponsorship field",
+                .func = [](SLE::pointer& sle) { sle->at(sfSponsoringOwnerCount) = 1; },
+            },
+            {
+                .expectedFailure = "pseudo-account has a sponsorship field",
+                .func = [](SLE::pointer& sle) { sle->at(sfSponsoringAccountCount) = 1; },
+            },
+            {
+                .expectedFailure = "pseudo-account has a sponsorship field",
+                .func = [](SLE::pointer& sle) { sle->at(sfSponsor) = Account("sponsor").id(); },
+            },
+        });
+
+        for (auto const& mod : mods)
+        {
+            doInvariantCheck(
+                {{mod.expectedFailure}},
+                [&](Account const& a1, Account const&, ApplyContext& ac) {
+                    auto sle = ac.view().peek(keylet::account(pseudoAccountID));
+                    if (!sle)
+                        return false;
+                    mod.func(sle);
+                    ac.view().update(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttACCOUNT_SET, [](STObject& tx) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                createPseudo);
+        }
+        for (auto const pField : getPseudoAccountFields())
+        {
+            // createPseudo creates a vault, so sfVaultID will be set, and
+            // setting it again will not cause an error
+            if (pField == &sfVaultID)
+                continue;
+            doInvariantCheck(
+                {{"pseudo-account has 2 pseudo-account fields set"}},
+                [&](Account const& a1, Account const&, ApplyContext& ac) {
+                    auto sle = ac.view().peek(keylet::account(pseudoAccountID));
+                    if (!sle)
+                        return false;
+
+                    auto const vaultID = ~sle->at(~sfVaultID);
+                    BEAST_EXPECT(vaultID && !sle->isFieldPresent(*pField));
+                    sle->setFieldH256(*pField, *vaultID);
+
+                    ac.view().update(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttACCOUNT_SET, [](STObject& tx) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                createPseudo);
+        }
+
+        // Take one of the regular accounts and set the sequence to 0, which
+        // will make it look like a pseudo-account
+        doInvariantCheck(
+            {{"pseudo-account has 0 pseudo-account fields set"},
+             {"pseudo-account sequence changed"},
+             {"pseudo-account flags are not set"}},
+            [&](Account const& a1, Account const&, ApplyContext& ac) {
+                auto sle = ac.view().peek(keylet::account(a1.id()));
+                if (!sle)
+                    return false;
+                sle->at(sfSequence) = 0;
+                ac.view().update(sle);
+                return true;
+            });
+    }
+
+    void
+    testValidLoanBroker()
+    {
+        testcase << "valid loan broker";
+
+        using namespace jtx;
+
+        enum class Asset { XRP, IOU, MPT };
+        auto const assetTypes = std::to_array({Asset::XRP, Asset::IOU, Asset::MPT});
+
+        for (auto const assetType : assetTypes)
+        {
+            // Initialize with a placeholder value because there's no default
+            // ctor
+            auto const setupAsset =
+                [&](Account const& alice, Account const& issuer, Env& env) -> PrettyAsset {
+                switch (assetType)
+                {
+                    case Asset::IOU: {
+                        PrettyAsset const iouAsset = issuer["IOU"];
+                        env(trust(alice, iouAsset(1000)));
+                        env(pay(issuer, alice, iouAsset(1000)));
+                        env.close();
+                        return iouAsset;
+                    }
+                    case Asset::MPT: {
+                        MPTTester mptt{env, issuer, kMptInitNoFund};
+                        mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
+                        PrettyAsset const mptAsset = mptt.issuanceID();
+                        mptt.authorize({.account = alice});
+                        env(pay(issuer, alice, mptAsset(1000)));
+                        env.close();
+                        return mptAsset;
+                    }
+                    case Asset::XRP:
+                    default:
+                        return PrettyAsset{xrpIssue(), 1'000'000};
+                }
+            };
+
+            Keylet loanBrokerKeylet = keylet::amendments();
+            Preclose const createLoanBroker =
+                [&, this](Account const& alice, Account const& issuer, Env& env) {
+                    auto const asset = setupAsset(alice, issuer, env);
+                    loanBrokerKeylet = this->createLoanBroker(alice, env, asset);
+                    return BEAST_EXPECT(env.le(loanBrokerKeylet));
+                };
+
+            // Ensure the test scenarios are set up completely. The test cases
+            // will need to recompute any of these values it needs for itself
+            // rather than trying to return a bunch of items
+            auto setupTest = [&, this](Account const& a1, Account const&, ApplyContext& ac)
+                -> std::optional> {
+                if (loanBrokerKeylet.type != ltLOAN_BROKER)
+                    return {};
+                auto sleBroker = ac.view().peek(loanBrokerKeylet);
+                if (!sleBroker)
+                    return {};
+                if (!BEAST_EXPECT(sleBroker->at(sfOwnerCount) == 0))
+                    return {};
+                // Need to touch sleBroker so that it is included in the
+                // modified entries for the invariant to find
+                ac.view().update(sleBroker);
+
+                // The pseudo-account holds the directory, so get it
+                auto const pseudoAccountID = sleBroker->at(sfAccount);
+                auto const pseudoAccountKeylet = keylet::account(pseudoAccountID);
+                // Strictly speaking, we don't need to load the
+                // ACCOUNT_ROOT, but check anyway
+                auto slePseudo = ac.view().peek(pseudoAccountKeylet);
+                if (!BEAST_EXPECT(slePseudo))
+                    return {};
+                // Make sure the directory doesn't already exist
+                auto const dirKeylet = keylet::ownerDir(pseudoAccountID);
+                auto sleDir = ac.view().peek(dirKeylet);
+                auto const describe = describeOwnerDir(pseudoAccountID);
+                if (!sleDir)
+                {
+                    // Create the directory
+                    BEAST_EXPECT(
+                        ::xrpl::directory::createRoot(
+                            ac.view(), dirKeylet, loanBrokerKeylet.key, describe) == 0);
+
+                    sleDir = ac.view().peek(dirKeylet);
+                }
+
+                return std::make_pair(slePseudo, sleDir);
+            };
+
+            doInvariantCheck(
+                {{"Loan Broker with zero OwnerCount has multiple directory "
+                  "pages"}},
+                [&setupTest, this](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    auto test = setupTest(a1, a2, ac);
+                    if (!test || !test->first || !test->second)
+                        return false;
+
+                    auto slePseudo = test->first;
+                    auto sleDir = test->second;
+                    auto const describe = describeOwnerDir(slePseudo->at(sfAccount));
+
+                    BEAST_EXPECT(
+                        ::xrpl::directory::insertPage(
+                            ac.view(),
+                            0,
+                            sleDir,
+                            0,
+                            sleDir,
+                            slePseudo->key(),
+                            keylet::page(sleDir->key(), 0),
+                            describe) == 1);
+
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                createLoanBroker);
+
+            doInvariantCheck(
+                {{"Loan Broker with zero OwnerCount has multiple indexes in "
+                  "the Directory root"}},
+                [&setupTest](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    auto test = setupTest(a1, a2, ac);
+                    if (!test || !test->first || !test->second)
+                        return false;
+
+                    auto slePseudo = test->first;
+                    auto sleDir = test->second;
+                    auto indexes = sleDir->getFieldV256(sfIndexes);
+
+                    // Put some extra garbage into the directory
+                    for (auto const& key : {slePseudo->key(), sleDir->key()})
+                    {
+                        ::xrpl::directory::insertKey(ac.view(), sleDir, 0, false, indexes, key);
+                    }
+
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                createLoanBroker);
+
+            doInvariantCheck(
+                {{"Loan Broker directory corrupt"}},
+                [&setupTest](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    auto test = setupTest(a1, a2, ac);
+                    if (!test || !test->first || !test->second)
+                        return false;
+
+                    auto slePseudo = test->first;
+                    auto sleDir = test->second;
+                    auto const describe = describeOwnerDir(slePseudo->at(sfAccount));
+                    // Empty vector will overwrite the existing entry for the
+                    // holding, if any, avoiding the "has multiple indexes"
+                    // failure.
+                    STVector256 indexes;
+
+                    // Put one meaningless key into the directory
+                    auto const key = keylet::account(Account("random").id()).key;
+                    ::xrpl::directory::insertKey(ac.view(), sleDir, 0, false, indexes, key);
+
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                createLoanBroker);
+
+            doInvariantCheck(
+                {{"Loan Broker with zero OwnerCount has an unexpected entry in "
+                  "the directory"}},
+                [&setupTest](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    auto test = setupTest(a1, a2, ac);
+                    if (!test || !test->first || !test->second)
+                        return false;
+
+                    auto slePseudo = test->first;
+                    auto sleDir = test->second;
+                    // Empty vector will overwrite the existing entry for the
+                    // holding, if any, avoiding the "has multiple indexes"
+                    // failure.
+                    STVector256 indexes;
+
+                    ::xrpl::directory::insertKey(
+                        ac.view(), sleDir, 0, false, indexes, slePseudo->key());
+
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                createLoanBroker);
+
+            doInvariantCheck(
+                {{"Loan Broker sequence number decreased"}},
+                [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    if (loanBrokerKeylet.type != ltLOAN_BROKER)
+                        return false;
+                    auto sleBroker = ac.view().peek(loanBrokerKeylet);
+                    if (!sleBroker)
+                        return false;
+                    if (!BEAST_EXPECT(sleBroker->at(sfLoanSequence) > 0))
+                        return false;
+                    // Need to touch sleBroker so that it is included in the
+                    // modified entries for the invariant to find
+                    ac.view().update(sleBroker);
+
+                    sleBroker->at(sfLoanSequence) -= 1;
+
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                createLoanBroker);
+
+            // Test: cover available less than pseudo-account asset balance
+            {
+                Keylet brokerKeylet = keylet::amendments();
+                Preclose const createBrokerWithCover =
+                    [&, this](Account const& alice, Account const& issuer, Env& env) {
+                        auto const asset = setupAsset(alice, issuer, env);
+                        brokerKeylet = this->createLoanBroker(alice, env, asset);
+                        if (!BEAST_EXPECT(env.le(brokerKeylet)))
+                            return false;
+                        env(loan_broker::coverDeposit(alice, brokerKeylet.key, asset(10)));
+                        env.close();
+                        return BEAST_EXPECT(env.le(brokerKeylet));
+                    };
+
+                doInvariantCheck(
+                    {{"Loan Broker cover available is less than pseudo-account asset balance"}},
+                    [&](Account const&, Account const&, ApplyContext& ac) {
+                        auto sle = ac.view().peek(brokerKeylet);
+                        if (!BEAST_EXPECT(sle))
+                            return false;
+                        // Pseudo-account holds 10 units, set cover to 5
+                        sle->at(sfCoverAvailable) = Number(5);
+                        ac.view().update(sle);
+                        return true;
+                    },
+                    XRPAmount{},
+                    STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
+                    {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                    createBrokerWithCover);
+            }
+
+            // Test: cover available greater than pseudo-account asset balance
+            // (requires fixCleanup3_1_3)
+            doInvariantCheck(
+                {{"Loan Broker cover available is greater than pseudo-account asset balance"}},
+                [&](Account const&, Account const&, ApplyContext& ac) {
+                    auto sle = ac.view().peek(loanBrokerKeylet);
+                    if (!BEAST_EXPECT(sle))
+                        return false;
+                    // Pseudo-account has no cover deposited; set cover
+                    // higher than any incidental balance
+                    sle->at(sfCoverAvailable) = Number(1'000'000);
+                    ac.view().update(sle);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                createLoanBroker);
+
+            // Deleting the IOU holding while leaving the broker unchanged must
+            // still expose CoverAvailable exceeding the now-zero balance: the
+            // broker is discovered through the deleted trust line. XRP has no
+            // holding SLE, while deleting an MPToken triggers other invariants,
+            // so IOU isolates this check. Verify that fixCleanup3_1_3 gates it
+            // by expecting failure only when the amendment is enabled.
+            if (assetType == Asset::IOU)
+            {
+                Keylet brokerKeylet = keylet::amendments();
+                Preclose const createBrokerWithCover =
+                    [&, this](Account const& alice, Account const& issuer, Env& env) {
+                        auto const asset = setupAsset(alice, issuer, env);
+                        brokerKeylet = this->createLoanBroker(alice, env, asset);
+                        if (!BEAST_EXPECT(env.le(brokerKeylet)))
+                            return false;
+                        env(loan_broker::coverDeposit(alice, brokerKeylet.key, asset(10)));
+                        env.close();
+                        return BEAST_EXPECT(env.le(brokerKeylet));
+                    };
+
+                Precheck const deleteHolding =
+                    [&](Account const&, Account const&, ApplyContext& ac) {
+                        if (brokerKeylet.type != ltLOAN_BROKER)
+                            return false;
+                        // Read (don't touch) the broker so it is only found via
+                        // the deleted holding, not as a modified entry.
+                        auto const sleBroker = ac.view().read(brokerKeylet);
+                        if (!BEAST_EXPECT(sleBroker))
+                            return false;
+                        auto const pseudoAccountID = sleBroker->at(sfAccount);
+
+                        // Erase every holding in the pseudo-account directory
+                        // and the directory root itself, mirroring a bug that
+                        // removed the cover holding without zeroing
+                        // CoverAvailable. Removing the root also keeps the
+                        // zero-OwnerCount directory check from firing first.
+                        auto sleDir = ac.view().peek(keylet::ownerDir(pseudoAccountID));
+                        if (!BEAST_EXPECT(sleDir))
+                            return false;
+                        for (auto const& index : sleDir->getFieldV256(sfIndexes))
+                        {
+                            if (auto holding = ac.view().peek(keylet::unchecked(index)))
+                            {
+                                ac.view().erase(holding);
+                            }
+                        }
+                        ac.view().erase(sleDir);
+                        return true;
+                    };
+
+                // With fixCleanup3_1_3: the invariant fires.
+                doInvariantCheck(
+                    makeEnv(all_),
+                    {{"Loan Broker cover available is greater than pseudo-account asset balance"}},
+                    deleteHolding,
+                    XRPAmount{},
+                    STTx{ttACCOUNT_SET, [](STObject&) {}},
+                    {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                    createBrokerWithCover);
+
+                // Without fixCleanup3_1_3: the same state is silently accepted.
+                doInvariantCheck(
+                    makeEnv(all_ - fixCleanup3_1_3),
+                    {},
+                    deleteHolding,
+                    XRPAmount{},
+                    STTx{ttACCOUNT_SET, [](STObject&) {}},
+                    {tesSUCCESS, tesSUCCESS},
+                    createBrokerWithCover);
+            }
+
+            // A LoanBroker may only be removed by ttLOAN_BROKER_DELETE. Erase
+            // the broker in the apply view under a non-delete tx type and
+            // expect the deletion-tx invariant to fire.
+            doInvariantCheck(
+                {{"Loan Broker deleted by a transaction other than LoanBrokerDelete"}},
+                [&](Account const&, Account const&, ApplyContext& ac) {
+                    if (loanBrokerKeylet.type != ltLOAN_BROKER)
+                        return false;
+                    auto sleBroker = ac.view().peek(loanBrokerKeylet);
+                    if (!BEAST_EXPECT(sleBroker))
+                        return false;
+                    ac.view().erase(sleBroker);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttACCOUNT_SET, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                createLoanBroker);
+        }
+
+        // A LoanBrokerDelete must not remove a broker whose pre-transaction
+        // DebtTotal is non-zero. visitEntry captures `before` from the parent
+        // view, so the DebtTotal must be seeded in the OpenView before the
+        // ApplyContext is constructed; a Precheck modification would only
+        // land in the applyView (visible as `after`) and would leave `before`
+        // at the createLoanBroker-produced zero.
+        {
+            Env env{*this};
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env.fund(XRP(1000), a1, a2);
+            env.close();
+
+            PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+            auto const brokerKeylet = createLoanBroker(a1, env, xrpAsset);
+            if (!BEAST_EXPECT(env.le(brokerKeylet)))
+                return;
+            env.close();
+
+            OpenView ov{*env.current()};
+
+            // Seed a non-zero DebtTotal in the base view so `before` at
+            // visitEntry time reports it.
+            {
+                auto const sleBrokerRead = ov.read(brokerKeylet);
+                if (!BEAST_EXPECT(sleBrokerRead))
+                    return;
+                auto sleBroker = std::make_shared(*sleBrokerRead);
+                sleBroker->at(sfDebtTotal) = Number(1);
+                ov.rawReplace(sleBroker);
+            }
+
+            STTx const tx{ttLOAN_BROKER_DELETE, [](STObject&) {}};
+            test::StreamSink sink{beast::Severity::Warning};
+            beast::Journal const jlog{sink};
+            ApplyContext ac{
+                env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+            CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+            auto sleBroker = ac.view().peek(brokerKeylet);
+            if (!BEAST_EXPECT(sleBroker))
+                return;
+            ac.view().erase(sleBroker);
+
+            auto transactor = makeTransactor(ac);
+            if (!BEAST_EXPECT(transactor))
+                return;
+            TER const result = transactor->checkInvariants(
+                tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
+            BEAST_EXPECT(result == tecINVARIANT_FAILED);
+            BEAST_EXPECT(
+                sink.messages().str().contains("Loan Broker deleted with non-zero debt total"));
+        }
+
+        // Residual DebtTotal dust that rounds to zero at the vault asset's
+        // scale must not trip the invariant: LoanBrokerDelete::preclaim
+        // deliberately permits it, so the invariant must not be stricter.
+        // Other invariants may still object to a hand-erased broker, so only
+        // the absence of the DebtTotal complaint is asserted.
+        {
+            Env env{*this};
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env.fund(XRP(1000), a1, a2);
+            env.close();
+
+            PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+            auto const brokerKeylet = createLoanBroker(a1, env, xrpAsset);
+            if (!BEAST_EXPECT(env.le(brokerKeylet)))
+                return;
+            env.close();
+
+            OpenView ov{*env.current()};
+
+            // A thousandth of a drop: non-zero, but zero once quantized to XRP.
+            {
+                auto const sleBrokerRead = ov.read(brokerKeylet);
+                if (!BEAST_EXPECT(sleBrokerRead))
+                    return;
+                auto sleBroker = std::make_shared(*sleBrokerRead);
+                sleBroker->at(sfDebtTotal) = Number(1, -3);
+                ov.rawReplace(sleBroker);
+            }
+
+            STTx const tx{ttLOAN_BROKER_DELETE, [](STObject&) {}};
+            test::StreamSink sink{beast::Severity::Warning};
+            beast::Journal const jlog{sink};
+            ApplyContext ac{
+                env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+            CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+            auto sleBroker = ac.view().peek(brokerKeylet);
+            if (!BEAST_EXPECT(sleBroker))
+                return;
+            ac.view().erase(sleBroker);
+
+            auto transactor = makeTransactor(ac);
+            if (!BEAST_EXPECT(transactor))
+                return;
+            [[maybe_unused]] TER const result = transactor->checkInvariants(
+                tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
+            BEAST_EXPECT(
+                !sink.messages().str().contains("Loan Broker deleted with non-zero debt total"));
+        }
+
+        // A LoanBrokerDelete must not remove a broker whose pre-transaction
+        // OwnerCount is non-zero. DebtTotal is left at zero so the earlier
+        // check passes and the OwnerCount check is what fires.
+        {
+            Env env{*this};
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env.fund(XRP(1000), a1, a2);
+            env.close();
+
+            PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+            auto const brokerKeylet = createLoanBroker(a1, env, xrpAsset);
+            if (!BEAST_EXPECT(env.le(brokerKeylet)))
+                return;
+            env.close();
+
+            OpenView ov{*env.current()};
+
+            {
+                auto const sleBrokerRead = ov.read(brokerKeylet);
+                if (!BEAST_EXPECT(sleBrokerRead))
+                    return;
+                auto sleBroker = std::make_shared(*sleBrokerRead);
+                sleBroker->at(sfOwnerCount) = 1;
+                ov.rawReplace(sleBroker);
+            }
+
+            STTx const tx{ttLOAN_BROKER_DELETE, [](STObject&) {}};
+            test::StreamSink sink{beast::Severity::Warning};
+            beast::Journal const jlog{sink};
+            ApplyContext ac{
+                env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+            CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+            auto sleBroker = ac.view().peek(brokerKeylet);
+            if (!BEAST_EXPECT(sleBroker))
+                return;
+            ac.view().erase(sleBroker);
+
+            auto transactor = makeTransactor(ac);
+            if (!BEAST_EXPECT(transactor))
+                return;
+            TER const result = transactor->checkInvariants(
+                tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
+            BEAST_EXPECT(result == tecINVARIANT_FAILED);
+            BEAST_EXPECT(
+                sink.messages().str().contains("Loan Broker deleted with non-zero owner count"));
+        }
+
+        // Only one LoanBroker may be deleted per transaction. Create two
+        // brokers under different owners, then erase both in the apply view
+        // and expect the multi-deletion invariant to fire.
+        {
+            Keylet loanBrokerKeylet1 = keylet::amendments();
+            Keylet loanBrokerKeylet2 = keylet::amendments();
+            Preclose const createTwoBrokers = [&, this](
+                                                  Account const& a1, Account const& a2, Env& env) {
+                PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+                loanBrokerKeylet1 = this->createLoanBroker(a1, env, xrpAsset);
+                loanBrokerKeylet2 = this->createLoanBroker(a2, env, xrpAsset);
+                return BEAST_EXPECT(env.le(loanBrokerKeylet1) && env.le(loanBrokerKeylet2));
+            };
+
+            doInvariantCheck(
+                {{"more than one Loan Broker deleted in a single transaction"}},
+                [&](Account const&, Account const&, ApplyContext& ac) {
+                    auto sle1 = ac.view().peek(loanBrokerKeylet1);
+                    auto sle2 = ac.view().peek(loanBrokerKeylet2);
+                    if (!BEAST_EXPECT(sle1 && sle2))
+                        return false;
+                    ac.view().erase(sle1);
+                    ac.view().erase(sle2);
+                    return true;
+                },
+                XRPAmount{},
+                STTx{ttLOAN_BROKER_DELETE, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                createTwoBrokers);
+        }
+    }
+
+    void
+    run() override
+    {
+        testValidPseudoAccounts();
+        testValidLoanBroker();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(InvariantsPseudoAccount, app, xrpl);
+
+}  // namespace xrpl::test
diff --git a/src/test/app/invariants/InvariantsTrustLine_test.cpp b/src/test/app/invariants/InvariantsTrustLine_test.cpp
new file mode 100644
index 0000000000..e0995fc431
--- /dev/null
+++ b/src/test/app/invariants/InvariantsTrustLine_test.cpp
@@ -0,0 +1,237 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+class InvariantsTrustLine_test : public InvariantsBase
+{
+    void
+    testNoXRPTrustLine()
+    {
+        using namespace test::jtx;
+        testcase << "trust lines with XRP not allowed";
+        doInvariantCheck(
+            {{"an XRP trust line was created"}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                // create simple trust SLE with xrp currency
+                auto const sleNew =
+                    std::make_shared(keylet::trustLine(a1, a2, xrpIssue().currency));
+                ac.view().insert(sleNew);
+                return true;
+            });
+    }
+
+    void
+    testNoDeepFreezeTrustLinesWithoutFreeze()
+    {
+        using namespace test::jtx;
+        testcase << "trust lines with deep freeze flag without freeze "
+                    "not allowed";
+        doInvariantCheck(
+            {{"a trust line with deep freeze flag without normal freeze was "
+              "created"}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sleNew =
+                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
+                sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
+                sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
+
+                std::uint32_t uFlags = 0u;
+                uFlags |= lsfLowDeepFreeze;
+                sleNew->setFieldU32(sfFlags, uFlags);
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"a trust line with deep freeze flag without normal freeze was "
+              "created"}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sleNew =
+                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
+                sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
+                sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
+                std::uint32_t uFlags = 0u;
+                uFlags |= lsfHighDeepFreeze;
+                sleNew->setFieldU32(sfFlags, uFlags);
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"a trust line with deep freeze flag without normal freeze was "
+              "created"}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sleNew =
+                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
+                sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
+                sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
+                std::uint32_t uFlags = 0u;
+                uFlags |= lsfLowDeepFreeze | lsfHighDeepFreeze;
+                sleNew->setFieldU32(sfFlags, uFlags);
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"a trust line with deep freeze flag without normal freeze was "
+              "created"}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sleNew =
+                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
+                sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
+                sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
+                std::uint32_t uFlags = 0u;
+                uFlags |= lsfLowDeepFreeze | lsfHighFreeze;
+                sleNew->setFieldU32(sfFlags, uFlags);
+                ac.view().insert(sleNew);
+                return true;
+            });
+
+        doInvariantCheck(
+            {{"a trust line with deep freeze flag without normal freeze was "
+              "created"}},
+            [](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sleNew =
+                    std::make_shared(keylet::trustLine(a1, a2, a1["USD"].currency));
+                sleNew->setFieldAmount(sfLowLimit, a1["USD"](0));
+                sleNew->setFieldAmount(sfHighLimit, a1["USD"](0));
+                std::uint32_t uFlags = 0u;
+                uFlags |= lsfLowFreeze | lsfHighDeepFreeze;
+                sleNew->setFieldU32(sfFlags, uFlags);
+                ac.view().insert(sleNew);
+                return true;
+            });
+    }
+
+    void
+    testTransfersNotFrozen()
+    {
+        using namespace test::jtx;
+        testcase << "transfers when frozen";
+
+        Account const g1{"G1"};
+        // Helper function to establish the trustlines
+        auto const createTrustlines = [&](Account const& a1, Account const& a2, Env& env) {
+            // Preclose callback to establish trust lines with gateway
+            env.fund(XRP(1000), g1);
+
+            env.trust(g1["USD"](10000), a1);
+            env.trust(g1["USD"](10000), a2);
+            env.close();
+
+            env(pay(g1, a1, g1["USD"](1000)));
+            env(pay(g1, a2, g1["USD"](1000)));
+            env.close();
+
+            return true;
+        };
+
+        auto const a1FrozenByIssuer = [&](Account const& a1, Account const& a2, Env& env) {
+            createTrustlines(a1, a2, env);
+            env(trust(g1, a1["USD"](10000), tfSetFreeze));
+            env.close();
+
+            return true;
+        };
+
+        auto const a1DeepFrozenByIssuer = [&](Account const& a1, Account const& a2, Env& env) {
+            a1FrozenByIssuer(a1, a2, env);
+            env(trust(g1, a1["USD"](10000), tfSetDeepFreeze));
+            env.close();
+
+            return true;
+        };
+
+        auto const changeBalances = [&](Account const& a1,
+                                        Account const& a2,
+                                        ApplyContext& ac,
+                                        int a1Balance,
+                                        int a2Balance) {
+            auto const sleA1 = ac.view().peek(keylet::trustLine(a1, g1["USD"]));
+            auto const sleA2 = ac.view().peek(keylet::trustLine(a2, g1["USD"]));
+
+            sleA1->setFieldAmount(sfBalance, g1["USD"](a1Balance));
+            sleA2->setFieldAmount(sfBalance, g1["USD"](a2Balance));
+
+            ac.view().update(sleA1);
+            ac.view().update(sleA2);
+        };
+
+        // test: imitating frozen A1 making a payment to A2.
+        doInvariantCheck(
+            {{"Attempting to move frozen funds"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                changeBalances(a1, a2, ac, -900, -1100);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPAYMENT, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            a1FrozenByIssuer);
+
+        // test: imitating deep frozen A1 making a payment to A2.
+        doInvariantCheck(
+            {{"Attempting to move frozen funds"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                changeBalances(a1, a2, ac, -900, -1100);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPAYMENT, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            a1DeepFrozenByIssuer);
+
+        // test: imitating A2 making a payment to deep frozen A1.
+        doInvariantCheck(
+            {{"Attempting to move frozen funds"}},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                changeBalances(a1, a2, ac, -1100, -900);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPAYMENT, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            a1DeepFrozenByIssuer);
+    }
+
+    void
+    run() override
+    {
+        testNoXRPTrustLine();
+        testNoDeepFreezeTrustLinesWithoutFreeze();
+        testTransfersNotFrozen();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(InvariantsTrustLine, app, xrpl);
+
+}  // namespace xrpl::test
diff --git a/src/test/app/invariants/InvariantsVault_test.cpp b/src/test/app/invariants/InvariantsVault_test.cpp
new file mode 100644
index 0000000000..dcf783a1b5
--- /dev/null
+++ b/src/test/app/invariants/InvariantsVault_test.cpp
@@ -0,0 +1,3206 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+class InvariantsVault_test : public InvariantsBase
+{
+    FeatureBitset const all_{test::jtx::testableAmendments()};
+
+    void
+    testVault()  // NOLINT(readability-function-size)
+    {
+        using namespace test::jtx;
+
+        struct AccountAmount
+        {
+            AccountID account;
+            int amount;
+        };
+        // Parameters for a synthetic loan object created alongside a vault
+        // adjustment. The interest due booked to the vault is
+        // totalValueOutstanding - principalOutstanding - managementFeeOutstanding.
+        struct LoanParams
+        {
+            int principalOutstanding = 0;
+            int totalValueOutstanding = 0;
+            int managementFeeOutstanding = 0;
+            AccountID borrower = beast::kZero;
+            // Broker the created loan references. Left unset when the test does
+            // not depend on the broker resolving to a real ledger entry.
+            uint256 brokerKey = beast::kZero;
+        };
+        struct Adjustments
+        {
+            // NOLINTBEGIN(readability-redundant-member-init)
+            std::optional assetsTotal = std::nullopt;
+            std::optional assetsAvailable = std::nullopt;
+            std::optional lossUnrealized = std::nullopt;
+            std::optional assetsMaximum = std::nullopt;
+            std::optional sharesTotal = std::nullopt;
+            std::optional vaultAssets = std::nullopt;
+            std::optional accountAssets = std::nullopt;
+            std::optional accountShares = std::nullopt;
+            std::optional createLoan = std::nullopt;
+            // Number of loan objects to create (only used when createLoan is
+            // set); a valid loan set creates exactly one.
+            int loanCount = 1;
+            // NOLINTEND(readability-redundant-member-init)
+        };
+        constexpr auto kAdjust = [&](ApplyView& ac, xrpl::Keylet keylet, Adjustments args) {
+            // Avoid uint64 + negative-int wrap (flagged by UBSan
+            // unsigned-integer-overflow) when adjusting UINT64 fields.
+            auto const addSigned = [](std::uint64_t current, int adj) -> std::uint64_t {
+                return adj >= 0  //
+                    ? current + static_cast(adj)
+                    : current - static_cast(-adj);
+            };
+            auto sleVault = ac.peek(keylet);
+            if (!sleVault)
+                return false;
+
+            auto const mptIssuanceID = (*sleVault)[sfShareMPTID];
+            auto sleShares = ac.peek(keylet::mptokenIssuance(mptIssuanceID));
+            if (!sleShares)
+                return false;
+
+            // These two fields are adjusted in absolute terms
+            if (args.lossUnrealized)
+                (*sleVault)[sfLossUnrealized] = *args.lossUnrealized;
+            if (args.assetsMaximum)
+                (*sleVault)[sfAssetsMaximum] = *args.assetsMaximum;
+
+            // Remaining fields are adjusted in terms of difference
+            if (args.assetsTotal)
+                (*sleVault)[sfAssetsTotal] = *(*sleVault)[sfAssetsTotal] + *args.assetsTotal;
+            if (args.assetsAvailable)
+            {
+                (*sleVault)[sfAssetsAvailable] =
+                    *(*sleVault)[sfAssetsAvailable] + *args.assetsAvailable;
+            }
+            ac.update(sleVault);
+
+            if (args.sharesTotal)
+            {
+                (*sleShares)[sfOutstandingAmount] =
+                    addSigned(*(*sleShares)[sfOutstandingAmount], *args.sharesTotal);
+                ac.update(sleShares);
+            }
+
+            auto const assets = *(*sleVault)[sfAsset];
+            auto const pseudoId = *(*sleVault)[sfAccount];
+            if (args.vaultAssets)
+            {
+                if (assets.native())
+                {
+                    auto slePseudoAccount = ac.peek(keylet::account(pseudoId));
+                    if (!slePseudoAccount)
+                        return false;
+                    (*slePseudoAccount)[sfBalance] =
+                        *(*slePseudoAccount)[sfBalance] + *args.vaultAssets;
+                    ac.update(slePseudoAccount);
+                }
+                else if (assets.holds())
+                {
+                    auto const mptId = assets.get().getMptID();
+                    auto sleMPToken = ac.peek(keylet::mptoken(mptId, pseudoId));
+                    if (!sleMPToken)
+                        return false;
+                    (*sleMPToken)[sfMPTAmount] =
+                        addSigned(*(*sleMPToken)[sfMPTAmount], *args.vaultAssets);
+                    ac.update(sleMPToken);
+                }
+                else
+                {
+                    return false;  // Not supporting testing with IOU
+                }
+            }
+
+            if (args.accountAssets)
+            {
+                auto const& pair = *args.accountAssets;
+                if (assets.native())
+                {
+                    auto sleAccount = ac.peek(keylet::account(pair.account));
+                    if (!sleAccount)
+                        return false;
+                    (*sleAccount)[sfBalance] = *(*sleAccount)[sfBalance] + pair.amount;
+                    ac.update(sleAccount);
+                }
+                else if (assets.holds())
+                {
+                    auto const mptID = assets.get().getMptID();
+                    auto sleMPToken = ac.peek(keylet::mptoken(mptID, pair.account));
+                    if (!sleMPToken)
+                        return false;
+                    (*sleMPToken)[sfMPTAmount] =
+                        addSigned(*(*sleMPToken)[sfMPTAmount], pair.amount);
+                    ac.update(sleMPToken);
+                }
+                else
+                {
+                    return false;  // Not supporting testing with IOU
+                }
+            }
+
+            if (args.accountShares)
+            {
+                auto const& pair = *args.accountShares;
+                auto sleMPToken = ac.peek(keylet::mptoken(mptIssuanceID, pair.account));
+                if (!sleMPToken)
+                    return false;
+                (*sleMPToken)[sfMPTAmount] = addSigned(*(*sleMPToken)[sfMPTAmount], pair.amount);
+                ac.update(sleMPToken);
+            }
+
+            if (args.createLoan)
+            {
+                auto const& lp = *args.createLoan;
+                bool const anyOutstanding = lp.principalOutstanding != 0 ||
+                    lp.totalValueOutstanding != 0 || lp.managementFeeOutstanding != 0;
+                // The vault key stands in for an unset broker: it keeps the loan
+                // keylet distinct per vault while resolving to no broker.
+                uint256 const brokerKey = lp.brokerKey != beast::kZero ? lp.brokerKey : keylet.key;
+                for (std::uint32_t seq = 1; seq <= static_cast(args.loanCount);
+                     ++seq)
+                {
+                    auto sleLoan = makeLoanSle(brokerKey, seq, lp.borrower);
+                    sleLoan->at(sfPrincipalOutstanding) = Number(lp.principalOutstanding);
+                    sleLoan->at(sfTotalValueOutstanding) = Number(lp.totalValueOutstanding);
+                    sleLoan->at(sfManagementFeeOutstanding) = Number(lp.managementFeeOutstanding);
+                    sleLoan->setFieldU32(sfPaymentRemaining, anyOutstanding ? 1 : 0);
+                    ac.insert(sleLoan);
+                }
+            }
+            return true;
+        };
+
+        static constexpr auto kArgs = [](AccountID id, int adjustment, auto fn) -> Adjustments {
+            Adjustments sample = {
+                .assetsTotal = adjustment,
+                .assetsAvailable = adjustment,
+                .lossUnrealized = 0,
+                .sharesTotal = adjustment,
+                .vaultAssets = adjustment,
+                .accountAssets =  //
+                AccountAmount{.account = id, .amount = -adjustment},
+                .accountShares =  //
+                AccountAmount{.account = id, .amount = adjustment}};
+            fn(sample);
+            return sample;
+        };
+
+        Account const a3{"A3"};
+        Account const a4{"A4"};
+        auto const precloseXrp = [&](Account const& a1,
+                                     Account const& a2,
+                                     Env& env,
+                                     VaultVersion version = VaultVersion::CashBasis) -> bool {
+            env.fund(XRP(1000), a3, a4);
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
+            env(tx);
+            env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
+            env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = XRP(10)}));
+            env(vault.deposit({.depositor = a3, .id = keylet.key, .amount = XRP(10)}));
+            return true;
+        };
+
+        auto const createClosedXrpBroker =
+            [&](Account const& owner, Env& env) -> std::optional> {
+            PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+            auto const brokerKeylet = createLoanBroker(owner, env, xrpAsset);
+            auto const sleBroker = env.le(brokerKeylet);
+            if (!BEAST_EXPECT(sleBroker))
+                return std::nullopt;
+            auto const vaultKeylet = keylet::vault(sleBroker->at(sfVaultID));
+            env.close(std::chrono::seconds{61});
+            return std::pair{vaultKeylet, brokerKeylet};
+        };
+
+        testcase << "Vault general checks";
+        doInvariantCheck(
+            {"vault deletion succeeded without deleting a vault"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DELETE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"vault updated by a wrong transaction type",
+             "deleted Vault without deleting its pseudo-account"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                ac.view().erase(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPAYMENT, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"vault updated by a wrong transaction type"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPAYMENT, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"vault updated by a wrong transaction type"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sequence = ac.view().seq();
+                auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
+                auto sleVault = std::make_shared(vaultKeylet);
+                auto const vaultPage = ac.view().dirInsert(
+                    keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
+                sleVault->setFieldU64(sfOwnerNode, *vaultPage);
+                sleVault->setAccountID(sfAccount, a1.id());
+                ac.view().insert(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttPAYMENT, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
+
+        doInvariantCheck(
+            {"vault deleted by a wrong transaction type",
+             "deleted Vault without deleting its pseudo-account"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                ac.view().erase(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"vault operation updated more than single vault",
+             "deleted Vault without deleting its pseudo-account"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                {
+                    auto const keylet =
+                        keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                    auto sleVault = ac.view().peek(keylet);
+                    if (!sleVault)
+                        return false;
+                    ac.view().erase(sleVault);
+                }
+                {
+                    auto const keylet =
+                        keylet::vault(a2.id(), SeqProxy::rawSequence(ac.view().seq()));
+                    auto sleVault = ac.view().peek(keylet);
+                    if (!sleVault)
+                        return false;
+                    ac.view().erase(sleVault);
+                }
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DELETE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                {
+                    auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
+                    env(tx);
+                }
+                {
+                    auto [tx, _] = vault.create({.owner = a2, .asset = xrpIssue()});
+                    env(tx);
+                }
+                return true;
+            });
+
+        doInvariantCheck(
+            {"vault operation updated more than single vault"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sequence = ac.view().seq();
+                auto const insertVault = [&](Account const a) {
+                    auto const vaultKeylet = keylet::vault(a.id(), SeqProxy::rawSequence(sequence));
+                    auto sleVault = std::make_shared(vaultKeylet);
+                    auto const vaultPage = ac.view().dirInsert(
+                        keylet::ownerDir(a.id()), sleVault->key(), describeOwnerDir(a.id()));
+                    sleVault->setFieldU64(sfOwnerNode, *vaultPage);
+                    sleVault->setAccountID(sfAccount, a.id());
+                    ac.view().insert(sleVault);
+                };
+                insertVault(a1);
+                insertVault(a2);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED});
+
+        doInvariantCheck(
+            {"deleted vault must also delete shares",
+             "deleted Vault without deleting its pseudo-account"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                ac.view().erase(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DELETE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"deleted vault must have no shares outstanding",
+             "deleted vault must have no assets outstanding",
+             "deleted vault must have no assets available"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
+                if (!sleShares)
+                    return false;
+                ac.view().erase(sleVault);
+                ac.view().erase(sleShares);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DELETE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
+                return true;
+            });
+
+        doInvariantCheck(
+            {"vault operation succeeded without modifying a vault"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
+                if (!sleShares)
+                    return false;
+                // Note, such an "orphaned" update of MPT issuance attached to a
+                // vault is invalid; ttVAULT_SET must also update Vault object.
+                sleShares->setFieldH256(sfDomainID, uint256(13));
+                ac.view().update(sleShares);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"vault operation succeeded without modifying a vault"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"vault operation succeeded without modifying a vault"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
+            XRPAmount{},
+            STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"vault operation succeeded without modifying a vault"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
+            XRPAmount{},
+            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"vault operation succeeded without modifying a vault"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
+            XRPAmount{},
+            STTx{ttVAULT_CLAWBACK, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"vault operation succeeded without modifying a vault"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) { return true; },
+            XRPAmount{},
+            STTx{ttVAULT_DELETE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"updated vault must have shares"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                (*sleVault)[sfAssetsMaximum] = 200;
+                ac.view().update(sleVault);
+
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
+                if (!sleShares)
+                    return false;
+                ac.view().erase(sleShares);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, _] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"vault operation succeeded without updating shares",
+             "assets available must not be greater than assets outstanding"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                (*sleVault)[sfAssetsTotal] = 9;
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
+                return true;
+            });
+
+        doInvariantCheck(
+            {"set must not change assets outstanding",
+             "set must not change assets available",
+             "set must not change shares outstanding",
+             "set must not change vault balance",
+             "assets available must not be negative",
+             "assets available must not be greater than assets outstanding",
+             "assets outstanding must not be negative"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                auto slePseudoAccount = ac.view().peek(keylet::account(*(*sleVault)[sfAccount]));
+                if (!slePseudoAccount)
+                    return false;
+                (*slePseudoAccount)[sfBalance] = *(*slePseudoAccount)[sfBalance] - 10;
+                ac.view().update(slePseudoAccount);
+
+                // Move 10 drops to A4 to enforce total XRP balance
+                auto sleA4 = ac.view().peek(keylet::account(a4.id()));
+                if (!sleA4)
+                    return false;
+                (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
+                ac.view().update(sleA4);
+
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
+                                   sample.assetsAvailable = (kDropsPerXrp * -100).value();
+                                   sample.assetsTotal = (kDropsPerXrp * -200).value();
+                                   sample.sharesTotal = -1;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        // Under featureLendingProtocolV1_1 the immutability of sfAsset, sfAccount,
+        // sfShareMPTID and sfLEVersion is enforced by NoModifiedUnmodifiableFields.
+        doInvariantCheck(
+            {"changed an unchangeable field"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                sleVault->setFieldIssue(sfAsset, STIssue{sfAsset, MPTIssue(MPTID(42))});
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp);
+
+        doInvariantCheck(
+            {"changed an unchangeable field"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                sleVault->setAccountID(sfAccount, a2.id());
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp);
+
+        doInvariantCheck(
+            {"changed an unchangeable field"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                (*sleVault)[sfShareMPTID] = MPTID(42);
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp);
+
+        doInvariantCheck(
+            {"changed an unchangeable field"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                (*sleVault)[sfLEVersion] = std::to_underlying(VaultVersion::Legacy);
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            [&precloseXrp](Account const& a1, Account const& a2, Env& env) {
+                return precloseXrp(a1, a2, env, VaultVersion::CashBasis);
+            });
+
+        // Pre-featureLendingProtocolV1_1 sfAsset, sfAccount and sfShareMPTID are
+        // guarded by ValidVault instead, so both paths need coverage. ValidVault
+        // returns early once the result is already tec, hence no escalation to
+        // tef on the second pass.
+        auto const preLendingV11Amendments = all_ - featureLendingProtocolV1_1;
+        doInvariantCheck(
+            makeEnv(preLendingV11Amendments),
+            {"violation of vault immutable data"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                sleVault->setFieldIssue(sfAsset, STIssue{sfAsset, MPTIssue(MPTID(42))});
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp);
+
+        doInvariantCheck(
+            makeEnv(preLendingV11Amendments),
+            {"violation of vault immutable data"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                sleVault->setAccountID(sfAccount, a2.id());
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp);
+
+        doInvariantCheck(
+            makeEnv(preLendingV11Amendments),
+            {"violation of vault immutable data"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                (*sleVault)[sfShareMPTID] = MPTID(42);
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp);
+
+        doInvariantCheck(
+            {"vault transaction must not change loss unrealized",
+             "set must not change assets outstanding"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
+                                   sample.lossUnrealized = 13;
+                                   sample.assetsTotal = 20;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"loss unrealized must not exceed the difference "
+             "between assets outstanding and available",
+             "vault transaction must not change loss unrealized"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 100, [&](Adjustments& sample) {
+                                   sample.lossUnrealized = 13;
+                               }));
+            },
+            XRPAmount{},
+            STTx{
+                ttVAULT_DEPOSIT, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        // A negative loss unrealized must trip the invariant. ttLOAN_MANAGE is
+        // allowed to change loss unrealized, so it isolates this check from the
+        // "must not change loss unrealized" invariant. Gated behind
+        // fixCleanup3_4_0 (see below).
+        doInvariantCheck(
+            {"loss unrealized must not be negative"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
+                                   sample.lossUnrealized = -1;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttLOAN_MANAGE, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        // Without fixCleanup3_4_0 the same state must NOT trip the invariant,
+        // preserving pre-amendment behavior (no fork risk). Also remove
+        // featureLendingProtocolV1_1 so finalizeLoanManage's stricter checks
+        // (exactly one loan touched) do not fire from a bare vault mutation
+        // that does not touch a loan.
+        doInvariantCheck(
+            makeEnv(all_ - fixCleanup3_4_0 - featureLendingProtocolV1_1),
+            {},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
+                                   sample.lossUnrealized = -1;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttLOAN_MANAGE, [](STObject& tx) {}},
+            {tesSUCCESS, tesSUCCESS},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"set assets outstanding must not exceed assets maximum"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
+                                   sample.assetsMaximum = 1;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        // The cap check has two post-fixCleanup3_4_0 triggers: the transaction
+        // supplied sfAssetsMaximum, or the cap changed. The case above covers
+        // the cap-changed one (its ttVAULT_SET carries no fields). This covers
+        // the other: the cap is left alone at 30 XRP and the transaction
+        // carries sfAssetsMaximum, so only the isFieldPresent disjunct can
+        // fire. AssetsTotal is pushed past the cap here rather than in
+        // preclose because VaultSet::doApply refuses to set a cap below
+        // AssetsTotal, so the over-cap state is only reachable by fabrication.
+        // Raising AssetsTotal also trips the "must not change assets
+        // outstanding" check, hence two expected messages.
+        Number const vaultCap = XRP(30).number();
+        doInvariantCheck(
+            {"set must not change assets outstanding",
+             "set assets outstanding must not exceed assets maximum"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
+                                   sample.assetsTotal = XRP(1).value().xrp().drops();
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [&](STObject& tx) { tx[sfAssetsMaximum] = vaultCap; }},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) -> bool {
+                env.fund(XRP(1000), a3, a4);
+                Vault const vault{env};
+                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
+                tx[sfAssetsMaximum] = vaultCap;
+                env(tx);
+                env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
+                env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = XRP(10)}));
+                env(vault.deposit({.depositor = a3, .id = keylet.key, .amount = XRP(10)}));
+                return true;
+            },
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"assets maximum must not be negative"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {
+                                   sample.assetsMaximum = -1;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"set must not change shares outstanding",
+             "updated zero sized vault must have no assets outstanding",
+             "updated zero sized vault must have no assets available"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                ac.view().update(sleVault);
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
+                if (!sleShares)
+                    return false;
+                (*sleShares)[sfOutstandingAmount] = 0;
+                ac.view().update(sleShares);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"updated shares must not exceed maximum"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
+                if (!sleShares)
+                    return false;
+                (*sleShares)[sfMaximumAmount] = 10;
+                ac.view().update(sleShares);
+
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"updated shares must not exceed maximum"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
+
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
+                if (!sleShares)
+                    return false;
+                (*sleShares)[sfOutstandingAmount] = kMaxMpTokenAmount + 1;
+                ac.view().update(sleShares);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        // ttLOAN_SET pre-featureLendingProtocolV1_1: finalizeLoanSet short-
+        // circuits and returns success without inspecting the loan or the
+        // vault. The same state that trips the principal-outstanding check
+        // under V1_1 must be silently accepted here.
+        doInvariantCheck(
+            makeEnv(all_ - featureLendingProtocolV1_1),
+            {},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(
+                    ac.view(),
+                    keylet,
+                    Adjustments{
+                        .assetsAvailable = -200,
+                        .vaultAssets = -200,
+                        .accountAssets = AccountAmount{.account = a2.id(), .amount = 200},
+                        .createLoan = LoanParams{
+                            .principalOutstanding = 300,
+                            .totalValueOutstanding = 300,
+                            .borrower = a1.id(),
+                        }});
+            },
+            XRPAmount{},
+            STTx{ttLOAN_SET, [](STObject& tx) { tx.at(sfPrincipalRequested) = Number(200); }},
+            {tesSUCCESS, tesSUCCESS},
+            precloseXrp);
+
+        // ttLOAN_MANAGE: a loan is created rather than modified. This object-
+        // existence rule applies on both invariant passes.
+        doInvariantCheck(
+            {"Loan created by a transaction other than LoanSet"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(
+                    ac.view(),
+                    keylet,
+                    Adjustments{
+                        .createLoan = LoanParams{
+                            .principalOutstanding = 100,
+                            .totalValueOutstanding = 100,
+                            .borrower = a1.id(),
+                        }});
+            },
+            XRPAmount{},
+            STTx{ttLOAN_MANAGE, [](STObject& tx) { tx.setFieldU32(sfFlags, tfLoanImpair); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp);
+
+        // ttLOAN_MANAGE: loss unrealized driven negative
+        doInvariantCheck(
+            {"loss unrealized must not be negative"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, Adjustments{.lossUnrealized = -1});
+            },
+            XRPAmount{},
+            STTx{ttLOAN_MANAGE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp);
+
+        // Loan flags may only change under the transaction types that own
+        // those transitions.
+        {
+            struct Case
+            {
+                std::uint32_t before;
+                std::uint32_t after;
+                std::string expected;
+            };
+            auto const cases = std::to_array({
+                {.before = 0,
+                 .after = lsfLoanImpaired,
+                 .expected = "lsfLoanImpaired changed outside LoanManage or LoanPay"},
+                {.before = lsfLoanImpaired,
+                 .after = 0,
+                 .expected = "lsfLoanImpaired changed outside LoanManage or LoanPay"},
+                {.before = 0,
+                 .after = lsfLoanDefault,
+                 .expected = "lsfLoanDefault changed outside LoanManage"},
+            });
+
+            for (auto const& c : cases)
+            {
+                Env env{*this, all_};
+                Account const a1{"A1"};
+                Account const a2{"A2"};
+                env.fund(XRP(1000), a1, a2);
+                auto const keys = createClosedXrpBroker(a1, env);
+                if (!keys)
+                    continue;
+                auto const& brokerKeylet = keys->second;
+
+                OpenView ov{*env.current()};
+                auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
+                {
+                    auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a1.id());
+                    sleLoan->at(sfPrincipalOutstanding) = Number(100);
+                    sleLoan->at(sfTotalValueOutstanding) = Number(150);
+                    sleLoan->setFieldU32(sfPaymentRemaining, 1);
+                    sleLoan->setFieldU32(sfFlags, c.before);
+                    ov.rawInsert(sleLoan);
+                }
+
+                STTx const tx{ttACCOUNT_SET, [](STObject&) {}};
+                test::StreamSink sink{beast::Severity::Warning};
+                beast::Journal const jlog{sink};
+                ApplyContext ac{
+                    env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+                CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+                auto sleLoan = ac.view().peek(loanKeylet);
+                if (!BEAST_EXPECT(sleLoan))
+                    continue;
+                sleLoan->setFieldU32(sfFlags, c.after);
+                ac.view().update(sleLoan);
+
+                auto transactor = makeTransactor(ac);
+                if (!BEAST_EXPECT(transactor))
+                    continue;
+                TER const result = transactor->checkInvariants(
+                    tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
+                BEAST_EXPECT(result == tecINVARIANT_FAILED);
+                BEAST_EXPECT(sink.messages().str().contains(c.expected));
+            }
+        }
+
+        // ttLOAN_MANAGE (default): a defaulted loan atomically enters a
+        // terminal state, which drops sfNextPaymentDueDate from the ledger
+        // entry. Seed a loan that already carries lsfLoanDefault so the
+        // "must newly set" check passes, then leave sfNextPaymentDueDate
+        // present and non-zero on the after-image; the residual due-date
+        // check must then fire.
+        {
+            Env env{*this, all_};
+            Account const a1{"A1"};
+            Account const a2{"A2"};
+            env.fund(XRP(1000), a1, a2);
+            BEAST_EXPECT(precloseXrp(a1, a2, env));
+            env.close();
+
+            OpenView ov{*env.current()};
+
+            auto const brokerKeylet = keylet::loanBroker(a1.id(), SeqProxy::rawSequence(1));
+            auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
+            // Pre-insert a loan that is not yet defaulted but has a
+            // NextPaymentDueDate set; the apply-view mutation below flips
+            // lsfLoanDefault (so the "must newly set" check passes) while
+            // leaving the due date behind.
+            {
+                auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
+                sleLoan->setFieldU32(sfNextPaymentDueDate, 123);
+                ov.rawInsert(sleLoan);
+            }
+
+            STTx const tx{
+                ttLOAN_MANAGE, [](STObject& t) { t.setFieldU32(sfFlags, tfLoanDefault); }};
+            test::StreamSink sink{beast::Severity::Warning};
+            beast::Journal const jlog{sink};
+            ApplyContext ac{
+                env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+            CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+            auto sleLoan = ac.view().peek(loanKeylet);
+            if (!BEAST_EXPECT(sleLoan))
+                return;
+            sleLoan->setFieldU32(sfFlags, lsfLoanDefault);
+            ac.view().update(sleLoan);
+
+            auto transactor = makeTransactor(ac);
+            if (!BEAST_EXPECT(transactor))
+                return;
+            TER const result = transactor->checkInvariants(
+                tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
+            BEAST_EXPECT(result == tecINVARIANT_FAILED);
+            BEAST_EXPECT(sink.messages().str().contains(
+                "Loan with zero payments must have zero next payment due date"));
+        }
+
+        // ttLOAN_PAY pre-featureLendingProtocolV1_1: finalizeLoanPay short-
+        // circuits and returns success. The same "no vault balance change"
+        // state that trips the check under V1_1 must be silently accepted
+        // here.
+        doInvariantCheck(
+            makeEnv(all_ - featureLendingProtocolV1_1),
+            {},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, Adjustments{});
+            },
+            XRPAmount{},
+            STTx{ttLOAN_PAY, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
+            {tesSUCCESS, tesSUCCESS},
+            precloseXrp);
+
+        // ttLOAN_PAY: cash is credited to the vault and a loan is created
+        // rather than modified. This object-existence rule applies on both
+        // invariant passes.
+        doInvariantCheck(
+            {"Loan created by a transaction other than LoanSet"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(
+                    ac.view(),
+                    keylet,
+                    Adjustments{
+                        .assetsTotal = 50,
+                        .assetsAvailable = 50,
+                        .vaultAssets = 50,
+                        .accountAssets = AccountAmount{.account = a2.id(), .amount = -50},
+                        .createLoan = LoanParams{
+                            .principalOutstanding = 100,
+                            .totalValueOutstanding = 100,
+                            .borrower = a1.id(),
+                        }});
+            },
+            XRPAmount{},
+            STTx{ttLOAN_PAY, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(50)); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp);
+
+        // ttLOAN_PAY: loss unrealized driven negative. The cash inflow is
+        // valid, but loss unrealized is set below zero.
+        doInvariantCheck(
+            {"loss unrealized must not be negative"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(
+                    ac.view(),
+                    keylet,
+                    Adjustments{
+                        .assetsTotal = 100,
+                        .assetsAvailable = 100,
+                        .lossUnrealized = -1,
+                        .vaultAssets = 100,
+                        .accountAssets = AccountAmount{.account = a2.id(), .amount = -100}});
+            },
+            XRPAmount{},
+            STTx{ttLOAN_PAY, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp);
+
+        // ttLOAN_PAY success post-conditions. A loan left with payments still
+        // remaining after a successful payment must show that payment in its
+        // balance and schedule: PrincipalOutstanding and PaymentRemaining both
+        // strictly decrease, and NextPaymentDueDate advances by a positive
+        // multiple of PaymentInterval. Each case seeds the same loan, then applies
+        // an after-image that breaks exactly one of those conditions.
+        {
+            struct Case
+            {
+                Number principal;
+                std::uint32_t remaining;
+                std::uint32_t dueDate;
+                std::string expected;
+            };
+            auto const cases = std::to_array({
+                {.principal = Number(100),
+                 .remaining = 1,
+                 .dueDate = 110,
+                 .expected = "loan pay must strictly decrease PrincipalOutstanding"},
+                {.principal = Number(50),
+                 .remaining = 2,
+                 .dueDate = 110,
+                 .expected = "loan pay must decrease PaymentRemaining"},
+                {.principal = Number(50),
+                 .remaining = 1,
+                 .dueDate = 100,
+                 .expected = "loan pay must advance NextPaymentDueDate"},
+                // Advanced, but not by a whole number of payment intervals.
+                {.principal = Number(50),
+                 .remaining = 1,
+                 .dueDate = 105,
+                 .expected = "loan pay must advance NextPaymentDueDate"},
+            });
+
+            for (auto const& c : cases)
+            {
+                Env env{*this, all_};
+                Account const a1{"A1"};
+                Account const a2{"A2"};
+                env.fund(XRP(1000), a1, a2);
+                auto const keys = createClosedXrpBroker(a1, env);
+                if (!keys)
+                    continue;
+                auto const& brokerKeylet = keys->second;
+
+                OpenView ov{*env.current()};
+                auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
+                {
+                    auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
+                    sleLoan->at(sfPrincipalOutstanding) = Number(100);
+                    sleLoan->at(sfTotalValueOutstanding) = Number(150);
+                    sleLoan->at(sfPaymentInterval) = 10u;
+                    sleLoan->setFieldU32(sfPaymentRemaining, 2);
+                    sleLoan->setFieldU32(sfNextPaymentDueDate, 100);
+                    ov.rawInsert(sleLoan);
+                }
+
+                STTx const tx{
+                    ttLOAN_PAY, [](STObject& t) { t.setFieldAmount(sfAmount, XRPAmount(50)); }};
+                test::StreamSink sink{beast::Severity::Warning};
+                beast::Journal const jlog{sink};
+                ApplyContext ac{
+                    env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+                CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+                auto sleLoan = ac.view().peek(loanKeylet);
+                if (!BEAST_EXPECT(sleLoan))
+                    continue;
+                sleLoan->at(sfPrincipalOutstanding) = c.principal;
+                sleLoan->setFieldU32(sfPaymentRemaining, c.remaining);
+                sleLoan->setFieldU32(sfNextPaymentDueDate, c.dueDate);
+                ac.view().update(sleLoan);
+
+                auto transactor = makeTransactor(ac);
+                if (!BEAST_EXPECT(transactor))
+                    continue;
+                TER const result = transactor->checkInvariants(
+                    tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
+                BEAST_EXPECT(result == tecINVARIANT_FAILED);
+                BEAST_EXPECT(sink.messages().str().contains(c.expected));
+            }
+        }
+
+        // ttLOAN_MANAGE (default): the write-off is rounded downward at the
+        // pre-default AssetsTotal scale. A near-total IOU default can leave
+        // valid positive dust while moving the posterior AssetsTotal to a much
+        // finer scale. The dust must be bounded by the former scale rather than
+        // compared with one unit at the posterior scale.
+        {
+            Env env{*this, all_ | featureLendingProtocolV1_1};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            Account const borrower{"borrower"};
+            env.fund(XRP(1000), issuer, owner, borrower);
+            env.close();
+
+            PrettyAsset const iouAsset{issuer["IOU"]};
+            auto const brokerKeylet = createLoanBroker(owner, env, iouAsset);
+            auto const sleBrokerBase = env.le(brokerKeylet);
+            if (!BEAST_EXPECT(sleBrokerBase))
+                return;
+            auto const vaultKeylet = keylet::vault(sleBrokerBase->at(sfVaultID));
+            env.close();
+
+            Number const assetsTotalBefore{1, 1};
+            Number const loanOwed{9'999'999'999'999'999LL, -15};
+            Number const assetsTotalAfter{1, -14};
+            auto const beforeScale = scale(assetsTotalBefore, iouAsset);
+            auto const afterScale = scale(assetsTotalAfter, iouAsset);
+            Number const residual = (assetsTotalAfter - assetsTotalBefore) - (-loanOwed);
+            Number const beforeTolerance{1, beforeScale};
+            Number const afterTolerance{1, afterScale};
+
+            BEAST_EXPECT(afterScale < beforeScale);
+            BEAST_EXPECT(residual > beast::kZero && residual < beforeTolerance);
+            BEAST_EXPECT(residual > afterTolerance);
+
+            OpenView ov{*env.current()};
+            {
+                auto const sleVaultRead = ov.read(vaultKeylet);
+                if (!BEAST_EXPECT(sleVaultRead))
+                    return;
+                auto sleVault = std::make_shared(*sleVaultRead);
+                sleVault->at(sfAssetsTotal) = assetsTotalBefore;
+                sleVault->at(sfAssetsAvailable) = Number(0);
+                ov.rawReplace(sleVault);
+
+                auto const sharesKeylet = keylet::mptokenIssuance(sleVaultRead->at(sfShareMPTID));
+                auto const sleSharesRead = ov.read(sharesKeylet);
+                if (!BEAST_EXPECT(sleSharesRead))
+                    return;
+                auto sleShares = std::make_shared(*sleSharesRead);
+                sleShares->at(sfOutstandingAmount) = 1;
+                ov.rawReplace(sleShares);
+            }
+            {
+                auto const sleBrokerRead = ov.read(brokerKeylet);
+                if (!BEAST_EXPECT(sleBrokerRead))
+                    return;
+                auto sleBroker = std::make_shared(*sleBrokerRead);
+                sleBroker->at(sfDebtTotal) = loanOwed;
+                ov.rawReplace(sleBroker);
+            }
+            auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
+            {
+                auto sleLoan = makeLoanSle(brokerKeylet.key, 1, borrower.id());
+                sleLoan->at(sfPrincipalOutstanding) = loanOwed;
+                sleLoan->at(sfTotalValueOutstanding) = loanOwed;
+                sleLoan->setFieldU32(sfPaymentRemaining, 1);
+                ov.rawInsert(sleLoan);
+            }
+
+            STTx const tx{
+                ttLOAN_MANAGE, [](STObject& t) { t.setFieldU32(sfFlags, tfLoanDefault); }};
+            test::StreamSink sink{beast::Severity::Warning};
+            beast::Journal const jlog{sink};
+            ApplyContext ac{
+                env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, TapNone, jlog};
+            CurrentTransactionRulesGuard const rulesGuard(ov.rules());
+
+            {
+                auto sleVault = ac.view().peek(vaultKeylet);
+                if (!BEAST_EXPECT(sleVault))
+                    return;
+                sleVault->at(sfAssetsTotal) = assetsTotalAfter;
+                ac.view().update(sleVault);
+            }
+            {
+                auto sleBroker = ac.view().peek(brokerKeylet);
+                if (!BEAST_EXPECT(sleBroker))
+                    return;
+                sleBroker->at(sfDebtTotal) = Number(0);
+                ac.view().update(sleBroker);
+            }
+            {
+                auto sleLoan = ac.view().peek(loanKeylet);
+                if (!BEAST_EXPECT(sleLoan))
+                    return;
+                sleLoan->at(sfPrincipalOutstanding) = Number(0);
+                sleLoan->at(sfTotalValueOutstanding) = Number(0);
+                sleLoan->setFieldU32(sfPaymentRemaining, 0);
+                sleLoan->setFieldU32(sfFlags, lsfLoanDefault);
+                ac.view().update(sleLoan);
+            }
+
+            auto transactor = makeTransactor(ac);
+            if (!BEAST_EXPECT(transactor))
+                return;
+            TER const result = transactor->checkInvariants(
+                tesSUCCESS, XRPAmount{}, Transactor::InvariantScope::Full);
+            BEAST_EXPECT(result == tesSUCCESS);
+        }
+
+        // A loan may only be deleted by a LoanDelete transaction, and only once
+        // it is fully paid off. Both branches are exercised by creating a real
+        // loan in the Preclose (so it exists in the base ledger with outstanding
+        // principal) and then erasing it in the Precheck.
+        {
+            Keylet loanKeylet = keylet::amendments();
+            auto const precloseLoan = [&loanKeylet, this](
+                                          Account const& a1, Account const& a2, Env& env) -> bool {
+                PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+                auto const brokerKeylet = createLoanBroker(a1, env, xrpAsset);
+                auto const brokerSle = env.le(brokerKeylet);
+                if (!BEAST_EXPECT(brokerSle))
+                    return false;
+                auto const vaultKeylet = keylet::vault(brokerSle->at(sfVaultID));
+                Vault const vault{env};
+                env(vault.deposit(
+                    {.depositor = a1, .id = vaultKeylet.key, .amount = xrpAsset(100)}));
+                env.close(std::chrono::seconds{61});
+
+                loanKeylet = keylet::loan(
+                    brokerKeylet.key, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
+                env(loan::set(a2, brokerKeylet.key, xrpAsset(50).value()),
+                    loan::kCounterparty(a1),
+                    Sig(sfCounterpartySignature, a1),
+                    loan::kPaymentInterval(60),
+                    loan::kPaymentTotal(1),
+                    Fee(env.current()->fees().base * 2));
+                env.close();
+                return BEAST_EXPECT(env.le(loanKeylet));
+            };
+
+            auto const eraseLoan = [&loanKeylet](Account const&, Account const&, ApplyContext& ac) {
+                auto sle = ac.view().peek(loanKeylet);
+                if (!sle)
+                    return false;
+                ac.view().erase(sle);
+                return true;
+            };
+
+            // Deleting the loan under any transaction type other than LoanDelete
+            // (here the neutral ttACCOUNT_SET) is a violation, even while the
+            // loan still has outstanding obligations: the transaction-type check
+            // fires before the not-fully-paid-off check.
+            doInvariantCheck(
+                {"Loan deleted by a transaction other than LoanDelete"},
+                eraseLoan,
+                XRPAmount{},
+                STTx{ttACCOUNT_SET, [](STObject&) {}},
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                precloseLoan);
+        }
+
+        STTx const loanSetTx{
+            ttLOAN_SET, [](STObject& tx) { tx.at(sfPrincipalRequested) = Number(0); }};
+
+        // Loan interest due (total value less principal and management fee) must
+        // never be negative. The loan below carries a total value short of its
+        // principal, while every individual field stays non-negative. A real
+        // broker over an XRP vault is created in the preclose, both so the
+        // earlier broker-existence checks pass and so the deficit is measured
+        // in an integral asset domain, where no rounding tolerance applies.
+        {
+            Keylet brokerKeylet = keylet::amendments();
+            auto const precloseBroker = [&brokerKeylet, this](
+                                            Account const& a1, Account const&, Env& env) -> bool {
+                PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+                brokerKeylet = this->createLoanBroker(a1, env, xrpAsset);
+                env.close();
+                return BEAST_EXPECT(env.le(brokerKeylet));
+            };
+
+            doInvariantCheck(
+                {"Loan interest due is negative"},
+                [&](Account const&, Account const& a2, ApplyContext& ac) {
+                    auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
+                    sleLoan->at(sfPrincipalOutstanding) = Number(100);
+                    sleLoan->at(sfTotalValueOutstanding) = Number(90);
+                    sleLoan->setFieldU32(sfPaymentRemaining, 1);
+                    ac.view().insert(sleLoan);
+                    return true;
+                },
+                XRPAmount{},
+                loanSetTx,
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                precloseBroker);
+        }
+
+        // Each of these loan STNumber fields must never be negative. The loan
+        // is created directly with a single field set negative while the
+        // paid-off bookkeeping is kept consistent, so that only the "
+        // is negative" check trips.
+        for (auto const field : {
+                 &sfLoanServiceFee,
+                 &sfLatePaymentFee,
+                 &sfClosePaymentFee,
+                 &sfPrincipalOutstanding,
+                 &sfTotalValueOutstanding,
+                 &sfManagementFeeOutstanding,
+             })
+        {
+            // The outstanding-balance fields also feed the paid-off checks, so
+            // a loan carrying one must still have payments remaining; a loan
+            // with only a negative fee stays fully paid off (zero remaining).
+            bool const isOutstanding = *field == sfPrincipalOutstanding ||
+                *field == sfTotalValueOutstanding || *field == sfManagementFeeOutstanding;
+            doInvariantCheck(
+                {field->getName() + " is negative"},
+                [&, field](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    auto const brokerKeylet = keylet::loanBroker(a1.id(), SeqProxy::rawSequence(1));
+                    auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
+                    sleLoan->at(*field) = Number(-10);
+                    sleLoan->setFieldU32(sfPaymentRemaining, isOutstanding ? 1 : 0);
+                    ac.view().insert(sleLoan);
+                    return true;
+                },
+                XRPAmount{},
+                loanSetTx);
+        }
+
+        // Mirror of the loop above for the strictly-positive constraint: a
+        // loan's sfPeriodicPayment must always be > 0. Cover both boundary
+        // failure modes (zero and negative).
+        for (Number const& badValue : {Number(0), Number(-1)})
+        {
+            doInvariantCheck(
+                {std::string{sfPeriodicPayment.getName()} + " is zero or negative"},
+                [&, badValue](Account const& a1, Account const& a2, ApplyContext& ac) {
+                    auto const brokerKeylet = keylet::loanBroker(a1.id(), SeqProxy::rawSequence(1));
+                    auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
+                    sleLoan->at(sfPeriodicPayment) = badValue;
+                    ac.view().insert(sleLoan);
+                    return true;
+                },
+                XRPAmount{},
+                loanSetTx);
+        }
+
+        // A loan with sfPaymentRemaining == 0 must be fully paid off in every
+        // outstanding-balance dimension. Insert a bare loan that reports zero
+        // payments remaining but still carries a non-zero principal owed; the
+        // paid-off invariant must reject it before the later broker-existence
+        // check has a chance to run.
+        doInvariantCheck(
+            {"Loan with zero payments remaining has not been paid off"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const brokerKeylet = keylet::loanBroker(a1.id(), SeqProxy::rawSequence(1));
+                auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
+                sleLoan->at(sfPrincipalOutstanding) = Number(100);
+                sleLoan->at(sfTotalValueOutstanding) = Number(100);
+                sleLoan->at(sfPeriodicPayment) = Number(1);
+                sleLoan->setFieldU32(sfPaymentRemaining, 0);
+                ac.view().insert(sleLoan);
+                return true;
+            },
+            XRPAmount{},
+            loanSetTx);
+
+        // Converse: a loan whose outstanding balances are all zero has been
+        // fully paid off and must carry zero payments remaining. Insert a
+        // fully-zeroed loan with sfPaymentRemaining = 1 to trip the check.
+        doInvariantCheck(
+            {"Fully paid off Loan still has payments remaining"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const brokerKeylet = keylet::loanBroker(a1.id(), SeqProxy::rawSequence(1));
+                auto sleLoan = makeLoanSle(brokerKeylet.key, 1, a2.id());
+                sleLoan->setFieldU32(sfPaymentRemaining, 1);
+                ac.view().insert(sleLoan);
+                return true;
+            },
+            XRPAmount{},
+            loanSetTx);
+
+        // A loan must reference a live loan broker. A bare loan SLE is
+        // inserted with every other loan-level field kept consistent so the
+        // earlier ValidLoan checks pass; sfLoanBrokerID defaults to zero,
+        // which resolves to no broker, and the broker-existence check trips.
+        doInvariantCheck(
+            {"Loan broker does not exist"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleLoan = makeLoanSle(uint256{}, 1, a2.id());
+                ac.view().insert(sleLoan);
+                return true;
+            },
+            XRPAmount{},
+            loanSetTx);
+
+        // A loan's broker must in turn reference a live vault. A real broker
+        // is created in the preclose so its sfVaultID points at an existing
+        // vault; the precheck then erases that vault and inserts a loan
+        // referencing the broker, so the broker-existence check passes and
+        // the broker-vault-existence check trips.
+        {
+            Keylet brokerKeylet = keylet::amendments();
+            auto const precloseBroker = [&brokerKeylet, this](
+                                            Account const& a1, Account const&, Env& env) -> bool {
+                PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+                brokerKeylet = this->createLoanBroker(a1, env, xrpAsset);
+                env.close();
+                return BEAST_EXPECT(env.le(brokerKeylet));
+            };
+
+            doInvariantCheck(
+                {"Loan broker vault does not exist"},
+                [&brokerKeylet](Account const&, Account const&, ApplyContext& ac) {
+                    auto sleBroker = ac.view().peek(brokerKeylet);
+                    if (!sleBroker)
+                        return false;
+                    auto sleVault = ac.view().peek(keylet::vault(sleBroker->at(sfVaultID)));
+                    if (!sleVault)
+                        return false;
+                    ac.view().erase(sleVault);
+
+                    auto const loanKeylet =
+                        keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
+                    auto sleLoan = std::make_shared(loanKeylet);
+                    sleLoan->at(sfLoanBrokerID) = brokerKeylet.key;
+                    sleLoan->at(sfPrincipalOutstanding) = Number(0);
+                    sleLoan->at(sfTotalValueOutstanding) = Number(0);
+                    sleLoan->at(sfManagementFeeOutstanding) = Number(0);
+                    sleLoan->at(sfPeriodicPayment) = Number(1);
+                    sleLoan->setFieldU32(sfPaymentRemaining, 0);
+                    ac.view().insert(sleLoan);
+                    return true;
+                },
+                XRPAmount{},
+                loanSetTx,
+                {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+                precloseBroker);
+        }
+
+        // ttVAULT_SET: owner is immutable (enforced by
+        // NoModifiedUnmodifiableFields under featureLendingProtocolV1_1.
+        doInvariantCheck(
+            {"changed an unchangeable field"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                sleVault->setAccountID(sfOwner, a2.id());
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp);
+
+        // ttVAULT_SET: withdrawal policy is immutable
+        doInvariantCheck(
+            {"changed an unchangeable field"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                sleVault->setFieldU8(
+                    sfWithdrawalPolicy,
+                    static_cast(sleVault->getFieldU8(sfWithdrawalPolicy) + 1));
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp);
+
+        // ttVAULT_SET: scale is immutable
+        doInvariantCheck(
+            {"changed an unchangeable field"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                sleVault->setFieldU8(
+                    sfScale, static_cast(sleVault->getFieldU8(sfScale) + 1));
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp);
+
+        // featureLendingProtocolV1_1 moves the vault immutability checks from VaultInvariant to
+        // InvariantCheck.
+        doInvariantCheck(
+            makeEnv(all_),
+            {"changed an unchangeable field"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                sleVault->setFieldU8(
+                    sfWithdrawalPolicy,
+                    static_cast(sleVault->getFieldU8(sfWithdrawalPolicy) + 1));
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject& tx) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp);
+
+        testcase << "Vault create";
+        doInvariantCheck(
+            {
+                "created vault must be empty",
+                "updated zero sized vault must have no assets outstanding",
+                "create operation must not have updated a vault",
+            },
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                (*sleVault)[sfAssetsTotal] = 9;
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {
+                "created vault must be empty",
+                "updated zero sized vault must have no assets available",
+                "assets available must not be greater than assets outstanding",
+                "create operation must not have updated a vault",
+            },
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                (*sleVault)[sfAssetsAvailable] = 9;
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {
+                "created vault must be empty",
+                "loss unrealized must not exceed the difference between assets "
+                "outstanding and available",
+                "vault transaction must not change loss unrealized",
+                "create operation must not have updated a vault",
+            },
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                (*sleVault)[sfLossUnrealized] = 1;
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {
+                "created vault must be empty",
+                "create operation must not have updated a vault",
+                "invalid OutstandingAmount balance 0 9 0",
+            },
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
+                if (!sleShares)
+                    return false;
+                ac.view().update(sleVault);
+                (*sleShares)[sfOutstandingAmount] = 9;
+                ac.view().update(sleShares);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {
+                "assets maximum must not be negative",
+                "create operation must not have updated a vault",
+            },
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                (*sleVault)[sfAssetsMaximum] = Number(-1);
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"create operation must not have updated a vault",
+             "shares issuer and vault pseudo-account must be the same",
+             "shares issuer must be a pseudo-account",
+             "shares issuer pseudo-account must point back to the vault"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                auto sleVault = ac.view().peek(keylet);
+                if (!sleVault)
+                    return false;
+                auto sleShares = ac.view().peek(keylet::mptokenIssuance((*sleVault)[sfShareMPTID]));
+                if (!sleShares)
+                    return false;
+                ac.view().update(sleVault);
+                (*sleShares)[sfIssuer] = a1.id();
+                ac.view().update(sleShares);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const& a2, Env& env) {
+                Vault const vault{env};
+                auto [tx, keylet] = vault.create({.owner = a1, .asset = xrpIssue()});
+                env(tx);
+                return true;
+            });
+
+        doInvariantCheck(
+            {"vault created by a wrong transaction type", "account root created illegally"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                // The code below will create a valid vault with (almost) all
+                // the invariants holding. Except one: it is created by the
+                // wrong transaction type.
+                auto const sequence = ac.view().seq();
+                auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
+                auto sleVault = std::make_shared(vaultKeylet);
+                auto const vaultPage = ac.view().dirInsert(
+                    keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
+                sleVault->setFieldU64(sfOwnerNode, *vaultPage);
+
+                auto pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
+                // Create pseudo-account.
+                auto sleAccount = std::make_shared(keylet::account(pseudoId));
+                sleAccount->setAccountID(sfAccount, pseudoId);
+                sleAccount->setFieldAmount(sfBalance, STAmount{});
+                std::uint32_t const seqno =                             //
+                    ac.view().rules().enabled(featureSingleAssetVault)  //
+                    ? 0                                                 //
+                    : sequence;
+                sleAccount->setFieldU32(sfSequence, seqno);
+                sleAccount->setFieldU32(
+                    sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
+                sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
+                ac.view().insert(sleAccount);
+
+                auto const sharesMptId = makeMptID(sequence, pseudoId);
+                auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
+                auto sleShares = std::make_shared(sharesKeylet);
+                auto const sharesPage = ac.view().dirInsert(
+                    keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
+                sleShares->setFieldU64(sfOwnerNode, *sharesPage);
+
+                sleShares->at(sfFlags) = 0;
+                sleShares->at(sfIssuer) = pseudoId;
+                sleShares->at(sfOutstandingAmount) = 0;
+                sleShares->at(sfSequence) = sequence;
+
+                sleVault->at(sfAccount) = pseudoId;
+                sleVault->at(sfFlags) = 0;
+                sleVault->at(sfSequence) = sequence;
+                sleVault->at(sfOwner) = a1.id();
+                sleVault->at(sfAssetsTotal) = Number(0);
+                sleVault->at(sfAssetsAvailable) = Number(0);
+                sleVault->at(sfLossUnrealized) = Number(0);
+                sleVault->at(sfShareMPTID) = sharesMptId;
+                sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
+
+                ac.view().insert(sleVault);
+                ac.view().insert(sleShares);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_SET, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+
+        doInvariantCheck(
+            {"shares issuer and vault pseudo-account must be the same",
+             "shares issuer pseudo-account must point back to the vault"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sequence = ac.view().seq();
+                auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
+                auto sleVault = std::make_shared(vaultKeylet);
+                auto const vaultPage = ac.view().dirInsert(
+                    keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
+                sleVault->setFieldU64(sfOwnerNode, *vaultPage);
+
+                auto pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
+                // Create pseudo-account.
+                auto sleAccount = std::make_shared(keylet::account(pseudoId));
+                sleAccount->setAccountID(sfAccount, pseudoId);
+                sleAccount->setFieldAmount(sfBalance, STAmount{});
+                std::uint32_t const seqno =                             //
+                    ac.view().rules().enabled(featureSingleAssetVault)  //
+                    ? 0                                                 //
+                    : sequence;
+                sleAccount->setFieldU32(sfSequence, seqno);
+                sleAccount->setFieldU32(
+                    sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
+                // sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
+                // Setting wrong vault key
+                sleAccount->setFieldH256(sfVaultID, uint256(42));
+                ac.view().insert(sleAccount);
+
+                auto const sharesMptId = makeMptID(sequence, pseudoId);
+                auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
+                auto sleShares = std::make_shared(sharesKeylet);
+                auto const sharesPage = ac.view().dirInsert(
+                    keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
+                sleShares->setFieldU64(sfOwnerNode, *sharesPage);
+
+                sleShares->at(sfFlags) = 0;
+                sleShares->at(sfIssuer) = pseudoId;
+                sleShares->at(sfOutstandingAmount) = 0;
+                sleShares->at(sfSequence) = sequence;
+
+                // sleVault->at(sfAccount) = pseudoId;
+                // Setting wrong pseudo account ID
+                sleVault->at(sfAccount) = a2.id();
+                sleVault->at(sfFlags) = 0;
+                sleVault->at(sfSequence) = sequence;
+                sleVault->at(sfOwner) = a1.id();
+                sleVault->at(sfAssetsTotal) = Number(0);
+                sleVault->at(sfAssetsAvailable) = Number(0);
+                sleVault->at(sfLossUnrealized) = Number(0);
+                sleVault->at(sfShareMPTID) = sharesMptId;
+                sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
+
+                ac.view().insert(sleVault);
+                ac.view().insert(sleShares);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+
+        doInvariantCheck(
+            {"shares issuer and vault pseudo-account must be the same", "shares issuer must exist"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const sequence = ac.view().seq();
+                auto const vaultKeylet = keylet::vault(a1.id(), SeqProxy::rawSequence(sequence));
+                auto sleVault = std::make_shared(vaultKeylet);
+                auto const vaultPage = ac.view().dirInsert(
+                    keylet::ownerDir(a1.id()), sleVault->key(), describeOwnerDir(a1.id()));
+                sleVault->setFieldU64(sfOwnerNode, *vaultPage);
+
+                auto const sharesMptId = makeMptID(sequence, a2.id());
+                auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
+                auto sleShares = std::make_shared(sharesKeylet);
+                auto const sharesPage = ac.view().dirInsert(
+                    keylet::ownerDir(a2.id()), sharesKeylet, describeOwnerDir(a2.id()));
+                sleShares->setFieldU64(sfOwnerNode, *sharesPage);
+
+                sleShares->at(sfFlags) = 0;
+                // Setting wrong pseudo account ID
+                sleShares->at(sfIssuer) = AccountID(42);
+                sleShares->at(sfOutstandingAmount) = 0;
+                sleShares->at(sfSequence) = sequence;
+
+                sleVault->at(sfAccount) = a2.id();
+                sleVault->at(sfFlags) = 0;
+                sleVault->at(sfSequence) = sequence;
+                sleVault->at(sfOwner) = a1.id();
+                sleVault->at(sfAssetsTotal) = Number(0);
+                sleVault->at(sfAssetsAvailable) = Number(0);
+                sleVault->at(sfLossUnrealized) = Number(0);
+                sleVault->at(sfShareMPTID) = sharesMptId;
+                sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
+
+                ac.view().insert(sleVault);
+                ac.view().insert(sleShares);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+
+        testcase << "Vault deposit";
+        doInvariantCheck(
+            {"deposit must change vault balance"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [](Adjustments& sample) {
+                                   sample.vaultAssets.reset();
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DEPOSIT, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp);
+
+        doInvariantCheck(
+            {"deposit assets outstanding must not exceed assets maximum"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 200, [&](Adjustments& sample) {
+                                   sample.assetsMaximum = 1;
+                               }));
+            },
+            XRPAmount{},
+            STTx{
+                ttVAULT_DEPOSIT, [](STObject& tx) { tx.setFieldAmount(sfAmount, XRPAmount(200)); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        // This really convoluted unit tests makes the zero balance on the
+        // depositor, by sending them the same amount as the transaction fee.
+        // The operation makes no sense, but the defensive check in
+        // ValidVault::finalize is otherwise impossible to trigger.
+        doInvariantCheck(
+            {"deposit must increase vault balance", "deposit must change depositor balance"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+
+                // Move 10 drops to A4 to enforce total XRP balance
+                auto sleA4 = ac.view().peek(keylet::account(a4.id()));
+                if (!sleA4)
+                    return false;
+                (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
+                ac.view().update(sleA4);
+
+                return kAdjust(ac.view(), keylet, kArgs(a3.id(), -10, [&](Adjustments& sample) {
+                                   sample.accountAssets->amount = -100;
+                               }));
+            },
+            XRPAmount{100},
+            STTx{
+                ttVAULT_DEPOSIT,
+                [&](STObject& tx) {
+                    tx[sfFee] = XRPAmount(100);
+                    tx[sfAccount] = a3.id();
+                }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp);
+
+        doInvariantCheck(
+            {"deposit must increase vault balance",
+             "deposit must decrease depositor balance",
+             "deposit must change vault and depositor balance by equal amount",
+             "deposit and assets outstanding must add up",
+             "deposit and assets available must add up"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+
+                // Move 10 drops from A2 to A3 to enforce total XRP balance
+                auto sleA3 = ac.view().peek(keylet::account(a3.id()));
+                if (!sleA3)
+                    return false;
+                (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10;
+                ac.view().update(sleA3);
+
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
+                                   sample.vaultAssets = -20;
+                                   sample.accountAssets->amount = 10;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"deposit must change depositor balance"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+
+                // Move 10 drops from A3 to vault to enforce total XRP balance
+                auto sleA3 = ac.view().peek(keylet::account(a3.id()));
+                if (!sleA3)
+                    return false;
+                (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 10;
+                ac.view().update(sleA3);
+
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
+                                   sample.accountAssets->amount = 0;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"deposit must change depositor shares"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
+                                   sample.accountShares.reset();
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"deposit must change vault shares"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [](Adjustments& sample) {
+                                   sample.sharesTotal = 0;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"deposit must increase depositor shares",
+             "deposit must change depositor and vault shares by equal amount",
+             "deposit must not change vault balance by more than deposited "
+             "amount"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
+                                   sample.accountShares->amount = -5;
+                                   sample.sharesTotal = -10;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(5); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"deposit and assets outstanding must add up"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleA3 = ac.view().peek(keylet::account(a3.id()));
+                (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
+                ac.view().update(sleA3);
+
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
+                                   sample.assetsTotal = 11;
+                               }));
+            },
+            XRPAmount{2000},
+            STTx{
+                ttVAULT_DEPOSIT,
+                [&](STObject& tx) {
+                    tx[sfAmount] = XRPAmount(10);
+                    tx[sfDelegate] = a3.id();
+                    tx[sfFee] = XRPAmount(2000);
+                }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"deposit and assets outstanding must add up",
+             "deposit and assets available must add up"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 10, [&](Adjustments& sample) {
+                                   sample.assetsTotal = 7;
+                                   sample.assetsAvailable = 7;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        testcase << "Vault withdrawal";
+        doInvariantCheck(
+            {"withdrawal must change vault balance"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [](Adjustments& sample) {
+                                   sample.vaultAssets.reset();
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp);
+
+        // Almost identical to the really convoluted test for deposit, where the
+        // depositor spends only the transaction fee. In case of withdrawal,
+        // this test is almost the same as normal withdrawal where the
+        // sfDestination would have been A4, but has been omitted.
+        doInvariantCheck(
+            {"withdrawal must change one destination balance"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+
+                // Move 10 drops to A4 to enforce total XRP balance
+                auto sleA4 = ac.view().peek(keylet::account(a4.id()));
+                if (!sleA4)
+                    return false;
+                (*sleA4)[sfBalance] = *(*sleA4)[sfBalance] + 10;
+                ac.view().update(sleA4);
+
+                return kAdjust(ac.view(), keylet, kArgs(a3.id(), -10, [&](Adjustments& sample) {
+                                   sample.accountAssets->amount = -100;
+                               }));
+            },
+            XRPAmount{100},
+            STTx{
+                ttVAULT_WITHDRAW,
+                [&](STObject& tx) {
+                    tx[sfFee] = XRPAmount(100);
+                    tx[sfAccount] = a3.id();
+                    // This commented out line causes the invariant violation.
+                    // tx[sfDestination] = A4.id();
+                }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp);
+
+        doInvariantCheck(
+            {
+                "withdrawal must change vault and destination balance by equal amount",
+                "withdrawal must decrease vault balance",
+                "withdrawal must increase destination balance",
+                "withdrawal and assets outstanding must add up",
+                "withdrawal and assets available must add up",
+            },
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+
+                // Move 10 drops from A2 to A3 to enforce total XRP balance
+                auto sleA3 = ac.view().peek(keylet::account(a3.id()));
+                if (!sleA3)
+                    return false;
+                (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 10;
+                ac.view().update(sleA3);
+
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
+                                   sample.vaultAssets = 10;
+                                   sample.accountAssets->amount = -20;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"withdrawal must change one destination balance"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                if (!kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
+                                 *sample.vaultAssets -= 5;
+                             })))
+                    return false;
+                auto sleA3 = ac.view().peek(keylet::account(a3.id()));
+                if (!sleA3)
+                    return false;
+                (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] + 5;
+                ac.view().update(sleA3);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttVAULT_WITHDRAW, [&](STObject& tx) { tx.setAccountID(sfDestination, a3.id()); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"withdrawal must change depositor shares"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
+                                   sample.accountShares.reset();
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"withdrawal must change vault shares"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [](Adjustments& sample) {
+                                   sample.sharesTotal = 0;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"withdrawal must decrease depositor shares",
+             "withdrawal must change depositor and vault shares by equal "
+             "amount"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
+                                   sample.accountShares->amount = 5;
+                                   sample.sharesTotal = 10;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"withdrawal and assets outstanding must add up",
+             "withdrawal and assets available must add up"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
+                                   sample.assetsTotal = -15;
+                                   sample.assetsAvailable = -15;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        doInvariantCheck(
+            {"withdrawal and assets outstanding must add up"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto sleA3 = ac.view().peek(keylet::account(a3.id()));
+                (*sleA3)[sfBalance] = *(*sleA3)[sfBalance] - 2000;
+                ac.view().update(sleA3);
+
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
+                                   sample.assetsTotal = -7;
+                               }));
+            },
+            XRPAmount{2000},
+            STTx{
+                ttVAULT_WITHDRAW,
+                [&](STObject& tx) {
+                    tx[sfAmount] = XRPAmount(10);
+                    tx[sfDelegate] = a3.id();
+                    tx[sfFee] = XRPAmount(2000);
+                }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseXrp,
+            TxAccount::A2);
+
+        auto const precloseMpt = [&](Account const& a1, Account const& a2, Env& env) -> bool {
+            env.fund(XRP(1000), a3, a4);
+
+            // Create MPT asset
+            {
+                json::Value jv;
+                jv[sfAccount] = a3.human();
+                jv[sfTransactionType] = jss::MPTokenIssuanceCreate;
+                jv[sfFlags] = tfMPTCanTransfer;
+                env(jv);
+                env.close();
+            }
+
+            auto const mptID = makeMptID(env.seq(a3) - 1, a3);
+            Asset const asset = MPTIssue(mptID);
+            // Authorize A1 A2 A4
+            {
+                json::Value jv;
+                jv[sfAccount] = a1.human();
+                jv[sfTransactionType] = jss::MPTokenAuthorize;
+                jv[sfMPTokenIssuanceID] = to_string(mptID);
+                env(jv);
+                jv[sfAccount] = a2.human();
+                env(jv);
+                jv[sfAccount] = a4.human();
+                env(jv);
+
+                env.close();
+            }
+            // Send tokens to A1 A2 A4
+            {
+                env(pay(a3, a1, asset(1000)));
+                env(pay(a3, a2, asset(1000)));
+                env(pay(a3, a4, asset(1000)));
+                env.close();
+            }
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = a1, .asset = asset});
+            env(tx);
+            env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = asset(10)}));
+            env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = asset(10)}));
+            env(vault.deposit({.depositor = a4, .id = keylet.key, .amount = asset(10)}));
+            return true;
+        };
+
+        doInvariantCheck(
+            {"withdrawal must decrease depositor shares",
+             "withdrawal must change depositor and vault shares by equal "
+             "amount"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet =
+                    keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -10, [&](Adjustments& sample) {
+                                   sample.accountShares->amount = 5;
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_WITHDRAW, [&](STObject& tx) { tx[sfAccount] = a3.id(); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseMpt,
+            TxAccount::A2);
+
+        testcase << "Vault clawback";
+        doInvariantCheck(
+            {"clawback must change vault balance"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet =
+                    keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), -1, [&](Adjustments& sample) {
+                                   sample.vaultAssets.reset();
+                               }));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CLAWBACK, [&](STObject& tx) { tx[sfAccount] = a3.id(); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseMpt);
+
+        // Not the same as below check: attempt to clawback XRP
+        doInvariantCheck(
+            {"clawback may only be performed by the asset issuer"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet = keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq()));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {}));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CLAWBACK, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseXrp);
+
+        // Not the same as above check: attempt to clawback MPT by bad account
+        doInvariantCheck(
+            {"clawback may only be performed by the asset issuer"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet =
+                    keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
+                return kAdjust(ac.view(), keylet, kArgs(a2.id(), 0, [&](Adjustments& sample) {}));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CLAWBACK, [&](STObject& tx) { tx[sfAccount] = a4.id(); }},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseMpt);
+
+        doInvariantCheck(
+            {"clawback must decrease vault balance",
+             "clawback must decrease holder shares",
+             "clawback must change vault shares"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet =
+                    keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
+                return kAdjust(ac.view(), keylet, kArgs(a4.id(), 10, [&](Adjustments& sample) {
+                                   sample.sharesTotal = 0;
+                               }));
+            },
+            XRPAmount{},
+            STTx{
+                ttVAULT_CLAWBACK,
+                [&](STObject& tx) {
+                    tx[sfAccount] = a3.id();
+                    tx[sfHolder] = a4.id();
+                }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseMpt);
+
+        doInvariantCheck(
+            {"clawback must change holder shares"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet =
+                    keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
+                return kAdjust(ac.view(), keylet, kArgs(a4.id(), -10, [&](Adjustments& sample) {
+                                   sample.accountShares.reset();
+                               }));
+            },
+            XRPAmount{},
+            STTx{
+                ttVAULT_CLAWBACK,
+                [&](STObject& tx) {
+                    tx[sfAccount] = a3.id();
+                    tx[sfHolder] = a4.id();
+                }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseMpt);
+
+        doInvariantCheck(
+            {"clawback must change holder and vault shares by equal amount",
+             "clawback and assets outstanding must add up",
+             "clawback and assets available must add up"},
+            [&](Account const& a1, Account const& a2, ApplyContext& ac) {
+                auto const keylet =
+                    keylet::vault(a1.id(), SeqProxy::rawSequence(ac.view().seq() - 2));
+                return kAdjust(ac.view(), keylet, kArgs(a4.id(), -10, [&](Adjustments& sample) {
+                                   sample.accountShares->amount = -8;
+                                   sample.assetsTotal = -7;
+                                   sample.assetsAvailable = -7;
+                               }));
+            },
+            XRPAmount{},
+            STTx{
+                ttVAULT_CLAWBACK,
+                [&](STObject& tx) {
+                    tx[sfAccount] = a3.id();
+                    tx[sfHolder] = a4.id();
+                }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseMpt);
+
+        // ─────────────────────────────────────────────────────────────
+        // Closed-ended vault invariants added in ValidVault::finalize (create must supply both
+        // dates and satisfy the redemption-buffer gap), deposit only in Subscription / NoPhase,
+        // withdraw not in Investment, loan origination only in Investment.
+
+        using d = NetClock::duration;
+        using tp = NetClock::time_point;
+
+        auto const closedEnded = std::to_underlying(VaultKind::ClosedEnded);
+
+        // Vault keylet captured by precloseClosedEnded so precheck does not have to rederive it
+        // from ac.view().seq(), which depends on how many env.close() calls preclose issued.
+        Keylet closedEndedKeylet = keylet::amendments();
+
+        // Preclose that creates a closed-ended vault (in Subscription), optionally seeds it with
+        // three deposits (so a1/a2/a3 hold a share MPToken that kAdjust can then adjust), and
+        // optionally advances parent close time past SubscriptionDate. A negative @p advanceBySub
+        // leaves the vault in Subscription.
+        auto const precloseClosedEnded = [&](std::int32_t advanceBySub, bool doDeposit) {
+            return [&, advanceBySub, doDeposit](
+                       Account const& a1, Account const& a2, Env& env) -> bool {
+                env.fund(XRP(1000), a3, a4);
+                auto const sub = env.now().time_since_epoch().count() + 60;
+                auto const red = sub + kMinInvestmentPeriod + 1'000'000;
+                Vault const vault{env};
+                auto [tx, keylet] = vault.create(
+                    {.owner = a1,
+                     .asset = xrpIssue(),
+                     .vaultKind = closedEnded,
+                     .subscriptionDate = sub,
+                     .redemptionDate = red});
+                env(tx);
+                closedEndedKeylet = keylet;
+                if (doDeposit)
+                {
+                    env(vault.deposit({.depositor = a1, .id = keylet.key, .amount = XRP(10)}));
+                    env(vault.deposit({.depositor = a2, .id = keylet.key, .amount = XRP(10)}));
+                    env(vault.deposit({.depositor = a3, .id = keylet.key, .amount = XRP(10)}));
+                }
+                if (advanceBySub >= 0)
+                    env.close(tp{d{sub + advanceBySub}});
+                return true;
+            };
+        };
+
+        // Manually insert a bare closed-ended vault (+ pseudo-account + share MPTokenIssuance)
+        // directly into the view, bypassing the transactor path. Used to synthesize ttVAULT_CREATE
+        // states no legitimate transactor would produce.
+        auto const insertBareClosedEndedVault =
+            [closedEnded](
+                ApplyContext& ac,
+                Account const& owner,
+                std::optional subscriptionDate,
+                std::optional redemptionDate) -> bool {
+            auto const sequence = ac.view().seq();
+            auto const vaultKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(sequence));
+            auto sleVault = std::make_shared(vaultKeylet);
+            auto const vaultPage = ac.view().dirInsert(
+                keylet::ownerDir(owner.id()), sleVault->key(), describeOwnerDir(owner.id()));
+            if (!vaultPage)
+                return false;
+            sleVault->setFieldU64(sfOwnerNode, *vaultPage);
+
+            auto const pseudoId = pseudoAccountAddress(ac.view(), vaultKeylet.key);
+            auto sleAccount = std::make_shared(keylet::account(pseudoId));
+            sleAccount->setAccountID(sfAccount, pseudoId);
+            sleAccount->setFieldAmount(sfBalance, STAmount{});
+            sleAccount->setFieldU32(sfSequence, 0);
+            sleAccount->setFieldU32(sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
+            sleAccount->setFieldH256(sfVaultID, vaultKeylet.key);
+            ac.view().insert(sleAccount);
+
+            auto const sharesMptId = makeMptID(sequence, pseudoId);
+            auto const sharesKeylet = keylet::mptokenIssuance(sharesMptId);
+            auto sleShares = std::make_shared(sharesKeylet);
+            auto const sharesPage = ac.view().dirInsert(
+                keylet::ownerDir(pseudoId), sharesKeylet, describeOwnerDir(pseudoId));
+            if (!sharesPage)
+                return false;
+            sleShares->setFieldU64(sfOwnerNode, *sharesPage);
+            sleShares->at(sfFlags) = 0;
+            sleShares->at(sfIssuer) = pseudoId;
+            sleShares->at(sfOutstandingAmount) = 0;
+            sleShares->at(sfSequence) = sequence;
+
+            sleVault->at(sfAccount) = pseudoId;
+            sleVault->at(sfFlags) = 0;
+            sleVault->at(sfSequence) = sequence;
+            sleVault->at(sfOwner) = owner.id();
+            sleVault->setFieldIssue(sfAsset, STIssue{sfAsset, Asset{xrpIssue()}});
+            sleVault->at(sfAssetsTotal) = Number(0);
+            sleVault->at(sfAssetsAvailable) = Number(0);
+            sleVault->at(sfLossUnrealized) = Number(0);
+            sleVault->at(sfShareMPTID) = sharesMptId;
+            sleVault->at(sfWithdrawalPolicy) = kVaultStrategyFirstComeFirstServe;
+            sleVault->at(sfVaultKind) = closedEnded;
+            if (subscriptionDate)
+                sleVault->at(sfSubscriptionDate) = *subscriptionDate;
+            if (redemptionDate)
+                sleVault->at(sfRedemptionDate) = *redemptionDate;
+
+            ac.view().insert(sleVault);
+            ac.view().insert(sleShares);
+            return true;
+        };
+
+        testcase << "Vault create closed-ended";
+
+        // A fresh closed-ended vault must carry both SubscriptionDate and RedemptionDate.
+        doInvariantCheck(
+            {"closed-ended vault must have SubscriptionDate and RedemptionDate"},
+            [&](Account const& a1, Account const&, ApplyContext& ac) {
+                return insertBareClosedEndedVault(ac, a1, std::nullopt, std::nullopt);
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+
+        // Gap smaller than MIN_INVESTMENT_PERIOD but with RedemptionDate > SubscriptionDate;
+        // exercises the sub-minimum branch of the gap check.
+        doInvariantCheck(
+            {"closed-ended vault RedemptionDate - SubscriptionDate must be "
+             "within [MIN_INVESTMENT_PERIOD, MAX_INVESTMENT_PERIOD)"},
+            [&](Account const& a1, Account const&, ApplyContext& ac) {
+                std::uint32_t const sub = 1'000'000'000;
+                std::uint32_t const red = sub + kMinInvestmentPeriod - 1;
+                return insertBareClosedEndedVault(ac, a1, sub, red);
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+
+        // RedemptionDate strictly before SubscriptionDate; the signed int64 gap is negative and
+        // is caught by the sub-minimum branch of the gap check.
+        doInvariantCheck(
+            {"closed-ended vault RedemptionDate - SubscriptionDate must be "
+             "within [MIN_INVESTMENT_PERIOD, MAX_INVESTMENT_PERIOD)"},
+            [&](Account const& a1, Account const&, ApplyContext& ac) {
+                std::uint32_t const sub = 1'000'000'000;
+                std::uint32_t const red = sub - 1;
+                return insertBareClosedEndedVault(ac, a1, sub, red);
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+
+        // Gap exactly MAX_INVESTMENT_PERIOD is out of range (bound is half-open on the right).
+        doInvariantCheck(
+            {"closed-ended vault RedemptionDate - SubscriptionDate must be "
+             "within [MIN_INVESTMENT_PERIOD, MAX_INVESTMENT_PERIOD)"},
+            [&](Account const& a1, Account const&, ApplyContext& ac) {
+                std::uint32_t const sub = 1'000'000'000;
+                std::uint32_t const red = sub + kMaxInvestmentPeriod;
+                return insertBareClosedEndedVault(ac, a1, sub, red);
+            },
+            XRPAmount{},
+            STTx{ttVAULT_CREATE, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED});
+
+        testcase << "Vault deposit closed-ended";
+
+        // A deposit into a closed-ended vault that has advanced past SubscriptionDate. kArgs
+        // simulates an otherwise valid deposit shape so only the phase invariant fires.
+        doInvariantCheck(
+            {"deposit only allowed in Subscription or NoPhase"},
+            [&](Account const&, Account const& a2, ApplyContext& ac) {
+                return kAdjust(
+                    ac.view(), closedEndedKeylet, kArgs(a2.id(), 10, [](Adjustments&) {}));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_DEPOSIT, [](STObject& tx) { tx[sfAmount] = XRPAmount(10); }},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseClosedEnded(/*advanceBySub=*/1, /*doDeposit=*/true),
+            TxAccount::A2);
+
+        testcase << "Vault withdrawal closed-ended";
+
+        // A withdrawal from a closed-ended vault in the Investment phase.
+        doInvariantCheck(
+            {"withdrawal not allowed during Investment phase"},
+            [&](Account const&, Account const& a2, ApplyContext& ac) {
+                return kAdjust(
+                    ac.view(), closedEndedKeylet, kArgs(a2.id(), -10, [](Adjustments&) {}));
+            },
+            XRPAmount{},
+            STTx{ttVAULT_WITHDRAW, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tefINVARIANT_FAILED},
+            precloseClosedEnded(/*advanceBySub=*/1, /*doDeposit=*/true),
+            TxAccount::A2);
+
+        testcase << "Vault loan set";
+
+        // ttLOAN_SET against a closed-ended vault that is not in Investment. finalizeLoanSet fires
+        // on any vault mutation; touching the vault SLE with no field change is sufficient.
+        doInvariantCheck(
+            {"loan origination only allowed in Investment phase"},
+            [&](Account const&, Account const&, ApplyContext& ac) {
+                auto sleVault = ac.view().peek(closedEndedKeylet);
+                if (!sleVault)
+                    return false;
+                ac.view().update(sleVault);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttLOAN_SET, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            precloseClosedEnded(/*advanceBySub=*/-1, /*doDeposit=*/false));
+
+        testcase << "Vault loan set - closed-ended final payment past "
+                    "RedemptionDate";
+
+        // A newly-created loan against a closed-ended vault must satisfy StartDate +
+        // PaymentInterval * PaymentRemaining + kLoanRedemptionBuffer <= RedemptionDate.
+        // LoanSet::preclaim enforces the same bound; this test synthesises a loan whose
+        // final payment is still before RedemptionDate (so the old unbuffered check would
+        // pass) but inside the buffer zone.
+        Keylet closedEndedBrokerKeylet = keylet::amendments();
+        std::uint32_t closedEndedRed = 0;
+        doInvariantCheck(
+            {"closed-ended loan final payment must precede RedemptionDate by at least "
+             "kLoanRedemptionBuffer"},
+            [&](Account const& a1, Account const&, ApplyContext& ac) {
+                // Touch the vault so ValidVault::finalizeLoanSet sees an
+                // entry in afterVault_; the vault is in Investment, so
+                // finalizeLoanSet itself passes.
+                auto sleVault = ac.view().peek(closedEndedKeylet);
+                if (!sleVault)
+                    return false;
+                ac.view().update(sleVault);
+
+                // Read the broker's next loan sequence to build the loan
+                // keylet the same way LoanSet::doApply would.
+                auto sleBroker = ac.view().peek(closedEndedBrokerKeylet);
+                if (!sleBroker)
+                    return false;
+                std::uint32_t const loanSeq = sleBroker->at(sfLoanSequence);
+
+                // Final payment at RedemptionDate - (kLoanRedemptionBuffer - 1): still
+                // strictly before RedemptionDate, but inside the buffer.
+                auto sleLoan = makeLoanSle(closedEndedBrokerKeylet.key, loanSeq, a1.id());
+                sleLoan->at(sfLoanBrokerID) = closedEndedBrokerKeylet.key;
+                sleLoan->at(sfLoanSequence) = loanSeq;
+                sleLoan->at(sfBorrower) = a1.id();
+                sleLoan->at(sfStartDate) = closedEndedRed - kLoanRedemptionBuffer;
+                sleLoan->at(sfPaymentInterval) = 1;
+                sleLoan->at(sfPaymentRemaining) = 1;
+                sleLoan->at(sfTotalValueOutstanding) = Number(100);
+                sleLoan->at(sfPeriodicPayment) = Number(1);
+                ac.view().insert(sleLoan);
+                return true;
+            },
+            XRPAmount{},
+            STTx{ttLOAN_SET, [](STObject&) {}},
+            {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+            [&](Account const& a1, Account const&, Env& env) -> bool {
+                auto const sub = env.now().time_since_epoch().count() + 60;
+                auto const red = sub + kMinInvestmentPeriod + 1'000'000;
+                closedEndedRed = red;
+
+                Vault const vault{env};
+                auto [tx, keylet] = vault.create(
+                    {.owner = a1,
+                     .asset = xrpIssue(),
+                     .vaultKind = closedEnded,
+                     .subscriptionDate = sub,
+                     .redemptionDate = red});
+                env(tx);
+                closedEndedKeylet = keylet;
+
+                // Create the loan broker; LoanBrokerSet has no phase gate.
+                closedEndedBrokerKeylet =
+                    keylet::loanBroker(a1.id(), SeqProxy::rawSequence(env.seq(a1)));
+                env(loan_broker::set(a1, keylet.key));
+
+                // Advance parent close time into Investment so
+                // ValidVault::finalizeLoanSet is satisfied.
+                env.close(tp{d{sub + 1}});
+                return true;
+            });
+    }
+
+    // Minimal impaired-loan setup for testVaultLossExceedsGap.  Kept
+    // inline here so this file has no dependency on LoanTestBase.
+    Keylet
+    makeImpairedVault(
+        test::jtx::Account const& owner,
+        test::jtx::Account const& borrower,
+        test::jtx::Account const& issuer,
+        test::jtx::Env& env)
+    {
+        using namespace test::jtx;
+
+        env.fund(XRP(1'000'000), issuer, borrower);
+        env.close();
+
+        PrettyAsset const usd = issuer["USD"];
+        STAmount const trustLimit{usd.raw(), Number{9'999'999'999'999'999LL}};
+        env(trust(owner, trustLimit));
+        env(trust(borrower, trustLimit));
+        env.close();
+
+        env(pay(issuer, owner, usd(100'000)));
+        env(pay(issuer, borrower, usd(1'000)));
+        env.close();
+
+        // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
+        // accepts closed-ended vaults. The 10-year investment window
+        // covers this helper's 120 monthly payments so LoanSet's
+        // RedemptionDate bound is satisfied.
+        Vault const vault{env};
+        auto [vaultTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
+            {.owner = owner,
+             .asset = usd,
+             .subscriptionOffset = std::chrono::seconds{60},
+             .investmentWindow = std::chrono::seconds{10ull * 365ull * 24ull * 60ull * 60ull}});
+        env(vaultTx);
+        env.close();
+
+        env(vault.deposit(
+            {.depositor = owner, .id = vaultKeylet.key, .amount = usd(1'000).value()}));
+        env.close();
+
+        auto const brokerKeylet =
+            keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+
+        {
+            using namespace loan_broker;
+            env(set(owner, vaultKeylet.key),
+                kCoverRateMinimum(percentageToTenthBips(1)),
+                kCoverRateLiquidation(xrpl::lending::kMaxCoverRate),
+                Fee(env.current()->fees().base * 2));
+            env.close();
+
+            env(coverDeposit(owner, brokerKeylet.key, usd(10'000).value()),
+                Fee(env.current()->fees().base * 2));
+            env.close();
+        }
+
+        // LoanSet is gated on Investment; advance out of Subscription.
+        vault.closePastSubscription(subscriptionDate);
+
+        auto const brokerSle = env.le(brokerKeylet);
+        if (!BEAST_EXPECT(brokerSle))
+            return vaultKeylet;
+
+        auto const loanKeylet =
+            keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
+
+        {
+            using namespace loan;
+            env(set(borrower, brokerKeylet.key, usd(100).value()),
+                kCounterparty(owner),
+                kInterestRate(TenthBips32{1000}),
+                kPaymentTotal(120),
+                kPaymentInterval(86400u * 30u),
+                kGracePeriod(86400u * 30u),
+                Sig(sfCounterpartySignature, owner),
+                Fee(env.current()->fees().base * 200));
+            env.close();
+
+            // Under fixCleanup3_4_0 impair requires the payment to already
+            // be late, so advance past the loan's due date first.
+            if (env.current()->rules().enabled(fixCleanup3_4_0))
+            {
+                auto const loanSle = env.le(loanKeylet);
+                if (!BEAST_EXPECT(loanSle))
+                    return vaultKeylet;
+                std::uint32_t const dueDate = loanSle->at(sfNextPaymentDueDate);
+                env.close(
+                    NetClock::time_point{NetClock::duration{dueDate}} + std::chrono::seconds{1});
+            }
+
+            env(manage(owner, loanKeylet.key, tfLoanImpair));
+            env.close();
+        }
+
+        return vaultKeylet;
+    }
+
+    // Regression test for the loss-vs-gap invariant relaxation introduced
+    // by fixCleanup3_4_0.  Even with the one-unit tolerance, a loss value
+    // exceeding (T - A) by more than one ULP must still fire.  Two
+    // mutations exercise this:
+    //   1. L = (T - A) * 2  — fires under both amendment settings.
+    //   2. L = (T - A) + 2 * oneUnit  — fires post-amendment, catching
+    //      any accidental widening of the tolerance beyond one unit.
+    void
+    testVaultLossExceedsGap()
+    {
+        testcase("vault loss exceeds gap (fixCleanup3_4_0 tolerance)");
+        using namespace test::jtx;
+
+        auto const kExpectedLog = std::vector{
+            "loss unrealized must not exceed the difference between assets "
+            "outstanding and available"};
+
+        for (auto const withFix : {false, true})
+        {
+            FeatureBitset amendments = all_;
+            if (!withFix)
+                amendments = amendments - fixCleanup3_4_0;
+
+            // Variant 1: L = (T - A) * 2. Fires under both settings.
+            {
+                Keylet vaultKeylet = keylet::vault(uint256{});
+                Account const issuer{"issuer_loss_gap"};
+                Account const borrower{"borrower_loss_gap"};
+
+                auto preclose = [&, this](Account const& owner, Account const&, Env& env) -> bool {
+                    vaultKeylet = this->makeImpairedVault(owner, borrower, issuer, env);
+                    return BEAST_EXPECT(env.le(vaultKeylet));
+                };
+
+                doInvariantCheck(
+                    makeEnv(amendments),
+                    kExpectedLog,
+                    [&vaultKeylet](Account const&, Account const&, ApplyContext& ac) -> bool {
+                        auto sle = ac.view().peek(vaultKeylet);
+                        if (!sle)
+                            return false;
+                        Number const total = sle->at(sfAssetsTotal);
+                        Number const available = sle->at(sfAssetsAvailable);
+                        (*sle)[sfLossUnrealized] = (total - available) * 2;
+                        ac.view().update(sle);
+                        return true;
+                    },
+                    XRPAmount{},
+                    STTx{
+                        ttVAULT_DEPOSIT,
+                        [&vaultKeylet](STObject& tx) {
+                            tx.setFieldH256(sfVaultID, vaultKeylet.key);
+                        }},
+                    {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+                    preclose,
+                    TxAccount::A1);
+            }
+
+            // Variant 2: L = (T - A) + 2 * oneUnit at scale(T).  Must fire
+            // post-fix because the tolerance is exactly one unit.  A
+            // regression that widened it to two units would silently accept
+            // this state.
+            {
+                Keylet vaultKeylet = keylet::vault(uint256{});
+                Account const issuer{"issuer_loss_gap2"};
+                Account const borrower{"borrower_loss_gap2"};
+
+                auto preclose = [&, this](Account const& owner, Account const&, Env& env) -> bool {
+                    vaultKeylet = this->makeImpairedVault(owner, borrower, issuer, env);
+                    return BEAST_EXPECT(env.le(vaultKeylet));
+                };
+
+                doInvariantCheck(
+                    makeEnv(amendments),
+                    kExpectedLog,
+                    [&vaultKeylet](Account const&, Account const&, ApplyContext& ac) -> bool {
+                        auto sle = ac.view().peek(vaultKeylet);
+                        if (!sle)
+                            return false;
+                        Number const total = sle->at(sfAssetsTotal);
+                        Number const available = sle->at(sfAssetsAvailable);
+                        Asset const asset = sle->at(sfAsset);
+                        Number const oneUnit{1, scale(total, asset)};
+                        (*sle)[sfLossUnrealized] = (total - available) + oneUnit * 2;
+                        ac.view().update(sle);
+                        return true;
+                    },
+                    XRPAmount{},
+                    STTx{
+                        ttVAULT_DEPOSIT,
+                        [&vaultKeylet](STObject& tx) {
+                            tx.setFieldH256(sfVaultID, vaultKeylet.key);
+                        }},
+                    {tecINVARIANT_FAILED, tecINVARIANT_FAILED},
+                    preclose,
+                    TxAccount::A1);
+            }
+        }
+    }
+
+    void
+    testVaultComputeCoarsestScale()
+    {
+        using namespace jtx;
+
+        Account const issuer{"issuer"};
+        PrettyAsset const vaultAsset = issuer["IOU"];
+
+        struct TestCase
+        {
+            std::string name;
+            std::int32_t expectedMinScale;
+            std::vector values;
+        };
+
+        for (auto const mantissaScale : MantissaRange::getAllScales())
+        {
+            if (mantissaScale == MantissaRange::MantissaScale::Small)
+                continue;
+            NumberMantissaScaleGuard const g{mantissaScale};
+
+            auto makeDelta = [&vaultAsset](Number const& n) -> ValidVault::DeltaInfo {
+                return {.delta = n, .scale = scale(n, vaultAsset.raw())};
+            };
+
+            auto const testCases = std::vector{
+                {
+                    .name = "No values",
+                    .expectedMinScale = 0,
+                    .values = {},
+                },
+                {
+                    .name = "Mixed integer and Number values",
+                    .expectedMinScale = -15,
+                    .values = {makeDelta(1), makeDelta(-1), makeDelta(Number{10, -1})},
+                },
+                {
+                    .name = "Mixed scales",
+                    .expectedMinScale = -17,
+                    .values =
+                        {makeDelta(Number{1, -2}),
+                         makeDelta(Number{5, -3}),
+                         makeDelta(Number{3, -2})},
+                },
+                {
+                    .name = "Equal scales",
+                    .expectedMinScale = -16,
+                    .values =
+                        {makeDelta(Number{1, -1}),
+                         makeDelta(Number{5, -1}),
+                         makeDelta(Number{1, -1})},
+                },
+                {
+                    .name = "Mixed mantissa sizes",
+                    .expectedMinScale = -12,
+                    .values =
+                        {makeDelta(Number{1}),
+                         makeDelta(Number{1234, -3}),
+                         makeDelta(Number{12345, -6}),
+                         makeDelta(Number{123, 1})},
+                },
+            };
+
+            for (auto const& tc : testCases)
+            {
+                testcase("vault computeCoarsestScale: " + tc.name);
+
+                auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
+
+                BEAST_EXPECTS(
+                    actualScale == tc.expectedMinScale,
+                    "expected: " + std::to_string(tc.expectedMinScale) +
+                        ", actual: " + std::to_string(actualScale));
+                for (auto const& num : tc.values)
+                {
+                    // None of these scales are far enough apart that rounding the
+                    // values would lose information, so check that the rounded
+                    // value matches the original.
+                    auto const actualRounded = roundToAsset(vaultAsset, num.delta, actualScale);
+                    BEAST_EXPECTS(
+                        actualRounded == num.delta,
+                        "number " + to_string(num.delta) + " rounded to scale " +
+                            std::to_string(actualScale) + " is " + to_string(actualRounded));
+                }
+            }
+
+            auto const testCases2 = std::vector{
+                {
+                    .name = "False equivalence",
+                    .expectedMinScale = -15,
+                    .values =
+                        {
+                            makeDelta(Number{1234567890123456789, -18}),
+                            makeDelta(Number{12345, -4}),
+                            makeDelta(Number{1}),
+                        },
+                },
+            };
+
+            // Unlike the first set of test cases, the values in these test could
+            // look equivalent if using the wrong scale.
+            for (auto const& tc : testCases2)
+            {
+                testcase("vault computeCoarsestScale: " + tc.name);
+
+                auto const actualScale = ValidVault::computeCoarsestScale(tc.values);
+
+                BEAST_EXPECTS(
+                    actualScale == tc.expectedMinScale,
+                    "expected: " + std::to_string(tc.expectedMinScale) +
+                        ", actual: " + std::to_string(actualScale));
+                std::optional first;
+                Number firstRounded;
+                for (auto const& num : tc.values)
+                {
+                    if (!first)
+                    {
+                        first = num.delta;
+                        firstRounded = roundToAsset(vaultAsset, num.delta, actualScale);
+                        continue;
+                    }
+                    auto const numRounded = roundToAsset(vaultAsset, num.delta, actualScale);
+                    BEAST_EXPECTS(
+                        numRounded != firstRounded,
+                        "at a scale of " + std::to_string(actualScale) + " " +
+                            to_string(num.delta) + " == " + to_string(*first));
+                }
+            }
+        }
+    }
+
+    void
+    run() override
+    {
+        testVault();
+        testVaultLossExceedsGap();
+        testVaultComputeCoarsestScale();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(InvariantsVault, app, xrpl);
+
+}  // namespace xrpl::test
diff --git a/src/test/app/lending/LendingHelpers_test.cpp b/src/test/app/lending/LendingHelpers_test.cpp
index 5d67cdc3c5..96adfd5254 100644
--- a/src/test/app/lending/LendingHelpers_test.cpp
+++ b/src/test/app/lending/LendingHelpers_test.cpp
@@ -2,18 +2,27 @@
 // DO NOT REMOVE
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 
 #include 
@@ -409,7 +418,7 @@ class LendingHelpers_test : public beast::unit_test::Suite
         Env const env{*this};
         auto const& rules = env.current()->rules();
 
-        // Inputs from the bug reproduction in Loan_test.cpp:
+        // Inputs from the near-zero-rate LoanPay bug reproduction:
         //   InterestRate = 1 TenthBips32 (0.001 % per year),
         //   PaymentInterval = 600 s, principal = 100, 3 payments.
         // periodicRate is ~1.9e-10.
@@ -1871,6 +1880,100 @@ public:
         }
     }
 
+    // Targeted unit test for getLoanDefaultFreezeExemptAccounts(): builds a real
+    // (XRP, so no trust lines needed) Vault/LoanBroker/Loan chain, then calls
+    // the function directly against hand-picked, unsubmitted transactions
+    // (via env.jt(), which never touches the ledger) to exercise every early
+    // return and the success path precisely.
+    void
+    testLoanDefaultFreezeExemptAccounts()
+    {
+        using namespace jtx;
+        using namespace loan;
+
+        testcase("getLoanDefaultFreezeExemptAccounts");
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        Env env{*this};
+        Vault const vault{env};
+        env.fund(XRP(10'000), lender, borrower);
+        env.close();
+
+        // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
+        // accepts closed-ended vaults, so build one with a near-future
+        // SubscriptionDate, deposit while still in the Subscription phase,
+        // and advance past SubscriptionDate before creating the broker.
+        auto [vaultTx, vaultKeylet, subscriptionDate] =
+            vault.createClosedEnded({.owner = lender, .asset = xrpIssue()});
+        env(vaultTx);
+        env.close();
+        env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = XRP(1'000)}));
+        env.close();
+
+        vault.closePastSubscription(subscriptionDate);
+
+        auto const brokerKeylet =
+            keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender)));
+        env(loan_broker::set(lender, vaultKeylet.key));
+        env.close();
+
+        env(set(borrower, brokerKeylet.key, Number{200'000}),
+            Sig(sfCounterpartySignature, lender),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+
+        auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
+
+        // Not a LoanManage transaction at all.
+        {
+            auto const jt = env.jt(jtx::pay(lender, borrower, XRP(1)));
+            BEAST_EXPECT(!getLoanDefaultFreezeExemptAccounts(*env.current(), *jt.stx));
+        }
+
+        // LoanManage, but not the tfLoanDefault flag.
+        {
+            auto const jt = env.jt(manage(lender, loanKeylet.key, tfLoanImpair));
+            BEAST_EXPECT(!getLoanDefaultFreezeExemptAccounts(*env.current(), *jt.stx));
+        }
+
+        // tfLoanDefault, but fixCleanup3_4_0 is disabled.
+        {
+            env.disableFeature(fixCleanup3_4_0);
+            auto const jt = env.jt(manage(lender, loanKeylet.key, tfLoanDefault));
+            BEAST_EXPECT(!getLoanDefaultFreezeExemptAccounts(*env.current(), *jt.stx));
+            env.enableFeature(fixCleanup3_4_0);
+        }
+
+        // tfLoanDefault, amendment enabled, but the referenced Loan doesn't
+        // exist (reusing the broker's own ID as a bogus LoanID, same trick
+        // testInvalidLoanManage-style tests use elsewhere in this suite).
+        {
+            auto const jt = env.jt(manage(lender, brokerKeylet.key, tfLoanDefault));
+            BEAST_EXPECT(!getLoanDefaultFreezeExemptAccounts(*env.current(), *jt.stx));
+        }
+
+        // tfLoanDefault, amendment enabled, Loan/LoanBroker/Vault all exist:
+        // resolves the issuer, broker, vault accounts, and the vault's asset.
+        {
+            auto const jt = env.jt(manage(lender, loanKeylet.key, tfLoanDefault));
+            auto const result = getLoanDefaultFreezeExemptAccounts(*env.current(), *jt.stx);
+            auto const brokerSle = env.le(brokerKeylet);
+            auto const vaultSle = env.le(vaultKeylet);
+            BEAST_EXPECT(result);
+            BEAST_EXPECT(brokerSle);
+            BEAST_EXPECT(vaultSle);
+            if (result && brokerSle && vaultSle)
+            {
+                BEAST_EXPECT(result->issuer == vaultSle->at(sfAsset).getIssuer());
+                BEAST_EXPECT(result->broker == brokerSle->at(sfAccount));
+                BEAST_EXPECT(result->vault == vaultSle->at(sfAccount));
+                BEAST_EXPECT(result->asset == vaultSle->at(sfAsset));
+            }
+        }
+    }
+
     void
     run() override
     {
@@ -1906,6 +2009,8 @@ public:
         testLoanOriginationExceedsVaultMaximumDispatcher();
         testLoanVaultExposureDispatcher();
         testLoanPaymentDeltasDispatcher();
+
+        testLoanDefaultFreezeExemptAccounts();
     }
 };
 
diff --git a/src/test/app/lending/LoanBroker_test.cpp b/src/test/app/lending/LoanBroker_test.cpp
index 5efa65d506..3bcda42c7e 100644
--- a/src/test/app/lending/LoanBroker_test.cpp
+++ b/src/test/app/lending/LoanBroker_test.cpp
@@ -6,6 +6,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -58,6 +60,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -69,7 +72,13 @@ class LoanBroker_test : public beast::unit_test::Suite
 {
     // Ensure that all the features needed for Lending Protocol are included,
     // even if they are set to unsupported.
-    FeatureBitset const all_{jtx::testableAmendments()};
+    //
+    // featureLendingProtocolV1_1 is excluded from the default set: it adds
+    // the closed-ended vault gate on LoanBrokerSet::preclaim (see
+    // LoanBrokerSet.cpp), but this suite exercises loan-broker mechanics on
+    // plain open-ended vaults. Tests that specifically exercise the
+    // amendment opt it back in explicitly and use closed-ended vaults.
+    FeatureBitset const all_{jtx::testableAmendments() - featureLendingProtocolV1_1};
 
     void
     testDisabled()
@@ -869,7 +878,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         using namespace loan_broker;
         Account const issuer{"issuer"};
         Account const alice{"alice"};
-        Env env(*this);
+        Env env(*this, all_);
         Vault const vault{env};
 
         env.fund(XRP(100'000), issuer, alice);
@@ -1105,7 +1114,7 @@ class LoanBroker_test : public beast::unit_test::Suite
             Account const alice{"alice"};
             Account const issuer{"issuer"};
             auto const usd = alice["USD"];
-            Env env(*this);
+            Env env(*this, all_);
             env.fund(XRP(100'000), alice);
             env.close();
 
@@ -1208,7 +1217,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         // This test is lifted directly from
         // https://bugs.immunefi.com/dashboard/submission/57808
         using namespace jtx;
-        Env env(*this);
+        Env env(*this, all_);
 
         Account const alice{"alice"};
         env.fund(XRP(10000), alice);
@@ -1266,7 +1275,7 @@ class LoanBroker_test : public beast::unit_test::Suite
 
         Account const issuer{"issuer"};
         Account const alice{"alice"};
-        Env env(*this);
+        Env env(*this, all_);
         Vault vault{env};
 
         env.fund(XRP(100'000), issuer, alice);
@@ -1374,7 +1383,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         using namespace loan_broker;
         Account const issuer{"issuer"};
         Account const alice{"alice"};
-        Env env(*this);
+        Env env(*this, all_);
         Vault const vault{env};
 
         env.fund(XRP(100'000), issuer, alice);
@@ -1540,7 +1549,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         Account const& broker = issuer;
 
         auto test = [&](auto&& getToken) {
-            Env env(*this);
+            Env env(*this, all_);
 
             env.fund(XRP(1'000), issuer, holder);
             env.close();
@@ -1613,7 +1622,7 @@ class LoanBroker_test : public beast::unit_test::Suite
     {
         testcase << "RIPD-4466 - LoanBrokerSet disallows frozen vaults";
         using namespace jtx;
-        Env env(*this);
+        Env env(*this, all_);
 
         Account const issuer{"issuer"}, lender{"lender"}, borrower{"borrower"};
         env.fund(XRP(20'000), issuer, lender, borrower);
@@ -1840,6 +1849,96 @@ class LoanBroker_test : public beast::unit_test::Suite
         BEAST_EXPECT(aliceBalanceAfter == aliceBalanceBefore);
     }
 
+    void
+    testLoanBrokerDeleteRequireAuthMPT(FeatureBitset features)
+    {
+        testcase << "LoanBrokerDelete - auth-required broker pseudo-account MPT "
+                 << (features[fixCleanup3_4_0] ? "post-fix" : "pre-fix");
+        using namespace jtx;
+        using namespace loan_broker;
+
+        Account const issuer("issuer");
+        Account const alice("alice");
+
+        Env env(*this, features);
+        env.fund(XRP(100'000), issuer, alice);
+        env.close();
+
+        // Create an auth-required MPT and authorize alice as a holder. The
+        // broker pseudo-account's cover MPToken is auto-created later
+        // (addEmptyHolding -> authorizeMPToken) with lsfMPTAuthorized clear;
+        // the pseudo-account is implicitly authorized to hold any MPT
+        // regardless of that flag.
+        auto tester = MPTTester(
+            {.env = env,
+             .issuer = issuer,
+             .holders = {alice},
+             .pay = 20'000,
+             .flags = tfMPTRequireAuth | tfMPTCanTransfer,
+             .authHolder = true});
+
+        PrettyAsset const mpt{tester.issuanceID()};
+
+        // Create vault
+        Vault const vault{env};
+        auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = mpt});
+        env(tx);
+        env.close();
+
+        // Deposit into vault
+        env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = mpt(10'000)}));
+        env.close();
+
+        // Create loan broker
+        auto const brokerKeylet =
+            keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
+        env(set(alice, vaultKeylet.key));
+        env.close();
+
+        // Deposit cover
+        env(coverDeposit(alice, brokerKeylet.key, mpt(5'000).value()));
+        env.close();
+
+        // Verify cover is deposited
+        auto const broker = env.le(brokerKeylet);
+        if (!BEAST_EXPECT(broker))
+            return;
+        BEAST_EXPECT(broker->at(sfCoverAvailable) > 0);
+
+        // Get the broker pseudo-account
+        auto const brokerPseudoID = broker->at(sfAccount);
+
+        // Verify the broker pseudo-account has an MPToken, and that it was
+        // never explicitly authorized (issuer cannot authorize a
+        // pseudo-account holder; see MPTokenAuthorize::preclaim).
+        auto const pseudoMptKey = keylet::mptoken(tester.issuanceID(), brokerPseudoID);
+        auto const pseudoMpt = env.le(pseudoMptKey);
+        if (!BEAST_EXPECT(pseudoMpt))
+            return;
+        BEAST_EXPECT(!pseudoMpt->isFlag(lsfMPTAuthorized));
+
+        // Record alice's balance before deletion
+        auto const aliceBalanceBefore = env.balance(alice, mpt);
+
+        // LoanBrokerDelete sends the remaining cover out of the broker pseudo-account, deletes its
+        // now-empty MPToken, and erases the pseudo AccountRoot. Before the fix,
+        // ValidMPTTransfer::isAuthorized evaluates isPseudoAccount() on the post-transaction view
+        // (where the pseudo-account is already gone) and falls back to the MPToken's
+        // lsfMPTAuthorized flag, which was never set, so the invariant treats the broker as an
+        // unauthorized sender and the whole transaction fails once fixCleanup3_4_0 makes the check
+        // enforcing.
+        env(del(alice, brokerKeylet.key), Ter(tesSUCCESS));
+        env.close();
+
+        // Broker and its pseudo-account MPToken are gone
+        BEAST_EXPECT(env.le(brokerKeylet) == nullptr);
+        BEAST_EXPECT(env.le(pseudoMptKey) == nullptr);
+
+        // Alice received the cover
+        auto const aliceBalanceAfter = env.balance(alice, mpt);
+        BEAST_EXPECT(aliceBalanceAfter > aliceBalanceBefore);
+    }
+
     void
     testCoverDepositFreezes()
     {
@@ -1852,7 +1951,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         // === IOU ===
         {
             testcase("LoanBrokerCoverDeposit IOU freeze checks");
-            Env env(*this);
+            Env env(*this, all_);
             Vault const vault{env};
 
             env.fund(XRP(100'000), issuer, alice);
@@ -1919,7 +2018,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         // === MPT ===
         {
             testcase("LoanBrokerCoverDeposit MPT lock checks");
-            Env env(*this);
+            Env env(*this, all_);
             Vault const vault{env};
 
             env.fund(XRP(100'000), issuer, alice);
@@ -2002,7 +2101,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         Account const issuer{"issuer"};
         Account const alice{"alice"};
         Account const dest{"dest"};
-        Env env{*this};
+        Env env{*this, all_};
         Vault const vault{env};
 
         env.fund(XRP(100'000), issuer, alice, dest);
@@ -2068,7 +2167,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         // === IOU ===
         {
             testcase("LoanBrokerCoverWithdraw IOU freeze checks");
-            Env env(*this);
+            Env env(*this, all_);
             Vault const vault{env};
 
             env.fund(XRP(100'000), issuer, alice);
@@ -2180,7 +2279,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         // === MPT ===
         {
             testcase("LoanBrokerCoverWithdraw MPT lock checks");
-            Env env(*this);
+            Env env(*this, all_);
             Vault const vault{env};
 
             env.fund(XRP(100'000), issuer, alice);
@@ -2301,7 +2400,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         };
 
         auto test = [&](TrustState trustState) {
-            Env env(*this);
+            Env env(*this, all_);
 
             testcase << "RIPD-4274 IOU with state: " << static_cast(trustState);
 
@@ -2426,7 +2525,7 @@ class LoanBroker_test : public beast::unit_test::Suite
         };
 
         auto test = [&](MPTState mptState) {
-            Env env(*this);
+            Env env(*this, all_);
 
             testcase << "RIPD-4274 MPT with state: " << static_cast(mptState);
 
@@ -2532,6 +2631,132 @@ class LoanBroker_test : public beast::unit_test::Suite
         testRIPD4274MPT();
     }
 
+    void
+    testCoverWithdrawCredentialDepositPreauth(FeatureBitset features)
+    {
+        testcase(
+            std::string{"CoverWithdraw with credential-based deposit preauth "} +
+            (features[fixCleanup3_4_0] ? "post-fix" : "pre-fix"));
+        using namespace jtx;
+        using namespace std::chrono_literals;
+
+        bool const fix340Enabled = features[fixCleanup3_4_0];
+
+        Env env(*this, features);
+
+        Account const broker{"broker"};
+        Account const dest{"dest"};
+        Account const credIssuer{"credIssuer"};
+        char const credType[] = "abcde";
+
+        env.fund(XRP(10'000), broker, dest, credIssuer);
+        env(fset(dest, asfDepositAuth));
+        env.close();
+
+        PrettyAsset const asset{xrpIssue(), 1'000'000};
+
+        Vault const vault(env);
+        auto const [vaultTx, vaultKeylet] = vault.create({.owner = broker, .asset = asset});
+        env(vaultTx);
+        env.close();
+
+        env(vault.deposit({.depositor = broker, .id = vaultKeylet.key, .amount = asset(1'000)}));
+        env.close();
+
+        auto const brokerKeylet =
+            keylet::loanBroker(broker.id(), SeqProxy::rawSequence(env.seq(broker)));
+        env(loan_broker::set(broker, vaultKeylet.key));
+        env.close();
+
+        env(loan_broker::coverDeposit(broker, brokerKeylet.key, asset(500)));
+        env.close();
+
+        auto coverWithdrawToDest = [&]() {
+            return loan_broker::coverWithdraw(broker, brokerKeylet.key, asset(10));
+        };
+
+        // Without any preauth, coverWithdraw to dest fails
+        env(coverWithdrawToDest(), loan_broker::kDestination(dest), Ter{tecNO_PERMISSION});
+        env.close();
+
+        // Issue and accept a credential for the broker (with expiration)
+        auto jv = credentials::create(broker, credIssuer, credType);
+        std::uint32_t const expiration =
+            env.current()->header().parentCloseTime.time_since_epoch().count() + 100;
+        jv[sfExpiration.jsonName] = expiration;
+        env(jv);
+        env(credentials::accept(broker, credIssuer, credType));
+        env.close();
+
+        auto const credKeylet = credentials::keylet(broker, credIssuer, credType);
+        auto const credIdx =
+            credentials::ledgerEntry(env, broker, credIssuer, credType)[jss::result][jss::index]
+                .asString();
+
+        // dest authorizes deposits from holders of credentials issued by credIssuer
+        env(deposit::authCredentials(dest, {{.issuer = credIssuer, .credType = credType}}));
+        env.close();
+
+        // Without supplying credentials, still fails
+        env(coverWithdrawToDest(), loan_broker::kDestination(dest), Ter{tecNO_PERMISSION});
+        env.close();
+
+        if (!fix340Enabled)
+        {
+            // Pre-fix: sfCredentialIDs in LoanBrokerCoverWithdraw is disabled
+            env(coverWithdrawToDest(),
+                loan_broker::kDestination(dest),
+                credentials::Ids({credIdx}),
+                Ter{temDISABLED});
+            env.close();
+            return;
+        }
+
+        // With credentials, succeeds
+        env(coverWithdrawToDest(), loan_broker::kDestination(dest), credentials::Ids({credIdx}));
+        env.close();
+
+        // Bad credential id is rejected
+        std::string const invalidIdx =
+            "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E01E034";
+        env(coverWithdrawToDest(),
+            loan_broker::kDestination(dest),
+            credentials::Ids({invalidIdx}),
+            Ter{tecBAD_CREDENTIALS});
+        env.close();
+
+        // Malformed credential array (duplicates) is rejected by checkFields
+        env(coverWithdrawToDest(),
+            loan_broker::kDestination(dest),
+            credentials::Ids({credIdx, credIdx}),
+            Ter{temMALFORMED});
+        env.close();
+
+        // Valid credential not authorized by dest hits authorizedDepositPreauth error path
+        char const credType2[] = "fghij";
+        env(credentials::create(broker, credIssuer, credType2));
+        env(credentials::accept(broker, credIssuer, credType2));
+        env.close();
+        auto const credIdx2 =
+            credentials::ledgerEntry(env, broker, credIssuer, credType2)[jss::result][jss::index]
+                .asString();
+        env(coverWithdrawToDest(),
+            loan_broker::kDestination(dest),
+            credentials::Ids({credIdx2}),
+            Ter{tecNO_PERMISSION});
+        env.close();
+
+        // Advance time past expiration: credentials yield tecEXPIRED and are deleted
+        env.close(150s);
+        BEAST_EXPECT(env.le(credKeylet));
+        env(coverWithdrawToDest(),
+            loan_broker::kDestination(dest),
+            credentials::Ids({credIdx}),
+            Ter{tecEXPIRED});
+        env.close();
+        BEAST_EXPECT(!env.le(credKeylet));
+    }
+
     // Exercises canApplyToBrokerCover (fixCleanup3_2_0): a deposit, withdraw,
     // or clawback whose amount rounds to zero at sfCoverAvailable's precision
     // scale must be rejected with tecPRECISION_LOSS once the amendment is on,
@@ -2770,11 +2995,21 @@ public:
 
         testRIPD4274();
 
+        testCoverWithdrawCredentialDepositPreauth(all_ - fixCleanup3_4_0);
+        testCoverWithdrawCredentialDepositPreauth(all_);
+
         testLoanBrokerDeleteLockedMPT(all_);
         testLoanBrokerDeleteLockedMPT(all_ - fixCleanup3_2_0);
 
         testLoanBrokerDeleteFrozenIOU(all_);
         testLoanBrokerDeleteFrozenIOU(all_ - fixCleanup3_2_0);
+
+        // featureMPTokensV2 independently makes ValidMPTTransfer enforcing,
+        // but it's Supported::No (never enabled on real networks); exclude
+        // it here so fixCleanup3_4_0 alone is the deciding amendment, as it
+        // would be on mainnet.
+        testLoanBrokerDeleteRequireAuthMPT(all_ - featureMPTokensV2);
+        testLoanBrokerDeleteRequireAuthMPT(all_ - featureMPTokensV2 - fixCleanup3_4_0);
         // TODO: Write clawback failure tests with an issuer / MPT that doesn't
         // have the right flags set.
     }
diff --git a/src/test/app/lending/LoanCashBasis_test.cpp b/src/test/app/lending/LoanCashBasis_test.cpp
index 11053b6fd0..e238838306 100644
--- a/src/test/app/lending/LoanCashBasis_test.cpp
+++ b/src/test/app/lending/LoanCashBasis_test.cpp
@@ -4,10 +4,12 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -25,6 +27,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl::test {
@@ -42,8 +45,9 @@ class LoanCashBasis_test : public LoanTestBase
 {
 private:
     // 1. LoanSet origination: Vault.AssetsTotal/LoanBroker.DebtTotal deltas,
-    // and the AssetsMaximum/DebtMaximum guards (which always check against
-    // principal + interestDue, regardless of the amendment).
+    // and the AssetsMaximum/DebtMaximum guards. Accrual AssetsMaximum still
+    // requires headroom for interestDue; cash-basis AssetsMaximum does not,
+    // because origination does not credit interest into AssetsTotal.
     void
     testCashBasisLoanSetOrigination()
     {
@@ -226,6 +230,10 @@ private:
             // Even far less headroom than interestDue still succeeds, since
             // cash-basis origination never adds interest to AssetsTotal.
             runVaultGuard(all_ | featureLendingProtocolV1_1, oneDrop, tesSUCCESS);
+            // Fully subscribed: AssetsTotal == AssetsMaximum. Accrual preclaim
+            // used to refuse this; origination must still succeed because it
+            // does not change AssetsTotal.
+            runVaultGuard(all_ | featureLendingProtocolV1_1, Number{0}, tesSUCCESS);
         }
 
         // DebtMaximum guard: cash-basis projects principal-only DebtTotal;
@@ -489,6 +497,252 @@ private:
         }
     }
 
+    // VaultSet must still succeed when cash-basis LoanPay has already pushed
+    // AssetsTotal above a nonzero AssetsMaximum. Before fixCleanup3_4_0,
+    // ValidVault rejects that with tecINVARIANT_FAILED even though
+    // VaultSet::doApply and the product rule allow the over-cap state when
+    // the excess is interest.
+    void
+    testVaultSetWhileAssetsTotalExceedsMaximum()
+    {
+        using namespace jtx;
+        using namespace loan;
+
+        PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+        BrokerParameters const brokerParams{
+            .vaultDeposit = 1'000'000,
+            .debtMax = 0,
+            .coverRateMin = TenthBips32{0},
+            .coverDeposit = 0,
+            .managementFeeRate = TenthBips16{0},
+            .coverRateLiquidation = TenthBips32{0}};
+
+        auto run =
+            [&](FeatureBitset features, TER expectedOverCapSet, bool native, bool vaultPrivate) {
+                bool const fix340Enabled = features[fixCleanup3_4_0];
+                testcase(
+                    std::string("cash-basis: VaultSet while AssetsTotal exceeds AssetsMaximum") +
+                    (native ? " XRP" : " IOU") + (vaultPrivate ? " private" : "") +
+                    (fix340Enabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
+
+                Account const issuer{"issuer"};
+                Account const lender{"lender"};
+                Account const borrower{"borrower"};
+                Env env(*this, features);
+
+                BrokerParameters params = brokerParams;
+                if (vaultPrivate)
+                    params.vaultFlags = tfVaultPrivate;
+
+                PrettyAsset vaultAsset = xrpAsset;
+                if (native)
+                {
+                    env.fund(XRP(10'000'000), lender, borrower);
+                    env.close();
+                }
+                else
+                {
+                    vaultAsset = createFundedIouAsset(env, issuer, lender, borrower);
+                }
+
+                BrokerInfo const broker{createVaultAndBroker(env, vaultAsset, lender, params)};
+                auto const vaultBefore = env.le(broker.vaultKeylet());
+                BEAST_EXPECT(vaultBefore);
+                // One unit at the vault's asset scale so the stored cap is
+                // strictly above AssetsTotal (a smaller ULP rounds away).
+                // Cash-basis origination does not credit interest, so LoanSet
+                // still succeeds.
+                Number const slack{1, -static_cast(vaultBefore->at(sfScale))};
+                Number const assetsMaximum = Number(vaultBefore->at(sfAssetsTotal)) + slack;
+
+                Vault const vault{env};
+                {
+                    auto tx = vault.set({.owner = lender, .id = broker.vaultID});
+                    tx[sfAssetsMaximum] = assetsMaximum;
+                    env(tx);
+                    env.close();
+                }
+
+                {
+                    auto tx = vault.set({.owner = lender, .id = broker.vaultID});
+                    tx[sfData] = "AA";
+                    env(tx, Ter(tesSUCCESS));
+                    env.close();
+                }
+
+                auto const brokerBeforeLoan = env.le(broker.brokerKeylet());
+                BEAST_EXPECT(brokerBeforeLoan);
+                auto const loanKeylet = keylet::loan(
+                    broker.brokerID, SeqProxy::rawSequence(brokerBeforeLoan->at(sfLoanSequence)));
+
+                LoanParameters const loanParams{
+                    .account = borrower,
+                    .counter = lender,
+                    .principalRequest = 12'000,
+                    .interest = TenthBips32{percentageToTenthBips(12)},
+                    .payTotal = 4,
+                    .payInterval = 600,
+                    .gracePd = 300,
+                };
+                env(loanParams(env, broker));
+                env.close();
+
+                auto const vaultAfterLoan = env.le(broker.vaultKeylet());
+                BEAST_EXPECT(vaultAfterLoan);
+                BEAST_EXPECT(vaultAfterLoan->at(sfAssetsTotal) <= assetsMaximum);
+
+                LoanState const state = getCurrentState(env, broker, loanKeylet);
+                STAmount const payment{
+                    vaultAsset,
+                    roundPeriodicPayment(vaultAsset, state.periodicPayment, state.loanScale) *
+                        Number{3, -1} * 5};
+                env(pay(borrower, loanKeylet.key, payment), Ter(tesSUCCESS));
+                env.close();
+
+                auto const vaultAboveMaximum = env.le(broker.vaultKeylet());
+                BEAST_EXPECT(vaultAboveMaximum);
+                BEAST_EXPECT(vaultAboveMaximum->at(sfAssetsTotal) > assetsMaximum);
+                BEAST_EXPECT(vaultAboveMaximum->at(sfAssetsMaximum) == assetsMaximum);
+
+                {
+                    auto tx = vault.set({.owner = lender, .id = broker.vaultID});
+                    tx[sfData] = "BB";
+                    env(tx, Ter(expectedOverCapSet));
+                    env.close();
+                }
+
+                if (vaultPrivate)
+                {
+                    pdomain::Credentials const credentials{
+                        {.issuer = lender, .credType = "credential"}};
+                    env(pdomain::setTx(lender, credentials));
+                    auto const domainId = pdomain::getNewDomain(env.meta());
+                    auto tx = vault.set({.owner = lender, .id = broker.vaultID});
+                    tx[sfDomainID] = to_string(domainId);
+                    env(tx, Ter(expectedOverCapSet));
+                    env.close();
+                }
+
+                if (!fix340Enabled)
+                    return;
+
+                {
+                    auto tx = vault.set({.owner = lender, .id = broker.vaultID});
+                    tx[sfAssetsMaximum] = assetsMaximum;
+                    env(tx, Ter(tecLIMIT_EXCEEDED));
+                    env.close();
+                }
+
+                {
+                    auto tx = vault.set({.owner = lender, .id = broker.vaultID});
+                    tx[sfAssetsMaximum] = Number{0};
+                    env(tx, Ter(tesSUCCESS));
+                    env.close();
+                }
+            };
+
+        FeatureBitset const withFix = all_ | featureLendingProtocolV1_1;
+        FeatureBitset const withoutFix = withFix - fixCleanup3_4_0;
+
+        run(withFix, tesSUCCESS, true, true);
+        run(withoutFix, tecINVARIANT_FAILED, true, true);
+        run(withFix, tesSUCCESS, false, false);
+        run(withoutFix, tecINVARIANT_FAILED, false, false);
+    }
+
+    void
+    testCashBasisLoanSetAfterInterestExceedsCap()
+    {
+        testcase("cash-basis: LoanSet after interest pushes AssetsTotal past AssetsMaximum");
+
+        using namespace jtx;
+        using namespace loan;
+
+        PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+        BrokerParameters const brokerParams{
+            .vaultDeposit = 1'000'000,
+            .debtMax = 0,
+            .coverRateMin = TenthBips32{0},
+            .coverDeposit = 0,
+            .managementFeeRate = TenthBips16{0},
+            .coverRateLiquidation = TenthBips32{0}};
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+        Env env(*this, all_ | featureLendingProtocolV1_1);
+        env.fund(XRP(10'000'000), lender, borrower);
+        env.close();
+
+        BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
+        auto const vaultBefore = env.le(broker.vaultKeylet());
+        BEAST_EXPECT(vaultBefore);
+        Number const assetsMaximum = Number(vaultBefore->at(sfAssetsTotal));
+
+        Vault const vault{env};
+        {
+            auto tx = vault.set({.owner = lender, .id = broker.vaultID});
+            tx[sfAssetsMaximum] = assetsMaximum;
+            env(tx);
+            env.close();
+        }
+
+        auto const brokerBeforeLoan = env.le(broker.brokerKeylet());
+        BEAST_EXPECT(brokerBeforeLoan);
+        auto const firstLoanKeylet = keylet::loan(
+            broker.brokerID, SeqProxy::rawSequence(brokerBeforeLoan->at(sfLoanSequence)));
+
+        Number const firstPrincipal = xrpAsset(12'000).value();
+        env(set(borrower, broker.brokerID, firstPrincipal),
+            kCounterparty(lender),
+            kInterestRate(TenthBips32{percentageToTenthBips(12)}),
+            kPaymentTotal(4),
+            kPaymentInterval(600),
+            Sig(sfCounterpartySignature, lender),
+            Fee(env.current()->fees().base * 2),
+            Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultAfterFirst = env.le(broker.vaultKeylet());
+        BEAST_EXPECT(vaultAfterFirst);
+        BEAST_EXPECT(vaultAfterFirst->at(sfAssetsTotal) == assetsMaximum);
+        BEAST_EXPECT(vaultAfterFirst->at(sfAssetsAvailable) == assetsMaximum - firstPrincipal);
+
+        LoanState const state = getCurrentState(env, broker, firstLoanKeylet);
+        STAmount const payment{
+            xrpAsset,
+            roundPeriodicPayment(xrpAsset, state.periodicPayment, state.loanScale) * Number{3, -1} *
+                5};
+        env(pay(borrower, firstLoanKeylet.key, payment), Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultAfterPay = env.le(broker.vaultKeylet());
+        BEAST_EXPECT(vaultAfterPay);
+        BEAST_EXPECT(vaultAfterPay->at(sfAssetsTotal) > assetsMaximum);
+        BEAST_EXPECT(vaultAfterPay->at(sfAssetsAvailable) > beast::kZero);
+
+        auto const brokerAfterPay = env.le(broker.brokerKeylet());
+        BEAST_EXPECT(brokerAfterPay);
+        auto const secondLoanKeylet = keylet::loan(
+            broker.brokerID, SeqProxy::rawSequence(brokerAfterPay->at(sfLoanSequence)));
+
+        Number const secondPrincipal = xrpAsset(1'000).value();
+        env(set(borrower, broker.brokerID, secondPrincipal),
+            kCounterparty(lender),
+            kInterestRate(TenthBips32{percentageToTenthBips(12)}),
+            kPaymentTotal(4),
+            kPaymentInterval(600),
+            Sig(sfCounterpartySignature, lender),
+            Fee(env.current()->fees().base * 2),
+            Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultAfterSecond = env.le(broker.vaultKeylet());
+        auto const secondLoan = env.le(secondLoanKeylet);
+        BEAST_EXPECT(vaultAfterSecond && secondLoan);
+        BEAST_EXPECT(vaultAfterSecond->at(sfAssetsTotal) == vaultAfterPay->at(sfAssetsTotal));
+        BEAST_EXPECT(secondLoan->at(sfPrincipalOutstanding) == secondPrincipal);
+    }
+
     // 3. LoanManage: impair, unimpair, and default.
     void
     testCashBasisLoanManage()
@@ -562,6 +816,7 @@ private:
             BEAST_EXPECT(vaultBeforeImpair);
             Number const lossBefore = vaultBeforeImpair->at(sfLossUnrealized);
 
+            advancePastDueDate(env, loanKeylet);
             env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
             env.close();
 
@@ -612,6 +867,7 @@ private:
                 ? principalOutstanding
                 : totalValueOutstanding - managementFeeOutstanding;
 
+            advancePastDueDate(env, loanKeylet);
             env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
             env.close();
 
@@ -822,12 +1078,17 @@ private:
         Number const managementFeeBeforeImpair = loanBeforeImpair->at(sfManagementFeeOutstanding);
         Number const expectedExposure = totalValueBeforeImpair - managementFeeBeforeImpair;
 
+        // With fixCleanup3_4_0, impairment is only allowed once the
+        // payment is late. After the earlier LoanPay the due date advanced by
+        // one interval, so use the current due date rather than startDate.
+        std::uint32_t const dueDateBeforeImpair = loanBeforeImpair->at(sfNextPaymentDueDate);
+        env.close(NetClock::time_point{NetClock::duration{dueDateBeforeImpair}} + 1s);
+
         env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
         env.close();
 
-        LoanState const stateAtImpair = getCurrentState(env, broker, loanKeylet);
         env.close(
-            stateAtImpair.startDate + std::chrono::seconds(paymentInterval) +
+            NetClock::time_point{NetClock::duration{dueDateBeforeImpair}} +
             std::chrono::seconds(gracePeriod) + 60s);
 
         auto const vaultBeforeDefault = env.le(broker.vaultKeylet());
@@ -1006,6 +1267,8 @@ public:
     {
         testCashBasisLoanSetOrigination();
         testCashBasisLoanPay();
+        testVaultSetWhileAssetsTotalExceedsMaximum();
+        testCashBasisLoanSetAfterInterestExceedsCap();
         testCashBasisLoanManage();
         testLegacyVaultKeepsAccrualAfterAmendmentEnabled();
         testCashBasisEndToEndTrajectory();
diff --git a/src/test/app/lending/LoanCoverFreezeAuth_test.cpp b/src/test/app/lending/LoanCoverFreezeAuth_test.cpp
index a9b3542c4e..f8bdb5a3d7 100644
--- a/src/test/app/lending/LoanCoverFreezeAuth_test.cpp
+++ b/src/test/app/lending/LoanCoverFreezeAuth_test.cpp
@@ -5,6 +5,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -13,6 +14,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -236,6 +238,9 @@ private:
             Ter(tesSUCCESS));
         env.close();
 
+        // Under fixCleanup3_4_0 impair requires the payment to be late.
+        advancePastDueDate(env, loanKeylet);
+
         // Impair the loan to create unrealized loss
         env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
         env.close();
@@ -368,6 +373,187 @@ private:
         };
     }
 
+    void
+    testLoanDefaultBypassesFreeze()
+    {
+        testcase("LoanManage: default bypasses asset freeze");
+        using namespace jtx;
+        using namespace loan;
+        Account const lender{"lender"};
+        Account const issuer{"issuer"};
+        Account const borrower{"borrower"};
+        auto const iou = issuer["IOU"];
+
+        Env env(*this);
+        env.fund(XRP(1'000), lender, issuer, borrower);
+        env(trust(lender, iou(10'000'000)));
+        env(pay(issuer, lender, iou(5'000'000)));
+        BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)};
+
+        auto const loanSetFee = Fee(env.current()->fees().base * 2);
+        STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value();
+
+        env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
+            Sig(sfCounterpartySignature, lender),
+            loanSetFee);
+        env.close();
+
+        auto const loanKeylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
+
+        using tp = NetClock::time_point;
+        using d = NetClock::duration;
+
+        // Get past the grace period so the loan is defaultable.
+        if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
+        {
+            env.close(tp{d{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}});
+        }
+
+        // Global freeze trips the post-apply TransfersNotFrozen invariant.
+        env(fset(issuer, asfGlobalFreeze));
+        env.close();
+
+        // Pre-fixCleanup3_4_0, the invariant blocks the default.
+        env.disableFeature(fixCleanup3_4_0);
+        env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecINVARIANT_FAILED));
+        env.close();
+
+        // Per XLS-0066, a default must succeed despite the freeze.
+        env.enableFeature(fixCleanup3_4_0);
+        env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
+    }
+
+    // A default must bypass an MPT global lock the same way it bypasses IOU
+    // freeze, including when the loan was already impaired beforehand
+    // (a different defaultLoan() accounting branch than the un-impaired
+    // path exercised above) and after an ordinary LoanPay was correctly
+    // blocked by the same lock.
+    void
+    testLoanDefaultBypassesMptLockAfterImpair()
+    {
+        testcase("LoanManage: default bypasses MPT lock after impairment");
+        using namespace jtx;
+        using namespace loan;
+
+        Account const issuer{"issuer"};
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        Env env(*this);
+        env.fund(XRP(1'000'000), issuer, lender, borrower);
+        env.close();
+
+        MPTTester mptt(
+            {.env = env,
+             .issuer = issuer,
+             .holders = {lender, borrower},
+             .flags = tfMPTCanTransfer | tfMPTCanLock});
+        PrettyAsset const asset = mptt.issuanceID();
+        env(pay(issuer, lender, asset(10'000'000)));
+        env.close();
+
+        BrokerInfo const brokerInfo{createVaultAndBroker(env, asset, lender)};
+
+        auto const loanSetFee = Fee(env.current()->fees().base * 2);
+        STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value();
+        env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
+            Sig(sfCounterpartySignature, lender),
+            loanSetFee);
+        env.close();
+
+        auto const loanKeylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
+
+        // Realize a loss via impairment before locking.
+        advancePastDueDate(env, loanKeylet);
+        env(manage(lender, loanKeylet.key, tfLoanImpair));
+        env.close();
+
+        // Issuer applies a global lock.
+        mptt.set({.account = issuer, .flags = tfMPTLock});
+        env.close();
+
+        // An ordinary payment is correctly blocked by the lock.
+        env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecLOCKED));
+        env.close();
+
+        using tp = NetClock::time_point;
+        using d = NetClock::duration;
+        if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
+        {
+            env.close(tp{d{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}});
+        }
+
+        // Pre-fixCleanup3_4_0 the ValidMPTTransfer invariant blocks the
+        // default, mirroring the IOU path above.
+        env.disableFeature(fixCleanup3_4_0);
+        env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecINVARIANT_FAILED));
+        env.close();
+
+        // The default itself must succeed despite the lock.
+        env.enableFeature(fixCleanup3_4_0);
+        env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
+    }
+
+    // The exemption must hold for an individually deep-frozen trust line, not
+    // just a global freeze: deep freeze is what the original report ran into,
+    // and it takes a different path through validateFrozenState (the frozen
+    // flag comes off the line rather than off the issuer).
+    void
+    testLoanDefaultBypassesDeepFreeze()
+    {
+        testcase("LoanManage: default bypasses asset deep freeze");
+        using namespace jtx;
+        using namespace loan;
+        Account const lender{"lender"};
+        Account const issuer{"issuer"};
+        Account const borrower{"borrower"};
+        auto const iou = issuer["IOU"];
+
+        Env env(*this);
+        env.fund(XRP(1'000), lender, issuer, borrower);
+        env(trust(lender, iou(10'000'000)));
+        env(pay(issuer, lender, iou(5'000'000)));
+        BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)};
+
+        auto const loanSetFee = Fee(env.current()->fees().base * 2);
+        STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value();
+
+        env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
+            Sig(sfCounterpartySignature, lender),
+            loanSetFee);
+        env.close();
+
+        auto const loanKeylet = keylet::loan(brokerInfo.brokerID, SeqProxy::rawSequence(1));
+
+        using tp = NetClock::time_point;
+        using d = NetClock::duration;
+
+        // Get past the grace period so the loan is defaultable.
+        if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
+        {
+            env.close(tp{d{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}});
+        }
+
+        // The default moves First-Loss Capital off the broker pseudo-account,
+        // so that is the line to freeze.
+        auto const brokerSle = env.le(brokerInfo.brokerKeylet());
+        if (!BEAST_EXPECT(brokerSle))
+            return;
+        Account const brokerPseudo{"brokerPseudo", brokerSle->at(sfAccount)};
+
+        env(trust(issuer, brokerPseudo["IOU"](0), tfSetFreeze | tfSetDeepFreeze));
+        env.close();
+
+        // Pre-fixCleanup3_4_0, the invariant blocks the default.
+        env.disableFeature(fixCleanup3_4_0);
+        env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecINVARIANT_FAILED));
+        env.close();
+
+        // Per XLS-0066, a default must succeed despite the deep freeze.
+        env.enableFeature(fixCleanup3_4_0);
+        env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
+    }
+
     void
     testLoanPayBrokerOwnerMissingTrustline(FeatureBitset features)
     {
@@ -694,6 +880,9 @@ private:
     runAmendmentIndependent()
     {
         testServiceFeeOnBrokerDeepFreeze();
+        testLoanDefaultBypassesFreeze();
+        testLoanDefaultBypassesDeepFreeze();
+        testLoanDefaultBypassesMptLockAfterImpair();
     }
 
     // Tests run under each entry in amendmentCombinations().
diff --git a/src/test/app/lending/LoanInvariants_test.cpp b/src/test/app/lending/LoanInvariants_test.cpp
index 264dbdcd24..b3d4803aff 100644
--- a/src/test/app/lending/LoanInvariants_test.cpp
+++ b/src/test/app/lending/LoanInvariants_test.cpp
@@ -25,7 +25,9 @@
 #include 
 #include 
 #include 
+#include 
 
+#include 
 #include 
 #include 
 
@@ -383,6 +385,85 @@ private:
             isRounded(broker.asset, newState.principalOutstanding, originalState.loanScale));
     }
 
+    // Verify an overpayment cannot reduce principal without covering and
+    // advancing at least one scheduled instalment: reject an extra-only amount,
+    // but accept an instalment plus extra. Enable V1_1 explicitly because
+    // LoanTestBase::all_ excludes it.
+    void
+    testLoanPayOverpaymentScheduleInvariant(FeatureBitset features)
+    {
+        testcase("LoanPay overpayment schedule advancement");
+
+        using namespace jtx;
+        using namespace loan;
+
+        Env env{*this, features | featureLendingProtocolV1_1};
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(10'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const asset{xrpIssue(), 1000};
+
+        BrokerInfo const broker = createVaultAndBroker(
+            env,
+            asset,
+            lender,
+            {
+                .vaultDeposit = asset(100'000).value(),
+                .managementFeeRate = TenthBips16(10'000),
+            });
+
+        auto const loanSetFee = Fee(env.current()->fees().base * 2);
+
+        // Principal 10,000 over 3 payments, overpayment enabled. One scheduled
+        // payment is ~3,333, so an amount well below that cannot cover one.
+        auto const loanKeylet = nextLoanKeylet(env, broker);
+        env(loan::set(borrower, broker.brokerID, asset(10'000).value(), tfLoanOverpayment),
+            Sig(sfCounterpartySignature, lender),
+            loan::kPaymentInterval(86400 * 30),
+            loan::kPaymentTotal(3),
+            loan::kOverpaymentInterestRate(TenthBips32(percentageToTenthBips(20))),
+            loanSetFee);
+        env.close();
+
+        auto const before = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(before.paymentRemaining == 3);
+
+        STAmount const belowOnePayment = asset(1'000).value();
+        BEAST_EXPECT((belowOnePayment < STAmount{asset, before.periodicPayment}));
+
+        auto const payFee = Fee(env.current()->fees().base * 2);
+
+        // The amount does not cover a scheduled payment, so makeRegularPayment makes zero scheduled
+        // payments and returns tecINSUFFICIENT_PAYMENT before the Extra branch runs. Were the
+        // payment to succeed while touching only principal, PaymentRemaining and NextPaymentDueDate
+        // would silently fail to advance.
+        env(pay(borrower, loanKeylet.key, belowOnePayment, tfLoanOverpayment),
+            payFee,
+            Ter(tecINSUFFICIENT_PAYMENT));
+        env.close();
+
+        auto const afterReject = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(afterReject.paymentRemaining == before.paymentRemaining);
+        BEAST_EXPECT(afterReject.principalOutstanding == before.principalOutstanding);
+        BEAST_EXPECT(afterReject.nextPaymentDate == before.nextPaymentDate);
+
+        // PaymentRemaining drops by one, NextPaymentDueDate advances by one interval, and
+        // PrincipalOutstanding strictly decreases (by more than a plain payment thanks to the
+        // extra).
+        STAmount const onePaymentPlusExtra = asset(5'000).value();
+        env(pay(borrower, loanKeylet.key, onePaymentPlusExtra, tfLoanOverpayment), payFee);
+        env.close();
+
+        auto const afterPay = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(afterPay.paymentRemaining == before.paymentRemaining - 1);
+        BEAST_EXPECT(afterPay.principalOutstanding < before.principalOutstanding);
+        BEAST_EXPECT(afterPay.nextPaymentDate == before.nextPaymentDate + before.paymentInterval);
+    }
+
     void
     testAccountSendMptMinAmountInvariant(FeatureBitset features)
     {
@@ -851,12 +932,175 @@ private:
             });
     }
 
+    void
+    testLoanSetRecipientScaleInvariant()
+    {
+        using namespace jtx;
+        using namespace loan;
+
+        auto const runCase = [&](bool coarseBorrower) {
+            testcase(
+                coarseBorrower ? "LoanSet borrower balance uses coarsest scale"
+                               : "LoanSet broker owner balance uses coarsest scale");
+
+            Env env(*this, all_ | featureLendingProtocolV1_1);
+            Account const issuer{"issuer"};
+            Account const lender{"lender"};
+            Account const borrower{"borrower"};
+
+            Number const coarseBalance{100'000'000'000LL};
+            Number const regularBalance{100'000'000};
+            PrettyAsset const asset = createFundedRippleIouAsset(
+                env,
+                issuer,
+                lender,
+                borrower,
+                coarseBorrower ? regularBalance : coarseBalance,
+                coarseBorrower ? coarseBalance : regularBalance);
+
+            BrokerParameters const brokerParams{
+                .vaultDeposit = 1'000'000,
+                .debtMax = 0,
+                .coverRateMin = TenthBips32{0},
+                .coverDeposit = 0,
+                .managementFeeRate = TenthBips16{0},
+                .coverRateLiquidation = TenthBips32{0}};
+            BrokerInfo const broker = createVaultAndBroker(env, asset, lender, brokerParams);
+
+            Number const principal{1'012'345, -5};
+            Number const originationFee{123'456, -6};
+            Account const& recipient = coarseBorrower ? borrower : lender;
+            Number const expected = coarseBorrower ? principal : originationFee;
+            auto const before = env.balance(recipient, asset);
+
+            if (coarseBorrower)
+            {
+                env(set(borrower, broker.brokerID, principal),
+                    kCounterparty(lender),
+                    Sig(sfCounterpartySignature, lender),
+                    kInterestRate(TenthBips32{0}),
+                    kPaymentTotal(1),
+                    Fee(env.current()->fees().base * 2),
+                    Ter(tesSUCCESS));
+            }
+            else
+            {
+                env(set(borrower, broker.brokerID, principal),
+                    kCounterparty(lender),
+                    Sig(sfCounterpartySignature, lender),
+                    kLoanOriginationFee(originationFee),
+                    kInterestRate(TenthBips32{0}),
+                    kPaymentTotal(1),
+                    Fee(env.current()->fees().base * 2),
+                    Ter(tesSUCCESS));
+            }
+            env.close();
+
+            auto const after = env.balance(recipient, asset);
+            Number const received = after.number() - before.number();
+            auto const recipientScale =
+                std::max(before.value().exponent(), after.value().exponent());
+            auto const vaultScale = broker.vaultScale(env);
+            Number const tolerance{1, recipientScale};
+
+            BEAST_EXPECT(recipientScale > vaultScale);
+            BEAST_EXPECT(received != expected);
+            BEAST_EXPECT(
+                abs(roundToAsset(asset, received, recipientScale) -
+                    roundToAsset(asset, expected, recipientScale)) <= tolerance);
+        };
+
+        runCase(/*coarseBorrower=*/true);
+        runCase(/*coarseBorrower=*/false);
+    }
+
+    // Under featureLendingProtocolV1_1, ValidLoan::finalize enforces
+    //   TotalValueOutstanding >= PrincipalOutstanding + ManagementFeeOutstanding
+    // ("interest due is non-negative"). This test drives the transactor
+    // through a multi-payment scenario with a non-zero management fee and
+    // messy IOU-scale rounding; if any rounding path in LoanPay were to
+    // inflate PrincipalOutstanding or ManagementFeeOutstanding relative to
+    // TotalValueOutstanding by even one ULP, the invariant would fire and
+    // the LoanPay would return tecINVARIANT_FAILED instead of tesSUCCESS.
+    void
+    testLoanPayInterestDueNonNegativeInvariant()
+    {
+        testcase("LoanPay interest-due non-negative invariant");
+
+        using namespace jtx;
+        using namespace loan;
+
+        Env env(*this, all_ | featureLendingProtocolV1_1);
+
+        Account const issuer{"issuer"};
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        PrettyAsset const iouAsset = createFundedIouAsset(env, issuer, lender, borrower);
+
+        // Default broker params carry managementFeeRate = 100 tenth-bips
+        // (1%), which is what makes managementFeeOutstanding accumulate
+        // non-trivially through the payment schedule.
+        BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
+
+        auto const loanSetFee = Fee(env.current()->fees().base * 2);
+        auto const loanKeylet = nextLoanKeylet(env, broker);
+
+        // Messy interest rate, non-trivial payment count. Values chosen so
+        // that periodicPayment and each roundedInterest/managementFee share
+        // are unlikely to be representable exactly at loanScale.
+        env(set(borrower, broker.brokerID, Number{1'000}),
+            Sig(sfCounterpartySignature, lender),
+            kInterestRate(TenthBips32{24'346}),
+            kPaymentTotal(24),
+            kPaymentInterval(86400 * 30),
+            loanSetFee);
+        env.close();
+
+        auto const payFee = Fee(env.current()->fees().base * 2);
+        // Boundary check up front on the freshly-created loan.
+        {
+            auto const initial = getCurrentState(env, broker, loanKeylet);
+            BEAST_EXPECT(
+                initial.totalValue >=
+                initial.principalOutstanding + initial.managementFeeOutstanding);
+        }
+
+        // Six regular scheduled payments. If the invariant fires the
+        // Ter(tesSUCCESS) assertion below catches it; the identity check
+        // then re-asserts it in the test for a clearer failure message.
+        std::uint32_t prevPaymentRemaining = 24;
+        for (int i = 0; i < 6; ++i)
+        {
+            auto const loanSle = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loanSle))
+                return;
+            // Match the amount LoanPay expects for a scheduled payment:
+            // periodicPayment rounded at loanScale, plus the flat service
+            // fee (0 here by default, but included for robustness).
+            auto const payAmount = STAmount{
+                iouAsset,
+                roundPeriodicPayment(
+                    iouAsset, loanSle->at(sfPeriodicPayment), loanSle->at(sfLoanScale)) +
+                    loanSle->at(sfLoanServiceFee)};
+            env(pay(borrower, loanKeylet.key, payAmount), payFee, Ter(tesSUCCESS));
+            env.close();
+
+            auto const state = getCurrentState(env, broker, loanKeylet);
+            BEAST_EXPECT(
+                state.totalValue >= state.principalOutstanding + state.managementFeeOutstanding);
+            BEAST_EXPECT(state.paymentRemaining == prevPaymentRemaining - 1);
+            prevPaymentRemaining = state.paymentRemaining;
+        }
+    }
+
     // Tests run under each entry in amendmentCombinations().
     void
     runAmendmentSensitive(FeatureBitset features)
     {
         testLoanPayComputePeriodicPaymentInvariants(features);
         testLoanPayDebtDecreaseInvariant(features);
+        testLoanPayOverpaymentScheduleInvariant(features);
         testAccountSendMptMinAmountInvariant(features);
         testMinimumBrokerCoverConsistency(features);
     }
@@ -865,6 +1109,8 @@ public:
     void
     run() override
     {
+        testLoanSetRecipientScaleInvariant();
+        testLoanPayInterestDueNonNegativeInvariant();
         for (auto const& features : jtx::amendmentCombinations(
                  {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
             runAmendmentSensitive(features);
diff --git a/src/test/app/lending/LoanLifecycle_test.cpp b/src/test/app/lending/LoanLifecycle_test.cpp
index 6cced5c97a..45420529c6 100644
--- a/src/test/app/lending/LoanLifecycle_test.cpp
+++ b/src/test/app/lending/LoanLifecycle_test.cpp
@@ -205,8 +205,14 @@ private:
         if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
             counterpartyJson[sfSigners] = createJson[sfSigners];
 
-        // The duplicated signature works
-        createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson));
+        // The duplicated signature does not work: the counterparty signs a
+        // different prefix than the account.
+        env(env.json(createJson, Json(sfCounterpartySignature, counterpartyJson)),
+            Ter(telENV_RPC_FAILED));
+
+        // Signing the counterparty field itself works, even though the lender
+        // is both the borrower and the counterparty.
+        createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
         env(createJson);
 
         env.close();
@@ -347,7 +353,11 @@ private:
             auto const& asset = debtMaximumRequest.asset();
             auto const initialVault = asset(debtMaximumRequest * 100);
 
-            auto [tx, vaultKeylet] = vault.create({.owner = broker, .asset = asset});
+            // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim
+            // only accepts closed-ended vaults, so build one and advance
+            // past SubscriptionDate before creating broker/loan.
+            auto [tx, vaultKeylet, subscriptionDate] =
+                vault.createClosedEnded({.owner = broker, .asset = asset});
             env(tx, txFee);
             env.close();
 
@@ -356,6 +366,8 @@ private:
                 txFee);
             env.close();
 
+            vault.closePastSubscription(subscriptionDate);
+
             auto const brokerKeylet =
                 keylet::loanBroker(broker.id(), SeqProxy::rawSequence(env.seq(broker)));
 
diff --git a/src/test/app/lending/LoanMisc_test.cpp b/src/test/app/lending/LoanMisc_test.cpp
index 2cb4f38ecf..7c4db3e2bf 100644
--- a/src/test/app/lending/LoanMisc_test.cpp
+++ b/src/test/app/lending/LoanMisc_test.cpp
@@ -113,21 +113,26 @@ private:
             txJson[sfTransactionType] = "AccountSet";
             txJson[sfAccount] = borrower.human();
 
-            auto const borrowerSignParams = [&]() {
-                json::Value params{json::ValueType::Object};
-                params[jss::passphrase] = borrowerPass;
-                params[jss::key_type] = "ed25519";
-                params[jss::signature_target] = "Destination";
-                params[jss::tx_json] = txJson;
-                return params;
-            }();
-            auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams));
-            BEAST_EXPECT(
-                jSignBorrower.isMember(jss::result) &&
-                jSignBorrower[jss::result].isMember(jss::error) &&
-                jSignBorrower[jss::result][jss::error] == "invalidParams" &&
-                jSignBorrower[jss::result].isMember(jss::error_message) &&
-                jSignBorrower[jss::result][jss::error_message] == "Destination");
+            // "Destination" is not an inner object at all. "Book" is one, but
+            // it holds no transaction signature, so it is not a target either.
+            for (char const* target : {"Destination", "Book", "Signer"})
+            {
+                auto const borrowerSignParams = [&]() {
+                    json::Value params{json::ValueType::Object};
+                    params[jss::passphrase] = borrowerPass;
+                    params[jss::key_type] = "ed25519";
+                    params[jss::signature_target] = target;
+                    params[jss::tx_json] = txJson;
+                    return params;
+                }();
+                auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams));
+                BEAST_EXPECT(
+                    jSignBorrower.isMember(jss::result) &&
+                    jSignBorrower[jss::result].isMember(jss::error) &&
+                    jSignBorrower[jss::result][jss::error] == "invalidParams" &&
+                    jSignBorrower[jss::result].isMember(jss::error_message) &&
+                    jSignBorrower[jss::result][jss::error_message] == target);
+            }
         }
         {
             testcase("RPC LoanSet - sign and submit borrower initiated");
@@ -473,14 +478,21 @@ protected:
         TenthBips16 const managementFeeRate{managementFeeRateDist_(engine_)};
         auto const serviceFee = serviceFeeDist_(engine_);
         TenthBips32 interest{interestRateDist_(engine_)};
-        auto const payTotal = paymentTotalDist_(engine_);
+        auto payTotal = paymentTotalDist_(engine_);
         auto const payInterval = paymentIntervalDist_(engine_);
+        // The end of the last payment's grace period must fit in a 32-bit
+        // ripple-epoch timestamp, or LoanSet fails with tecKILLED. Cap the
+        // schedule well below that horizon (2e9 seconds is roughly 63 years,
+        // leaving ample headroom over the ledger start date).
+        constexpr std::uint32_t kMaxScheduleSeconds = 2'000'000'000;
+        payTotal = std::min(payTotal, static_cast(kMaxScheduleSeconds / payInterval));
 
         BrokerParameters const brokerParams{
             .vaultDeposit = principalRequest * 10,
             .debtMax = 0,
             .coverRateMin = TenthBips32{0},
-            .managementFeeRate = managementFeeRate};
+            .managementFeeRate = managementFeeRate,
+            .coverRateLiquidation = TenthBips32{0}};
         LoanParameters const loanParams{
             .account = lender,
             .counter = borrower,
diff --git a/src/test/app/lending/LoanPay_test.cpp b/src/test/app/lending/LoanPay_test.cpp
index 9d840fe1bf..7cd31b7988 100644
--- a/src/test/app/lending/LoanPay_test.cpp
+++ b/src/test/app/lending/LoanPay_test.cpp
@@ -4,18 +4,26 @@
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -28,6 +36,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 
 namespace xrpl::test {
@@ -728,10 +738,762 @@ private:
         }
     }
 
+    // Which pseudo-account is left holding an unauthorized trust line when the
+    // repayment lands.
+    enum class UnauthorizedPayee {
+        // The vault's own line, as VaultCreate leaves it.
+        Vault,
+        // Same vault, but the issuer authorized the line by hand first.
+        VaultAuthorized,
+        // Vault line authorized, broker owner unable to take the fee, so the
+        // fee goes to the loan broker's pseudo-account instead.
+        Broker,
+    };
+
+    // A vault holding an IOU whose issuer requires authorization ends up with
+    // its own trust line unauthorized: VaultCreate opens the line without the
+    // auth flag, and the pseudo-account has no key to sign a TrustSet for
+    // itself. Neither deposits nor loan origination look at that line, so the
+    // vault appears to work right up to the first repayment, which is the only
+    // step that has to credit the vault back.
+    //
+    // The loan broker's pseudo-account has the same defect for the same reason,
+    // and LoanPay reaches it whenever the broker owner cannot take the fee.
+    //
+    // The issuer can still repair either line by hand, because TrustSet accepts
+    // a line that already exists even when its owner is a pseudo-account.
+    void
+    testRepayIntoUnauthorizedVault()
+    {
+        using namespace jtx;
+
+        Account const issuer{"issuer"};
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        auto runTestCases = [&](FeatureBitset features, UnauthorizedPayee payee) {
+            bool const pseudoExempt = features[fixCleanup3_4_0];
+            // With the vault's line repaired by the issuer, the only remaining
+            // unauthorized payee is the broker's pseudo-account.
+            bool const expectSuccess = pseudoExempt || payee == UnauthorizedPayee::VaultAuthorized;
+
+            auto const payeeLabel = [payee]() -> char const* {
+                switch (payee)
+                {
+                    case UnauthorizedPayee::Vault:
+                        return "vault";
+                    case UnauthorizedPayee::VaultAuthorized:
+                        return "vault authorized by the issuer";
+                    case UnauthorizedPayee::Broker:
+                        return "loan broker";
+                }
+                return "";  // LCOV_EXCL_LINE
+            }();
+
+            testcase << "LoanPay crediting an unauthorized " << payeeLabel << ": pseudo-account "
+                     << (pseudoExempt ? "exempt" : "not exempt");
+
+            Env env{*this, features};
+
+            env.fund(XRP(1'000'000), issuer, lender, borrower);
+            env.close();
+
+            env(fset(issuer, asfRequireAuth));
+            env.close();
+
+            PrettyAsset const asset = issuer[iouCurrency_];
+            env(trust(lender, asset(100'000'000)));
+            env(trust(borrower, asset(100'000'000)));
+            env.close();
+
+            // Authorize the two participants. Nothing asks the issuer to also
+            // authorize the vault, which is the whole point of this test.
+            env(trust(issuer, asset(0), lender, tfSetfAuth));
+            env(trust(issuer, asset(0), borrower, tfSetfAuth));
+            env.close();
+
+            env(pay(issuer, lender, asset(10'000'000)));
+            env(pay(issuer, borrower, asset(10'000)));
+            env.close();
+
+            // Creating the vault and funding it with deposits succeeds even
+            // though the vault cannot be authorized to hold the asset.
+            BrokerInfo const broker{createVaultAndBroker(env, asset, lender)};
+
+            auto const vaultSle = env.le(broker.vaultKeylet());
+            auto const brokerSle = env.le(broker.brokerKeylet());
+            if (!BEAST_EXPECT(vaultSle && brokerSle))
+                return;
+
+            Account const vaultPseudo{"vault pseudo-account", vaultSle->at(sfAccount)};
+            Account const brokerPseudo{"broker pseudo-account", brokerSle->at(sfAccount)};
+
+            auto const lineIsAuthorized = [&](Account const& holder) -> bool {
+                auto const line = env.le(keylet::trustLine(holder, asset.raw().get()));
+                if (!BEAST_EXPECT(line))
+                    return false;
+                return line->isFlag(holder.id() > issuer.id() ? lsfLowAuth : lsfHighAuth);
+            };
+
+            BEAST_EXPECT(!lineIsAuthorized(vaultPseudo));
+            BEAST_EXPECT(!lineIsAuthorized(brokerPseudo));
+
+            if (payee != UnauthorizedPayee::Vault)
+            {
+                env(trust(issuer, asset(0), vaultPseudo, tfSetfAuth));
+                env.close();
+                BEAST_EXPECT(lineIsAuthorized(vaultPseudo));
+            }
+
+            using namespace loan;
+
+            // The service fee guarantees the broker is owed something on the
+            // first payment, so the broker leg of the transfer is exercised.
+            Number const serviceFee = asset(2).value();
+            auto const loanKeylet = nextLoanKeylet(env, broker);
+            env(set(borrower, broker.brokerID, asset(1'000).value()),
+                Sig(sfCounterpartySignature, lender),
+                kLoanServiceFee(serviceFee),
+                kInterestRate(percentageToTenthBips(12)),
+                kPaymentTotal(12),
+                kPaymentInterval(600),
+                Fee(env.current()->fees().base * 2));
+            env.close();
+
+            // Paying the principal out of the vault never needed authorization.
+            BEAST_EXPECT(env.le(loanKeylet));
+
+            if (payee == UnauthorizedPayee::Broker)
+            {
+                // A deep-frozen owner cannot take the fee, so LoanPay pays it
+                // into the broker's pseudo-account instead.
+                env(trust(issuer, asset(0), lender, tfSetFreeze | tfSetDeepFreeze));
+                env.close();
+            }
+
+            auto const state = getCurrentState(env, broker, loanKeylet);
+            STAmount const payment{
+                broker.asset,
+                roundPeriodicPayment(
+                    broker.asset, state.periodicPayment + serviceFee, state.loanScale)};
+
+            // Repayment turns an outstanding loan back into cash the vault can
+            // lend again, so AssetsAvailable is what moves. AssetsTotal already
+            // counted the loan.
+            auto const assetsAvailable = [&]() -> Number {
+                auto const sle = env.le(broker.vaultKeylet());
+                if (!BEAST_EXPECT(sle))
+                    return Number{};
+                return sle->at(sfAssetsAvailable);
+            };
+
+            auto const borrowerBefore = env.balance(borrower, asset).number();
+            auto const vaultBefore = env.balance(vaultPseudo, asset).number();
+            auto const brokerBefore = env.balance(brokerPseudo, asset).number();
+            auto const assetsAvailableBefore = assetsAvailable();
+
+            env(pay(borrower, loanKeylet.key, payment),
+                Ter(expectSuccess ? TER{tesSUCCESS} : TER{tecNO_AUTH}));
+            env.close();
+
+            if (expectSuccess)
+            {
+                BEAST_EXPECT(env.balance(borrower, asset).number() < borrowerBefore);
+                BEAST_EXPECT(env.balance(vaultPseudo, asset).number() > vaultBefore);
+                BEAST_EXPECT(assetsAvailable() > assetsAvailableBefore);
+                // Confirms the broker variant really did route the fee to the
+                // pseudo-account rather than to the owner.
+                BEAST_EXPECT(
+                    (env.balance(brokerPseudo, asset).number() > brokerBefore) ==
+                    (payee == UnauthorizedPayee::Broker));
+
+                // The payee is skipped by the check, not authorized by it: the line that just
+                // took the credit is still missing its auth flag.
+                if (payee == UnauthorizedPayee::Vault)
+                    BEAST_EXPECT(!lineIsAuthorized(vaultPseudo));
+                if (payee == UnauthorizedPayee::Broker)
+                    BEAST_EXPECT(!lineIsAuthorized(brokerPseudo));
+            }
+            else
+            {
+                // A rejected repayment must leave every balance untouched.
+                BEAST_EXPECT(env.balance(borrower, asset).number() == borrowerBefore);
+                BEAST_EXPECT(env.balance(vaultPseudo, asset).number() == vaultBefore);
+                BEAST_EXPECT(env.balance(brokerPseudo, asset).number() == brokerBefore);
+                BEAST_EXPECT(assetsAvailable() == assetsAvailableBefore);
+            }
+        };
+
+        for (auto const& features : {all_, all_ - fixCleanup3_4_0})
+        {
+            runTestCases(features, UnauthorizedPayee::Vault);
+            runTestCases(features, UnauthorizedPayee::VaultAuthorized);
+            runTestCases(features, UnauthorizedPayee::Broker);
+        }
+    }
+
+    void
+    testLoanPayFundsConservedPayeeBelowReserve(FeatureBitset features)
+    {
+        // Regression test: LoanPay::doApply's fund-conservation check used to
+        // read XRP balances via accountHolds(..., SpendableHandling::
+        // FullBalance), which for XRP always defers to xrpLiquid (balance
+        // minus reserve, clamped at zero). When the broker fee landed on a
+        // payee sitting below its own reserve, that payee's clamped balance
+        // stayed zero and the fee vanished from the conservation sum,
+        // tripping "funds are conserved (with rounding)".
+        testcase("LoanPay funds conserved: broker fee payee below reserve");
+
+        using namespace jtx;
+
+        Env env(*this, features);
+
+        Account const issuer{"issuer"};
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        // Broker defaults match the fuzz workload: ManagementFeeRate = 100
+        // tenth-bips. The service fee guarantees feePaid > 0 on the first
+        // regular payment.
+        BrokerParameters const brokerParams;
+        Number const serviceFeeValue{2};
+        LoanParameters const loanParams{
+            .account = borrower,
+            .counter = lender,
+            .principalRequest = 1000,
+            .serviceFee = serviceFeeValue,
+            .interest = TenthBips32{percentageToTenthBips(12)},
+            .payTotal = 12,
+            .payInterval = 3600};
+
+        auto const loanOpt =
+            createLoan(env, AssetType::XRP, brokerParams, loanParams, issuer, lender, borrower);
+        if (BEAST_EXPECT(loanOpt); !loanOpt.has_value())
+            return;
+        auto const& [broker, loanKeylet, brokerPseudo] = *loanOpt;
+
+        auto const vaultPseudo = [&]() {
+            auto const vaultSle = env.le(keylet::vault(broker.vaultID));
+            if (!BEAST_EXPECT(vaultSle))
+                return AccountID{};
+            return vaultSle->at(sfAccount);
+        }();
+
+        // Raw AccountRoot balance, matching LoanPay::doApply's conservation
+        // check (not the reserve-clamped accountHolds()/xrpLiquid() value).
+        auto rawBalance = [&](AccountID const& id) -> STAmount {
+            auto const sle = env.le(keylet::account(id));
+            if (!BEAST_EXPECT(sle))
+                return STAmount{};
+            return sle->getFieldAmount(sfBalance);
+        };
+        auto lenderReserve = [&] {
+            return env.current()->fees().accountReserve(ownerCount(env, lender), 1);
+        };
+
+        STAmount const baseFee{env.current()->fees().base};
+
+        // Park the lender (broker owner, fee payee) exactly at its reserve,
+        // then burn part of the reserve with an oversized transaction fee.
+        // Fees are exempt from the reserve check, so the balance ends up
+        // below the reserve.
+        env(pay(lender, issuer, rawBalance(lender.id()) - lenderReserve() - baseFee));
+        env(noop(lender), Fee(XRP(100)));
+        env.close();
+        BEAST_EXPECT(env.balance(lender) < lenderReserve());
+
+        // First regular payment, exactly the amount due.
+        auto const state = getCurrentState(env, broker, loanKeylet);
+        STAmount const serviceFee = broker.asset(serviceFeeValue);
+        STAmount const roundedPeriodicPayment{
+            broker.asset,
+            roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)};
+        STAmount const totalDue = roundToScale(
+            roundedPeriodicPayment + serviceFee, state.loanScale, Number::RoundingMode::Upward);
+
+        auto const borrowerBefore = rawBalance(borrower.id());
+        auto const vaultBefore = rawBalance(vaultPseudo);
+        auto const lenderBefore = rawBalance(lender.id());
+
+        // Before the fix, this aborted inside LoanPay::doApply on
+        // XRPL_ASSERT_PARTS(goodRounding, "xrpl::LoanPay::doApply", "funds
+        // are conserved (with rounding)").
+        env(loan::pay(borrower, loanKeylet.key, totalDue));
+        env.close();
+
+        auto const borrowerAfter = rawBalance(borrower.id());
+        auto const vaultAfter = rawBalance(vaultPseudo);
+        auto const lenderAfter = rawBalance(lender.id());
+
+        // The broker fee reached the lender's AccountRoot, even though the
+        // lender's balance remains below its reserve.
+        BEAST_EXPECT(lenderAfter > lenderBefore);
+        BEAST_EXPECT(lenderAfter < lenderReserve());
+
+        // Total funds conserved across the payer, vault, and fee payee.
+        BEAST_EXPECT(
+            borrowerBefore - baseFee + vaultBefore + lenderBefore ==
+            borrowerAfter + vaultAfter + lenderAfter);
+    }
+
+    // Env::close() cannot land the ledger's parentCloseTime on an arbitrary
+    // instant: it always rounds the requested time forward to the next
+    // close-time-resolution boundary (see Env::close() and
+    // roundCloseTime()/effCloseTime() in LedgerTiming.h), so it can only be
+    // used to reach times strictly *after* a given due date, never exactly
+    // on it. To pin the exact-boundary behavior of isPaymentLate(), directly
+    // overwrite the loan's NextPaymentDueDate so that it matches the
+    // *current* (already fixed) parentCloseTime of the open ledger, without
+    // closing again. This exercises the same comparison
+    // (parentCloseTime vs. NextPaymentDueDate) at the exact boundary that
+    // env.close() cannot reliably reach.
+    void
+    setLoanNextPaymentDueDate(jtx::Env& env, Keylet const& loanKeylet, std::uint32_t dueDate)
+    {
+        using namespace jtx;
+        bool const ok = env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
+            auto const sle = view.read(loanKeylet);
+            if (!sle)
+                return false;
+            auto replacement = std::make_shared(*sle);
+            (*replacement)[sfNextPaymentDueDate] = dueDate;
+            view.rawReplace(replacement);
+            return true;
+        });
+        BEAST_EXPECT(ok);
+    }
+
+    // With fixCleanup3_4_0, isPaymentLate() uses a strict (Exclusive)
+    // comparison: a payment due exactly "now" is not yet late. A plain
+    // (non-late) LoanPay submitted at the exact NextPaymentDueDate instant
+    // must therefore succeed, advance the due date by exactly one
+    // PaymentInterval, and charge only the regular periodic payment amount
+    // (no late interest / late fee).
+    void
+    testLoanPayAtExactDueDateSucceedsPostAmendment()
+    {
+        testcase("LoanPay at exact due date succeeds with fixCleanup3_4_0");
+
+        using namespace jtx;
+        using namespace loan;
+
+        Env env(*this, all_);
+        BEAST_EXPECT(env.enabled(fixCleanup3_4_0));
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(10'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const asset{xrpIssue(), 1000};
+        auto const broker = createVaultAndBroker(env, asset, lender);
+
+        auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
+        if (!BEAST_EXPECT(brokerSle))
+            return;
+        auto const loanKeylet =
+            keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
+
+        // Set a large, non-zero late interest rate and late fee so that if
+        // the late-payment path were incorrectly taken, the extra charge
+        // would be large and easy to detect (far more than any rounding
+        // slack in the regular periodic payment amount).
+        env(set(borrower, broker.brokerID, asset(1'000).value()),
+            Sig(sfCounterpartySignature, lender),
+            kPaymentTotal(12),
+            kPaymentInterval(600),
+            kLateInterestRate(TenthBips32(percentageToTenthBips(24))),
+            kLatePaymentFee(asset(50).value()),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+
+        auto const stateBefore = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(stateBefore.paymentRemaining == 12);
+
+        STAmount const roundedPeriodicPayment{
+            asset, roundPeriodicPayment(asset, stateBefore.periodicPayment, stateBefore.loanScale)};
+
+        // Set NextPaymentDueDate to exactly the current parentCloseTime,
+        // without closing the ledger again.
+        std::uint32_t const exactDueDate =
+            env.current()->parentCloseTime().time_since_epoch().count();
+        setLoanNextPaymentDueDate(env, loanKeylet, exactDueDate);
+
+        STAmount const payFee{env.current()->fees().base};
+        auto const borrowerBefore = env.balance(borrower, asset).number();
+
+        // A plain payment (no tfLoanLatePayment) for exactly the regular
+        // periodic amount must succeed: at this instant the payment is not
+        // yet late.
+        //
+        // Note: deliberately not calling env.close() after this: closing
+        // the ledger re-derives the resulting state from the last validated
+        // ledger plus the recorded transaction set, which would discard the
+        // direct NextPaymentDueDate override made above via rawReplace().
+        // Reading state from the still-open ledger (as env.le()/env.balance()
+        // do) reflects the transaction as it was actually applied.
+        env(pay(borrower, loanKeylet.key, roundedPeriodicPayment), Fee(payFee), Ter(tesSUCCESS));
+
+        auto const borrowerAfter = env.balance(borrower, asset).number();
+
+        // No more than the regular periodic amount (plus the transaction
+        // fee) was charged: if the late-payment path had wrongly been
+        // taken, the (large, non-zero) late interest and late fee set above
+        // would have pushed the charge well past this bound.
+        Number const charged = borrowerBefore - borrowerAfter - Number{payFee};
+        BEAST_EXPECT(charged > Number{});
+        BEAST_EXPECT(charged <= Number{roundedPeriodicPayment});
+
+        auto const stateAfter = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining - 1);
+        BEAST_EXPECT(stateAfter.nextPaymentDate == exactDueDate + stateBefore.paymentInterval);
+    }
+
+    // Pins the amendment gate itself (as opposed to
+    // testLoanPayAtExactDueDateSucceedsPostAmendment, which pins the
+    // comparison operator): without fixCleanup3_4_0, isPaymentLate() keeps
+    // using the pre-amendment Inclusive comparison, so a payment due exactly
+    // "now" is already considered late, and a plain (non-late) LoanPay is
+    // rejected.
+    void
+    testLoanPayAtExactDueDateFailsPreAmendment()
+    {
+        testcase("LoanPay at exact due date fails without fixCleanup3_4_0");
+
+        using namespace jtx;
+        using namespace loan;
+
+        Env env(*this, all_ - fixCleanup3_4_0);
+        BEAST_EXPECT(!env.enabled(fixCleanup3_4_0));
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(10'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const asset{xrpIssue(), 1000};
+        auto const broker = createVaultAndBroker(env, asset, lender);
+
+        auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
+        if (!BEAST_EXPECT(brokerSle))
+            return;
+        auto const loanKeylet =
+            keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
+
+        env(set(borrower, broker.brokerID, asset(1'000).value()),
+            Sig(sfCounterpartySignature, lender),
+            kPaymentTotal(12),
+            kPaymentInterval(600),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+
+        auto const stateBefore = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(stateBefore.paymentRemaining == 12);
+
+        STAmount const roundedPeriodicPayment{
+            asset, roundPeriodicPayment(asset, stateBefore.periodicPayment, stateBefore.loanScale)};
+
+        // Set NextPaymentDueDate to exactly the current parentCloseTime,
+        // without closing the ledger again.
+        std::uint32_t const exactDueDate =
+            env.current()->parentCloseTime().time_since_epoch().count();
+        setLoanNextPaymentDueDate(env, loanKeylet, exactDueDate);
+
+        // Without the amendment, the due date is already considered late at
+        // this exact instant, so a plain payment must be rejected.
+        //
+        // Note: deliberately not calling env.close() after this: closing
+        // the ledger re-derives the resulting state from the last validated
+        // ledger plus the recorded transaction set, which would discard the
+        // direct NextPaymentDueDate override made above via rawReplace().
+        // Reading state from the still-open ledger (as env.le() does)
+        // reflects the transaction as it was actually applied.
+        env(pay(borrower, loanKeylet.key, roundedPeriodicPayment), Ter(tecEXPIRED));
+
+        auto const stateAfter = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining);
+        BEAST_EXPECT(stateAfter.nextPaymentDate == exactDueDate);
+    }
+
+    // computeLatePayment() must agree with isPaymentLate() at the exact
+    // due-date boundary: once fixCleanup3_4_0 is enabled, a payment due
+    // exactly "now" is not yet late, so a tfLoanLatePayment submitted at
+    // that same instant must be rejected with tecTOO_SOON rather than being
+    // admitted and charged the late interest/fee.
+    void
+    testLoanLatePaymentAtExactDueDateRejectedPostAmendment()
+    {
+        testcase("LoanPay(tfLoanLatePayment) at exact due date rejected with fixCleanup3_4_0");
+
+        using namespace jtx;
+        using namespace loan;
+
+        Env env(*this, all_);
+        BEAST_EXPECT(env.enabled(fixCleanup3_4_0));
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(10'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const asset{xrpIssue(), 1000};
+        auto const broker = createVaultAndBroker(env, asset, lender);
+
+        auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
+        if (!BEAST_EXPECT(brokerSle))
+            return;
+        auto const loanKeylet =
+            keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
+
+        env(set(borrower, broker.brokerID, asset(1'000).value()),
+            Sig(sfCounterpartySignature, lender),
+            kPaymentTotal(12),
+            kPaymentInterval(600),
+            kLateInterestRate(TenthBips32(percentageToTenthBips(24))),
+            kLatePaymentFee(asset(50).value()),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+
+        auto const stateBefore = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(stateBefore.paymentRemaining == 12);
+
+        // Overpay generously so that, if the late-payment path were
+        // incorrectly admitted, funds would not be the limiting factor;
+        // we want to isolate the timing check itself.
+        STAmount const generousAmount{
+            asset,
+            roundPeriodicPayment(asset, stateBefore.periodicPayment, stateBefore.loanScale) * 2};
+
+        // Set NextPaymentDueDate to exactly the current parentCloseTime,
+        // without closing the ledger again.
+        std::uint32_t const exactDueDate =
+            env.current()->parentCloseTime().time_since_epoch().count();
+        setLoanNextPaymentDueDate(env, loanKeylet, exactDueDate);
+
+        // At this exact instant the loan is not yet late (Exclusive
+        // comparison), so even an explicit late payment must be rejected
+        // as premature, matching the plain-payment path.
+        //
+        // Note: deliberately not calling env.close() after this, for the
+        // same reason given in testLoanPayAtExactDueDateSucceedsPostAmendment
+        // above: closing would discard the direct NextPaymentDueDate
+        // override made via rawReplace().
+        env(pay(borrower, loanKeylet.key, generousAmount, tfLoanLatePayment), Ter(tecTOO_SOON));
+
+        auto const stateAfter = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining);
+        BEAST_EXPECT(stateAfter.nextPaymentDate == exactDueDate);
+    }
+
+    // calculateBaseFee must use isPaymentLate(), not a raw inclusive
+    // hasExpired(): once fixCleanup3_4_0 is enabled, a plain catch-up at
+    // exactly NextPaymentDueDate succeeds and can process many payments, so
+    // the fee has to scale with that work. Charging a single base fee here
+    // would disagree with apply (and with the fixCleanup3_1_3 cap).
+    void
+    testLoanPayCatchUpFeeAtExactDueDatePostAmendment()
+    {
+        testcase("LoanPay catch-up fee at exact due date with fixCleanup3_4_0");
+
+        using namespace jtx;
+        using namespace loan;
+        using namespace lending;
+
+        Env env(*this, all_);
+        BEAST_EXPECT(env.enabled(fixCleanup3_4_0));
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(10'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const asset{xrpIssue(), 1000};
+        auto const broker = createVaultAndBroker(env, asset, lender);
+
+        auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
+        if (!BEAST_EXPECT(brokerSle))
+            return;
+        auto const loanKeylet =
+            keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
+
+        env(set(borrower, broker.brokerID, asset(10'000).value()),
+            Sig(sfCounterpartySignature, lender),
+            kPaymentTotal(50),
+            kPaymentInterval(600),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+
+        auto const stateBefore = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(stateBefore.paymentRemaining == 50);
+        BEAST_EXPECT(stateBefore.paymentRemaining > kLoanPaymentsPerFeeIncrement);
+
+        auto const loanSle = env.le(loanKeylet);
+        if (!BEAST_EXPECT(loanSle))
+            return;
+        Number const regularPayment =
+            roundPeriodicPayment(asset, stateBefore.periodicPayment, stateBefore.loanScale) +
+            loanSle->at(sfLoanServiceFee);
+        int const payCount = kLoanPaymentsPerFeeIncrement * 4;
+        STAmount const catchUp{asset, regularPayment * payCount};
+        XRPAmount const baseFee = env.current()->fees().base;
+        XRPAmount const escalatedFee{baseFee * (payCount / kLoanPaymentsPerFeeIncrement)};
+
+        std::uint32_t const exactDueDate =
+            env.current()->parentCloseTime().time_since_epoch().count();
+        setLoanNextPaymentDueDate(env, loanKeylet, exactDueDate);
+
+        // Under-fee: apply would process `payCount` payments, so a single
+        // base fee is not enough.
+        env(pay(borrower, loanKeylet.key, catchUp), Fee(baseFee), Ter(telINSUF_FEE_P));
+
+        // Same catch-up with the scaled fee must succeed at this instant.
+        // Do not env.close() after the SLE override (see
+        // testLoanPayAtExactDueDateSucceedsPostAmendment).
+        env(pay(borrower, loanKeylet.key, catchUp), Fee(escalatedFee), Ter(tesSUCCESS));
+
+        auto const stateAfter = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining - payCount);
+    }
+
+    // Without the amendment, inclusive hasExpired still treats the exact
+    // due-date instant as late, so calculateBaseFee correctly charges a
+    // single base fee and apply rejects a plain LoanPay with tecEXPIRED.
+    void
+    testLoanPayCatchUpFeeAtExactDueDatePreAmendment()
+    {
+        testcase("LoanPay catch-up fee at exact due date without fixCleanup3_4_0");
+
+        using namespace jtx;
+        using namespace loan;
+        using namespace lending;
+
+        Env env(*this, all_ - fixCleanup3_4_0);
+        BEAST_EXPECT(!env.enabled(fixCleanup3_4_0));
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(10'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const asset{xrpIssue(), 1000};
+        auto const broker = createVaultAndBroker(env, asset, lender);
+
+        auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
+        if (!BEAST_EXPECT(brokerSle))
+            return;
+        auto const loanKeylet =
+            keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
+
+        env(set(borrower, broker.brokerID, asset(10'000).value()),
+            Sig(sfCounterpartySignature, lender),
+            kPaymentTotal(50),
+            kPaymentInterval(600),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+
+        auto const stateBefore = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(stateBefore.paymentRemaining == 50);
+
+        auto const loanSle = env.le(loanKeylet);
+        if (!BEAST_EXPECT(loanSle))
+            return;
+        Number const regularPayment =
+            roundPeriodicPayment(asset, stateBefore.periodicPayment, stateBefore.loanScale) +
+            loanSle->at(sfLoanServiceFee);
+        int const payCount = kLoanPaymentsPerFeeIncrement * 4;
+        STAmount const catchUp{asset, regularPayment * payCount};
+        XRPAmount const baseFee = env.current()->fees().base;
+
+        std::uint32_t const exactDueDate =
+            env.current()->parentCloseTime().time_since_epoch().count();
+        setLoanNextPaymentDueDate(env, loanKeylet, exactDueDate);
+
+        env(pay(borrower, loanKeylet.key, catchUp), Fee(baseFee), Ter(tecEXPIRED));
+
+        auto const stateAfter = getCurrentState(env, broker, loanKeylet);
+        BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining);
+        BEAST_EXPECT(stateAfter.nextPaymentDate == exactDueDate);
+    }
+
+    // LoanPay does not call canAddHolding. addEmptyHolding recreates the
+    // broker-owner holding when the borrower is also the broker owner. After
+    // fixCleanup3_4_0 an existing line is a no-op even if DefaultRipple is
+    // off; pre-fix that path dies with tecINTERNAL.
+    void
+    testLoanPaySelfBrokerExistingLineDefaultRipple()
+    {
+        using namespace jtx;
+        using namespace loan;
+
+        auto run = [this](FeatureBitset features, TER expected) {
+            testcase(
+                std::string(
+                    "LoanPay broker-owner borrower existing line after "
+                    "issuer clears asfDefaultRipple (") +
+                (features[fixCleanup3_4_0] ? "post" : "pre") + "-fixCleanup3_4_0)");
+
+            Env env(*this, features);
+            Account const issuer{"issuer"};
+            Account const alice{"alice"};
+
+            env.fund(XRP(10'000), issuer, alice);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            env(trust(alice, usd(10'000'000)));
+            env.close();
+            env(pay(issuer, alice, usd(2'000'000)));
+            env.close();
+
+            auto const broker = createVaultAndBroker(env, usd, alice);
+            auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
+            if (!BEAST_EXPECT(brokerSle))
+                return;
+            auto const loanKeylet =
+                keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
+
+            Number const serviceFee = usd(2).value();
+            env(set(alice, broker.brokerID, usd(1'000).value()),
+                Sig(sfCounterpartySignature, alice),
+                kLoanServiceFee(serviceFee),
+                Fee(env.current()->fees().base * 2));
+            env.close();
+
+            env(fclear(issuer, asfDefaultRipple));
+            env.close();
+            BEAST_EXPECT(env.le(keylet::trustLine(alice.id(), usd.raw().get())));
+
+            auto const state = getCurrentState(env, broker, loanKeylet);
+            STAmount const payment{
+                usd,
+                roundPeriodicPayment(usd, state.periodicPayment + serviceFee, state.loanScale)};
+
+            env(pay(alice, loanKeylet.key, payment), Ter(expected));
+            env.close();
+        };
+
+        run(all_ - fixCleanup3_4_0, tecINTERNAL);
+        run(all_, tesSUCCESS);
+    }
+
     void
     runAmendmentIndependent()
     {
         testLoanSetNearZeroInterestRateSucceeds();
+        testLoanPayAtExactDueDateSucceedsPostAmendment();
+        testLoanPayAtExactDueDateFailsPreAmendment();
+        testLoanLatePaymentAtExactDueDateRejectedPostAmendment();
+        testLoanPayCatchUpFeeAtExactDueDatePostAmendment();
+        testLoanPayCatchUpFeeAtExactDueDatePreAmendment();
+        testRepayIntoUnauthorizedVault();
+        testLoanPaySelfBrokerExistingLineDefaultRipple();
     }
 
     // Tests run under each entry in amendmentCombinations().
@@ -741,6 +1503,7 @@ private:
 #if LOAN_TODO
         testLoanPayLateFullPaymentBypassesPenalties(features);
 #endif
+        testLoanPayFundsConservedPayeeBelowReserve(features);
         testOverpaymentManagementFee(features);
         testDosLoanPay(features);
         testLoanNextPaymentDueDateOverflow(features);
diff --git a/src/test/app/lending/LoanRounding_test.cpp b/src/test/app/lending/LoanRounding_test.cpp
index 5e69c9f79e..ded1c816a2 100644
--- a/src/test/app/lending/LoanRounding_test.cpp
+++ b/src/test/app/lending/LoanRounding_test.cpp
@@ -889,6 +889,261 @@ private:
         env.close();
     }
 
+    // Pre-fixCleanup3_4_0 bug: VaultWithdraw for a fixed *share* amount that
+    // rounds to zero assets trips tecINVARIANT_FAILED instead of failing
+    // cleanly or succeeding, depending on why it's zero. The fixed-shares
+    // branch had no zero guard, unlike the fixed-assets branch.
+    // XRP case: pool value is nonzero (2,000,000) but 1 share's worth (0.5
+    // drops) truncates to zero drops -> real precision loss -> tecPRECISION_LOSS.
+    // IOU case: loan drew 100% of the vault and is fully impaired, so
+    // AssetsTotal == LossUnrealized exactly -> pool value is genuinely zero
+    // -> legitimate zero-value withdrawal -> tesSUCCESS.
+    void
+    testBugVaultWithdrawFixedSharesRoundsToZero(FeatureBitset features)
+    {
+        testcase("bug: VaultWithdraw fixed shares round down to zero assets");
+
+        using namespace jtx;
+        using namespace loan;
+
+        bool const fixed = features[fixCleanup3_4_0];
+
+        Env env(*this, features);
+
+        Account const lender{"lender"};
+        Account const depositorB{"depositorB"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(10'000'000), lender, depositorB, borrower);
+        env.close();
+
+        // asset(n) == n drops.
+        PrettyAsset const xrpAsset{xrpIssue(), 1};
+
+        auto const broker = createVaultAndBroker(
+            env,
+            xrpAsset,
+            lender,
+            {.vaultDeposit = 1'000'000, .debtMax = 3'000'000, .coverDeposit = 1'000'000});
+
+        Vault const v{env};
+        env(v.deposit(
+            {.depositor = depositorB,
+             .id = broker.vaultKeylet().key,
+             .amount = xrpAsset(3'000'000)}));
+        env.close();
+
+        auto const brokerSle = env.le(broker.brokerKeylet());
+        if (!BEAST_EXPECT(brokerSle))
+            return;
+        auto const loanKeylet =
+            keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
+
+        env(set(borrower, broker.brokerID, Number{2'000'000}),
+            Sig(sfCounterpartySignature, lender),
+            kPaymentTotal(2),
+            kPaymentInterval(600),
+            Fee(env.current()->fees().base * 2),
+            Ter(tesSUCCESS));
+        env.close();
+
+        // Impair the loan so LossUnrealized > 0.
+        advancePastDueDate(env, loanKeylet);
+        env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultSle = env.le(broker.vaultKeylet());
+        if (!BEAST_EXPECT(vaultSle))
+            return;
+        BEAST_EXPECT(vaultSle->at(sfLossUnrealized) > beast::kZero);
+
+        // (AssetsTotal 4M - LossUnrealized 2M) * 1 share / 4M shares = 0.5,
+        // rounds down to zero drops.
+        auto const shareAsset = vaultSle->at(sfShareMPTID);
+        STAmount const oneShare{MPTIssue{shareAsset}, Number(1)};
+
+        env(v.withdraw({.depositor = lender, .id = broker.vaultKeylet().key, .amount = oneShare}),
+            Ter(fixed ? tecPRECISION_LOSS : tecINVARIANT_FAILED));
+        env.close();
+
+        // Same bug, IOU asset. Needs a 2nd, minimal depositor: a sole
+        // shareholder would waive the loss subtraction (fixCleanup3_2_0),
+        // returning full value instead of zero.
+        {
+            Account const issuer{"issuer"};
+            Account const iouLender{"iouLender"};
+            Account const iouDepositorB{"iouDepositorB"};
+            Account const iouBorrower{"iouBorrower"};
+
+            env.fund(XRP(10'000'000), issuer, iouLender, iouDepositorB, iouBorrower);
+            env.close();
+
+            PrettyAsset const iouAsset = issuer[iouCurrency_];
+            env(trust(iouLender, iouAsset(10'000'000)));
+            env(trust(iouDepositorB, iouAsset(10'000'000)));
+            env(trust(iouBorrower, iouAsset(10'000'000)));
+            // iouLender funds the vault deposit and the broker's cover deposit.
+            env(pay(issuer, iouLender, iouAsset(9'000'000)));
+            env(pay(issuer, iouDepositorB, iouAsset(1)));
+            env.close();
+
+            // No management fee -> LossUnrealized ends up == AssetsTotal.
+            auto const iouBroker = createVaultAndBroker(
+                env,
+                iouAsset,
+                iouLender,
+                {.vaultDeposit = 3'999'999,
+                 .debtMax = 4'000'000,
+                 .coverDeposit = 4'000'000,
+                 .managementFeeRate = TenthBips16{0}});
+
+            env(v.deposit(
+                {.depositor = iouDepositorB,
+                 .id = iouBroker.vaultKeylet().key,
+                 .amount = iouAsset(1)}));
+            env.close();
+
+            auto const iouBrokerSle = env.le(iouBroker.brokerKeylet());
+            if (!BEAST_EXPECT(iouBrokerSle))
+                return;
+            auto const iouLoanKeylet = keylet::loan(
+                iouBroker.brokerID, SeqProxy::rawSequence(iouBrokerSle->at(sfLoanSequence)));
+
+            // Draw the entire vault out as a single loan.
+            env(set(iouBorrower, iouBroker.brokerID, Number{4'000'000}),
+                Sig(sfCounterpartySignature, iouLender),
+                kPaymentTotal(2),
+                kPaymentInterval(600),
+                Fee(env.current()->fees().base * 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            advancePastDueDate(env, iouLoanKeylet);
+            env(manage(iouLender, iouLoanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
+            env.close();
+
+            auto const iouVaultSle = env.le(iouBroker.vaultKeylet());
+            if (!BEAST_EXPECT(iouVaultSle))
+                return;
+            BEAST_EXPECT(iouVaultSle->at(sfLossUnrealized) == iouVaultSle->at(sfAssetsTotal));
+
+            auto const iouShareAsset = iouVaultSle->at(sfShareMPTID);
+            STAmount const oneIouShare{MPTIssue{iouShareAsset}, Number(1)};
+
+            auto const iouLenderBalanceBefore = env.balance(iouLender, iouAsset);
+            auto const iouVaultAvailableBefore = iouVaultSle->at(sfAssetsAvailable);
+            // Env::balance can't be used for shares: it resolves the issuer
+            // name, and the share issuer is the vault pseudo-account, which
+            // Env doesn't know.
+            auto const lenderShares = [&]() -> std::uint64_t {
+                auto const sle = env.le(keylet::mptoken(iouShareAsset, iouLender.id()));
+                return sle ? sle->at(sfMPTAmount) : 0;
+            };
+            auto const iouLenderSharesBefore = lenderShares();
+            auto const iouIssuanceBefore = env.le(keylet::mptokenIssuance(iouShareAsset));
+            if (!BEAST_EXPECT(iouIssuanceBefore))
+                return;
+            auto const iouSharesOutstandingBefore = iouIssuanceBefore->at(sfOutstandingAmount);
+            env(v.withdraw(
+                    {.depositor = iouLender,
+                     .id = iouBroker.vaultKeylet().key,
+                     .amount = oneIouShare}),
+                fixed ? Ter(tesSUCCESS) : Ter(tecINVARIANT_FAILED));
+            env.close();
+
+            if (fixed)
+            {
+                // Confirm this was a true zero-value transfer: balances
+                // unchanged even though a share was burned.
+                BEAST_EXPECT(env.balance(iouLender, iouAsset) == iouLenderBalanceBefore);
+                BEAST_EXPECT(lenderShares() == iouLenderSharesBefore - 1);
+                auto const iouIssuanceAfter = env.le(keylet::mptokenIssuance(iouShareAsset));
+                if (BEAST_EXPECT(iouIssuanceAfter))
+                {
+                    BEAST_EXPECT(
+                        iouIssuanceAfter->at(sfOutstandingAmount) ==
+                        iouSharesOutstandingBefore - 1);
+                }
+                auto const iouVaultAfter = env.le(iouBroker.vaultKeylet());
+                if (BEAST_EXPECT(iouVaultAfter))
+                {
+                    BEAST_EXPECT(iouVaultAfter->at(sfAssetsAvailable) == iouVaultAvailableBefore);
+                }
+            }
+        }
+    }
+
+    // Companion to the Vault_test dust-debit tests, which use a single
+    // depositor so AssetsTotal == AssetsAvailable and both debitIsNonZeroDust
+    // operands in VaultWithdraw::doApply trip together. Here a loan draws
+    // almost the entire vault, leaving AssetsTotal (1e7) far above
+    // AssetsAvailable (100): redeeming 1 share moves 1e-10 assets, which is
+    // dust against AssetsTotal but representable against AssetsAvailable, so
+    // the AssetsTotal operand alone carries the rejection.
+    void
+    testBugVaultWithdrawDustVsAssetsTotal(FeatureBitset features)
+    {
+        testcase("bug: VaultWithdraw dust debit vs AssetsTotal only");
+
+        using namespace jtx;
+        using namespace loan;
+
+        bool const fixed = features[fixCleanup3_4_0];
+
+        Env env(*this, features);
+
+        Account const issuer{"issuer"};
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(10'000'000), issuer, lender, borrower);
+        env.close();
+
+        PrettyAsset const iouAsset = issuer[iouCurrency_];
+        env(trust(lender, iouAsset(100'000'000)));
+        env(trust(borrower, iouAsset(100'000'000)));
+        env(pay(issuer, lender, iouAsset(20'000'000)));
+        env.close();
+
+        // Scale 10 so 1 share is worth 1e-10 assets against the 1e7 pool.
+        auto const broker = createVaultAndBroker(
+            env,
+            iouAsset,
+            lender,
+            {.vaultDeposit = 10'000'000,
+             .debtMax = 10'000'000,
+             .coverDeposit = 1'000'000,
+             .vaultScale = 10});
+
+        // Draw all but 100 units: AssetsAvailable drops to 100 while
+        // AssetsTotal stays at 1e7 (the loan is still an asset of the vault).
+        env(set(borrower, broker.brokerID, Number{9'999'900}),
+            Sig(sfCounterpartySignature, lender),
+            kPaymentTotal(2),
+            kPaymentInterval(600),
+            Fee(env.current()->fees().base * 2),
+            Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultSle = env.le(broker.vaultKeylet());
+        if (!BEAST_EXPECT(vaultSle))
+            return;
+        BEAST_EXPECT(vaultSle->at(sfAssetsTotal) == Number{10'000'000});
+        BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == Number{100});
+
+        // 1 share redeems 1e7 * 1 / 1e17 = 1e-10 assets. Subtracting that
+        // from AssetsTotal needs 18 significant digits and canonicalizes
+        // straight back to 1e7 (no-op), while AssetsAvailable would become
+        // 99.9999999999 — perfectly representable.
+        auto const shareAsset = vaultSle->at(sfShareMPTID);
+        STAmount const oneShare{MPTIssue{shareAsset}, Number(1)};
+
+        Vault const v{env};
+        env(v.withdraw({.depositor = lender, .id = broker.vaultKeylet().key, .amount = oneShare}),
+            Ter(fixed ? tecPRECISION_LOSS : tecINVARIANT_FAILED));
+        env.close();
+    }
+
     // A near-zero interest rate on a 100 USD loan
     // produces total interest of ~6 units at loanScale -9. Numerical error
     // in the amortization formula pushes the theoretical principal above
@@ -966,6 +1221,10 @@ private:
             testYieldTheftRounding(flags);
         testBugOverpaymentPrincipalChange();
         testBugOverpayUnroundedAmount();
+        testBugVaultWithdrawFixedSharesRoundsToZero(all_ - fixCleanup3_4_0);
+        testBugVaultWithdrawFixedSharesRoundsToZero(all_);
+        testBugVaultWithdrawDustVsAssetsTotal(all_ - fixCleanup3_4_0);
+        testBugVaultWithdrawDustVsAssetsTotal(all_);
         testBugInterestDueDeltaCrash();
     }
 
diff --git a/src/test/app/lending/LoanSecurity_test.cpp b/src/test/app/lending/LoanSecurity_test.cpp
index 21772d0617..54c972b505 100644
--- a/src/test/app/lending/LoanSecurity_test.cpp
+++ b/src/test/app/lending/LoanSecurity_test.cpp
@@ -5,27 +5,37 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl::test {
@@ -33,6 +43,30 @@ namespace xrpl::test {
 class LoanSecurity_test : public LoanTestBase
 {
 private:
+    // Env::close() cannot land the ledger's parentCloseTime on an arbitrary
+    // instant: it always rounds the requested time forward to the next
+    // close-time-resolution boundary (see Env::close() and
+    // roundCloseTime()/effCloseTime() in LedgerTiming.h), so it can only be
+    // used to reach times strictly *after* a given due date, never exactly
+    // on it. To pin the exact-boundary behavior of isPaymentLate(), directly
+    // overwrite the loan's NextPaymentDueDate instead, without closing the
+    // ledger again.
+    void
+    setLoanNextPaymentDueDate(jtx::Env& env, Keylet const& loanKeylet, std::uint32_t dueDate)
+    {
+        using namespace jtx;
+        bool const ok = env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
+            auto const sle = view.read(loanKeylet);
+            if (!sle)
+                return false;
+            auto replacement = std::make_shared(*sle);
+            (*replacement)[sfNextPaymentDueDate] = dueDate;
+            view.rawReplace(replacement);
+            return true;
+        });
+        BEAST_EXPECT(ok);
+    }
+
     void
     testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(FeatureBitset features)
     {
@@ -411,13 +445,17 @@ private:
         Account const depositor{"depositor"};
         auto const txFee = Fee(XRP(100));
 
+        // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
+        // accepts closed-ended vaults, so build one and advance past
+        // SubscriptionDate before creating the broker and the loan.
         Env env(*this);
         Vault const vault(env);
 
         env.fund(XRP(10'000), lender, issuer, borrower, depositor);
         env.close();
 
-        auto [tx, vaultKeyLet] = vault.create({.owner = lender, .asset = xrpIssue()});
+        auto [tx, vaultKeyLet, subscriptionDate] =
+            vault.createClosedEnded({.owner = lender, .asset = xrpIssue()});
         env(tx, txFee);
         env.close();
 
@@ -425,6 +463,10 @@ private:
             txFee);
         env.close();
 
+        // Move into the Investment phase before creating the broker and
+        // the loan.
+        vault.closePastSubscription(subscriptionDate);
+
         auto const brokerKeyLet =
             keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender)));
 
@@ -508,10 +550,622 @@ private:
             PaymentParameters{.showStepBalances = true});
     }
 
+    // Verify that with fixCleanup3_4_0:
+    // 1. A loan cannot be impaired before its payment is late.
+    // 2. Impairing a late loan does not change sfNextPaymentDueDate.
+    // 3. The unimpair operation does not change sfNextPaymentDueDate.
+    void
+    testImpairmentPaymentDateUnchanged()
+    {
+        using namespace jtx;
+        using namespace loan;
+        using namespace std::chrono_literals;
+
+        testcase("Impairment does not change payment due date");
+
+        Env env(*this, all_ | fixCleanup3_4_0);
+        BEAST_EXPECT(env.enabled(fixCleanup3_4_0));
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(100'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+        auto const broker = createVaultAndBroker(env, xrpAsset, lender);
+
+        auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
+        if (!BEAST_EXPECT(sleBroker))
+            return;
+        auto const loanKeylet =
+            keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
+
+        Number const principalRequest{1, 3};
+        env(set(borrower, broker.brokerID, broker.asset(principalRequest).value()),
+            Sig(sfCounterpartySignature, lender),
+            kPaymentTotal(12),
+            kPaymentInterval(600),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+
+        auto const loanSle = env.le(loanKeylet);
+        if (!BEAST_EXPECT(loanSle))
+            return;
+        std::uint32_t const originalNextDueDate = loanSle->at(sfNextPaymentDueDate);
+        BEAST_EXPECT(originalNextDueDate > 0);
+
+        // 1. Impairment must fail when payment is not yet late
+        env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecTOO_SOON));
+
+        {
+            auto const loan = env.le(loanKeylet);
+            BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
+        }
+
+        // 1b. Impairment must still fail at the exact due date instant: a
+        // payment due "now" is not yet late (strict/Exclusive comparison).
+        // Temporarily set NextPaymentDueDate to exactly the current
+        // parentCloseTime (without closing the ledger again), exercise the
+        // check, then restore the original due date.
+        {
+            std::uint32_t const exactNow =
+                env.current()->parentCloseTime().time_since_epoch().count();
+            setLoanNextPaymentDueDate(env, loanKeylet, exactNow);
+
+            env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecTOO_SOON));
+
+            setLoanNextPaymentDueDate(env, loanKeylet, originalNextDueDate);
+        }
+
+        {
+            auto const loan = env.le(loanKeylet);
+            BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
+        }
+
+        env.close(NetClock::time_point{NetClock::duration{originalNextDueDate}} + 1s);
+
+        // 2. Impairment succeeds when payment is late
+        env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
+
+        {
+            auto const loan = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loan))
+                return;
+            BEAST_EXPECT(loan->isFlag(lsfLoanImpaired));
+            BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
+        }
+
+        // 3. Unimpair also does not change sfNextPaymentDueDate
+        env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tesSUCCESS));
+
+        {
+            auto const loan = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loan))
+                return;
+            BEAST_EXPECT(!loan->isFlag(lsfLoanImpaired));
+            BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
+        }
+    }
+
+    // Verify that without fixCleanup3_4_0, the pre-amendment
+    // impair/unimpair behaviour is preserved:
+    // 1. Impairing a loan before its payment is late moves
+    //    sfNextPaymentDueDate to "now".
+    // 2a. Unimpair within the original payment interval restores
+    //     sfNextPaymentDueDate to StartDate + PaymentInterval.
+    // 2b. Unimpair after the original due date sets
+    //     sfNextPaymentDueDate to now + PaymentInterval.
+    void
+    testImpairmentPaymentDatePreAmendment()
+    {
+        using namespace jtx;
+        using namespace loan;
+        using namespace std::chrono_literals;
+
+        testcase("Pre-amendment impair/unimpair date restoration");
+
+        Env env(*this, all_ - fixCleanup3_4_0);
+        BEAST_EXPECT(!env.enabled(fixCleanup3_4_0));
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(100'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+        auto const broker = createVaultAndBroker(env, xrpAsset, lender);
+
+        Number const principalRequest{1, 3};
+        auto createNewLoan = [&]() {
+            auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
+            if (!BEAST_EXPECT(sleBroker))
+                return keylet::loan(uint256{});
+            auto const lk =
+                keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
+            env(set(borrower, broker.brokerID, broker.asset(principalRequest).value()),
+                Sig(sfCounterpartySignature, lender),
+                kPaymentTotal(12),
+                kPaymentInterval(600),
+                Fee(env.current()->fees().base * 2));
+            env.close();
+            return lk;
+        };
+
+        // Default + delete a loan and replenish first-loss capital so the
+        // broker is ready for the next loan.
+        auto cleanupLoan = [&](Keylet const& loanKeylet, std::uint32_t dueDate) {
+            env.close(NetClock::time_point{NetClock::duration{dueDate + 60}} + 1s);
+            env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
+            env.close();
+
+            auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
+            if (!BEAST_EXPECT(brokerSle))
+                return;
+            auto const coverNeeded =
+                broker.asset(broker.params.coverDeposit).value() - brokerSle->at(sfCoverAvailable);
+            if (coverNeeded > 0)
+            {
+                env(loan_broker::coverDeposit(
+                    lender, broker.brokerID, STAmount{broker.asset, coverNeeded}));
+                env.close();
+            }
+            env(del(lender, loanKeylet.key));
+            env.close();
+        };
+
+        // ---- Case A: impair before late, unimpair within original interval ----
+        {
+            auto const loanKeylet = createNewLoan();
+            auto const loanSle = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loanSle))
+                return;
+            std::uint32_t const startDate = loanSle->at(sfStartDate);
+            std::uint32_t const originalNextDueDate = loanSle->at(sfNextPaymentDueDate);
+            BEAST_EXPECT(originalNextDueDate == startDate + 600);
+
+            // Payment is not late yet - impair succeeds and moves due date
+            // to now (pre-amendment allows immediate impairment)
+            env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
+
+            {
+                auto const loan = env.le(loanKeylet);
+                if (!BEAST_EXPECT(loan))
+                    return;
+                BEAST_EXPECT(loan->isFlag(lsfLoanImpaired));
+                std::uint32_t const movedDueDate = loan->at(sfNextPaymentDueDate);
+                BEAST_EXPECT(movedDueDate != originalNextDueDate);
+                BEAST_EXPECT(movedDueDate < originalNextDueDate);
+            }
+
+            // Unimpair while still within the original payment interval. The
+            // normal due date (startDate + 600) has not yet expired, so it
+            // should be restored.
+            env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tesSUCCESS));
+
+            {
+                auto const loan = env.le(loanKeylet);
+                if (!BEAST_EXPECT(loan))
+                    return;
+                BEAST_EXPECT(!loan->isFlag(lsfLoanImpaired));
+                BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
+            }
+
+            cleanupLoan(loanKeylet, originalNextDueDate);
+        }
+
+        // ---- Case B: impair before late, unimpair after original due date ----
+        {
+            auto const loanKeylet = createNewLoan();
+            auto const loanSle = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loanSle))
+                return;
+            std::uint32_t const startDate = loanSle->at(sfStartDate);
+            std::uint32_t const originalNextDueDate = loanSle->at(sfNextPaymentDueDate);
+            BEAST_EXPECT(originalNextDueDate == startDate + 600);
+
+            env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
+
+            env.close(NetClock::time_point{NetClock::duration{originalNextDueDate}} + 10s);
+
+            auto const timeBeforeUnimpair =
+                env.current()->header().parentCloseTime.time_since_epoch().count();
+
+            env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tesSUCCESS));
+
+            {
+                auto const loan = env.le(loanKeylet);
+                if (!BEAST_EXPECT(loan))
+                    return;
+                BEAST_EXPECT(!loan->isFlag(lsfLoanImpaired));
+                std::uint32_t const newDueDate = loan->at(sfNextPaymentDueDate);
+                BEAST_EXPECT(newDueDate > originalNextDueDate);
+                BEAST_EXPECT(newDueDate == timeBeforeUnimpair + 600);
+            }
+        }
+    }
+
+    // FN-68: a borrower must not be able to bypass late-payment charges by
+    // paying an impaired, overdue loan with a plain LoanPay. Under
+    // fixCleanup3_4_0 impairment no longer moves the due date, so
+    // the payment logic sees the real (overdue) date: a regular payment is
+    // rejected with tecEXPIRED, and only a tfLoanLatePayment (which charges
+    // the late fee + late interest) is accepted.
+    void
+    testImpairedOverdueLoanPayRequiresLateFlag()
+    {
+        using namespace jtx;
+        using namespace loan;
+        using namespace std::chrono_literals;
+
+        testcase("Impaired overdue LoanPay requires late-payment flag");
+
+        Env env(*this, all_ | fixCleanup3_4_0);
+        BEAST_EXPECT(env.enabled(fixCleanup3_4_0));
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(100'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+        auto const broker = createVaultAndBroker(env, xrpAsset, lender);
+
+        auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
+        if (!BEAST_EXPECT(sleBroker))
+            return;
+        auto const loanKeylet =
+            keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
+
+        // Loan with non-zero late-payment terms, so the late path carries a
+        // real penalty that the exploit would otherwise avoid.
+        Number const principalRequest{1, 3};
+        env(set(borrower, broker.brokerID, broker.asset(principalRequest).value()),
+            Sig(sfCounterpartySignature, lender),
+            kPaymentTotal(12),
+            kPaymentInterval(600),
+            kLatePaymentFee(broker.asset(3).number()),
+            kLateInterestRate(TenthBips32{30322}),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+
+        auto const loanSle = env.le(loanKeylet);
+        if (!BEAST_EXPECT(loanSle))
+            return;
+        std::uint32_t const originalNextDueDate = loanSle->at(sfNextPaymentDueDate);
+        std::uint32_t const paymentsBefore = loanSle->at(sfPaymentRemaining);
+        BEAST_EXPECT(originalNextDueDate > 0);
+
+        // Advance past the due date so the loan is overdue, then impair it
+        // (impairment is only allowed once the payment is late).
+        env.close(NetClock::time_point{NetClock::duration{originalNextDueDate}} + 1s);
+        env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
+        env.close();
+
+        {
+            auto const loan = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loan))
+                return;
+            BEAST_EXPECT(loan->isFlag(lsfLoanImpaired));
+            BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
+        }
+
+        auto const payAmount = broker.asset(500).value();
+
+        // The exploit: a plain LoanPay (Flags = 0) on an impaired, overdue
+        // loan must be rejected. Before FN-9 the auto-unimpair pushed the due
+        // date into the future and this returned tesSUCCESS, letting the
+        // borrower skip the late fee and late interest.
+        env(pay(borrower, loanKeylet.key, payAmount), Ter(tecEXPIRED));
+        env.close();
+
+        {
+            auto const loan = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loan))
+                return;
+            BEAST_EXPECT(loan->isFlag(lsfLoanImpaired));
+            BEAST_EXPECT(loan->at(sfPaymentRemaining) == paymentsBefore);
+            BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == originalNextDueDate);
+        }
+
+        env(pay(borrower, loanKeylet.key, payAmount, tfLoanLatePayment), Ter(tesSUCCESS));
+        env.close();
+        {
+            auto const loan = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loan))
+                return;
+            BEAST_EXPECT(!loan->isFlag(lsfLoanImpaired));
+            BEAST_EXPECT(loan->at(sfPaymentRemaining) == paymentsBefore - 1);
+        }
+
+        {
+            auto const vaultSle = env.le(broker.vaultKeylet());
+            if (!BEAST_EXPECT(vaultSle))
+                return;
+            BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0);
+        }
+    }
+
+    // FN-68 (pre-amendment): documents the original vulnerability. Without
+    // fixCleanup3_4_0, impairing moves the due date and LoanPay
+    // auto-unimpair pushes it into the future before the late check, so a
+    // plain (Flags = 0) LoanPay on an impaired, overdue loan is accepted as
+    // on-time (tesSUCCESS) and the borrower dodges the late-payment charges.
+    // This is what testImpairedOverdueLoanPayRequiresLateFlag closes once the
+    // amendment is enabled.
+    void
+    testImpairedOverdueLoanPayBypassPreAmendment()
+    {
+        using namespace jtx;
+        using namespace loan;
+        using namespace std::chrono_literals;
+
+        testcase("Impaired overdue LoanPay bypass (pre-amendment)");
+
+        Env env(*this, all_ - fixCleanup3_4_0);
+        BEAST_EXPECT(!env.enabled(fixCleanup3_4_0));
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(100'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+        auto const broker = createVaultAndBroker(env, xrpAsset, lender);
+
+        auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
+        if (!BEAST_EXPECT(sleBroker))
+            return;
+        auto const loanKeylet =
+            keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
+
+        Number const principalRequest{1, 3};
+        env(set(borrower, broker.brokerID, broker.asset(principalRequest).value()),
+            Sig(sfCounterpartySignature, lender),
+            kPaymentTotal(12),
+            kPaymentInterval(600),
+            kLatePaymentFee(broker.asset(3).number()),
+            kLateInterestRate(TenthBips32{30322}),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+
+        auto const loanSle = env.le(loanKeylet);
+        if (!BEAST_EXPECT(loanSle))
+            return;
+        std::uint32_t const originalNextDueDate = loanSle->at(sfNextPaymentDueDate);
+        BEAST_EXPECT(originalNextDueDate > 0);
+
+        env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
+        env.close();
+
+        env.close(NetClock::time_point{NetClock::duration{originalNextDueDate}} + 1s);
+
+        {
+            auto const loan = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loan))
+                return;
+            BEAST_EXPECT(loan->isFlag(lsfLoanImpaired));
+        }
+
+        auto const payAmount = broker.asset(500).value();
+
+        // The bug: a plain LoanPay is accepted as on-time and clears the
+        // loan's impaired flag, so the late fee / late interest are never
+        // charged.
+        env(pay(borrower, loanKeylet.key, payAmount), Ter(tesSUCCESS));
+        env.close();
+        {
+            auto const loan = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loan))
+                return;
+            BEAST_EXPECT(!loan->isFlag(lsfLoanImpaired));
+        }
+    }
+
+    // Default uses NextPaymentDueDate + GracePeriod. Once fixCleanup3_4_0
+    // is enabled, that gate is Exclusive, matching impair/isPaymentLate:
+    // default is allowed only after grace has passed, not at the instant
+    // it expires.
+    void
+    testLoanDefaultAtExactGraceExpiryRejectedPostAmendment()
+    {
+        testcase("LoanManage default at exact grace expiry rejected with fixCleanup3_4_0");
+
+        using namespace jtx;
+        using namespace loan;
+        using namespace std::chrono_literals;
+
+        Env env(*this, all_);
+        BEAST_EXPECT(env.enabled(fixCleanup3_4_0));
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(100'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+        auto const broker = createVaultAndBroker(env, xrpAsset, lender);
+
+        auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
+        if (!BEAST_EXPECT(sleBroker))
+            return;
+        auto const loanKeylet =
+            keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
+
+        env(set(borrower, broker.brokerID, broker.asset(Number{1, 3}).value()),
+            Sig(sfCounterpartySignature, lender),
+            kPaymentTotal(12),
+            kPaymentInterval(600),
+            kGracePeriod(60),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+
+        // Advance far enough that parentCloseTime > GracePeriod, so
+        // (now - grace) cannot underflow when pinning the exact expiry.
+        env.close(env.now() + 1000s);
+
+        auto const loanSle = env.le(loanKeylet);
+        if (!BEAST_EXPECT(loanSle))
+            return;
+        auto const grace = loanSle->at(sfGracePeriod);
+        std::uint32_t const now = env.current()->parentCloseTime().time_since_epoch().count();
+        BEAST_EXPECT(now > grace + 1);
+
+        // parentCloseTime == NextPaymentDueDate + GracePeriod: grace expires
+        // this instant, so default must still be too soon.
+        setLoanNextPaymentDueDate(env, loanKeylet, now - grace);
+        env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecTOO_SOON));
+        {
+            auto const loan = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loan))
+                return;
+            BEAST_EXPECT(!loan->isFlag(lsfLoanDefault));
+        }
+
+        // One second after grace expires, default succeeds.
+        setLoanNextPaymentDueDate(env, loanKeylet, now - grace - 1);
+        env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
+        {
+            auto const loan = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loan))
+                return;
+            BEAST_EXPECT(loan->isFlag(lsfLoanDefault));
+        }
+    }
+
+    void
+    testLoanDefaultAtExactGraceExpirySucceedsPreAmendment()
+    {
+        testcase("LoanManage default at exact grace expiry succeeds without fixCleanup3_4_0");
+
+        using namespace jtx;
+        using namespace loan;
+        using namespace std::chrono_literals;
+
+        Env env(*this, all_ - fixCleanup3_4_0);
+        BEAST_EXPECT(!env.enabled(fixCleanup3_4_0));
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(100'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+        auto const broker = createVaultAndBroker(env, xrpAsset, lender);
+
+        auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
+        if (!BEAST_EXPECT(sleBroker))
+            return;
+        auto const loanKeylet =
+            keylet::loan(broker.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
+
+        env(set(borrower, broker.brokerID, broker.asset(Number{1, 3}).value()),
+            Sig(sfCounterpartySignature, lender),
+            kPaymentTotal(12),
+            kPaymentInterval(600),
+            kGracePeriod(60),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+
+        env.close(env.now() + 1000s);
+
+        auto const loanSle = env.le(loanKeylet);
+        if (!BEAST_EXPECT(loanSle))
+            return;
+        auto const grace = loanSle->at(sfGracePeriod);
+        std::uint32_t const now = env.current()->parentCloseTime().time_since_epoch().count();
+        BEAST_EXPECT(now > grace);
+
+        setLoanNextPaymentDueDate(env, loanKeylet, now - grace);
+        env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
+        {
+            auto const loan = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loan))
+                return;
+            BEAST_EXPECT(loan->isFlag(lsfLoanDefault));
+        }
+    }
+
+    // Every signature on a transaction covered the same bytes before
+    // fixCleanup3_4_0, so a signature could be moved from the role that made
+    // it into another role. Here the lender signs a LoanSet as the
+    // counterparty, and the borrower copies that signature into the
+    // SponsorSignature, making the lender pay the fee without the lender ever
+    // agreeing to sponsor it.
+    void
+    testSignatureCopiedBetweenRoles(bool fixEnabled)
+    {
+        testcase(
+            std::string("Counterparty signature copied into the sponsor slot") +
+            (fixEnabled ? "" : " (pre-amendment)"));
+
+        using namespace jtx;
+        using namespace loan;
+
+        Env env(*this, fixEnabled ? all_ : all_ - fixCleanup3_4_0);
+        BEAST_EXPECT(env.enabled(fixCleanup3_4_0) == fixEnabled);
+
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(100'000'000), lender, borrower);
+        env.close();
+
+        PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
+        auto const broker = createVaultAndBroker(env, xrpAsset, lender);
+
+        auto const feeAmt = XRP(1);
+
+        // The lender agrees to the loan by signing the Counterparty slot of a
+        // LoanSet that names the lender as the fee sponsor. The lender signs
+        // nothing else.
+        auto loanSet = env.json(
+            set(borrower, broker.brokerID, broker.asset(Number{1, 3}).value()),
+            sponsor::As(lender, spfSponsorFee),
+            Sig(sfCounterpartySignature, lender),
+            Fee(feeAmt));
+
+        // The borrower copies the lender's signature into the sponsor slot.
+        loanSet[sfSponsorSignature.jsonName] = loanSet[sfCounterpartySignature.jsonName];
+
+        auto const lenderBalance = env.balance(lender);
+        auto const borrowerBalance = env.balance(borrower);
+
+        env(loanSet, Ter(fixEnabled ? TER{telENV_RPC_FAILED} : TER{tesSUCCESS}));
+        env.close();
+
+        if (fixEnabled)
+        {
+            // The copied signature does not verify in the sponsor slot, so
+            // nothing happens at all.
+            BEAST_EXPECT(env.balance(lender) == lenderBalance);
+            BEAST_EXPECT(env.balance(borrower) == borrowerBalance);
+        }
+        else
+        {
+            // The lender paid the fee, and the borrower got the loan.
+            BEAST_EXPECT(env.balance(lender) == lenderBalance - feeAmt);
+            BEAST_EXPECT(env.balance(borrower).value() > borrowerBalance.value());
+        }
+    }
+
     void
     runAmendmentIndependent()
     {
+        testSignatureCopiedBetweenRoles(true);
+        testSignatureCopiedBetweenRoles(false);
         testRIPD3901();
+        testImpairmentPaymentDateUnchanged();
+        testImpairmentPaymentDatePreAmendment();
+        testImpairedOverdueLoanPayRequiresLateFlag();
+        testImpairedOverdueLoanPayBypassPreAmendment();
+        testLoanDefaultAtExactGraceExpiryRejectedPostAmendment();
+        testLoanDefaultAtExactGraceExpirySucceedsPreAmendment();
     }
 
     // Tests run under each entry in amendmentCombinations().
diff --git a/src/test/app/lending/LoanSet_test.cpp b/src/test/app/lending/LoanSet_test.cpp
index 85528ee9a0..5eea6f83fe 100644
--- a/src/test/app/lending/LoanSet_test.cpp
+++ b/src/test/app/lending/LoanSet_test.cpp
@@ -13,20 +13,25 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 
@@ -592,6 +597,237 @@ private:
             nullptr);
     }
 
+    // LoanSet in a closed-ended vault — phase gating and maturity bound.
+    void
+    testLoanSetClosedEnded()
+    {
+        testcase("LoanSet closed-ended: phase and maturity bound");
+        using namespace jtx;
+        using namespace loan;
+        using d = NetClock::duration;
+        using tp = NetClock::time_point;
+
+        Account const issuer{"issuer"};
+        Account const lender{"lender"};
+        Account const borrower{"borrower"};
+
+        // Common loan schedule used by the phase-rejection cases below.
+        constexpr std::uint32_t kInterval = 3600u * 24u;  // 1 day
+        constexpr std::uint32_t kTotal = 2u;
+
+        // featureLendingProtocolV1_1 is excluded from `all_` by convention (see the comment on
+        // `all_`), so callers must opt in. Closed-ended vaults are gated on this amendment; without
+        // it VaultCreate returns temDISABLED and every follow-on txn sees tecNO_ENTRY.
+        auto const withEnv = [&, this](auto&& body) {
+            Env env(*this, testableAmendments() | featureLendingProtocolV1_1);
+            env.fund(XRP(1'000'000'000), issuer, lender, borrower);
+            env.close();
+            PrettyAsset const asset{xrpIssue(), 1'000'000};
+            body(env, asset);
+        };
+
+        auto const setLoan = [&](Env& env, BrokerInfo const& broker, TER expected) {
+            env(set(lender, broker.brokerID, broker.asset(100).value()),
+                kCounterparty(borrower),
+                Sig(sfCounterpartySignature, borrower),
+                Fee(env.current()->fees().base * 5),
+                kPaymentTotal(kTotal),
+                kPaymentInterval(kInterval),
+                Ter(expected));
+            env.close();
+        };
+
+        // 1. Rejected during Subscription: the broker is created in Subscription (skipPhaseAdvance
+        // = true), then LoanSet is attempted before advancing past SubscriptionDate.
+        withEnv([&](Env& env, PrettyAsset const& asset) {
+            auto const broker = createVaultAndBroker(
+                env,
+                asset,
+                lender,
+                BrokerParameters{.vaultKind = VaultKind::ClosedEnded, .skipPhaseAdvance = true});
+            setLoan(env, broker, tecTOO_SOON);
+        });
+
+        // 2. Rejected during Redemption: broker is set up normally (which lands the vault in
+        // Investment), then advance the clock past RedemptionDate before attempting LoanSet.
+        withEnv([&](Env& env, PrettyAsset const& asset) {
+            auto const broker = createVaultAndBroker(
+                env, asset, lender, BrokerParameters{.vaultKind = VaultKind::ClosedEnded});
+            BEAST_EXPECT(broker.redemptionDate.has_value());
+            using d = NetClock::duration;
+            using tp = NetClock::time_point;
+            env.close(tp{d{*broker.redemptionDate + 1}});
+            setLoan(env, broker, tecEXPIRED);
+        });
+
+        // 3. Accepted during Investment when the schedule comfortably fits before RedemptionDate.
+        withEnv([&](Env& env, PrettyAsset const& asset) {
+            auto const broker = createVaultAndBroker(
+                env, asset, lender, BrokerParameters{.vaultKind = VaultKind::ClosedEnded});
+            setLoan(env, broker, tesSUCCESS);
+        });
+
+        // 4. Rejected during Investment when the loan's final payment would land fewer than
+        // kLoanRedemptionBuffer seconds before RedemptionDate. Use a tight redemptionOffset and a
+        // schedule whose final payment is well past that boundary.
+        withEnv([&](Env& env, PrettyAsset const& asset) {
+            constexpr std::uint32_t kRedemptionOffset = 3u * 24u * 3600u;
+            auto const broker = createVaultAndBroker(
+                env,
+                asset,
+                lender,
+                BrokerParameters{
+                    .vaultKind = VaultKind::ClosedEnded, .redemptionOffset = kRedemptionOffset});
+            env(set(lender, broker.brokerID, broker.asset(100).value()),
+                kCounterparty(borrower),
+                Sig(sfCounterpartySignature, borrower),
+                Fee(env.current()->fees().base * 5),
+                kPaymentTotal(10u),
+                kPaymentInterval(kInterval),
+                Ter(tecNO_PERMISSION));
+            env.close();
+        });
+
+        // 5. Boundary: a finalPayment exactly kLoanRedemptionBuffer seconds before
+        // RedemptionDate is accepted; one second later is rejected. Uses payTotal = 1 so
+        // finalPayment = startDate + interval.
+        withEnv([&](Env& env, PrettyAsset const& asset) {
+            auto const broker = createVaultAndBroker(
+                env, asset, lender, BrokerParameters{.vaultKind = VaultKind::ClosedEnded});
+            BEAST_EXPECT(broker.redemptionDate.has_value());
+
+            auto const startDate = env.now().time_since_epoch().count();
+            auto const acceptInterval = *broker.redemptionDate - kLoanRedemptionBuffer - startDate;
+            env(set(lender, broker.brokerID, broker.asset(100).value()),
+                kCounterparty(borrower),
+                Sig(sfCounterpartySignature, borrower),
+                Fee(env.current()->fees().base * 5),
+                kPaymentTotal(1u),
+                kPaymentInterval(acceptInterval),
+                Ter(tesSUCCESS));
+            env.close();
+
+            auto const rejectInterval = *broker.redemptionDate - (kLoanRedemptionBuffer - 1) -
+                env.now().time_since_epoch().count();
+            env(set(lender, broker.brokerID, broker.asset(100).value()),
+                kCounterparty(borrower),
+                Sig(sfCounterpartySignature, borrower),
+                Fee(env.current()->fees().base * 5),
+                kPaymentTotal(1u),
+                kPaymentInterval(rejectInterval),
+                Ter(tecNO_PERMISSION));
+            env.close();
+        });
+
+        // 6. A vault whose Investment window is exactly kMinInvestmentPeriod can originate a
+        // minimum-interval, single-payment loan at the start of Investment, and rejects the same
+        // schedule once StartDate no longer leaves kLoanRedemptionBuffer before RedemptionDate.
+        // Do not pin an unrounded wall-clock instant: Env::close rounds to the close-time
+        // resolution. Read env.now() (the same clock LoanSet::preclaim uses) and assert the
+        // buffer relationship before each LoanSet.
+        withEnv([&](Env& env, PrettyAsset const& asset) {
+            auto const broker = createVaultAndBroker(
+                env,
+                asset,
+                lender,
+                BrokerParameters{
+                    .vaultKind = VaultKind::ClosedEnded,
+                    .subscriptionOffset = 300u,
+                    .redemptionOffset = kMinInvestmentPeriod,
+                    .skipPhaseAdvance = true});
+            BEAST_EXPECT(broker.subscriptionDate.has_value());
+            BEAST_EXPECT(broker.redemptionDate.has_value());
+
+            auto const red = *broker.redemptionDate;
+            auto const startDate = [&]() { return env.now().time_since_epoch().count(); };
+            auto const minLoan = [&](TER expected) {
+                env(set(lender, broker.brokerID, broker.asset(100).value()),
+                    kCounterparty(borrower),
+                    Sig(sfCounterpartySignature, borrower),
+                    Fee(env.current()->fees().base * 5),
+                    kPaymentTotal(1u),
+                    kPaymentInterval(LoanSet::kMinPaymentInterval),
+                    Ter(expected));
+                env.close();
+            };
+
+            // First Investment ledger: the minimum schedule still clears the buffer.
+            env.close(tp{d{*broker.subscriptionDate + 1}});
+            BEAST_EXPECT(startDate() > *broker.subscriptionDate);
+            BEAST_EXPECT(startDate() + LoanSet::kMinPaymentInterval + kLoanRedemptionBuffer <= red);
+            minLoan(tesSUCCESS);
+
+            // Still Investment, but the minimum schedule no longer clears the buffer.
+            while (startDate() + LoanSet::kMinPaymentInterval + kLoanRedemptionBuffer <= red)
+                env.close();
+            BEAST_EXPECT(startDate() < red);
+            minLoan(tecNO_PERMISSION);
+        });
+    }
+
+    // LoanSet used to call canAddHolding unconditionally, so an existing
+    // borrower line still failed with terNO_RIPPLE after the issuer cleared
+    // DefaultRipple. After fixCleanup3_4_0, skip that gate when the holding
+    // already exists.
+    void
+    testLoanSetExistingLineAfterIssuerClearsDefaultRipple()
+    {
+        using namespace jtx;
+        using namespace loan;
+
+        auto run = [this](FeatureBitset features, TER expected) {
+            testcase(
+                std::string(
+                    "LoanSet existing borrower line after issuer "
+                    "clears asfDefaultRipple (") +
+                (features[fixCleanup3_4_0] ? "post" : "pre") + "-fixCleanup3_4_0)");
+
+            Env env(*this, features);
+            Account const issuer{"issuer"};
+            Account const lender{"lender"};
+            Account const borrower{"borrower"};
+
+            env.fund(XRP(10'000), issuer, lender, borrower);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            env(trust(lender, usd(10'000'000)));
+            env(trust(borrower, usd(10'000'000)));
+            env.close();
+            env(pay(issuer, lender, usd(2'000'000)));
+            env(pay(issuer, borrower, usd(1'000)));
+            env.close();
+            BEAST_EXPECT(env.le(keylet::trustLine(borrower.id(), usd.raw().get())));
+
+            auto const broker = createVaultAndBroker(env, usd, lender);
+
+            env(fclear(issuer, asfDefaultRipple));
+            env.close();
+
+            Number const destBefore = env.balance(borrower, usd.raw()).number();
+            env(set(borrower, broker.brokerID, usd(100).value()),
+                Sig(sfCounterpartySignature, lender),
+                Fee(env.current()->fees().base * 2),
+                Ter(expected));
+            env.close();
+
+            Number const destAfter = env.balance(borrower, usd.raw()).number();
+            if (isTesSuccess(expected))
+            {
+                BEAST_EXPECT(destAfter == destBefore + Number{100});
+            }
+            else
+            {
+                BEAST_EXPECT(destAfter == destBefore);
+            }
+        };
+
+        run(all_ - fixCleanup3_4_0, terNO_RIPPLE);
+        run(all_, tesSUCCESS);
+    }
+
 public:
     void
     run() override
@@ -599,6 +835,9 @@ public:
         for (auto const& features : jtx::amendmentCombinations(
                  {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
             testLoanSet(features);
+
+        testLoanSetClosedEnded();
+        testLoanSetExistingLineAfterIssuerClearsDefaultRipple();
     }
 };
 
diff --git a/src/test/app/lending/LoanTestBase.h b/src/test/app/lending/LoanTestBase.h
index dabdfc9bed..13dffb6b9e 100644
--- a/src/test/app/lending/LoanTestBase.h
+++ b/src/test/app/lending/LoanTestBase.h
@@ -67,6 +67,16 @@
 
 namespace xrpl::test {
 
+/**
+ * Shared base for the Loan*_test family under src/test/app/lending/.
+ *
+ * Run all suites in this family with
+ *   xrpld -u Loan,LendingHelpers
+ * The "Loan" prefix is matched against every suite name via
+ * beast::unit_test::Selector::ModeT::Automatch; LendingHelpers is listed
+ * explicitly because it does not share the "Loan" prefix (and lives in a
+ * different module: app vs tx).
+ */
 class LoanTestBase : public beast::unit_test::Suite
 {
 protected:
@@ -91,10 +101,31 @@ protected:
         TenthBips32 coverRateLiquidation = percentageToTenthBips(25);
         std::string data = {};  // NOLINT(readability-redundant-member-init)
         std::uint32_t flags = 0;
+        // VaultCreate flags (e.g. tfVaultPrivate). Distinct from `flags`,
+        // which are passed to LoanBrokerSet.
+        std::optional vaultFlags =
+            std::nullopt;  // NOLINT(readability-redundant-member-init)
         // If set, the vault is created with this sfScale value. Useful for
         // tests that need finer loanScale to exercise rounding edge cases.
         std::optional vaultScale =
             std::nullopt;  // NOLINT(readability-redundant-member-init)
+        // Vault kind axis. When ClosedEnded, createVaultAndBroker sets sfSubscriptionDate /
+        // sfRedemptionDate from env.now() using the offsets below and advances the ledger clock
+        // past SubscriptionDate so the vault is in the Investment phase by the time the broker is
+        // set up. Requires featureLendingProtocolV1_1.
+        VaultKind vaultKind = VaultKind::OpenEnded;
+        // Seconds past env.now() at which SubscriptionDate lands. Must be strictly positive
+        // (VaultCreate::preclaim rejects SubscriptionDate <= parentCloseTime).
+        std::uint32_t subscriptionOffset = 60;
+        // Seconds between SubscriptionDate and RedemptionDate. Must be >= kMinInvestmentPeriod, <
+        // kMaxInvestmentPeriod, and generous enough to fit any loan schedule the test runs
+        // (finalPayment must precede RedemptionDate by at least kLoanRedemptionBuffer). Default
+        // sized to comfortably exceed any schedule realistic tests are likely to configure.
+        std::uint32_t redemptionOffset = 10u * 365u * 24u * 60u * 60u;
+        // When true, createVaultAndBroker skips its automatic clock advance past SubscriptionDate.
+        // Useful for tests that need to observe the vault while it is still in the Subscription
+        // phase. Ignored for open-ended vaults.
+        bool skipPhaseAdvance = false;
 
         [[nodiscard]] Number
         maxCoveredLoanValue(Number const& currentDebt) const
@@ -122,15 +153,23 @@ protected:
         uint256 brokerID;
         uint256 vaultID;
         BrokerParameters params;
+        // Absolute dates resolved by createVaultAndBroker when params.vaultKind
+        // is ClosedEnded; std::nullopt for open-ended vaults.
+        std::optional subscriptionDate;
+        std::optional redemptionDate;
         BrokerInfo(
             jtx::PrettyAsset const& asset,
             Keylet const& brokerKeylet,
             Keylet const& vaultKeylet,
-            BrokerParameters p)
+            BrokerParameters p,
+            std::optional subscriptionDate = std::nullopt,
+            std::optional redemptionDate = std::nullopt)
             : asset(asset)
             , brokerID(brokerKeylet.key)
             , vaultID(vaultKeylet.key)
             , params(std::move(p))
+            , subscriptionDate(subscriptionDate)
+            , redemptionDate(redemptionDate)
         {
         }
 
@@ -461,7 +500,42 @@ protected:
 
         auto const coverRateMinValue = params.coverRateMin;
 
-        auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset});
+        // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim rejects
+        // brokers attached to open-ended vaults. Many callers of this
+        // helper leave vaultKind at the OpenEnded default and don't care
+        // about the vault kind per se — they just need a broker on a
+        // vault. When LP V1.1 is enabled, transparently promote to
+        // ClosedEnded so those tests keep working without threading
+        // vaultKind through every call site. Callers that explicitly
+        // asked for ClosedEnded are left untouched. Tests that want to
+        // exercise the open-ended rejection under LP V1.1 build their own
+        // vault directly instead of going through this helper, since it
+        // always promotes OpenEnded once the amendment is enabled.
+        auto effectiveVaultKind = params.vaultKind;
+        if (env.current()->rules().enabled(featureLendingProtocolV1_1) &&
+            effectiveVaultKind == VaultKind::OpenEnded)
+        {
+            effectiveVaultKind = VaultKind::ClosedEnded;
+        }
+
+        std::optional subscriptionDate;
+        std::optional redemptionDate;
+        if (effectiveVaultKind == VaultKind::ClosedEnded)
+        {
+            auto const nowSec = env.now().time_since_epoch().count();
+            subscriptionDate = nowSec + params.subscriptionOffset;
+            redemptionDate = *subscriptionDate + params.redemptionOffset;
+        }
+
+        auto [tx, vaultKeylet] = vault.create(
+            {.owner = lender,
+             .asset = asset,
+             .flags = params.vaultFlags,
+             .vaultKind = effectiveVaultKind == VaultKind::OpenEnded
+                 ? std::optional{}
+                 : std::optional{std::to_underlying(effectiveVaultKind)},
+             .subscriptionDate = subscriptionDate,
+             .redemptionDate = redemptionDate});
         if (params.vaultScale)
             tx[sfScale] = *params.vaultScale;
         env(tx);
@@ -475,6 +549,15 @@ protected:
             BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value());
         }
 
+        // For closed-ended vaults, advance past SubscriptionDate so subsequent LoanSet operations
+        // run in the Investment phase (unless the caller explicitly asked to stay in Subscription).
+        if (subscriptionDate && !params.skipPhaseAdvance)
+        {
+            using d = NetClock::duration;
+            using tp = NetClock::time_point;
+            env.close(tp{d{*subscriptionDate + 1}});
+        }
+
         auto const keylet = keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender)));
 
         using namespace loan_broker;
@@ -490,7 +573,7 @@ protected:
 
         env.close();
 
-        return {asset, keylet, vaultKeylet, params};
+        return {asset, keylet, vaultKeylet, params, subscriptionDate, redemptionDate};
     }
 
     /**
@@ -596,6 +679,23 @@ protected:
         return true;
     }
 
+    // Under fixCleanup3_4_0, LoanManage rejects tfLoanImpair with tecTOO_SOON
+    // unless the loan payment is already late. Advance the ledger past the
+    // loan's sfNextPaymentDueDate so shared lifecycle flows still exercise
+    // the tesSUCCESS branch when the amendment is active. No-op when the
+    // amendment is disabled.
+    void
+    advancePastDueDate(jtx::Env& env, Keylet const& loanKeylet)
+    {
+        if (!env.current()->rules().enabled(fixCleanup3_4_0))
+            return;
+        auto const loan = env.le(loanKeylet);
+        if (!BEAST_EXPECT(loan))
+            return;
+        std::uint32_t const dueDate = loan->at(sfNextPaymentDueDate);
+        env.close(NetClock::time_point{NetClock::duration{dueDate}} + std::chrono::seconds{1});
+    }
+
     enum class AssetType { XRP = 0, IOU = 1, MPT = 2 };
 
     // Specify the accounts as params to allow other accounts to be used
@@ -1514,12 +1614,30 @@ protected:
 
         // Check the vault
         bool const canImpair = canImpairLoan(env, broker, state);
-        // Impair the loan, if possible
-        env(manage(lender, keylet.key, tfLoanImpair),
-            canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED));
-        // Unimpair the loan
-        env(manage(lender, keylet.key, tfLoanUnimpair),
-            canImpair ? Ter(tesSUCCESS) : Ter(tecNO_PERMISSION));
+        // Under fixCleanup3_4_0, impair rejects a not-yet-late loan with
+        // tecTOO_SOON. Advancing time to satisfy the gate here would push
+        // the loan into a "late" state and break the toEndOfLife flows
+        // (singlePayment/fullPayment) that expect a fresh loan without the
+        // tfLoanLatePayment flag. The tesSUCCESS/tecLIMIT_EXCEEDED impair
+        // path is already covered under fixCleanup3_4_0 by dedicated tests
+        // in LoanSecurity_test.cpp and LoanCashBasis_test.cpp.
+        if (!env.current()->rules().enabled(fixCleanup3_4_0))
+        {
+            // Impair the loan, if possible
+            env(manage(lender, keylet.key, tfLoanImpair),
+                canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED));
+            // Unimpair the loan
+            env(manage(lender, keylet.key, tfLoanUnimpair),
+                canImpair ? Ter(tesSUCCESS) : Ter(tecNO_PERMISSION));
+        }
+        else
+        {
+            // With the fix on, a not-yet-late loan can never be impaired
+            // (tecTOO_SOON) and the follow-up unimpair on an unimpaired
+            // loan is still tecNO_PERMISSION.
+            env(manage(lender, keylet.key, tfLoanImpair), Ter(tecTOO_SOON));
+            env(manage(lender, keylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION));
+        }
 
         auto const nextDueDate = startDate + *loanParams.payInterval;
 
@@ -2110,6 +2228,11 @@ protected:
                 {
                     // Check the vault
                     bool const canImpair = canImpairLoan(env, broker, state);
+                    // Under fixCleanup3_4_0 impair requires the payment to
+                    // already be late. Advance past the loan's next due
+                    // date so this exercises the tesSUCCESS branch. No-op
+                    // when the fix is disabled.
+                    advancePastDueDate(env, loanKeylet);
                     // Impair the loan, if possible
                     env(manage(lender, loanKeylet.key, tfLoanImpair),
                         canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED));
@@ -2117,7 +2240,11 @@ protected:
                     if (canImpair)
                     {
                         state.flags |= tfLoanImpair;
-                        state.nextPaymentDate = env.now().time_since_epoch().count();
+                        // Prior to fixCleanup3_4_0 impair rewrote
+                        // sfNextPaymentDueDate to parentCloseTime. Under the
+                        // fix, the due date is preserved.
+                        if (!env.current()->rules().enabled(fixCleanup3_4_0))
+                            state.nextPaymentDate = env.now().time_since_epoch().count();
 
                         // Once the loan is impaired, it can't be impaired again
                         env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION));
@@ -2737,7 +2864,17 @@ protected:
 
                     auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset);
 
-                    if (canImpairLoan(env, broker, state))
+                    // Under fixCleanup3_4_0 impair requires the payment to
+                    // already be late. This periodic-payment loop stays
+                    // within each payment interval, so the loan is never
+                    // late here; skip the impair rather than perturb the
+                    // payment schedule.
+                    auto const loanSle = env.le(loanKeylet);
+                    bool const impairAllowed = BEAST_EXPECT(loanSle) &&
+                        canImpairLoan(env, broker, state) &&
+                        (!env.current()->rules().enabled(fixCleanup3_4_0) ||
+                         isPaymentLate(*env.current(), loanSle));
+                    if (impairAllowed)
                     {
                         // Making a payment will unimpair the loan
                         env(manage(lender, loanKeylet.key, tfLoanImpair));
diff --git a/src/test/app/lending/LoanValidation_test.cpp b/src/test/app/lending/LoanValidation_test.cpp
index 884384db55..566633b690 100644
--- a/src/test/app/lending/LoanValidation_test.cpp
+++ b/src/test/app/lending/LoanValidation_test.cpp
@@ -6,13 +6,13 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
@@ -26,6 +26,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -90,9 +91,11 @@ private:
     }
 
     void
-    testInvalidLoanSet()
+    testInvalidLoanSet(VaultKind vaultKind)
     {
-        testcase("Invalid LoanSet");
+        testcase(
+            std::string("Invalid LoanSet (") +
+            (vaultKind == VaultKind::OpenEnded ? "open-ended" : "closed-ended") + " vault)");
         using namespace jtx;
         using namespace loan;
         Account const lender{"lender"};
@@ -106,7 +109,8 @@ private:
             env.fund(XRP(1'000), lender, issuer, borrower, sponsor);
             env(trust(lender, iou(10'000'000)));
             env(pay(issuer, lender, iou(5'000'000)));
-            BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)};
+            BrokerInfo const brokerInfo{
+                createVaultAndBroker(env, issuer["IOU"], lender, {.vaultKind = vaultKind})};
 
             auto const loanSetFee = Fee(env.current()->fees().base * 2);
             Number const debtMaximumRequest = brokerInfo.asset(1'000).value();
@@ -340,7 +344,12 @@ private:
         env(trust(issuer, lender["IOU"](1'000), tfClearFreeze | tfClearDeepFreeze));
         env.close();
 
-        // The payment is late by this point
+        // The payment is late by this point. With fixCleanup3_4_0,
+        // isPaymentLate() uses a strict (Exclusive) comparison, so advance
+        // one more ledger close to be sure the due date instant itself has
+        // passed, not merely reached.
+        env.close();
+
         env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecEXPIRED));
         env.close();
         env(pay(borrower, loanKeylet.key, debtMaximumRequest, tfLoanLatePayment));
@@ -512,30 +521,85 @@ private:
         auto const loanSetFee = Fee(env.current()->fees().base * 2);
         Number const principalRequest{1, 3};
 
-        auto createJson = env.json(set(lender, broker.brokerID, principalRequest), Fee(loanSetFee));
-
-        json::Value counterpartyJson{json::ValueType::Object};
-        counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature];
-        counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey];
-        if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
-            counterpartyJson[sfSigners] = createJson[sfSigners];
-
-        createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson));
+        // The lender is both the borrower and the counterparty here, but the
+        // two roles sign different bytes, so each signature must be made for
+        // the field it goes into.
+        auto const createJson = env.json(
+            set(lender, broker.brokerID, principalRequest),
+            Sig(sfCounterpartySignature, lender),
+            Fee(loanSetFee));
         env(createJson);
 
         env.close();
     }
 
+    // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim rejects
+    // attaching a broker to an open-ended vault. VaultCreate itself is
+    // not gated by the amendment, so the same open-ended vault can be
+    // built under either feature set; only the broker create is
+    // amendment-sensitive. Cover both branches: LP V1.1 disabled lets
+    // the broker create succeed, LP V1.1 enabled rejects it. The gate
+    // only fires on the create path; existing brokers keep working.
+    void
+    testLoanBrokerRequiresClosedEndedVault()
+    {
+        testcase("LoanBrokerSet requires closed-ended vault under LP V1.1");
+        using namespace jtx;
+
+        Account const owner{"lp11_owner"};
+
+        auto const build = [&](FeatureBitset features,
+                               TER expected,
+                               std::optional updateExpected = std::nullopt) {
+            Env env(*this, features);
+            env.fund(XRP(1'000), owner);
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = xrpIssue()});
+            env(tx);
+            env.close();
+            env(vault.deposit({.depositor = owner, .id = vaultKeylet.key, .amount = XRP(100)}));
+            env.close();
+
+            auto const brokerKeylet =
+                keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+            env(loan_broker::set(owner, vaultKeylet.key), Ter(expected));
+            env.close();
+
+            // The create-path gate is the only new check; updates to an
+            // existing broker on the same open-ended vault are not
+            // affected. Only exercise the update path when the create
+            // succeeded (so there is a broker to update).
+            if (updateExpected && expected == tesSUCCESS)
+            {
+                env(loan_broker::set(owner, vaultKeylet.key),
+                    loan_broker::kLoanBrokerId(brokerKeylet.key),
+                    loan_broker::kDebtMaximum(XRP(1'000).value()),
+                    Ter(*updateExpected));
+                env.close();
+            }
+        };
+
+        // Baseline: LP V1.1 disabled -> open-ended vault + broker succeeds.
+        build(all_, tesSUCCESS, tesSUCCESS);
+
+        // LP V1.1 enabled -> open-ended vault + broker rejected on create.
+        build(all_ | featureLendingProtocolV1_1, tecNO_PERMISSION);
+    }
+
     void
     runAmendmentIndependent()
     {
         testDisabled();
-        testInvalidLoanSet();
+        for (auto const kind : {VaultKind::OpenEnded, VaultKind::ClosedEnded})
+            testInvalidLoanSet(kind);
         testInvalidLoanDelete();
         testInvalidLoanManage();
         testInvalidLoanPay();
         testRequireAuth();
         testLimitExceeded();
+        testLoanBrokerRequiresClosedEndedVault();
     }
 
     // Tests run under each entry in amendmentCombinations().
diff --git a/src/test/app/lending/Loan_test.cpp b/src/test/app/lending/Loan_test.cpp
deleted file mode 100644
index 717387665e..0000000000
--- a/src/test/app/lending/Loan_test.cpp
+++ /dev/null
@@ -1,46 +0,0 @@
-#include 
-#include 
-
-#include 
-#include 
-#include 
-
-namespace xrpl::test {
-
-/**
- * Aggregator: running this suite ("Loan") reruns every topical Loan/Lending
- * suite in one invocation. Each member suite below remains independently
- * runnable under its own name. Declared manual so an unfiltered full test
- * run doesn't execute every case twice.
- */
-class Loan_test : public beast::unit_test::Suite
-{
-    void
-    run() override
-    {
-        static constexpr std::array kMembers{
-            "LendingHelpers",
-            "LoanBroker",
-            "LoanCashBasis",
-            "LoanCoverFreezeAuth",
-            "LoanInvariants",
-            "LoanLifecycle",
-            "LoanMisc",
-            "LoanPay",
-            "LoanRounding",
-            "LoanSecurity",
-            "LoanSet",
-            "LoanValidation",
-        };
-
-        for (auto const& info : beast::unit_test::globalSuites())
-        {
-            if (std::ranges::find(kMembers, info.name()) != kMembers.end())
-                info.run(runner());
-        }
-    }
-};
-
-BEAST_DEFINE_TESTSUITE_MANUAL(Loan, tx, xrpl);
-
-}  // namespace xrpl::test
diff --git a/src/test/app/tx/apply_test.cpp b/src/test/app/tx/apply_test.cpp
index 8f71d47fcf..39289a6264 100644
--- a/src/test/app/tx/apply_test.cpp
+++ b/src/test/app/tx/apply_test.cpp
@@ -1,12 +1,23 @@
 // Copyright (c) 2020 Dev Null Productions
 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
@@ -22,6 +33,136 @@ public:
     {
         testcase("Require Fully Canonical Signature");
         testFullyCanonicalSigs();
+        testRoleSignatureCacheIsEraSpecific();
+        testForcedValidityIgnoresPrefixEra();
+    }
+
+    // forceValidity means the caller verified nothing and wants the result
+    // trusted, so it has to hold in both prefix eras. If it marked only the
+    // ordinary slot, a role-signature transaction would still be verified
+    // under pre-fix rules, defeating the cluster path and the configurations
+    // that turn signature checks off.
+    void
+    testForcedValidityIgnoresPrefixEra()
+    {
+        testcase("Forced validity ignores the prefix era");
+
+        using namespace test::jtx;
+
+        Env preFix{*this, testableAmendments() - fixCleanup3_4_0};
+        Env postFix{*this, testableAmendments()};
+        auto const preFixRules = preFix.current()->rules();
+
+        Account const alice{"alice"};
+        Account const sponsor{"sponsor"};
+        postFix.fund(XRP(10'000), alice, sponsor);
+        postFix.close();
+
+        // Signed under the post-fix rules, so this signature does not verify
+        // under the pre-fix prefix. Only the forced verdict can make the check
+        // below pass.
+        auto const jt = postFix.jt(
+            noop(alice),
+            Fee(XRP(1)),
+            sponsor::As(sponsor, spfSponsorFee),
+            Sig(sfSponsorSignature, sponsor));
+        if (!BEAST_EXPECT(jt.stx))
+            return;
+
+        // A router that has never seen this transaction, so the only cached
+        // state is what forceValidity writes.
+        auto& router = preFix.app().getHashRouter();
+        forceValidity(router, jt.stx->getTransactionID(), Validity::SigGoodOnly);
+        BEAST_EXPECT(checkValidity(router, *jt.stx, preFixRules).first != Validity::SigBad);
+    }
+
+    // A signature verdict reached under one prefix era must not be honored in
+    // the other, because the two eras require the sponsor signature to cover
+    // different bytes. Each direction below uses one HashRouter and differs
+    // only in the rules, which is what the flag ledger looks like in practice:
+    // relay and submit verify against the validated rules, which lag the open
+    // ledger rules that preflight2 verifies against, so one transaction gets
+    // checked under both prefixes at the same time.
+    void
+    testRoleSignatureCacheIsEraSpecific()
+    {
+        testcase("Role signature cache is era specific");
+
+        using namespace test::jtx;
+
+        Env preFix{*this, testableAmendments() - fixCleanup3_4_0};
+        Env postFix{*this, testableAmendments()};
+        auto const preFixRules = preFix.current()->rules();
+        auto const postFixRules = postFix.current()->rules();
+
+        Account const alice{"alice"};
+        Account const sponsor{"sponsor"};
+        Account const counterparty{"counterparty"};
+        for (auto* env : {&preFix, &postFix})
+        {
+            env->fund(XRP(10'000), alice, sponsor, counterparty);
+            env->close();
+        }
+
+        // Both directions for a transaction whose role signature sits in the
+        // field that makeTx signs. makeTx builds the transaction in the Env it
+        // is given, so the role signature carries that era's prefix.
+        auto checkBothDirections = [&](std::function const& makeTx) {
+            // Direction 1: a good verdict under the old prefix must not let a
+            // signature moved between roles survive the amendment.
+            {
+                auto const jt = makeTx(preFix);
+                if (!BEAST_EXPECT(jt.stx))
+                    return;
+
+                auto& router = preFix.app().getHashRouter();
+                BEAST_EXPECT(checkValidity(router, *jt.stx, preFixRules).first == Validity::Valid);
+
+                // Same router, asked again under the post-fix rules. The Valid
+                // verdict above was reached under the old prefix and must not
+                // be reused, or a signature moved between roles would survive
+                // the amendment.
+                BEAST_EXPECT(
+                    checkValidity(router, *jt.stx, postFixRules).first == Validity::SigBad);
+            }
+
+            // Direction 2: a bad verdict under the old prefix must not condemn
+            // a transaction that the new prefixes accept. A node whose
+            // validated rules still lag the open ledger will run this check
+            // pre-fix first and reject a correctly new-prefix-signed
+            // transaction; the post-fix check must then verify it afresh
+            // instead of reusing the pre-fix verdict.
+            {
+                auto const jt = makeTx(postFix);
+                if (!BEAST_EXPECT(jt.stx))
+                    return;
+
+                auto& router = postFix.app().getHashRouter();
+                BEAST_EXPECT(checkValidity(router, *jt.stx, preFixRules).first == Validity::SigBad);
+                BEAST_EXPECT(checkValidity(router, *jt.stx, postFixRules).first == Validity::Valid);
+            }
+        };
+
+        // sfSponsorSignature, which uses the SPN and SPM prefixes.
+        checkBothDirections([&](Env& env) {
+            return env.jt(
+                noop(alice),
+                Fee(XRP(1)),
+                sponsor::As(sponsor, spfSponsorFee),
+                Sig(sfSponsorSignature, sponsor));
+        });
+
+        // sfCounterpartySignature, which uses its own prefixes, CPT and CPM,
+        // and only appears on a LoanSet. The transaction does not have to be
+        // applicable: checkValidity verifies signatures without consulting the
+        // ledger, so a placeholder LoanBrokerID is enough.
+        checkBothDirections([&](Env& env) {
+            return env.jt(
+                loan::set(alice, uint256{1}, Number{1}),
+                loan::kCounterparty(counterparty),
+                Fee(XRP(1)),
+                Sig(sfCounterpartySignature, counterparty));
+        });
     }
 
     void
diff --git a/src/test/app/vault/VaultBugs_test.cpp b/src/test/app/vault/VaultBugs_test.cpp
new file mode 100644
index 0000000000..cc30bd6091
--- /dev/null
+++ b/src/test/app/vault/VaultBugs_test.cpp
@@ -0,0 +1,2776 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include   // IWYU pragma: keep
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class VaultBugs_test : public VaultTestBase
+{
+private:
+    // Bug: the equality check (vault outflow == destination inflow) was
+    // skipped whenever the destination delta rounded to zero at localMinScale,
+    // including cases where the vault outflow rounded to a non-zero value and
+    // a representable amount of value was genuinely destroyed.
+    //
+    // Scenario: Bob's IOU balance sits 5 units below the 10^16 STAmount
+    // precision boundary (atEdge2 = 9,999,999,999,999,995).  A withdrawal of
+    // 6 USD shifts his balance across that boundary: the exponent increments
+    // (0 → 1), so his effective inflow in Number space is only +5 — 1 USD is
+    // consumed by the precision-boundary rounding and cannot be credited.
+    //
+    // The destroyed amount (1 USD) is sub-ULP at destinationScale=1 (step=10),
+    // so the check treats it as an unavoidable IOU-precision artefact and
+    // lets the transaction succeed.
+    //
+    // Contrast: if 15 USD were destroyed at the same scale (destroyed ≥ step),
+    // floor(15/10)=1 ≠ 0 and the invariant would fire — that discrepancy IS
+    // representable and indicates a real accounting bug.
+    //
+    // Pre-fixCleanup3_2_0: the "must increase destination balance" check fires
+    // because roundedDestinationDelta = 0 ≤ 0.
+    void
+    testVaultWithdrawEqualityEnforced()
+    {
+        using namespace test::jtx;
+
+        auto runScenario = [this](FeatureBitset features, TER expected) {
+            std::string logs;
+            Env env(*this, features, std::make_unique(&logs));
+
+            Account const issuer{"issuer"};
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+
+            env.fund(XRP(100'000), issuer, alice, bob);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            STAmount const aliceLimit{usd.raw(), 2, 16};
+            STAmount const bobLimit{usd.raw(), 2, 16};
+            // Bob's balance sits 5 units below the 10^16 STAmount precision
+            // boundary.  Receiving 6 USD shifts his exponent 0 → 1; the
+            // STAmount records +5, not +6 (1 USD is lost to rounding).
+            STAmount const atEdge2{usd.raw(), Number{9'999'999'999'999'995LL}};
+
+            env(trust(alice, aliceLimit));
+            env(trust(bob, bobLimit));
+            env.close();
+
+            env(pay(issuer, alice, usd(1'000)));
+            env(pay(issuer, bob, atEdge2));
+            env.close();
+
+            Vault const vault{env};
+            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
+            vaultTx[sfScale] = 0;
+            env(vaultTx);
+            env.close();
+
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1'000)}));
+            env.close();
+
+            // Withdraw 6 USD to Bob: vault loses 6, Bob gains only 5.
+            // Destroyed amount = 1 USD, which is sub-ULP at destinationScale=1.
+            auto tx = vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(6)});
+            tx[sfDestination] = bob.human();
+            env(tx, Ter(expected));
+            env.close();
+        };
+
+        {
+            testcase(
+                "bug: VaultWithdraw to destination at IOU precision boundary fires "
+                "invariant (pre-fixCleanup3_2_0)");
+            runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
+        }
+        {
+            testcase(
+                "bug: VaultWithdraw to destination at IOU precision boundary succeeds "
+                "when destroyed amount is sub-ULP (post-fixCleanup3_2_0)");
+            runScenario(testableAmendments(), tesSUCCESS);
+        }
+    }
+
+    // VaultDeposit by issuer with the vault parked at the IOU 16-digit
+    // edge (9.999e15). Issuer mints 2 more USD; the vault trust line
+    // goes 9.999e15 → 10^16, gaining 1 unit instead of 2 (canonicalization).
+    //
+    // Pre-fixCleanup3_2_0: the proactive check is absent; the deposit
+    // applies, then VaultInvariant's "deposit must increase vault
+    // balance" assertion fires at finalize time on the rounded vault
+    // delta of zero, returning tecINVARIANT_FAILED.
+    // Post-amendment: reject deposit that is not representable at Vault scale.
+    void
+    testBugIssuerVaultDepositAtEdge()
+    {
+        using namespace test::jtx;
+
+        auto runScenario = [this](FeatureBitset features, TER expected) {
+            std::string logs;
+            Env env(*this, features, std::make_unique(&logs));
+
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+
+            env.fund(XRP(100'000), issuer, owner);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            STAmount const trustLimit{usd.raw(), 2, 16};
+            STAmount const ownerFund{usd.raw(), Number{9'999'999'999'999'999LL}};
+
+            env(trust(owner, trustLimit));
+            env.close();
+            env(pay(issuer, owner, ownerFund));
+            env.close();
+
+            Vault const vault{env};
+            auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = usd});
+            vaultTx[sfScale] = 0;
+            env(vaultTx);
+            env.close();
+            env(vault.deposit({.depositor = owner, .id = vaultKeylet.key, .amount = ownerFund}));
+            env.close();
+
+            // Vault pseudo-account is now at 9.999e15. Issuer mints 2
+            // more USD. Pre: tecINVARIANT_FAILED at finalize. Post:
+            // tecPRECISION_LOSS proactively. Either way, no value moves.
+            env(vault.deposit({.depositor = issuer, .id = vaultKeylet.key, .amount = usd(2)}),
+                Ter(expected));
+            env.close();
+        };
+
+        {
+            testcase(
+                "bug: VaultDeposit by issuer at IOU edge fires "
+                "tecINVARIANT_FAILED at finalize (pre-fixCleanup3_2_0)");
+            runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
+        }
+        {
+            testcase(
+                "bug: VaultDeposit by issuer at IOU edge rejects with "
+                "tecPRECISION_LOSS proactively (post-fixCleanup3_2_0)");
+            runScenario(testableAmendments(), tecPRECISION_LOSS);
+        }
+    }
+
+    // Bug: DeltaInfo::makeDelta uses max(scale(after), scale(before)) for
+    // sfAssetsTotal/Available deltas.  This is symmetric to
+    // testBugMakeDeltaAnteriorScale but in the opposite direction: a deposit
+    // pushes assetsTotal from just below 1e16 (IOU exponent 0, ULP = 1) to just
+    // above it (exponent 1, ULP = 10).  makeDelta picks the coarser *posterior*
+    // scale 1.  The trust line balance rounds from atEdge + 2 = 10,000,000,000,000,001
+    // → 1e16, so the pseudo-account delta is only +1 in IOU space.
+    // roundToAsset(+1, scale=1) = 0 fires "deposit must increase vault balance"
+    // even though the state change is consistent at every precision boundary.
+    //
+    // Fix (fixCleanup3_2_0): computeVaultMinScale uses the posterior Number-space
+    // scale of sfAssetsTotal (which retains the full value 10,000,000,000,000,001,
+    // exponent 0), giving minScale = 0.  roundToAsset(+1, scale=0) = 1 > 0 and
+    // the invariant passes.  However the transactor's own precision guard fires
+    // first (bob pays 2 USD, vault receives only 1 due to IOU rounding), so the
+    // post-amendment result is tecPRECISION_LOSS rather than tesSUCCESS —
+    // the depositor is protected from silently losing 1 USD to rounding.
+    void
+    testBugMakeDeltaPosteriorScale()
+    {
+        using namespace test::jtx;
+
+        auto runScenario = [this](FeatureBitset features, TER expected) {
+            std::string logs;
+            Env env(*this, features, std::make_unique(&logs));
+
+            Account const issuer{"issuer"};
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+
+            env.fund(XRP(100'000), issuer, alice, bob);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            // atEdge is the largest IOU value with exponent 0 (ULP = 1).
+            // A deposit of 2 USD brings assetsTotal to 10,000,000,000,000,001
+            // in Number space, crossing the 1e16 boundary in IOU space.
+            STAmount const atEdge{usd.raw(), Number{9'999'999'999'999'999LL}};
+
+            env(trust(alice, STAmount{usd.raw(), 2, 16}));
+            env(trust(bob, usd(100)));
+            env.close();
+            env(pay(issuer, alice, atEdge));
+            env(pay(issuer, bob, usd(2)));
+            env.close();
+
+            Vault const vault{env};
+            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
+            vaultTx[sfScale] = 0;
+            env(vaultTx);
+            env.close();
+
+            // sfAssetsTotal = sfAssetsAvailable = atEdge (exponent 0, ULP = 1)
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = atEdge}));
+            env.close();
+
+            // Deposit 2 USD: +2 is sub-ULP at the posterior IOU scale (ULP = 10)
+            // but exact at the Number scale retained by sfAssetsTotal.
+            env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = usd(2)}),
+                Ter(expected));
+            env.close();
+        };
+
+        {
+            testcase(
+                "bug: VaultDeposit across IOU scale boundary fires invariant "
+                "(pre-fixCleanup3_2_0)");
+            runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
+        }
+        {
+            testcase(
+                "bug: VaultDeposit across IOU scale boundary succeeds "
+                "(post-fixCleanup3_2_0)");
+            runScenario(testableAmendments(), tecPRECISION_LOSS);
+        }
+    }
+
+    // Bug: DeltaInfo::makeDelta uses max(scale(after), scale(before)) for the
+    // sfAssetsTotal and sfAssetsAvailable deltas, and visitEntry applies the
+    // same max() for the vault pseudo-account RippleState.  When
+    // sfAssetsTotal sits exactly at 1e16 (IOU exponent 1, ULP = 10) and a
+    // withdrawal of 5 USD brings it to 9.999...995e15 (IOU exponent 0,
+    // ULP = 1), all three computations pick the anterior coarser scale 1.
+    // roundToAsset(-5, scale=1) collapses to 0, so the invariant check
+    // vaultPseudoDeltaAssets >= kZero fires even though the state change is
+    // valid and fully consistent at IOU precision.
+    //
+    // Fix (fixCleanup3_2_0): finalize compares the vault pseudo-account and
+    // sfAssetsTotal/Available deltas directly in Number space, bypassing
+    // scale-coarsened rounding.
+    void
+    testBugMakeDeltaAnteriorScale()
+    {
+        using namespace test::jtx;
+
+        auto runScenario = [this](FeatureBitset features, TER expected) {
+            std::string logs;
+            Env env(*this, features, std::make_unique(&logs));
+
+            Account const issuer{"issuer"};
+            Account const alice{"alice"};
+
+            env.fund(XRP(100'000), issuer, alice);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            // Trust limit of 2e16, fund exactly 1e16 so deposit lands at the
+            // IOU scale-1 boundary (exponent 1, ULP = 10).
+            STAmount const fundAndDeposit{usd.raw(), Number{1, 16}};
+
+            env(trust(alice, STAmount{usd.raw(), 2, 16}));
+            env.close();
+            env(pay(issuer, alice, fundAndDeposit));
+            env.close();
+
+            Vault const vault{env};
+            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
+            vaultTx[sfScale] = 0;
+            env(vaultTx);
+            env.close();
+
+            // sfAssetsTotal = sfAssetsAvailable = 1e16 (exponent 1, ULP = 10).
+            env(vault.deposit(
+                {.depositor = alice, .id = vaultKeylet.key, .amount = fundAndDeposit}));
+            env.close();
+
+            // Withdraw 5 USD: -5 is sub-ULP at the anterior scale (ULP = 10)
+            // but exact at the posterior scale (ULP = 1).  The state change is
+            // consistent; only the invariant's scale selection is wrong.
+            env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(5)}),
+                Ter(expected));
+            env.close();
+        };
+
+        {
+            testcase(
+                "bug: VaultWithdraw across IOU scale boundary fires invariant "
+                "(pre-fixCleanup3_2_0)");
+            runScenario(testableAmendments() - fixCleanup3_2_0, tecINVARIANT_FAILED);
+        }
+        {
+            testcase(
+                "bug: VaultWithdraw across IOU scale boundary succeeds "
+                "(post-fixCleanup3_2_0)");
+            runScenario(testableAmendments(), tesSUCCESS);
+        }
+    }
+
+    // Bug: when a depositor's IOU trustline balance is very large (e.g.
+    // ~1e17), adding a small deposit (e.g. 1 USD) leaves sfAssetsTotal
+    // unchanged at IOU precision because the increment is sub-ULP at the
+    // vault's current asset scale.  The vault records the deposit, mints
+    // shares, and decrements the depositor's trustline, but sfAssetsTotal
+    // does not change — the conservation invariant fires because the rail
+    // delta is zero.
+    //
+    // Two sub-cases are exercised:
+    //   1. First-ever deposit into an empty vault: the depositor's own
+    //      trustline has a large balance so 1 USD canonicalizes to zero
+    //      when written back through the IOU rail.
+    //   2. Subsequent deposit after the vault already holds a large
+    //      sfAssetsTotal: a different depositor (bob, with a small balance)
+    //      sends 1 USD, which again rounds to zero at the vault's coarse
+    //      asset scale.
+    //
+    // Fix (fixCleanup3_2_0): the deposit transactor checks whether
+    // roundToAsset(amount, vault_scale) == 0 and rejects early with
+    // tecPRECISION_LOSS before any state is modified.
+    void
+    testVaultDepositCanonicalizeToZero()
+    {
+        using namespace test::jtx;
+        auto runScenario = [this](FeatureBitset features, TER expected) {
+            std::string logs;
+            Env env(*this, features, std::make_unique(&logs));
+
+            Account const issuer{"issuer"};
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+
+            env.fund(XRP(100'000), issuer, alice, bob);
+            env.close();
+
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+
+            STAmount const trustLimit{usd.raw(), Number{99'999'999'999'999'999LL}};
+            STAmount const aliceFund{usd.raw(), Number{99'999'999'999'999'999LL}};
+
+            env(trust(alice, trustLimit));
+            env(trust(bob, trustLimit));
+            env.close();
+
+            env(pay(issuer, alice, aliceFund));
+            env(pay(issuer, bob, usd(1000)));
+            env.close();
+
+            Vault const vault{env};
+
+            // Scale=0 so sfAssetsTotal stores whole USD
+            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
+            vaultTx[sfScale] = 0;
+            env(vaultTx);
+            env.close();
+
+            // Alice's deposit canonicalizes to zero at her own trustline scale
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1)}),
+                Ter(expected));
+
+            // Increase vault-scale
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = aliceFund}));
+            env.close();
+
+            env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = usd(1)}),
+                Ter(expected));
+            env.close();
+        };
+
+        {
+            // fixCleanup3_4_0 has to be off as well: its depositor-side check
+            // rejects alice's deposit for the same reason, so the invariant is
+            // only reachable with neither guard in place.
+            testcase(
+                "bug: VaultDeposit below Vault precision canonicalized to zero "
+                "(pre-fixCleanup3_2_0)");
+            // Also remove fixCleanup3_4_0 so the VaultDeposit clamp
+            // introduced by that amendment does not short-circuit this
+            // pre-fixCleanup3_2_0 scenario with tecPRECISION_LOSS.
+            runScenario(
+                testableAmendments() - fixCleanup3_2_0 - fixCleanup3_4_0, tecINVARIANT_FAILED);
+        }
+        {
+            testcase(
+                "bug: VaultDeposit below Vault precision canonicalized to zero "
+                "(post-fixCleanup3_2_0)");
+            runScenario(testableAmendments(), tecPRECISION_LOSS);
+        }
+    }
+
+    // A deposit does not transfer the requested amount. It transfers the
+    // request truncated to a whole number of shares and converted back, which
+    // can be strictly smaller. When that smaller value is below half a ULP at
+    // the depositor's own trust-line scale, the debit rounds away to nothing:
+    // the depositor pays nothing, while the vault books the assets and mints
+    // shares. ValidVault catches the desync at finalize time.
+    //
+    // Only a non-power-of-ten assets-to-shares ratio is needed, and that
+    // happens through ordinary use: LoanPay books accrued interest into
+    // sfAssetsTotal without minting shares.
+    //
+    // The fixCleanup3_2_0 guard in preclaim does not help, because it tests the
+    // raw requested amount, which is large enough to survive the rounding.
+    // Post-fixCleanup3_4_0 the post-truncation value is checked as well and the
+    // deposit is rejected with tecPRECISION_LOSS before anything moves.
+    void
+    testBugDepositShareTruncationSubUlp()
+    {
+        using namespace test::jtx;
+        using namespace loan_broker;
+        using namespace loan;
+
+        // How bob's trust line is set up before he deposits. Holding is the plain case: a large
+        // positive balance whose ULP swallows the debit. InDebt is the case where the stored
+        // balance and the spendable amount diverge: bob owes the issuer 1e16, and the issuer's
+        // limit on the same line lets him spend 1000 anyway. Reading the spendable amount there
+        // reports a small, finely scaled number, while the rounding of the debit is still governed
+        // by the 1e16 he actually holds.
+        enum class Line { Holding, InDebt };
+
+        auto runScenario = [this](FeatureBitset features, Line line, TER expected) {
+            std::string logs;
+            Env env(*this, features, std::make_unique(&logs));
+
+            Account const issuer{"issuer"};
+            Account const alice{"alice"};
+            Account const carol{"carol"};
+            Account const bob{"bob"};
+
+            env.fund(XRP(100'000), issuer, alice, carol, bob);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            PrettyAsset const bobUsd{bob["USD"]};
+            STAmount const trustLimit{usd.raw(), Number{99'999'999'999'999'999LL}};
+            // Bob's balance sits exactly on a multiple-of-10 boundary at the
+            // 1e16 IOU precision cusp, where one ULP is 10.
+            STAmount const bobEdge{usd.raw(), Number{10'000'000'000'000'010LL}};
+            STAmount const bobDebt{bobUsd.raw(), Number{10'000'000'000'000'000LL}};
+            STAmount const oppositeLimit{bobUsd.raw(), Number{10'000'000'000'001'000LL}};
+
+            env(trust(alice, trustLimit));
+            env(trust(carol, trustLimit));
+            env(trust(bob, trustLimit));
+            env.close();
+
+            env(pay(issuer, alice, usd(1'000)));
+            env(pay(issuer, carol, usd(1'000)));
+            if (line == Line::Holding)
+            {
+                env(pay(issuer, bob, bobEdge));
+            }
+            else
+            {
+                // The issuer trusts bob's own USD, so bob can issue 1e16 back and still have
+                // 1000 of spendable room left on the same line.
+                env(trust(issuer, oppositeLimit));
+                env.close();
+                env(pay(bob, issuer, bobDebt));
+            }
+            env.close();
+
+            Vault const vault{env};
+            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
+            vaultTx[sfScale] = 0;
+            env(vaultTx);
+            env.close();
+
+            // Alice deposits 1000 USD, minting 1000 shares 1:1.
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1'000)}));
+            env.close();
+
+            // A loan broker on the vault, then a bullet loan at 24% interest:
+            // a single payment, one year out.
+            auto const brokerKeylet =
+                keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
+            env(set(alice, vaultKeylet.key));
+            env.close();
+
+            auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
+            env(set(carol, brokerKeylet.key, usd(1'000).value()),
+                loan::kInterestRate(percentageToTenthBips(24)),
+                kGracePeriod(60),
+                kPaymentInterval(365 * 24 * 60 * 60),
+                kPaymentTotal(1),
+                Sig(sfCounterpartySignature, alice),
+                Fee(env.current()->fees().base * 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Advance to just before the single payment falls due and let carol
+            // repay principal plus interest. LoanPay is what books the accrued
+            // interest into sfAssetsTotal; under cash-basis accounting LoanSet
+            // alone does not. Share supply stays at 1000, so
+            // assetsTotal/sharesTotal becomes 1240/1000.
+            env.close(std::chrono::seconds{(365 * 24 * 60 * 60) - 3600});
+            env(pay(carol, loanKeylet.key, usd(2'000).value()), Ter(tesSUCCESS));
+            env.close();
+
+            // Pin the ratio the rest of the scenario reasons about, so the test cannot quietly
+            // stop exercising the bug if the setup drifts.
+            auto const sleVault = env.le(vaultKeylet);
+            BEAST_EXPECT(sleVault && sleVault->at(sfAssetsTotal) == Number{1'240});
+            auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
+            BEAST_EXPECT(sleIssuance && sleIssuance->at(sfOutstandingAmount) == 1'000);
+
+            // Bob deposits 6 USD, which rounds to 10 at his own trust-line
+            // scale and so clears the fixCleanup3_2_0 guard. But
+            // floor(1000 * 6 / 1240) is 4 shares, worth 4 * 1240 / 1000 = 4.96,
+            // and that is below half a ULP of his balance, so it rounds away to
+            // nothing when subtracted.
+            env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = usd(6)}),
+                Ter(expected));
+            env.close();
+        };
+
+        // Strip featureLendingProtocolV1_1: this scenario runs an
+        // open-ended vault through deposit/broker/loan/repay/deposit,
+        // which spans both Subscription and post-loan lifetime — a phase
+        // pattern that only makes sense on open-ended vaults. The gate
+        // added by LP V1.1 is unrelated to the truncation bug asserted
+        // here.
+        auto const legacy = testableAmendments() - featureLendingProtocolV1_1;
+        {
+            testcase(
+                "bug: VaultDeposit share truncation lets depositor debit "
+                "round away to zero (pre-fixCleanup3_4_0)");
+            runScenario(legacy - fixCleanup3_4_0, Line::Holding, tecINVARIANT_FAILED);
+        }
+        {
+            testcase(
+                "bug: VaultDeposit share truncation lets depositor debit "
+                "round away to zero (pre-fixCleanup3_2_0 and pre-fixCleanup3_4_0)");
+            runScenario(
+                legacy - fixCleanup3_2_0 - fixCleanup3_4_0, Line::Holding, tecINVARIANT_FAILED);
+        }
+        {
+            testcase(
+                "bug: VaultDeposit share truncation rejected with "
+                "tecPRECISION_LOSS (post-fixCleanup3_4_0)");
+            runScenario(legacy, Line::Holding, tecPRECISION_LOSS);
+        }
+        {
+            testcase(
+                "bug: VaultDeposit share truncation rejected with "
+                "tecPRECISION_LOSS (post-fixCleanup3_4_0, pre-fixCleanup3_2_0)");
+            runScenario(legacy - fixCleanup3_2_0, Line::Holding, tecPRECISION_LOSS);
+        }
+        {
+            testcase(
+                "bug: VaultDeposit share truncation against a debt balance "
+                "round away to zero (pre-fixCleanup3_4_0)");
+            runScenario(legacy - fixCleanup3_4_0, Line::InDebt, tecINVARIANT_FAILED);
+        }
+        {
+            testcase(
+                "bug: VaultDeposit share truncation against a debt balance rejected with "
+                "tecPRECISION_LOSS (post-fixCleanup3_4_0)");
+            runScenario(legacy, Line::InDebt, tecPRECISION_LOSS);
+        }
+    }
+
+    // Bug: ValidVault::visitEntry computes destinationDelta.scale as
+    // max(before_exponent, after_exponent) for RippleState entries.  When a
+    // withdrawal credits a destination whose IOU balance sits just below a
+    // power-of-10 boundary (atEdge = 9'999'999'999'999'999), the post-credit
+    // STAmount rounds up one exponent (exponent 0 → 1), making
+    // destinationDelta.scale = 1.  The invariant then calls
+    // roundToAsset(+2 USD, scale=1) = 0 and incorrectly fires
+    // "withdrawal must increase destination balance".
+    //
+    // Fix (fixCleanup3_2_0): finalize compares destination delta directly in
+    // Number space, bypassing scale-coarsened rounding.  The transaction
+    // itself succeeds because the effective IOU credit is non-trivial at
+    // Number precision even though the STAmount exponent shifted.
+    void
+    testVaultWithdrawCanonicalizeToZero()
+    {
+        using namespace test::jtx;
+
+        enum class DestKind : bool { ThirdParty = false, Self = true };
+
+        auto runScenario = [this](FeatureBitset features, DestKind destKind, TER expected) {
+            std::string logs;
+            Env env(*this, features, std::make_unique(&logs));
+
+            Account const issuer{"issuer"};
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+
+            env.fund(XRP(100'000), issuer, alice, bob);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            STAmount const aliceLimit{usd.raw(), 2, 16};
+            STAmount const bobLimit{usd.raw(), 2, 16};
+            STAmount const atEdge{usd.raw(), Number{9'999'999'999'999'999LL}};
+
+            env(trust(alice, aliceLimit));
+            if (destKind == DestKind::ThirdParty)
+                env(trust(bob, bobLimit));
+            env.close();
+
+            env(pay(issuer, alice, usd(1'000)));
+            if (destKind == DestKind::ThirdParty)
+                env(pay(issuer, bob, atEdge));
+            env.close();
+
+            Vault const vault{env};
+            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
+            vaultTx[sfScale] = 0;
+            env(vaultTx);
+            env.close();
+
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(1'000)}));
+            env.close();
+
+            // For the self-destination case, push alice's own trust line to
+            // the IOU edge so the next withdraw inflow crosses the boundary.
+            if (destKind == DestKind::Self)
+            {
+                env(pay(issuer, alice, atEdge));
+                env.close();
+            }
+
+            auto tx = vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(2)});
+            if (destKind == DestKind::ThirdParty)
+                tx[sfDestination] = bob.human();
+            env(tx, Ter(expected));
+            env.close();
+        };
+
+        {
+            testcase(
+                "bug: VaultWithdraw to third-party at IOU edge fires invariant "
+                "(pre-fixCleanup3_2_0)");
+            runScenario(
+                testableAmendments() - fixCleanup3_2_0, DestKind::ThirdParty, tecINVARIANT_FAILED);
+        }
+        {
+            testcase(
+                "bug: VaultWithdraw to third-party at IOU edge succeeds "
+                "(post-fixCleanup3_2_0)");
+            runScenario(testableAmendments(), DestKind::ThirdParty, tesSUCCESS);
+        }
+        {
+            testcase(
+                "bug: VaultWithdraw to self at IOU edge fires invariant "
+                "(pre-fixCleanup3_2_0)");
+            runScenario(
+                testableAmendments() - fixCleanup3_2_0, DestKind::Self, tecINVARIANT_FAILED);
+        }
+        {
+            testcase(
+                "bug: VaultWithdraw to self at IOU edge succeeds "
+                "(post-fixCleanup3_2_0)");
+            runScenario(testableAmendments(), DestKind::Self, tesSUCCESS);
+        }
+    }
+
+    // Bug: a debit can be genuinely non-zero yet still be dust relative to a
+    // sfAssetsTotal/sfAssetsAvailable large enough to exceed STAmount's precision, e.g.
+    // AssetsTotal 2e12 minus a 1e-6 debit needs 19 significant digits and rounds straight
+    // back to 2e12. The shares still move, so ValidVault later fails with "must decrease
+    // vault balance" instead of a clean upfront rejection.
+    //
+    // Fix (fixCleanup3_4_0): reject upfront with tecPRECISION_LOSS if the debit would
+    // canonicalize back to the prior stored value.
+    //
+    // With a single depositor AssetsTotal == AssetsAvailable, so both
+    // debitIsNonZeroDust operands trip together here. LoanRounding_test's
+    // "dust debit vs AssetsTotal only" case isolates the AssetsTotal operand
+    // via a heavily-loaned vault.
+    void
+    testBugVaultDustDebitCanonicalizesToNoOp()
+    {
+        using namespace test::jtx;
+
+        // Fund a single depositor and have them deposit `total` USD in one shot (default
+        // scale 6, so shares mint at exactly total*1e6).
+        auto const seedVault = [](Env& env, Number const& total) {
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            Account const holder{"holder"};
+
+            env.fund(XRP(1'000'000), issuer, owner, holder);
+            env.close();
+            env(fset(issuer, asfAllowTrustLineClawback));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            env(trust(holder, usd(100'000'000'000'000LL)));
+            env.close();
+            env(pay(issuer, holder, usd(total)));
+            env.close();
+
+            Vault const vault{env};
+            auto const [tx, keylet] = vault.create({.owner = owner, .asset = usd.raw()});
+            env(tx);
+            env.close();
+            env(vault.deposit({.depositor = holder, .id = keylet.key, .amount = usd(total)}),
+                Ter(tesSUCCESS));
+            env.close();
+
+            return keylet;
+        };
+
+        {
+            auto runScenario = [&](FeatureBitset features, TER expected) {
+                Env env(*this, features);
+                Number const total{2, 12};
+                auto const keylet = seedVault(env, total);
+
+                Account const issuer{"issuer"};
+                PrettyAsset const usd{issuer["USD"]};
+
+                // 1 share's worth of assets: 1e-6, below AssetsTotal's storage precision.
+                env(Vault::clawback(
+                        {.issuer = issuer,
+                         .id = keylet.key,
+                         .holder = Account{"holder"},
+                         .amount = usd(Number{1, -6}).value()}),
+                    Ter(expected));
+                env.close();
+            };
+
+            testcase("bug: VaultClawback dust debit fires invariant (pre-fixCleanup3_4_0)");
+            runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
+            testcase("bug: VaultClawback dust debit rejected cleanly (post-fixCleanup3_4_0)");
+            runScenario(all_, tecPRECISION_LOSS);
+        }
+
+        {
+            auto runScenario = [&](FeatureBitset features, TER expected) {
+                Env env(*this, features);
+                Number const total{2, 12};
+                auto const keylet = seedVault(env, total);
+
+                MPTIssue const share{env.le(keylet)->at(sfShareMPTID)};
+
+                // Redeem 1 share, worth 1e-6 assets, below AssetsTotal's storage precision.
+                env(Vault::withdraw(
+                        {.depositor = Account{"holder"},
+                         .id = keylet.key,
+                         .amount = STAmount{share, 1}}),
+                    Ter(expected));
+                env.close();
+            };
+
+            testcase("bug: VaultWithdraw dust debit fires invariant (pre-fixCleanup3_4_0)");
+            runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
+            testcase("bug: VaultWithdraw dust debit rejected cleanly (post-fixCleanup3_4_0)");
+            runScenario(all_, tecPRECISION_LOSS);
+        }
+    }
+
+    // Scale 15 seed + deposit 5: pre-fix credited > paid; post-fix credited <= paid.
+    // fixCleanup3_2_0 is off so roundToVaultScale does not shrink the deposit first.
+    void
+    testBugVaultDepositOvercreditsAcrossScaleBoundary()
+    {
+        using namespace test::jtx;
+
+        auto runScenario = [this](FeatureBitset features, bool expectOvercredit) {
+            Env env(*this, features);
+            Account const owner{"owner"};
+            Account const issuer{"issuer"};
+            Account const depositor{"depositor"};
+            env.fund(XRP(1'000'000), owner, issuer, depositor);
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            Number const seed{9'999'999'999'999'999LL, -15};
+            Number const deposit{5};
+
+            env(trust(depositor, usd(1'000'000'000)));
+            env.close();
+            env(pay(issuer, depositor, usd(deposit)));
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = usd.raw()});
+            tx[sfScale] = 15;
+            env(tx);
+            env.close();
+            env(vault.deposit({.depositor = issuer, .id = keylet.key, .amount = usd(seed)}));
+            env.close();
+
+            Number const totalBefore = env.le(keylet)->at(sfAssetsTotal);
+            Number const depositorBefore = env.balance(depositor, usd.raw()).number();
+
+            env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = usd(deposit)}));
+            env.close();
+
+            Number const totalAfter = env.le(keylet)->at(sfAssetsTotal);
+            Number const depositorAfter = env.balance(depositor, usd.raw()).number();
+            Number const paid = depositorBefore - depositorAfter;
+            Number const credited = totalAfter - totalBefore;
+
+            if (expectOvercredit)
+            {
+                BEAST_EXPECTS(
+                    credited > paid,
+                    "AssetsTotal credited " + to_string(credited) + " for a payment of " +
+                        to_string(paid) + ", expected an overcredit");
+            }
+            else
+            {
+                BEAST_EXPECTS(
+                    credited <= paid,
+                    "AssetsTotal credited " + to_string(credited) + " for a payment of " +
+                        to_string(paid));
+            }
+        };
+
+        testcase(
+            "bug: VaultDeposit overcredits across an IOU scale boundary "
+            "(pre-fixCleanup3_4_0)");
+        runScenario(all_ - fixCleanup3_2_0 - fixCleanup3_4_0, true);
+
+        testcase(
+            "bug: VaultDeposit no longer overcredits across an IOU scale boundary "
+            "(post-fixCleanup3_4_0)");
+        runScenario(all_, false);
+    }
+
+    // 1e17 IOU at scale 0. Withdraw all-but-one, then the last share:
+    // pre-fix tecINVARIANT_FAILED, post-fix tesSUCCESS.
+    void
+    testBugVaultLockedByPartialWithdraw()
+    {
+        using namespace test::jtx;
+
+        auto runScenario = [this](FeatureBitset features, TER expected) {
+            Env env(*this, features);
+            Account const owner{"owner"};
+            Account const issuer{"issuer"};
+            Account const holder{"holder"};
+            env.fund(XRP(1'000'000), owner, issuer, holder);
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            env(trust(holder, usd(Number{1, 18})));
+            env.close();
+            env(pay(issuer, holder, usd(Number{1, 17})));
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = usd.raw()});
+            tx[sfScale] = 0;
+            env(tx);
+            env.close();
+            env(vault.deposit(
+                {.depositor = holder, .id = keylet.key, .amount = usd(Number{1, 17})}));
+            env.close();
+
+            MPTIssue const share{env.le(keylet)->at(sfShareMPTID)};
+            std::int64_t const allButOne = 100'000'000'000'000'000LL - 1;
+            env(vault.withdraw(
+                {.depositor = holder, .id = keylet.key, .amount = STAmount{share, allButOne}}));
+            env.close();
+
+            env(vault.withdraw(
+                    {.depositor = holder, .id = keylet.key, .amount = STAmount{share, 1}}),
+                Ter(expected));
+            env.close();
+        };
+
+        testcase(
+            "bug: VaultWithdraw permanently locks a large IOU vault "
+            "(pre-fixCleanup3_4_0)");
+        runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
+        testcase(
+            "bug: VaultWithdraw no longer locks a large IOU vault "
+            "(post-fixCleanup3_4_0)");
+        runScenario(all_, tesSUCCESS);
+    }
+
+    // VaultDeposit::preclaim uses accountHolds(..., SpendableHandling::
+    // shFULL_BALANCE), which for an IOU asset adds the counterparty's
+    // LowLimit/HighLimit to the depositor's raw balance (TokenHelpers.cpp:
+    // getTrustLineBalance with includeOppositeLimit=true). When the
+    // depositor's raw balance < deposit amount but raw + opposite limit >=
+    // amount, preclaim is satisfied. doApply then calls
+    // directSendNoFeeIOU, which unconditionally subtracts saAmount from
+    // saBalance — driving the trust line negative — and returns tesSUCCESS.
+    // The post-send sanity check uses the default shSIMPLE_BALANCE (no
+    // opposite-limit add), sees a negative balance, and returns tefINTERNAL.
+    void
+    testVaultDepositNegativeBalanceFromOppositeLimit()
+    {
+        auto runTest = [&](FeatureBitset f, TER expected) {
+            using namespace test::jtx;
+            using namespace std::literals;
+
+            Env env{*this, f};
+            Account const gw{"gateway"};
+            Account const owner{"owner"};
+            Account const depositor{"depositor"};
+
+            env.fund(XRP(10000), gw, owner, depositor);
+            env.close();
+
+            // Gateway with DefaultRipple so vault creation on its IOU works.
+            env(fset(gw, asfDefaultRipple));
+            env.close();
+
+            // Depositor opens a trust line to gateway and receives a small
+            // balance.
+            PrettyAsset const usd = gw["USD"];
+            env.trust(usd(1000), depositor);
+            env(pay(gw, depositor, usd(100)));  // raw trust-line balance: 100
+            env.close();
+
+            // Key precondition: gateway sets a non-zero limit on the same
+            // RippleState — the "opposite field" from depositor's perspective.
+            // This is what inflates shFULL_BALANCE in preclaim above the raw
+            // balance.
+            env(trust(gw, depositor["USD"](1000)));
+            env.close();
+
+            // Create the IOU vault.
+            Vault const vault{env};
+            auto [vaultTx, keylet] = vault.create({.owner = owner, .asset = usd});
+            env(vaultTx);
+            env.close();
+
+            // Submit a deposit of 500 USD:
+            //   - raw balance:                100 USD
+            //   - opposite limit (gw's side): 1000 USD
+            //   - preclaim sees 100 + 1000 = 1100, passes (>= 500)
+            //   - doApply transfers 500, depositor's trust-line balance
+            //     becomes -400
+            //   - sanity check at VaultDeposit.cpp:256 fires
+            //   - tx returns tefINTERNAL (BUG — should be tesSUCCESS.
+            auto depositTx =
+                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = usd(500)});
+            env(depositTx, Ter(expected));
+            env.close();
+        };
+
+        {
+            testcase(
+                "IOU vault deposit exceeding depositor's balance but "
+                "within counterparty's trust limit, pre-fixCleanup3_2_0 "
+                "(tefINTERNAL)");
+            runTest(test::jtx::testableAmendments() - fixCleanup3_2_0, tefINTERNAL);
+        }
+        {
+            testcase(
+                "IOU vault deposit exceeding depositor's balance but "
+                "within counterparty's trust limit, post-fixCleanup3_2_0 "
+                "(tesSUCCESS)");
+            runTest(test::jtx::testableAmendments(), tesSUCCESS);
+        }
+    }
+
+    // Reproduction: canWithdraw IOU limit check bypassed when
+    // withdrawal amount is specified in shares (MPT) rather than in assets.
+    void
+    testBug6LimitBypassWithShares()
+    {
+        using namespace test::jtx;
+        testcase("Bug6 - limit bypass with share-denominated withdrawal");
+
+        auto const allAmendments = testableAmendments() | featureSingleAssetVault;
+
+        for (auto const& features : {allAmendments, allAmendments - fixCleanup3_1_3})
+        {
+            bool const withFix = features[fixCleanup3_1_3];
+
+            Env env{*this, features};
+            Account const owner{"owner"};
+            Account const issuer{"issuer"};
+            Account const depositor{"depositor"};
+            Account const charlie{"charlie"};
+            Vault const vault{env};
+
+            env.fund(XRP(1000), issuer, owner, depositor, charlie);
+            env(fset(issuer, asfAllowTrustLineClawback));
+            env.close();
+
+            PrettyAsset const asset = issuer["IOU"];
+            env.trust(asset(1000), owner);
+            env.trust(asset(1000), depositor);
+            env(pay(issuer, owner, asset(200)));
+            env(pay(issuer, depositor, asset(200)));
+            env.close();
+
+            // Charlie gets a LOW trustline limit of 5
+            env.trust(asset(5), charlie);
+            env.close();
+
+            auto const [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            auto const depositTx =
+                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+            env(depositTx);
+            env.close();
+
+            // Get the share MPT info
+            auto const vaultSle = env.le(keylet);
+            if (!BEAST_EXPECT(vaultSle))
+                return;
+            auto const mptIssuanceID = vaultSle->at(sfShareMPTID);
+            MPTIssue const shares(mptIssuanceID);
+            PrettyAsset const share(shares);
+
+            // CONTROL: Withdraw 10 IOU (asset-denominated) to charlie.
+            // Charlie's limit is 5, so this should be rejected with tecNO_LINE
+            // regardless of the amendment.
+            {
+                auto withdrawTx =
+                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
+                withdrawTx[sfDestination] = charlie.human();
+                env(withdrawTx, Ter{tecNO_LINE});
+                env.close();
+            }
+            auto const charlieBalanceBefore = env.balance(charlie, asset.raw().get());
+
+            // Withdraw the equivalent amount in shares to charlie.
+            // Post-fix: rejected (tecNO_LINE) because the share amount is
+            //   converted to assets and the trustline limit is checked.
+            // Pre-fix: succeeds (tesSUCCESS) because the limit check was
+            //   skipped for share-denominated withdrawals.
+            {
+                auto withdrawTx = vault.withdraw(
+                    {.depositor = depositor,
+                     .id = keylet.key,
+                     .amount = STAmount(share, 10'000'000)});
+                withdrawTx[sfDestination] = charlie.human();
+                env(withdrawTx, Ter{withFix ? TER{tecNO_LINE} : TER{tesSUCCESS}});
+                env.close();
+
+                auto const charlieBalanceAfter = env.balance(charlie, asset.raw().get());
+                if (withFix)
+                {
+                    // Post-fix: charlie's balance is unchanged — the withdrawal
+                    // was correctly rejected despite being share-denominated.
+                    BEAST_EXPECT(charlieBalanceAfter == charlieBalanceBefore);
+                }
+                else
+                {
+                    // Pre-fix: charlie received the assets, bypassing the
+                    // trustline limit.
+                    BEAST_EXPECT(charlieBalanceAfter > charlieBalanceBefore);
+                }
+            }
+        }
+    }
+
+    // Shared setup for testBugClawbackRoundTripOvershoot and
+    // testBugWithdrawRoundTripOvershoot, which both need a vault at
+    // assetsTotal=7, sharesTotal=5 and differ only in what they do once
+    // that state is reached.
+    //
+    // The (7, 5) state is reached through ordinary transactions: a 5 USD
+    // deposit mints 5 shares 1:1, then a loan broker on the vault issues a
+    // single-payment bullet loan for the full 5 USD at 40% interest. When
+    // the borrower repays a year later, LoanPay books the 2 USD of accrued
+    // interest into sfAssetsTotal without minting shares, leaving
+    // assetsTotal=7 against sharesTotal=5 (see
+    // testBugDepositShareTruncationSubUlp for the same technique in more
+    // detail).
+    struct RoundTripOvershootVault
+    {
+        test::jtx::Account issuer;
+        test::jtx::Account holder;
+        PrettyAsset usd;
+        test::jtx::Vault vault;
+        Keylet vaultKeylet;
+        Number initialAssetsTotal;
+        Number initialAssetsAvailable;
+    };
+
+    std::optional
+    makeRoundTripOvershootVault(test::jtx::Env& env)
+    {
+        using namespace test::jtx;
+        using namespace loan_broker;
+        using namespace loan;
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const holder{"holder"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(10'000), issuer, owner, holder, borrower);
+        env.close();
+
+        env(fset(issuer, asfAllowTrustLineClawback));
+        env.close();
+
+        PrettyAsset const usd = issuer["USD"];
+        env.trust(usd(1'000), owner);
+        env.trust(usd(1'000), holder);
+        env.trust(usd(1'000), borrower);
+        env.close();
+
+        env(pay(issuer, holder, usd(100)));
+        env(pay(issuer, borrower, usd(100)));
+        env.close();
+
+        Vault const vault{env};
+        auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = usd});
+        vaultTx[sfScale] = 0;
+        env(vaultTx);
+        env.close();
+
+        // Holder deposits 5 USD, minting 5 shares 1:1.
+        env(vault.deposit({.depositor = holder, .id = vaultKeylet.key, .amount = usd(5)}));
+        env.close();
+
+        // A loan broker on the vault, then a single bullet loan for the
+        // entire deposit at 40% interest, one payment, one year out.
+        auto const brokerKeylet =
+            keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+        env(set(owner, vaultKeylet.key));
+        env.close();
+
+        auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
+        env(set(borrower, brokerKeylet.key, usd(5).value()),
+            loan::kInterestRate(percentageToTenthBips(40)),
+            kGracePeriod(60),
+            kPaymentInterval(365 * 24 * 60 * 60),
+            kPaymentTotal(1),
+            Sig(sfCounterpartySignature, owner),
+            Fee(env.current()->fees().base * 2),
+            Ter(tesSUCCESS));
+        env.close();
+
+        // Advance to just before the single payment falls due and let the
+        // borrower repay principal plus interest. Share supply stays at 5,
+        // so assetsTotal/sharesTotal becomes 7/5.
+        env.close(std::chrono::seconds{(365 * 24 * 60 * 60) - 3600});
+        env(pay(borrower, loanKeylet.key, usd(10).value()), Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultSle = env.le(vaultKeylet);
+        if (!BEAST_EXPECT(vaultSle))
+            return std::nullopt;
+        auto const mptIssuanceID = vaultSle->at(sfShareMPTID);
+
+        Number const initialAssetsTotal = vaultSle->at(sfAssetsTotal);
+        Number const initialAssetsAvailable = vaultSle->at(sfAssetsAvailable);
+        BEAST_EXPECT(initialAssetsTotal == usd(7).number());
+        BEAST_EXPECT(initialAssetsAvailable == usd(7).number());
+        {
+            auto const sleIssuance = env.le(keylet::mptokenIssuance(mptIssuanceID));
+            if (!BEAST_EXPECT(sleIssuance))
+                return std::nullopt;
+            BEAST_EXPECT(sleIssuance->getFieldU64(sfOutstandingAmount) == 5);
+        }
+
+        return RoundTripOvershootVault{
+            .issuer = issuer,
+            .holder = holder,
+            .usd = usd,
+            .vault = vault,
+            .vaultKeylet = vaultKeylet,
+            .initialAssetsTotal = initialAssetsTotal,
+            .initialAssetsAvailable = initialAssetsAvailable};
+    }
+
+    // VaultClawback::assetsToClawback converts clawbackAmount to shares
+    // with round-to-nearest, then round-trips back to assets. When shares
+    // round up, assetsRecovered can exceed clawbackAmount.
+    //
+    // Repro: assetsTotal=7, sharesTotal=5, request 4:
+    //   shares = round(20/7) = 3, assets = 7*3/5 = 4.2 > 4.
+    //
+    // Post-fixCleanup3_4_0: truncate shares so assetsRecovered <=
+    // clawbackAmount by construction.
+    void
+    testBugClawbackRoundTripOvershoot()
+    {
+        using namespace test::jtx;
+
+        auto runScenario = [this](FeatureBitset features, bool withFix) {
+            // This regression requires the open-ended vault lifecycle: deposit,
+            // originate and repay a loan, then claw back shares. LP V1.1
+            // independently rejects attaching a broker to an open-ended vault.
+            Env env{*this, features - featureLendingProtocolV1_1};
+
+            auto const setup = makeRoundTripOvershootVault(env);
+            if (!BEAST_EXPECT(setup))
+                return;
+
+            auto const clawbackAmount = setup->usd(4);
+            env(setup->vault.clawback(
+                {.issuer = setup->issuer,
+                 .id = setup->vaultKeylet.key,
+                 .holder = setup->holder,
+                 .amount = clawbackAmount.value()}));
+
+            auto const vaultSleAfter = env.current()->read(setup->vaultKeylet);
+            if (!BEAST_EXPECT(vaultSleAfter))
+                return;
+            Number const finalAssetsTotal = vaultSleAfter->at(sfAssetsTotal);
+            Number const assetsRecovered = setup->initialAssetsTotal - finalAssetsTotal;
+            Number const clawbackNum = clawbackAmount.number();
+
+            Number const expectedPost{28LL, -1};
+            Number const expectedPre{42LL, -1};
+            if (withFix)
+            {
+                BEAST_EXPECT(assetsRecovered <= clawbackNum);
+                BEAST_EXPECT(assetsRecovered == expectedPost);
+            }
+            else
+            {
+                BEAST_EXPECT(assetsRecovered > clawbackNum);
+                BEAST_EXPECT(assetsRecovered == expectedPre);
+            }
+        };
+
+        {
+            testcase(
+                "bug: VaultClawback round-trip overshoot lets issuer recover "
+                "more than requested (pre-fixCleanup3_4_0)");
+            runScenario(testableAmendments() - fixCleanup3_4_0, false);
+        }
+        {
+            testcase(
+                "bug: VaultClawback round-trip overshoot is clamped so "
+                "assetsRecovered <= clawbackAmount (post-fixCleanup3_4_0)");
+            runScenario(testableAmendments(), true);
+        }
+    }
+
+    // Same root cause as testBugClawbackRoundTripOvershoot on the
+    // withdraw path. Also bypasses the preclaim canWithdraw check, which
+    // validates destination limits against the requested amount only.
+    //
+    // Repro: assetsTotal=7, sharesTotal=5, request 4:
+    //   pre-fix : shares = round(20/7) = 3, assets = 7*3/5 = 4.2 > 4.
+    //   post-fix: shares = floor(20/7) = 2, assets = 7*2/5 = 2.8 <= 4.
+    void
+    testBugWithdrawRoundTripOvershoot()
+    {
+        using namespace test::jtx;
+
+        auto runScenario = [this](FeatureBitset features, bool withFix) {
+            // This regression requires the open-ended vault lifecycle: deposit,
+            // originate and repay a loan, then withdraw shares. LP V1.1
+            // independently rejects attaching a broker to an open-ended vault.
+            Env env{*this, features - featureLendingProtocolV1_1};
+
+            auto const setup = makeRoundTripOvershootVault(env);
+            if (!BEAST_EXPECT(setup))
+                return;
+
+            auto const requested = setup->usd(4);
+            env(setup->vault.withdraw(
+                {.depositor = setup->holder,
+                 .id = setup->vaultKeylet.key,
+                 .amount = requested.value()}));
+
+            auto const vaultSleAfter = env.current()->read(setup->vaultKeylet);
+            if (!BEAST_EXPECT(vaultSleAfter))
+                return;
+            Number const finalAssetsTotal = vaultSleAfter->at(sfAssetsTotal);
+            Number const assetsWithdrawn = setup->initialAssetsTotal - finalAssetsTotal;
+            Number const requestedNum = requested.number();
+
+            Number const expectedPost{28LL, -1};
+            Number const expectedPre{42LL, -1};
+            if (withFix)
+            {
+                BEAST_EXPECT(assetsWithdrawn <= requestedNum);
+                BEAST_EXPECT(assetsWithdrawn == expectedPost);
+            }
+            else
+            {
+                BEAST_EXPECT(assetsWithdrawn > requestedNum);
+                BEAST_EXPECT(assetsWithdrawn == expectedPre);
+            }
+        };
+
+        {
+            testcase(
+                "bug: VaultWithdraw round-trip overshoot delivers more than "
+                "requested (pre-fixCleanup3_4_0)");
+            runScenario(testableAmendments() - fixCleanup3_4_0, false);
+        }
+        {
+            testcase(
+                "bug: VaultWithdraw round-trip overshoot is clamped so "
+                "assetsWithdrawn <= requested (post-fixCleanup3_4_0)");
+            runScenario(testableAmendments(), true);
+        }
+    }
+
+    struct ImpairedLoanVault
+    {
+        test::jtx::Account issuer;
+        test::jtx::Account holder;
+        PrettyAsset usd;
+        test::jtx::Vault vault;
+        Keylet vaultKeylet;
+        MPTID shareId;
+    };
+
+    // Impairing a 1,000 loan in a 10,000 vault leaves AssetsAvailable=9,000
+    // and AssetsTotal=10,000. otherDeposit > 0 splits the shares, 0 leaves
+    // holder as the sole shareholder.
+    std::optional
+    makeImpairedLoanVault(test::jtx::Env& env, int otherDeposit)
+    {
+        using namespace test::jtx;
+        using namespace loan_broker;
+        using namespace loan;
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const holder{"holder"};
+        Account const other{"other"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(100'000), issuer, owner, holder, other, borrower);
+        env.close();
+
+        env(fset(issuer, asfAllowTrustLineClawback));
+        env(fset(issuer, asfDefaultRipple));
+        env.close();
+
+        PrettyAsset const usd = issuer["USD"];
+        env.trust(usd(100'000), owner);
+        env.trust(usd(100'000), holder);
+        env.trust(usd(100'000), other);
+        env.trust(usd(100'000), borrower);
+        env.close();
+
+        int const holderDeposit = 10'000 - otherDeposit;
+        env(pay(issuer, holder, usd(holderDeposit)));
+        if (otherDeposit != 0)
+        {
+            env(pay(issuer, other, usd(otherDeposit)));
+        }
+        env.close();
+
+        Vault const vault{env};
+        auto const [createTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
+            {.owner = owner, .asset = usd, .subscriptionOffset = std::chrono::seconds{60}});
+        env(createTx);
+        env.close();
+
+        auto const vaultSle = env.le(vaultKeylet);
+        if (!BEAST_EXPECT(vaultSle))
+            return std::nullopt;
+        MPTID const shareId = vaultSle->at(sfShareMPTID);
+
+        env(vault.deposit(
+            {.depositor = holder, .id = vaultKeylet.key, .amount = usd(holderDeposit)}));
+        if (otherDeposit != 0)
+        {
+            env(vault.deposit(
+                {.depositor = other, .id = vaultKeylet.key, .amount = usd(otherDeposit)}));
+        }
+        env.close();
+
+        vault.closePastSubscription(subscriptionDate);
+
+        auto const brokerKeylet =
+            keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+        env(set(owner, vaultKeylet.key));
+        env.close();
+
+        auto const sleBroker = env.le(brokerKeylet);
+        if (!BEAST_EXPECT(sleBroker))
+            return std::nullopt;
+        auto const loanKeylet =
+            keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
+
+        env(set(borrower, brokerKeylet.key, usd(1'000).value()),
+            loan::kInterestRate(percentageToTenthBips(0)),
+            kGracePeriod(60),
+            kPaymentInterval(120),
+            kPaymentTotal(10),
+            Sig(sfCounterpartySignature, owner),
+            Fee(env.current()->fees().base * 2),
+            Ter(tesSUCCESS));
+        env.close();
+
+        // Under fixCleanup3_4_0, LoanManage rejects tfLoanImpair with
+        // tecTOO_SOON unless the payment is already late; advance the ledger
+        // past sfNextPaymentDueDate so impairment succeeds. No-op otherwise.
+        if (env.current()->rules().enabled(fixCleanup3_4_0))
+        {
+            auto const loanBefore = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loanBefore))
+                return std::nullopt;
+            std::uint32_t const dueDate = loanBefore->at(sfNextPaymentDueDate);
+            env.close(NetClock::time_point{NetClock::duration{dueDate}} + std::chrono::seconds{1});
+        }
+
+        env(manage(owner, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultAfter = env.le(vaultKeylet);
+        if (!BEAST_EXPECT(vaultAfter))
+            return std::nullopt;
+        BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == usd(9'000).value());
+        BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == usd(1'000).value());
+
+        return ImpairedLoanVault{
+            .issuer = issuer,
+            .holder = holder,
+            .usd = usd,
+            .vault = vault,
+            .vaultKeylet = vaultKeylet,
+            .shareId = shareId};
+    }
+
+    // Legacy clawback pricing burns every share; fixCleanup3_4_0 leaves 10%
+    // outstanding, backed by the impaired receivable.
+    void
+    testBugClawbackAfterLoanImpair()
+    {
+        using namespace test::jtx;
+
+        auto clawbackHolder = [](ImpairedLoanVault const& setup, STAmount const& amount) {
+            return setup.vault.clawback(
+                {.issuer = setup.issuer,
+                 .id = setup.vaultKeylet.key,
+                 .holder = setup.holder,
+                 .amount = amount});
+        };
+
+        auto runSole = [this, &clawbackHolder](FeatureBitset features, TER expected) {
+            testcase(
+                features[fixCleanup3_4_0]
+                    ? "VaultClawback after impaired loan (post-fixCleanup3_4_0)"
+                    : "VaultClawback after impaired loan (pre-fixCleanup3_4_0)");
+
+            Env env(*this, features);
+            auto const maybeSetup = makeImpairedLoanVault(env, 0);
+            if (!maybeSetup)
+            {
+                BEAST_EXPECT(false);
+                return;
+            }
+            ImpairedLoanVault const& setup = *maybeSetup;
+
+            auto const tokenBefore = env.le(keylet::mptoken(setup.shareId, setup.holder.id()));
+            auto const vaultBefore = env.le(setup.vaultKeylet);
+            auto const issuanceBefore = env.le(keylet::mptokenIssuance(setup.shareId));
+            if (!BEAST_EXPECT(tokenBefore) || !BEAST_EXPECT(vaultBefore) ||
+                !BEAST_EXPECT(issuanceBefore))
+                return;
+            std::uint64_t const sharesBefore = tokenBefore->getFieldU64(sfMPTAmount);
+
+            // The clawback of 19,000 exceeds AssetsAvailable (9,000), so
+            // VaultClawback clamps sharesDestroyed to whatever redeems
+            // exactly AssetsAvailable; compute that expected value using the
+            // same conversion helper VaultClawback itself uses, rather than
+            // assuming an exact 90/10 split holds under truncation.
+            auto const maybeSharesDestroyed = assetsToSharesWithdraw(
+                vaultBefore,
+                issuanceBefore,
+                setup.usd(9'000).value(),
+                TruncateShares::Yes,
+                WaiveUnrealizedLoss::Yes);
+            if (!BEAST_EXPECT(maybeSharesDestroyed))
+                return;
+            std::uint64_t const expectedSharesAfter =
+                sharesBefore - maybeSharesDestroyed->mpt().value();
+
+            env(clawbackHolder(setup, setup.usd(19'000).value()), Ter(expected));
+            env.close();
+            if (expected != tesSUCCESS)
+                return;
+
+            auto const vaultAfter = env.le(setup.vaultKeylet);
+            if (!BEAST_EXPECT(vaultAfter))
+                return;
+            BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == setup.usd(0).value());
+            BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == setup.usd(1'000).value());
+            BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == setup.usd(1'000).value());
+            auto const tokenAfter = env.le(keylet::mptoken(setup.shareId, setup.holder.id()));
+            if (!BEAST_EXPECT(tokenAfter))
+                return;
+            BEAST_EXPECT(tokenAfter->getFieldU64(sfMPTAmount) == expectedSharesAfter);
+        };
+
+        runSole(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
+        runSole(all_, tesSUCCESS);
+
+        testcase("VaultClawback after impaired loan, non-sole holder");
+        {
+            Env env(*this, all_);
+            auto const maybeSetup = makeImpairedLoanVault(env, 1'000);
+            if (!maybeSetup)
+            {
+                BEAST_EXPECT(false);
+                return;
+            }
+            ImpairedLoanVault const& setup = *maybeSetup;
+            // The waiver does not apply, so the holder's 9,000 shares are
+            // still priced at the discounted rate and cannot cover 9,000.
+            env(clawbackHolder(setup, setup.usd(9'000).value()), Ter(tecINSUFFICIENT_FUNDS));
+        }
+    }
+
+    // Bug: a fully impaired vault may pay zero assets for a share burn.
+    // Sending zero MPT is a no-op, so the vault pseudo-account's asset
+    // MPToken is never written and ValidVault, which only records deltas for
+    // created, modified or deleted entries, sees no vault delta at all.
+    //
+    // Pre-fixCleanup3_4_0 that alone makes the withdrawal impossible:
+    // zeroDeltaIsLegitimate is gated on the amendment, so the absent vault
+    // delta fails "withdrawal must change vault balance". Every pre-amendment
+    // arm below dies there, before any destination-side check runs.
+    //
+    // The destination side differs per arm, and only the vault-delta return
+    // hides that pre-amendment. With Alice's asset MPToken already present
+    // nothing touches it, so she has no delta either. With it missing,
+    // doWithdraw still called addEmptyHolding for a self-destination on a
+    // zero payout and created her MPToken at amount 0; a created MPToken is
+    // recorded even at zero, so she arrives with a present-and-zero delta,
+    // which for an integral MPT asset the destination check would reject if
+    // it were reached.
+    //
+    // ValidMPTIssuance is a separate checker and still runs. It only trips on
+    // the one arm that both creates and deletes an MPToken: Alice's last
+    // share with the asset MPToken missing, where addEmptyHolding creates the
+    // asset token while her share token is deleted (created + deleted > 1).
+    // Leftover shares with the token missing is create-only, and a last share
+    // with the token present is delete-only; neither exceeds one. Bob still
+    // owns shares throughout, so this is never the vault's final outstanding
+    // share.
+    //
+    // Post-fixCleanup3_4_0, doWithdraw skips addEmptyHolding on a zero
+    // payout and zeroDeltaIsLegitimate lets the vault-delta and
+    // missing-recipient-delta checks accept the transfer. A present
+    // destination delta of zero is still rejected.
+    void
+    testBugMptZeroWithdrawMissingHolding()
+    {
+        using namespace test::jtx;
+        using namespace loan_broker;
+        using namespace loan;
+        using namespace std::chrono_literals;
+
+        auto runScenario = [this](
+                               FeatureBitset features,
+                               bool removeAssetToken,
+                               bool withdrawAllAliceShares,
+                               TER expected) {
+            testcase(
+                std::string{"bug: MPT vault zero-value withdraw "} +
+                (removeAssetToken ? "without asset MPToken" : "with asset MPToken") +
+                (withdrawAllAliceShares ? ", Alice's last share" : ", Alice has leftover shares") +
+                (features[fixCleanup3_4_0] ? " (post-fixCleanup3_4_0)" : " (pre-fixCleanup3_4_0)"));
+
+            Env env(*this, features);
+
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+            Account const borrower{"borrower"};
+
+            env.fund(XRP(100'000), issuer, owner, alice, bob, borrower);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanTransfer});
+            PrettyAsset const asset = mptt.issuanceID();
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = alice});
+            mptt.authorize({.account = bob});
+            mptt.authorize({.account = borrower});
+            env.close();
+
+            env(pay(issuer, alice, asset(2)));
+            env(pay(issuer, bob, asset(8)));
+            env.close();
+
+            Vault const vault{env};
+            auto const [createTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
+                {.owner = owner, .asset = asset, .subscriptionOffset = 60s});
+            env(createTx);
+            env.close();
+
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(2)}));
+            env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = asset(8)}));
+            env.close();
+
+            vault.closePastSubscription(subscriptionDate);
+
+            auto const brokerKeylet =
+                keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+            env(set(owner, vaultKeylet.key));
+            env.close();
+
+            auto const sleBroker = env.le(brokerKeylet);
+            if (!BEAST_EXPECT(sleBroker))
+                return;
+            auto const loanKeylet = keylet::loan(
+                brokerKeylet.key, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
+
+            env(set(borrower, brokerKeylet.key, asset(10).value()),
+                kInterestRate(percentageToTenthBips(0)),
+                kGracePeriod(60),
+                kPaymentInterval(120),
+                kPaymentTotal(10),
+                Sig(sfCounterpartySignature, owner),
+                Fee(env.current()->fees().base * 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            auto const loanBefore = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loanBefore))
+                return;
+            std::uint32_t const dueDate = loanBefore->at(sfNextPaymentDueDate);
+            env.close(NetClock::time_point{NetClock::duration{dueDate}} + 1s);
+
+            env(manage(owner, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
+            env.close();
+
+            auto const vaultImpaired = env.le(vaultKeylet);
+            if (!BEAST_EXPECT(vaultImpaired))
+                return;
+            BEAST_EXPECT(vaultImpaired->at(sfAssetsAvailable) == asset(0).value());
+            BEAST_EXPECT(vaultImpaired->at(sfAssetsTotal) == vaultImpaired->at(sfLossUnrealized));
+            Number const totalBefore = vaultImpaired->at(sfAssetsTotal);
+            Number const lossBefore = vaultImpaired->at(sfLossUnrealized);
+
+            MPTID const shareId = vaultImpaired->at(sfShareMPTID);
+            auto const issuanceBefore = env.le(keylet::mptokenIssuance(shareId));
+            if (!BEAST_EXPECT(issuanceBefore))
+                return;
+            std::uint64_t const outstandingBefore =
+                issuanceBefore->getFieldU64(sfOutstandingAmount);
+
+            auto const tokenAlice = env.le(keylet::mptoken(shareId, alice.id()));
+            if (!BEAST_EXPECT(tokenAlice))
+                return;
+            std::uint64_t const sharesBefore = tokenAlice->getFieldU64(sfMPTAmount);
+            BEAST_EXPECT(sharesBefore == 2);
+            std::uint64_t const sharesToRedeem = withdrawAllAliceShares ? sharesBefore : 1;
+            STAmount const redeemShares{MPTIssue{shareId}, Number(sharesToRedeem)};
+
+            auto const assetTokenKeylet = keylet::mptoken(mptt.issuanceID(), alice.id());
+            if (removeAssetToken)
+            {
+                mptt.authorize({.account = alice, .flags = tfMPTUnauthorize});
+                env.close();
+                BEAST_EXPECT(!env.le(assetTokenKeylet));
+            }
+            else
+            {
+                auto const existing = env.le(assetTokenKeylet);
+                if (!BEAST_EXPECT(existing))
+                    return;
+                BEAST_EXPECT(existing->getFieldU64(sfMPTAmount) == 0);
+            }
+
+            std::uint32_t const redemptionDate = vaultImpaired->at(sfRedemptionDate);
+            env.close(NetClock::time_point{NetClock::duration{redemptionDate}} + 1s);
+
+            env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = redeemShares}),
+                Ter(expected));
+            env.close();
+            if (expected != tesSUCCESS)
+                return;
+
+            if (removeAssetToken)
+            {
+                BEAST_EXPECT(!env.le(assetTokenKeylet));
+            }
+            else
+            {
+                auto const assetAfter = env.le(assetTokenKeylet);
+                if (!BEAST_EXPECT(assetAfter))
+                    return;
+                BEAST_EXPECT(assetAfter->getFieldU64(sfMPTAmount) == 0);
+            }
+
+            auto const shareAfter = env.le(keylet::mptoken(shareId, alice.id()));
+            if (withdrawAllAliceShares)
+            {
+                BEAST_EXPECT(!shareAfter);
+            }
+            else if (BEAST_EXPECT(shareAfter))
+            {
+                BEAST_EXPECT(shareAfter->getFieldU64(sfMPTAmount) == sharesBefore - sharesToRedeem);
+            }
+
+            auto const vaultAfter = env.le(vaultKeylet);
+            if (!BEAST_EXPECT(vaultAfter))
+                return;
+            BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore);
+            BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
+            BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == asset(0).value());
+
+            auto const issuanceAfter = env.le(keylet::mptokenIssuance(shareId));
+            if (!BEAST_EXPECT(issuanceAfter))
+                return;
+            BEAST_EXPECT(
+                issuanceAfter->getFieldU64(sfOutstandingAmount) ==
+                outstandingBefore - sharesToRedeem);
+        };
+
+        runScenario(
+            all_, false /* removeAssetToken */, false /* withdrawAllAliceShares */, tesSUCCESS);
+        runScenario(
+            all_, false /* removeAssetToken */, true /* withdrawAllAliceShares */, tesSUCCESS);
+        runScenario(
+            all_, true /* removeAssetToken */, false /* withdrawAllAliceShares */, tesSUCCESS);
+        runScenario(
+            all_, true /* removeAssetToken */, true /* withdrawAllAliceShares */, tesSUCCESS);
+        runScenario(
+            all_ - fixCleanup3_4_0,
+            false /* removeAssetToken */,
+            false /* withdrawAllAliceShares */,
+            tecINVARIANT_FAILED);
+        runScenario(
+            all_ - fixCleanup3_4_0,
+            false /* removeAssetToken */,
+            true /* withdrawAllAliceShares */,
+            tecINVARIANT_FAILED);
+        runScenario(
+            all_ - fixCleanup3_4_0,
+            true /* removeAssetToken */,
+            false /* withdrawAllAliceShares */,
+            tecINVARIANT_FAILED);
+        runScenario(
+            all_ - fixCleanup3_4_0,
+            true /* removeAssetToken */,
+            true /* withdrawAllAliceShares */,
+            tecINVARIANT_FAILED);
+    }
+
+    // IOU analogue of the missing-MPToken case above. Alice removes her
+    // zero-balance trust line after depositing, then burns one unit from her
+    // scaled share balance after the vault is fully impaired. Bob's share
+    // balance keeps this out of the sole-shareholder loss-waiver and
+    // final-outstanding-share paths. A zero payout must not recreate Alice's
+    // unsolicited trust line.
+    void
+    testBugIouZeroWithdrawMissingTrustLine()
+    {
+        using namespace test::jtx;
+        using namespace loan_broker;
+        using namespace loan;
+        using namespace std::chrono_literals;
+
+        Env env(*this, all_);
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const alice{"alice"};
+        Account const bob{"bob"};
+        Account const borrower{"borrower"};
+
+        env.fund(XRP(100'000), issuer, owner, alice, bob, borrower);
+        env.close();
+        env(fset(issuer, asfDefaultRipple));
+        env.close();
+
+        PrettyAsset const asset = issuer["USD"];
+        env.trust(asset(100), owner);
+        env.trust(asset(100), alice);
+        env.trust(asset(100), bob);
+        env.trust(asset(100), borrower);
+        env.close();
+
+        env(pay(issuer, alice, asset(2)));
+        env(pay(issuer, bob, asset(8)));
+        env.close();
+
+        Vault const vault{env};
+        auto const [createTx, vaultKeylet, subscriptionDate] =
+            vault.createClosedEnded({.owner = owner, .asset = asset, .subscriptionOffset = 60s});
+        env(createTx);
+        env.close();
+
+        env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(2)}));
+        env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = asset(8)}));
+        env.close();
+
+        auto const assetLine = keylet::trustLine(alice, asset.raw().get());
+        if (!BEAST_EXPECT(env.le(assetLine)))
+            return;
+        env.trust(asset(0), alice);
+        env.close();
+        BEAST_EXPECT(!env.le(assetLine));
+
+        vault.closePastSubscription(subscriptionDate);
+
+        auto const brokerKeylet =
+            keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+        env(set(owner, vaultKeylet.key));
+        env.close();
+
+        auto const sleBroker = env.le(brokerKeylet);
+        if (!BEAST_EXPECT(sleBroker))
+            return;
+        auto const loanKeylet =
+            keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
+
+        env(set(borrower, brokerKeylet.key, asset(10).value()),
+            kInterestRate(percentageToTenthBips(0)),
+            kGracePeriod(60),
+            kPaymentInterval(120),
+            kPaymentTotal(10),
+            Sig(sfCounterpartySignature, owner),
+            Fee(env.current()->fees().base * 2),
+            Ter(tesSUCCESS));
+        env.close();
+
+        auto const loanBefore = env.le(loanKeylet);
+        if (!BEAST_EXPECT(loanBefore))
+            return;
+        std::uint32_t const dueDate = loanBefore->at(sfNextPaymentDueDate);
+        env.close(NetClock::time_point{NetClock::duration{dueDate}} + 1s);
+
+        env(manage(owner, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultImpaired = env.le(vaultKeylet);
+        if (!BEAST_EXPECT(vaultImpaired))
+            return;
+        BEAST_EXPECT(vaultImpaired->at(sfAssetsAvailable) == asset(0).value());
+        BEAST_EXPECT(vaultImpaired->at(sfAssetsTotal) == vaultImpaired->at(sfLossUnrealized));
+        Number const totalBefore = vaultImpaired->at(sfAssetsTotal);
+        Number const lossBefore = vaultImpaired->at(sfLossUnrealized);
+
+        MPTID const shareId = vaultImpaired->at(sfShareMPTID);
+        auto const tokenAlice = env.le(keylet::mptoken(shareId, alice.id()));
+        if (!BEAST_EXPECT(tokenAlice))
+            return;
+        std::uint64_t const sharesBefore = tokenAlice->getFieldU64(sfMPTAmount);
+        // Default IOU vault scale is 6, so 2 USD mints 2e6 shares. Redeem one
+        // leftover share; do not require 1:1 like the MPT case.
+        BEAST_EXPECT(sharesBefore > 1);
+        STAmount const redeemShares{MPTIssue{shareId}, Number(1)};
+
+        std::uint32_t const redemptionDate = vaultImpaired->at(sfRedemptionDate);
+        env.close(NetClock::time_point{NetClock::duration{redemptionDate}} + 1s);
+
+        env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = redeemShares}),
+            Ter(tesSUCCESS));
+        env.close();
+
+        // A regression in the View guard would recreate this line even though
+        // no asset value was paid.
+        BEAST_EXPECT(!env.le(assetLine));
+
+        auto const shareAfter = env.le(keylet::mptoken(shareId, alice.id()));
+        if (!BEAST_EXPECT(shareAfter))
+            return;
+        BEAST_EXPECT(shareAfter->getFieldU64(sfMPTAmount) == sharesBefore - 1);
+
+        auto const vaultAfter = env.le(vaultKeylet);
+        if (!BEAST_EXPECT(vaultAfter))
+            return;
+        BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore);
+        BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
+        BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == asset(0).value());
+    }
+
+    // Same zero-payout withdrawal as testBugMptZeroWithdrawMissingHolding, but
+    // the vault asset is XRP. addEmptyHolding is a no-op for native assets.
+    // Sequence processing still touches the sender AccountRoot; a sponsored
+    // fee leaves that XRP balance economically unchanged. After the
+    // sponsored-withdraw fee-payer fix, deltaAssetsForParty collapses that
+    // economically-zero XRP delta to absence, so tesSUCCESS takes the
+    // missing-recipient-delta arm gated by zeroDeltaIsLegitimate. This test
+    // covers that live SUCCESS path. Pre-fixCleanup3_4_0 still fails the
+    // invariant.
+    void
+    testBugXrpZeroWithdrawSponsoredFee()
+    {
+        using namespace test::jtx;
+        using namespace loan_broker;
+        using namespace loan;
+        using namespace std::chrono_literals;
+
+        auto runScenario = [this](FeatureBitset features, TER expected) {
+            testcase(
+                std::string{"bug: XRP vault zero-value withdraw with sponsored fee"} +
+                (features[fixCleanup3_4_0] ? " (post-fixCleanup3_4_0)" : " (pre-fixCleanup3_4_0)"));
+
+            Env env(*this, features);
+
+            Account const owner{"owner"};
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+            Account const borrower{"borrower"};
+            Account const sponsor{"sponsor"};
+
+            env.fund(XRP(100'000), owner, alice, bob, borrower, sponsor);
+            env.close();
+
+            PrettyAsset const asset{xrpIssue()};
+            Vault const vault{env};
+            auto const [createTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
+                {.owner = owner, .asset = asset, .subscriptionOffset = 60s});
+            env(createTx);
+            env.close();
+
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(2)}));
+            env(vault.deposit({.depositor = bob, .id = vaultKeylet.key, .amount = asset(8)}));
+            env.close();
+
+            vault.closePastSubscription(subscriptionDate);
+
+            auto const brokerKeylet =
+                keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+            env(set(owner, vaultKeylet.key));
+            env.close();
+
+            auto const sleBroker = env.le(brokerKeylet);
+            if (!BEAST_EXPECT(sleBroker))
+                return;
+            auto const loanKeylet = keylet::loan(
+                brokerKeylet.key, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
+
+            env(set(borrower, brokerKeylet.key, asset(10).value()),
+                kInterestRate(percentageToTenthBips(0)),
+                kGracePeriod(60),
+                kPaymentInterval(120),
+                kPaymentTotal(10),
+                Sig(sfCounterpartySignature, owner),
+                Fee(env.current()->fees().base * 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            auto const loanBefore = env.le(loanKeylet);
+            if (!BEAST_EXPECT(loanBefore))
+                return;
+            std::uint32_t const dueDate = loanBefore->at(sfNextPaymentDueDate);
+            env.close(NetClock::time_point{NetClock::duration{dueDate}} + 1s);
+
+            env(manage(owner, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
+            env.close();
+
+            auto const vaultImpaired = env.le(vaultKeylet);
+            if (!BEAST_EXPECT(vaultImpaired))
+                return;
+            BEAST_EXPECT(vaultImpaired->at(sfAssetsAvailable) == asset(0).value());
+            BEAST_EXPECT(vaultImpaired->at(sfAssetsTotal) == vaultImpaired->at(sfLossUnrealized));
+            Number const totalBefore = vaultImpaired->at(sfAssetsTotal);
+            Number const lossBefore = vaultImpaired->at(sfLossUnrealized);
+
+            MPTID const shareId = vaultImpaired->at(sfShareMPTID);
+            auto const tokenAlice = env.le(keylet::mptoken(shareId, alice.id()));
+            if (!BEAST_EXPECT(tokenAlice))
+                return;
+            std::uint64_t const sharesBefore = tokenAlice->getFieldU64(sfMPTAmount);
+            BEAST_EXPECT(sharesBefore == 2);
+            STAmount const redeemShares{MPTIssue{shareId}, Number(1)};
+
+            std::uint32_t const redemptionDate = vaultImpaired->at(sfRedemptionDate);
+            env.close(NetClock::time_point{NetClock::duration{redemptionDate}} + 1s);
+
+            auto const aliceBalanceBefore = env.balance(alice);
+            auto const sponsorBalanceBefore = env.balance(sponsor);
+            auto const fee = env.current()->fees().base;
+
+            env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = redeemShares}),
+                Fee(fee),
+                sponsor::As(sponsor, spfSponsorFee),
+                Sig(sfSponsorSignature, sponsor),
+                Ter(expected));
+            env.close();
+
+            BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore - fee);
+            BEAST_EXPECT(env.balance(alice) == aliceBalanceBefore);
+
+            if (expected != tesSUCCESS)
+                return;
+
+            auto const shareAfter = env.le(keylet::mptoken(shareId, alice.id()));
+            if (!BEAST_EXPECT(shareAfter))
+                return;
+            BEAST_EXPECT(shareAfter->getFieldU64(sfMPTAmount) == sharesBefore - 1);
+
+            auto const vaultAfter = env.le(vaultKeylet);
+            if (!BEAST_EXPECT(vaultAfter))
+                return;
+            BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore);
+            BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
+            BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == asset(0).value());
+        };
+
+        runScenario(all_, tesSUCCESS);
+        runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
+    }
+
+    // addEmptyHolding() used to check isGlobalFrozen(issuer) and
+    // !lsfDefaultRipple before the "line already exists" tecDUPLICATE
+    // short circuit. doWithdraw() calls addEmptyHolding() for a
+    // self-destination payout and only tolerates tecDUPLICATE, so
+    // tecINTERNAL from a missing DefaultRipple flag aborted the
+    // withdrawal. fixCleanup3_4_0 checks existence first and maps the
+    // create-path DefaultRipple miss to terNO_RIPPLE. Global freeze on an
+    // existing line is still rejected later by checkWithdrawFreeze.
+    void
+    testBugSelfWithdrawAfterIssuerClearsDefaultRipple()
+    {
+        using namespace test::jtx;
+
+        auto runExistingLine = [this](
+                                   FeatureBitset features,
+                                   TER selfExpected,
+                                   bool issuerGlobalFreeze = false) {
+            Env env(*this, features);
+            Account const issuer{"issuer"};
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+
+            env.fund(XRP(10'000), issuer, alice, bob);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            Issue const usdIssue = usd.raw().get();
+            env(trust(alice, usd(10'000)));
+            env(trust(bob, usd(10'000)));
+            env.close();
+            env(pay(issuer, alice, usd(1'000)));
+            env.close();
+
+            Vault const vault{env};
+            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
+            env(vaultTx);
+            env.close();
+
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(500)}));
+            env.close();
+
+            env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(50)}));
+            env.close();
+
+            env(fclear(issuer, asfDefaultRipple));
+            env.close();
+            if (issuerGlobalFreeze)
+            {
+                env(fset(issuer, asfGlobalFreeze));
+                env.close();
+            }
+
+            BEAST_EXPECT(env.le(keylet::trustLine(alice.id(), usdIssue)));
+
+            // Alice's USD line is unchanged; a later deposit still succeeds
+            // unless the issuer is globally frozen.
+            if (!issuerGlobalFreeze)
+            {
+                env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(10)}));
+                env.close();
+            }
+
+            Number const destBefore = env.balance(alice, usd.raw()).number();
+            Number const vaultBefore = env.le(vaultKeylet)->at(sfAssetsTotal);
+            Number const withdrawAmt{50};
+
+            env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(50)}),
+                Ter(selfExpected));
+            env.close();
+
+            Number const destAfter = env.balance(alice, usd.raw()).number();
+            Number const vaultAfter = env.le(vaultKeylet)->at(sfAssetsTotal);
+            if (isTesSuccess(selfExpected))
+            {
+                BEAST_EXPECT(destAfter == destBefore + withdrawAmt);
+                BEAST_EXPECT(vaultAfter == vaultBefore - withdrawAmt);
+            }
+            else
+            {
+                BEAST_EXPECT(destAfter == destBefore);
+                BEAST_EXPECT(vaultAfter == vaultBefore);
+            }
+
+            if (!issuerGlobalFreeze)
+            {
+                auto destTx =
+                    vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(50)});
+                destTx[sfDestination] = bob.human();
+                env(destTx);
+                env.close();
+            }
+        };
+
+        auto runDeletedLine = [this](FeatureBitset features, TER selfExpected) {
+            Env env(*this, features);
+            Account const issuer{"issuer"};
+            Account const alice{"alice"};
+
+            env.fund(XRP(10'000), issuer, alice);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            Issue const usdIssue = usd.raw().get();
+            env(trust(alice, usd(10'000)));
+            env.close();
+            env(pay(issuer, alice, usd(500)));
+            env.close();
+
+            Vault const vault{env};
+            auto [vaultTx, vaultKeylet] = vault.create({.owner = alice, .asset = usd});
+            env(vaultTx);
+            env.close();
+
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(500)}));
+            env.close();
+
+            env(trust(alice, usd(0)));
+            env.close();
+            BEAST_EXPECT(!env.le(keylet::trustLine(alice.id(), usdIssue)));
+            env(fclear(issuer, asfDefaultRipple));
+            env.close();
+
+            env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(50)}),
+                Ter(selfExpected));
+            env.close();
+        };
+
+        auto runCoverWithdraw = [this](FeatureBitset features, TER selfExpected) {
+            using namespace loan_broker;
+
+            Env env(*this, features);
+            Account const issuer{"issuer"};
+            Account const alice{"alice"};
+
+            env.fund(XRP(10'000), issuer, alice);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            Issue const usdIssue = usd.raw().get();
+            env(trust(alice, usd(10'000)));
+            env.close();
+            env(pay(issuer, alice, usd(1'000)));
+            env.close();
+
+            Vault const vault{env};
+            auto const [createTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
+                {.owner = alice, .asset = usd, .subscriptionOffset = std::chrono::seconds{60}});
+            (void)subscriptionDate;
+            env(createTx);
+            env.close();
+
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(500)}));
+            env.close();
+
+            auto const brokerKeylet =
+                keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
+            env(set(alice, vaultKeylet.key));
+            env.close();
+            env(coverDeposit(alice, brokerKeylet.key, usd(100).value()));
+            env.close();
+
+            env(fclear(issuer, asfDefaultRipple));
+            env.close();
+            BEAST_EXPECT(env.le(keylet::trustLine(alice.id(), usdIssue)));
+
+            Number const destBefore = env.balance(alice, usd.raw()).number();
+            Number const coverBefore = env.le(brokerKeylet)->at(sfCoverAvailable);
+            Number const withdrawAmt{50};
+
+            env(coverWithdraw(alice, brokerKeylet.key, usd(50).value()), Ter(selfExpected));
+            env.close();
+
+            Number const destAfter = env.balance(alice, usd.raw()).number();
+            Number const coverAfter = env.le(brokerKeylet)->at(sfCoverAvailable);
+            if (isTesSuccess(selfExpected))
+            {
+                BEAST_EXPECT(destAfter == destBefore + withdrawAmt);
+                BEAST_EXPECT(coverAfter == coverBefore - withdrawAmt);
+            }
+            else
+            {
+                BEAST_EXPECT(destAfter == destBefore);
+                BEAST_EXPECT(coverAfter == coverBefore);
+            }
+        };
+
+        auto runDeletedCoverWithdraw = [this](FeatureBitset features, TER selfExpected) {
+            using namespace loan_broker;
+
+            Env env(*this, features);
+            Account const issuer{"issuer"};
+            Account const alice{"alice"};
+
+            env.fund(XRP(10'000), issuer, alice);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            Issue const usdIssue = usd.raw().get();
+            env(trust(alice, usd(10'000)));
+            env.close();
+            env(pay(issuer, alice, usd(600)));
+            env.close();
+
+            Vault const vault{env};
+            auto const [createTx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
+                {.owner = alice, .asset = usd, .subscriptionOffset = std::chrono::seconds{60}});
+            (void)subscriptionDate;
+            env(createTx);
+            env.close();
+
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(500)}));
+            env.close();
+
+            auto const brokerKeylet =
+                keylet::loanBroker(alice.id(), SeqProxy::rawSequence(env.seq(alice)));
+            env(set(alice, vaultKeylet.key));
+            env.close();
+            env(coverDeposit(alice, brokerKeylet.key, usd(100).value()));
+            env.close();
+
+            env(trust(alice, usd(0)));
+            env.close();
+            BEAST_EXPECT(!env.le(keylet::trustLine(alice.id(), usdIssue)));
+            env(fclear(issuer, asfDefaultRipple));
+            env.close();
+
+            env(coverWithdraw(alice, brokerKeylet.key, usd(50).value()), Ter(selfExpected));
+            env.close();
+        };
+
+        auto runPrivateVault = [this](FeatureBitset features, TER selfExpected) {
+            Env env(*this, features);
+            Account const issuer{"issuer"};
+            Account const alice{"alice"};
+            Account const pdOwner{"pdOwner"};
+            Account const credIssuer{"credIssuer"};
+            std::string const credType = "credential";
+
+            env.fund(XRP(10'000), issuer, alice, pdOwner, credIssuer);
+            env.close();
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const usd{issuer["USD"]};
+            env(trust(alice, usd(10'000)));
+            env.close();
+            env(pay(issuer, alice, usd(1'000)));
+            env.close();
+
+            Vault const vault{env};
+            auto [vaultTx, vaultKeylet] =
+                vault.create({.owner = alice, .asset = usd, .flags = tfVaultPrivate});
+            env(vaultTx);
+            env.close();
+
+            pdomain::Credentials const credentials{{.issuer = credIssuer, .credType = credType}};
+            env(pdomain::setTx(pdOwner, credentials));
+            auto const domainId = pdomain::getNewDomain(env.meta());
+            {
+                auto domainTx = vault.set({.owner = alice, .id = vaultKeylet.key});
+                domainTx[sfDomainID] = to_string(domainId);
+                env(domainTx);
+                env.close();
+            }
+
+            env(credentials::create(alice, credIssuer, credType));
+            env(credentials::accept(alice, credIssuer, credType));
+            env.close();
+
+            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = usd(500)}));
+            env.close();
+
+            env(fclear(issuer, asfDefaultRipple));
+            env.close();
+
+            env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = usd(50)}),
+                Ter(selfExpected));
+            env.close();
+        };
+
+        testcase(
+            "bug: VaultWithdraw to self fails with tecINTERNAL after issuer "
+            "clears asfDefaultRipple even though the trust line exists "
+            "(pre-fixCleanup3_4_0)");
+        runExistingLine(all_ - fixCleanup3_4_0, tecINTERNAL);
+
+        testcase(
+            "bug: VaultWithdraw to self succeeds after issuer clears "
+            "asfDefaultRipple when the trust line exists (post-fixCleanup3_4_0)");
+        runExistingLine(all_, tesSUCCESS);
+
+        testcase(
+            "bug: VaultWithdraw to self with an existing line still gets "
+            "tecFROZEN under asfGlobalFreeze (post-fixCleanup3_4_0)");
+        runExistingLine(all_, tecFROZEN, true);
+
+        testcase(
+            "bug: VaultWithdraw to self fails with tecINTERNAL after issuer "
+            "clears asfDefaultRipple and the trust line was deleted "
+            "(pre-fixCleanup3_4_0)");
+        runDeletedLine(all_ - fixCleanup3_4_0, tecINTERNAL);
+
+        testcase(
+            "bug: VaultWithdraw to self fails with terNO_RIPPLE after issuer "
+            "clears asfDefaultRipple and the trust line was deleted "
+            "(post-fixCleanup3_4_0)");
+        runDeletedLine(all_, terNO_RIPPLE);
+
+        testcase(
+            "bug: LoanBrokerCoverWithdraw to self fails with tecINTERNAL after "
+            "issuer clears asfDefaultRipple even though the trust line exists "
+            "(pre-fixCleanup3_4_0)");
+        runCoverWithdraw(all_ - fixCleanup3_4_0, tecINTERNAL);
+
+        testcase(
+            "bug: LoanBrokerCoverWithdraw to self succeeds after issuer clears "
+            "asfDefaultRipple when the trust line exists (post-fixCleanup3_4_0)");
+        runCoverWithdraw(all_, tesSUCCESS);
+
+        testcase(
+            "bug: LoanBrokerCoverWithdraw to self fails with tecINTERNAL after "
+            "issuer clears asfDefaultRipple and the trust line was deleted "
+            "(pre-fixCleanup3_4_0)");
+        runDeletedCoverWithdraw(all_ - fixCleanup3_4_0, tecINTERNAL);
+
+        testcase(
+            "bug: LoanBrokerCoverWithdraw to self fails with terNO_RIPPLE after "
+            "issuer clears asfDefaultRipple and the trust line was deleted "
+            "(post-fixCleanup3_4_0)");
+        runDeletedCoverWithdraw(all_, terNO_RIPPLE);
+
+        testcase(
+            "bug: private VaultWithdraw to self fails with tecINTERNAL after "
+            "issuer clears asfDefaultRipple even though the trust line exists "
+            "(pre-fixCleanup3_4_0)");
+        runPrivateVault(all_ - fixCleanup3_4_0, tecINTERNAL);
+
+        testcase(
+            "bug: private VaultWithdraw to self succeeds after issuer clears "
+            "asfDefaultRipple when the trust line exists (post-fixCleanup3_4_0)");
+        runPrivateVault(all_, tesSUCCESS);
+    }
+
+    // Bug 1: a sponsored XRP VaultWithdraw to a distinct destination is
+    // rejected because the vault invariant treats the holder's touched
+    // but economically unchanged AccountRoot as a second payout
+    // recipient. Sequence/ticket processing still touches the holder
+    // while the sponsor pays the fee, so the holder's XRP delta is
+    // present-zero and is not normalized away. If this happens on the
+    // last Subscription ledger of a closed-ended vault, the holder
+    // cannot retry until Redemption (tecTOO_SOON during Investment).
+    //
+    // Fixed by ValidVault::deltaAssetsForParty always collapsing an
+    // economically-zero XRP delta to absence, regardless of who paid the
+    // fee.
+    void
+    testBugSponsoredWithdrawZeroDeltaMisclassifiedAsSecondRecipient()
+    {
+        using namespace test::jtx;
+
+        auto runScenario = [this](FeatureBitset features, TER expected) {
+            Env env{*this, features};
+            Account const owner{"owner"};
+            Account const holder{"holder"};
+            Account const destination{"destination"};
+            Account const sponsor{"sponsor"};
+            env.fund(XRP(10'000), owner, holder, destination, sponsor);
+            env.close();
+
+            constexpr std::uint32_t investmentPeriod = 14u * 24u * 60u * 60u;
+            auto const [vault, vaultKeylet, subscriptionDate, redemptionDate] =
+                makeClosedEndedVault(env, owner, xrpIssue(), 120u, investmentPeriod);
+            BEAST_EXPECT(redemptionDate - subscriptionDate == investmentPeriod);
+
+            env(vault.deposit(
+                {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()}));
+            env.close();
+
+            // Inclusive SubscriptionDate boundary: still Subscription, so an
+            // ordinary withdrawal is allowed.
+            closeToTime(env, tp{d{subscriptionDate}});
+
+            auto const vaultBefore = env.le(vaultKeylet);
+            if (!BEAST_EXPECT(vaultBefore))
+                return;
+            auto const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
+            auto const holderBalanceBefore = env.balance(holder);
+            auto const destinationBalanceBefore = env.balance(destination);
+            auto const sponsorBalanceBefore = env.balance(sponsor);
+            auto const fee = env.current()->fees().base;
+
+            auto withdraw = vault.withdraw(
+                {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()});
+            withdraw[sfDestination] = destination.human();
+            env(withdraw,
+                Fee(fee),
+                sponsor::As(sponsor, spfSponsorFee),
+                Sig(sfSponsorSignature, sponsor),
+                Ter(expected));
+            env.close();
+
+            auto const vaultAfter = env.le(vaultKeylet);
+            if (!BEAST_EXPECT(vaultAfter))
+                return;
+            BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore - fee);
+
+            if (expected == tesSUCCESS)
+            {
+                BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore - XRP(100).value());
+                BEAST_EXPECT(env.balance(holder) == holderBalanceBefore);
+                BEAST_EXPECT(env.balance(destination) == destinationBalanceBefore + XRP(100));
+                return;
+            }
+
+            // Invariant rollback: the payout and share burn are undone, but
+            // sequence processing and the sponsored fee charge remain.
+            BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore);
+            BEAST_EXPECT(env.balance(holder) == holderBalanceBefore);
+            BEAST_EXPECT(env.balance(destination) == destinationBalanceBefore);
+
+            // Once the ledger advances into Investment, the same holder
+            // cannot retry until Redemption.
+            auto retry = vault.withdraw(
+                {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()});
+            retry[sfDestination] = destination.human();
+            env(retry, Ter(tecTOO_SOON));
+        };
+
+        testcase(
+            "bug: sponsored XRP withdrawal to a distinct destination misreads a "
+            "touched-but-zero sender delta as a second recipient "
+            "(pre-fixCleanup3_4_0)");
+        runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
+
+        testcase(
+            "bug: sponsored XRP withdrawal to a distinct destination succeeds "
+            "(post-fixCleanup3_4_0)");
+        runScenario(all_, tesSUCCESS);
+    }
+
+    // Bug 2: a co-signed fee sponsor named as the withdrawal's own
+    // destination pays its fee from the same AccountRoot it is paid into,
+    // so its net XRP delta is (payout - fee). The invariant never fee-
+    // corrected the destination side at all, so this always failed the
+    // equal-amount check against the vault's outflow (payout).
+    //
+    // Fixed by ValidVault::deltaAssetsForParty adding the fee back onto
+    // whichever inspected party's AccountRoot actually paid it -- the
+    // sender, or a distinct destination -- not just the sender.
+    void
+    testBugSponsorAsDestinationFeeMisappliedToPayout()
+    {
+        using namespace test::jtx;
+
+        auto runScenario = [this](FeatureBitset features, TER expected) {
+            Env env{*this, features};
+            Account const owner{"owner"};
+            Account const holder{"holder"};
+            Account const sponsor{"sponsor"};
+            env.fund(XRP(10'000), owner, holder, sponsor);
+            env.close();
+
+            Vault const vault{env};
+            auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = xrpIssue()});
+            env(vaultTx);
+            env.close();
+
+            env(vault.deposit(
+                {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()}));
+            env.close();
+
+            auto const vaultBefore = env.le(vaultKeylet);
+            if (!BEAST_EXPECT(vaultBefore))
+                return;
+            auto const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
+            auto const sponsorBalanceBefore = env.balance(sponsor);
+            auto const fee = env.current()->fees().base;
+
+            // The sponsor both receives the withdrawal (as sfDestination)
+            // and pays its own fee (co-signed) from the same AccountRoot.
+            auto withdraw = vault.withdraw(
+                {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()});
+            withdraw[sfDestination] = sponsor.human();
+            env(withdraw,
+                Fee(fee),
+                sponsor::As(sponsor, spfSponsorFee),
+                Sig(sfSponsorSignature, sponsor),
+                Ter(expected));
+            env.close();
+
+            auto const vaultAfter = env.le(vaultKeylet);
+            if (!BEAST_EXPECT(vaultAfter))
+                return;
+
+            if (expected == tesSUCCESS)
+            {
+                BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore - XRP(100).value());
+                // Paid the withdrawal, then separately debited for the fee
+                // it chose to cover; net effect is payout minus fee.
+                BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore + XRP(100) - fee);
+                return;
+            }
+
+            BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore);
+            BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore - fee);
+        };
+
+        testcase(
+            "bug: co-signed sponsor named as withdrawal destination has its "
+            "own fee debit misread as breaking the payout equality "
+            "(pre-fixCleanup3_4_0)");
+        runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED);
+
+        testcase(
+            "bug: co-signed sponsor named as withdrawal destination succeeds "
+            "(post-fixCleanup3_4_0)");
+        runScenario(all_, tesSUCCESS);
+    }
+
+    // Pre-funded fee sponsorship draws the fee from ltSponsorship.sfFeeAmount,
+    // so feePayerAccountRoot must return nullopt rather than the sponsor's
+    // AccountRoot. A bystander sponsor leaves that branch unexercised: the
+    // result is only consulted by deltaAssetsForParty via `payer && *payer ==
+    // id`. Naming the sponsor as sfDestination makes the early return
+    // load-bearing -- returning the sponsor's id instead of nullopt would add
+    // the fee back onto a balance that never paid it, and the equal-amount
+    // check against the vault outflow would fail.
+    //
+    // Contrast testBugSponsorAsDestinationFeeMisappliedToPayout, where the
+    // sponsor co-signs and so really does pay from its own AccountRoot.
+    void
+    testPrefundedFeeWithdraw()
+    {
+        using namespace test::jtx;
+
+        auto runScenario = [this](
+                               FeatureBitset features,
+                               TER expected,
+                               bool const sponsorIsDestination) {
+            Env env{*this, features};
+            Account const owner{"owner"};
+            Account const holder{"holder"};
+            Account const destination{"destination"};
+            Account const sponsor{"sponsor"};
+            env.fund(XRP(10'000), owner, holder, destination, sponsor);
+            env.close();
+
+            Vault const vault{env};
+            auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = xrpIssue()});
+            env(vaultTx);
+            env.close();
+
+            env(vault.deposit(
+                {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()}));
+            env.close();
+
+            auto const fee = env.current()->fees().base;
+            env(sponsor::set_fee(sponsor, 0, fee), sponsor::SponseeAcc(holder));
+            env.close();
+
+            auto const vaultBefore = env.le(vaultKeylet);
+            if (!BEAST_EXPECT(vaultBefore))
+                return;
+            auto const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
+            auto const holderBalanceBefore = env.balance(holder);
+            auto const destinationBalanceBefore = env.balance(destination);
+            auto const sponsorBalanceBefore = env.balance(sponsor);
+
+            Account const& recipient = sponsorIsDestination ? sponsor : destination;
+            auto withdraw = vault.withdraw(
+                {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()});
+            withdraw[sfDestination] = recipient.human();
+            env(withdraw, Fee(fee), sponsor::As(sponsor, spfSponsorFee), Ter(expected));
+            env.close();
+
+            auto const vaultAfter = env.le(vaultKeylet);
+            if (!BEAST_EXPECT(vaultAfter))
+                return;
+            // Holder is economically unchanged (sequence only); the fee is
+            // taken from the sponsorship object, not any AccountRoot.
+            BEAST_EXPECT(env.balance(holder) == holderBalanceBefore);
+
+            if (expected == tesSUCCESS)
+            {
+                BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore - XRP(100).value());
+                if (sponsorIsDestination)
+                {
+                    // The sponsor receives the payout and is not debited for
+                    // the fee. The sponsor has to BE the destination for
+                    // FeePayerType::SponsorPreFunded to matter.
+                    BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore + XRP(100));
+                }
+                else
+                {
+                    BEAST_EXPECT(env.balance(destination) == destinationBalanceBefore + XRP(100));
+                    BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore);
+                }
+                auto const sponsorship = env.le(keylet::sponsorship(sponsor, holder));
+                if (!BEAST_EXPECT(sponsorship))
+                    return;
+                BEAST_EXPECT(!sponsorship->isFieldPresent(sfFeeAmount));
+                return;
+            }
+
+            BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore);
+            BEAST_EXPECT(env.balance(sponsor) == sponsorBalanceBefore);
+            if (!sponsorIsDestination)
+                BEAST_EXPECT(env.balance(destination) == destinationBalanceBefore);
+        };
+
+        testcase(
+            "pre-funded fee XRP withdrawal to a distinct destination succeeds "
+            "(post-fixCleanup3_4_0)");
+        runScenario(all_, tesSUCCESS, false);
+
+        testcase(
+            "bug: pre-funded sponsor named as withdrawal destination misreads "
+            "the sender's touched-but-zero delta as a second recipient "
+            "(pre-fixCleanup3_4_0)");
+        runScenario(all_ - fixCleanup3_4_0, tecINVARIANT_FAILED, true);
+
+        testcase(
+            "bug: pre-funded sponsor named as withdrawal destination receives "
+            "the full payout (post-fixCleanup3_4_0)");
+        runScenario(all_, tesSUCCESS, true);
+    }
+
+    // Unsponsored third-party XRP withdrawal: the sender's AccountRoot moves
+    // by exactly -fee. Pre-amendment, the sender-only fee correction then
+    // collapses that to absence so the dual-recipient guard does not fire.
+    void
+    testUnsponsoredWithdrawToDistinctDestinationPreAmendment()
+    {
+        using namespace test::jtx;
+
+        testcase(
+            "unsponsored XRP withdrawal to a distinct destination succeeds "
+            "(pre-fixCleanup3_4_0)");
+
+        Env env{*this, all_ - fixCleanup3_4_0};
+        Account const owner{"owner"};
+        Account const holder{"holder"};
+        Account const destination{"destination"};
+        env.fund(XRP(10'000), owner, holder, destination);
+        env.close();
+
+        Vault const vault{env};
+        auto [vaultTx, vaultKeylet] = vault.create({.owner = owner, .asset = xrpIssue()});
+        env(vaultTx);
+        env.close();
+
+        env(vault.deposit(
+            {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()}));
+        env.close();
+
+        auto const vaultBefore = env.le(vaultKeylet);
+        if (!BEAST_EXPECT(vaultBefore))
+            return;
+        auto const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
+        auto const holderBalanceBefore = env.balance(holder);
+        auto const destinationBalanceBefore = env.balance(destination);
+        auto const fee = env.current()->fees().base;
+
+        auto withdraw = vault.withdraw(
+            {.depositor = holder, .id = vaultKeylet.key, .amount = XRP(100).value()});
+        withdraw[sfDestination] = destination.human();
+        env(withdraw, Fee(fee), Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultAfter = env.le(vaultKeylet);
+        if (!BEAST_EXPECT(vaultAfter))
+            return;
+        BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == assetsTotalBefore - XRP(100).value());
+        BEAST_EXPECT(env.balance(holder) == holderBalanceBefore - fee);
+        BEAST_EXPECT(env.balance(destination) == destinationBalanceBefore + XRP(100));
+    }
+
+public:
+    void
+    run() override
+    {
+        testVaultWithdrawEqualityEnforced();
+        testBugIssuerVaultDepositAtEdge();
+        testBugMakeDeltaPosteriorScale();
+        testBugMakeDeltaAnteriorScale();
+        testVaultDepositCanonicalizeToZero();
+        testBugDepositShareTruncationSubUlp();
+        testVaultWithdrawCanonicalizeToZero();
+        testBugVaultDustDebitCanonicalizesToNoOp();
+        testBugVaultDepositOvercreditsAcrossScaleBoundary();
+        testBugVaultLockedByPartialWithdraw();
+        testVaultDepositNegativeBalanceFromOppositeLimit();
+        testBug6LimitBypassWithShares();
+        testBugClawbackRoundTripOvershoot();
+        testBugWithdrawRoundTripOvershoot();
+        testBugClawbackAfterLoanImpair();
+        testBugMptZeroWithdrawMissingHolding();
+        testBugIouZeroWithdrawMissingTrustLine();
+        testBugXrpZeroWithdrawSponsoredFee();
+        testBugSelfWithdrawAfterIssuerClearsDefaultRipple();
+        testBugSponsoredWithdrawZeroDeltaMisclassifiedAsSecondRecipient();
+        testBugSponsorAsDestinationFeeMisappliedToPayout();
+        testPrefundedFeeWithdraw();
+        testUnsponsoredWithdrawToDistinctDestinationPreAmendment();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(VaultBugs, app, xrpl);
+
+}  // namespace xrpl
diff --git a/src/test/app/vault/VaultClawback_test.cpp b/src/test/app/vault/VaultClawback_test.cpp
new file mode 100644
index 0000000000..0290b67047
--- /dev/null
+++ b/src/test/app/vault/VaultClawback_test.cpp
@@ -0,0 +1,1227 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class VaultClawback_test : public VaultTestBase
+{
+private:
+    void
+    testVaultClawbackBurnShares()
+    {
+        using namespace test::jtx;
+        using namespace loan_broker;
+        using namespace loan;
+        Env env(*this, beast::Severity::Warning);
+
+        auto const vaultAssetBalance = [&](Keylet const& vaultKeylet) {
+            auto const sleVault = env.le(vaultKeylet);
+            BEAST_EXPECT(sleVault != nullptr);
+
+            return std::make_pair(sleVault->at(sfAssetsAvailable), sleVault->at(sfAssetsTotal));
+        };
+
+        auto const vaultShareBalance = [&](Keylet const& vaultKeylet) {
+            auto const sleVault = env.le(vaultKeylet);
+            BEAST_EXPECT(sleVault != nullptr);
+
+            auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
+            BEAST_EXPECT(sleIssuance != nullptr);
+
+            return sleIssuance->at(sfOutstandingAmount);
+        };
+
+        // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
+        // accepts closed-ended vaults, so build vaults in this suite as
+        // closed-ended and advance past SubscriptionDate before creating
+        // brokers/loans. VaultClawback itself is not phase-gated. The
+        // subscription offset must be large enough that the deposit
+        // ledger close does not accidentally push us past SubscriptionDate
+        // (which would land the deposit in Investment phase and fail).
+        auto const setupVault = [&](PrettyAsset const& asset,
+                                    Account const& owner,
+                                    Account const& depositor) -> std::pair {
+            Vault const vault{env};
+
+            auto const& [tx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
+                {.owner = owner, .asset = asset, .subscriptionOffset = std::chrono::seconds{60}});
+            env(tx, Ter(tesSUCCESS));
+            env.close();
+
+            auto const& vaultSle = env.le(vaultKeylet);
+            BEAST_EXPECT(vaultSle != nullptr);
+
+            Asset const share = vaultSle->at(sfShareMPTID);
+
+            env(vault.deposit(
+                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Move past SubscriptionDate so LoanBrokerSet/LoanSet run in
+            // the Investment phase.
+            vault.closePastSubscription(subscriptionDate);
+
+            auto const& [availablePreDefault, totalPreDefault] = vaultAssetBalance(vaultKeylet);
+            BEAST_EXPECT(availablePreDefault == totalPreDefault);
+            BEAST_EXPECT(availablePreDefault == asset(100).value());
+
+            // attempt to clawback shares while there are assets fails
+            env(vault.clawback(
+                    {.issuer = owner,
+                     .id = vaultKeylet.key,
+                     .holder = depositor,
+                     .amount = share(0).value()}),
+                Ter(tecNO_PERMISSION));
+            env.close();
+
+            auto const& sharesAvailable = vaultShareBalance(vaultKeylet);
+            auto const& brokerKeylet =
+                keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+
+            env(set(owner, vaultKeylet.key));
+            env.close();
+
+            auto const& loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1));
+
+            // Create a simple Loan for the full amount of Vault assets
+            env(set(depositor, brokerKeylet.key, asset(100).value()),
+                loan::kInterestRate(TenthBips32(0)),
+                kGracePeriod(60),
+                kPaymentInterval(120),
+                kPaymentTotal(10),
+                Sig(sfCounterpartySignature, owner),
+                Fee(env.current()->fees().base * 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // attempt to clawback shares while there assetsAvailable == 0 and
+            // assetsTotal > 0 fails
+            env(vault.clawback(
+                    {.issuer = owner,
+                     .id = vaultKeylet.key,
+                     .holder = depositor,
+                     .amount = share(0).value()}),
+                Ter(tecNO_PERMISSION));
+            env.close();
+
+            env.close(std::chrono::seconds{120 + 60});
+
+            env(manage(owner, loanKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
+
+            auto const& [availablePostDefault, totalPostDefault] = vaultAssetBalance(vaultKeylet);
+
+            BEAST_EXPECT(availablePostDefault == totalPostDefault);
+            BEAST_EXPECT(availablePostDefault == asset(0).value());
+            BEAST_EXPECT(vaultShareBalance(vaultKeylet) == sharesAvailable);
+
+            return std::make_pair(vault, vaultKeylet);
+        };
+
+        auto const testCase = [&](PrettyAsset const& asset,
+                                  std::string const& prefix,
+                                  Account const& owner,
+                                  Account const& depositor) {
+            {
+                testcase("VaultClawback (share) - " + prefix + " owner asset clawback fails");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
+                // when asset is XRP or owner is not issuer clawback fail
+                // when owner is issuer precision loss occurs as vault is
+                // empty
+                auto const expectedTer = [&]() {
+                    if (asset.native())
+                        return Ter(temMALFORMED);
+                    if (asset.raw().getIssuer() != owner.id())
+                        return Ter(tecNO_PERMISSION);
+                    return Ter(tecPRECISION_LOSS);
+                }();
+                env(vault.clawback({
+                        .issuer = owner,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                        .amount = asset(100).value(),
+                    }),
+                    expectedTer);
+                env.close();
+            }
+
+            {
+                testcase(
+                    "VaultClawback (share) - " + prefix + " owner incomplete share clawback fails");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
+                auto const& vaultSle = env.le(vaultKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+                Asset const share = vaultSle->at(sfShareMPTID);
+                env(vault.clawback({
+                        .issuer = owner,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                        .amount = share(1).value(),
+                    }),
+                    Ter(tecLIMIT_EXCEEDED));
+                env.close();
+            }
+
+            {
+                testcase(
+                    "VaultClawback (share) - " + prefix +
+                    " owner implicit complete share clawback");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
+                env(vault.clawback({
+                        .issuer = owner,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                    }),
+                    // when owner is issuer implicit clawback fails
+                    asset.native() || asset.raw().getIssuer() != owner.id() ? Ter(tesSUCCESS)
+                                                                            : Ter(tecWRONG_ASSET));
+                env.close();
+            }
+
+            {
+                testcase(
+                    "VaultClawback (share) - " + prefix +
+                    " owner explicit complete share clawback succeeds");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor);
+                auto const& vaultSle = env.le(vaultKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+                Asset const share = vaultSle->at(sfShareMPTID);
+                env(vault.clawback({
+                        .issuer = owner,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                        .amount = share(vaultShareBalance(vaultKeylet)).value(),
+                    }),
+                    Ter(tesSUCCESS));
+                env.close();
+            }
+            {
+                testcase("VaultClawback (share) - " + prefix + " owner can clawback own shares");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, owner);
+                auto const& vaultSle = env.le(vaultKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+                Asset const share = vaultSle->at(sfShareMPTID);
+                env(vault.clawback({
+                        .issuer = owner,
+                        .id = vaultKeylet.key,
+                        .holder = owner,
+                        .amount = share(vaultShareBalance(vaultKeylet)).value(),
+                    }),
+                    Ter(tesSUCCESS));
+                env.close();
+            }
+
+            {
+                testcase("VaultClawback (share) - " + prefix + " empty vault share clawback fails");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, owner);
+                auto const& vaultSle = env.le(vaultKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+                Asset const share = vaultSle->at(sfShareMPTID);
+                env(vault.clawback({
+                        .issuer = owner,
+                        .id = vaultKeylet.key,
+                        .holder = owner,
+                        .amount = share(vaultShareBalance(vaultKeylet)).value(),
+                    }),
+                    Ter(tesSUCCESS));
+
+                // Now the vault is empty, clawback again fails
+                env(vault.clawback({
+                        .issuer = owner,
+                        .id = vaultKeylet.key,
+                        .holder = owner,
+                        .amount = share(vaultShareBalance(vaultKeylet)).value(),
+                    }),
+                    Ter(tecNO_PERMISSION));
+                env.close();
+            }
+        };
+
+        Account const owner{"alice"};
+        Account const depositor{"bob"};
+        Account const issuer{"issuer"};
+
+        env.fund(XRP(10000), issuer, owner, depositor);
+        env.close();
+
+        // Test XRP
+        PrettyAsset const xrp = xrpIssue();
+        testCase(xrp, "XRP", owner, depositor);
+        testCase(xrp, "XRP (depositor is owner)", owner, owner);
+
+        // Test IOU
+        PrettyAsset const iou = issuer["IOU"];
+        env(fset(issuer, asfAllowTrustLineClawback));
+        env.close();
+
+        env.trust(iou(1000), owner);
+        env.trust(iou(1000), depositor);
+        env(pay(issuer, owner, iou(100)));
+        env(pay(issuer, depositor, iou(100)));
+        env.close();
+        testCase(iou, "IOU", owner, depositor);
+        testCase(iou, "IOU (owner is issuer)", issuer, depositor);
+
+        // Test MPT
+        MPTTester mptt{env, issuer, kMptInitNoFund};
+        mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
+        PrettyAsset const mpt = mptt.issuanceID();
+        mptt.authorize({.account = owner});
+        mptt.authorize({.account = depositor});
+        env(pay(issuer, owner, mpt(1000)));
+        env(pay(issuer, depositor, mpt(1000)));
+        env.close();
+        testCase(mpt, "MPT", owner, depositor);
+        testCase(mpt, "MPT (owner is issuer)", issuer, depositor);
+    }
+
+    void
+    testVaultClawbackAssets()
+    {
+        using namespace test::jtx;
+        using namespace loan_broker;
+        using namespace loan;
+        Env env(*this);
+        env.enableFeature(fixCleanup3_1_3);
+
+        // Under featureLendingProtocolV1_1 LoanBrokerSet::preclaim only
+        // accepts closed-ended vaults; some tests using this helper later
+        // attach loan brokers to the vault. Build it as closed-ended and
+        // advance past SubscriptionDate so subsequent broker/loan setup
+        // runs in the Investment phase. VaultClawback itself is not
+        // phase-gated. See the other setupVault (share tests) for why the
+        // subscription offset must be generous.
+        auto const setupVault = [&](PrettyAsset const& asset,
+                                    Account const& owner,
+                                    Account const& depositor,
+                                    Account const& issuer) -> std::pair {
+            Vault const vault{env};
+
+            auto const& [tx, vaultKeylet, subscriptionDate] = vault.createClosedEnded(
+                {.owner = owner, .asset = asset, .subscriptionOffset = std::chrono::seconds{60}});
+            env(tx, Ter(tesSUCCESS));
+            env.close();
+
+            auto const& vaultSle = env.le(vaultKeylet);
+            BEAST_EXPECT(vaultSle != nullptr);
+            env.memoize(Account("vault", vaultSle->at(sfAccount)));
+            env(vault.deposit(
+                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
+                Ter(tesSUCCESS));
+            env.close();
+
+            vault.closePastSubscription(subscriptionDate);
+
+            return std::make_pair(vault, vaultKeylet);
+        };
+
+        auto const testCase = [&](PrettyAsset const& asset,
+                                  std::string const& prefix,
+                                  Account const& owner,
+                                  Account const& depositor,
+                                  Account const& issuer) {
+            if (asset.native())
+            {
+                testcase("VaultClawback (asset) - " + prefix + " issuer XRP clawback fails");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
+                // If the asset is XRP, clawback with amount fails as malformed
+                // when asset is specified.
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = issuer,
+                        .amount = asset(1).value(),
+                    }),
+                    Ter(temMALFORMED));
+                // When asset is implicit, clawback fails as no permission.
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = issuer,
+                    }),
+                    Ter(tecNO_PERMISSION));
+                return;
+            }
+
+            {
+                testcase(
+                    "VaultClawback (asset) - " + prefix + " clawback for different asset fails");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
+
+                Account const issuer2{"issuer2"};
+                PrettyAsset const asset2 = issuer2["FOO"];
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                        .amount = asset2(1).value(),
+                    }),
+                    Ter(tecWRONG_ASSET));
+            }
+
+            {
+                testcase(
+                    "VaultClawback (asset) - " + prefix +
+                    " ambiguous owner/issuer asset clawback fails");
+                auto [vault, vaultKeylet] = setupVault(asset, issuer, depositor, issuer);
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = issuer,
+                    }),
+                    Ter(tecWRONG_ASSET));
+            }
+
+            {
+                testcase("VaultClawback (asset) - " + prefix + " non-issuer asset clawback fails");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
+
+                env(vault.clawback({
+                        .issuer = owner,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                    }),
+                    Ter(tecNO_PERMISSION));
+
+                env(vault.clawback({
+                        .issuer = owner,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                        .amount = asset(1).value(),
+                    }),
+                    Ter(tecNO_PERMISSION));
+            }
+
+            {
+                testcase("VaultClawback (asset) - " + prefix + " issuer clawback from self fails");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, issuer, issuer);
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = issuer,
+                    }),
+                    Ter(tecNO_PERMISSION));
+            }
+
+            {
+                testcase("VaultClawback (asset) - " + prefix + " issuer share clawback fails");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
+                auto const& vaultSle = env.le(vaultKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+                Asset const share = vaultSle->at(sfShareMPTID);
+
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                        .amount = share(1).value(),
+                    }),
+                    Ter(tecNO_PERMISSION));
+            }
+
+            {
+                testcase(
+                    "VaultClawback (asset) - " + prefix +
+                    " partial issuer asset clawback succeeds");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
+
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                        .amount = asset(1).value(),
+                    }),
+                    Ter(tesSUCCESS));
+            }
+
+            {
+                testcase(
+                    "VaultClawback (asset) - " + prefix + " full issuer asset clawback succeeds");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
+
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                        .amount = asset(100).value(),
+                    }),
+                    Ter(tesSUCCESS));
+            }
+
+            {
+                testcase(
+                    "VaultClawback (asset) - " + prefix +
+                    " implicit full issuer asset clawback succeeds");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
+
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                    }),
+                    Ter(tesSUCCESS));
+            }
+
+            {
+                testcase(
+                    "VaultClawback (asset) - " + prefix +
+                    " zero-amount clawback clamped with outstanding loan");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
+
+                auto const vaultSle = env.le(vaultKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+
+                PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
+
+                // Create a loan broker backed by this vault
+                auto const brokerKeylet =
+                    keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+                env(set(owner, vaultKeylet.key));
+                env.close();
+
+                // Depositor borrows 40 units, reducing assetsAvailable to 60
+                // while assetsTotal stays at 100
+                env(set(depositor, brokerKeylet.key, asset(40).value()),
+                    loan::kInterestRate(TenthBips32(0)),
+                    kGracePeriod(60),
+                    kPaymentInterval(120),
+                    kPaymentTotal(10),
+                    Sig(sfCounterpartySignature, owner),
+                    Fee(env.current()->fees().base * 2),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                {
+                    auto const sle = env.le(vaultKeylet);
+                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
+                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
+                }
+
+                // Zero-amount clawback (= "clawback all") should succeed,
+                // clamped to assetsAvailable (60) rather than the full
+                // share value (100).
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                    }),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                // Only 60 assets clawed back; loan's 40 still outstanding
+                {
+                    auto const sle = env.le(vaultKeylet);
+                    BEAST_EXPECT(sle != nullptr);
+                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
+                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
+
+                    // 60 of 100 shares destroyed (1:1 ratio), 40 remain
+                    auto const sharesAfter = env.balance(depositor, shares);
+                    BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1}));
+                }
+            }
+
+            {
+                testcase(
+                    "VaultClawback (asset) - " + prefix +
+                    " non-zero clawback clamped with outstanding loan");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
+
+                auto const vaultSle = env.le(vaultKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+                PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
+
+                // Create a loan broker backed by this vault
+                auto const brokerKeylet =
+                    keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+                env(set(owner, vaultKeylet.key));
+                env.close();
+
+                // Depositor borrows 40 units
+                env(set(depositor, brokerKeylet.key, asset(40).value()),
+                    loan::kInterestRate(TenthBips32(0)),
+                    kGracePeriod(60),
+                    kPaymentInterval(120),
+                    kPaymentTotal(10),
+                    Sig(sfCounterpartySignature, owner),
+                    Fee(env.current()->fees().base * 2),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                {
+                    auto const sle = env.le(vaultKeylet);
+                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
+                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
+                }
+
+                // Request 100 but only 60 available — clamped to 60
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                        .amount = asset(100).value(),
+                    }),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                {
+                    auto const sle = env.le(vaultKeylet);
+                    BEAST_EXPECT(sle != nullptr);
+                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
+                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
+
+                    // 60 of 100 shares destroyed (1:1 ratio), 40 remain
+                    auto const sharesAfter = env.balance(depositor, shares);
+                    BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1}));
+                }
+            }
+
+            {
+                testcase(
+                    "VaultClawback (asset) - " + prefix +
+                    " partial clawback below available with outstanding loan");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
+
+                auto const vaultSle = env.le(vaultKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+                PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
+
+                // Create a loan broker backed by this vault
+                auto const brokerKeylet =
+                    keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+                env(set(owner, vaultKeylet.key));
+                env.close();
+
+                // Depositor borrows 40 units: assetsAvailable=60, assetsTotal=100
+                env(set(depositor, brokerKeylet.key, asset(40).value()),
+                    loan::kInterestRate(TenthBips32(0)),
+                    kGracePeriod(60),
+                    kPaymentInterval(120),
+                    kPaymentTotal(10),
+                    Sig(sfCounterpartySignature, owner),
+                    Fee(env.current()->fees().base * 2),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                {
+                    auto const sle = env.le(vaultKeylet);
+                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
+                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
+                }
+
+                // Clawback 30 — well under available (60), no clamping needed
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                        .amount = asset(30).value(),
+                    }),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                {
+                    auto const sle = env.le(vaultKeylet);
+                    BEAST_EXPECT(sle != nullptr);
+                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(30).value());
+                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(70).value());
+
+                    // 30 of 100 shares destroyed (1:1 ratio), 70 remain
+                    auto const sharesAfter = env.balance(depositor, shares);
+                    BEAST_EXPECT(sharesAfter == shares(Number{7, sle->at(sfScale) + 1}));
+                }
+            }
+
+            {
+                testcase(
+                    "VaultClawback (asset) - " + prefix +
+                    " clawback exactly equal to available with outstanding loan");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
+
+                auto const vaultSle = env.le(vaultKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+                PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
+
+                auto const brokerKeylet =
+                    keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+                env(set(owner, vaultKeylet.key));
+                env.close();
+
+                // Depositor borrows 40 units: assetsAvailable=60, assetsTotal=100
+                env(set(depositor, brokerKeylet.key, asset(40).value()),
+                    loan::kInterestRate(TenthBips32(0)),
+                    kGracePeriod(60),
+                    kPaymentInterval(120),
+                    kPaymentTotal(10),
+                    Sig(sfCounterpartySignature, owner),
+                    Fee(env.current()->fees().base * 2),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                // Clawback exactly 60 — at the boundary, no clamping needed
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                        .amount = asset(60).value(),
+                    }),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                {
+                    auto const sle = env.le(vaultKeylet);
+                    BEAST_EXPECT(sle != nullptr);
+                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
+                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
+
+                    // 60 of 100 shares destroyed (1:1 ratio), 40 remain
+                    auto const sharesAfter = env.balance(depositor, shares);
+                    BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1}));
+                }
+            }
+
+            {
+                testcase(
+                    "VaultClawback (asset) - " + prefix +
+                    " clawback with zero available (fully borrowed)");
+                auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer);
+
+                auto const vaultSle = env.le(vaultKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+                PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
+
+                auto const brokerKeylet =
+                    keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+                env(set(owner, vaultKeylet.key));
+                env.close();
+
+                // Depositor borrows all 100 units: assetsAvailable=0, assetsTotal=100
+                env(set(depositor, brokerKeylet.key, asset(100).value()),
+                    loan::kInterestRate(TenthBips32(0)),
+                    kGracePeriod(60),
+                    kPaymentInterval(120),
+                    kPaymentTotal(10),
+                    Sig(sfCounterpartySignature, owner),
+                    Fee(env.current()->fees().base * 2),
+                    Ter(tesSUCCESS));
+                env.close();
+
+                {
+                    auto const sle = env.le(vaultKeylet);
+                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
+                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
+                }
+
+                auto const sharesBefore = env.balance(depositor, shares);
+
+                // Zero-amount clawback — nothing available, clamped to 0,
+                // resulting in zero shares destroyed → tecPRECISION_LOSS
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                    }),
+                    Ter(tecPRECISION_LOSS));
+                env.close();
+
+                // Explicit amount clawback — also nothing available
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = vaultKeylet.key,
+                        .holder = depositor,
+                        .amount = asset(50).value(),
+                    }),
+                    Ter(tecPRECISION_LOSS));
+                env.close();
+
+                {
+                    // Nothing changed — vault and shares unchanged
+                    auto const sle = env.le(vaultKeylet);
+                    BEAST_EXPECT(sle != nullptr);
+                    BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value());
+                    BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value());
+                    auto const sharesAfter = env.balance(depositor, shares);
+                    BEAST_EXPECT(sharesAfter == sharesBefore);
+                }
+            }
+        };
+
+        Account const owner{"alice"};
+        Account const depositor{"bob"};
+        Account const issuer{"issuer"};
+
+        env.fund(XRP(10000), issuer, owner, depositor);
+        env.close();
+
+        // Test XRP
+        PrettyAsset const xrp = xrpIssue();
+        testCase(xrp, "XRP", owner, depositor, issuer);
+
+        // Test IOU
+        PrettyAsset const iou = issuer["IOU"];
+        env(fset(issuer, asfAllowTrustLineClawback));
+        env.close();
+        env.trust(iou(2000), owner);
+        env.trust(iou(2000), depositor);
+        env(pay(issuer, owner, iou(2000)));
+        env(pay(issuer, depositor, iou(2000)));
+        env.close();
+        testCase(iou, "IOU", owner, depositor, issuer);
+
+        // Test MPT
+        MPTTester mptt{env, issuer, kMptInitNoFund};
+        mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
+
+        PrettyAsset const mpt = mptt.issuanceID();
+        mptt.authorize({.account = owner});
+        mptt.authorize({.account = depositor});
+        env(pay(issuer, depositor, mpt(2000)));
+        env.close();
+        testCase(mpt, "MPT", owner, depositor, issuer);
+
+        // Test pre-fixCleanup3_1_3 legacy path: zero-amount clawback
+        // returns early without clamping to assetsAvailable.
+        {
+            testcase(
+                "VaultClawback (asset) - IOU pre-fixCleanup3_1_3"
+                " zero-amount clawback unclamped with outstanding loan");
+
+            env.disableFeature(fixCleanup3_1_3);
+
+            auto [vault, vaultKeylet] = setupVault(iou, owner, depositor, issuer);
+
+            auto const vaultSle = env.le(vaultKeylet);
+            BEAST_EXPECT(vaultSle != nullptr);
+            if (!vaultSle)
+                return;
+
+            PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
+
+            // Create a loan broker backed by this vault
+            auto const brokerKeylet =
+                keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+            env(set(owner, vaultKeylet.key));
+            env.close();
+
+            // Depositor borrows 40 units, reducing assetsAvailable to 60
+            // while assetsTotal stays at 100
+            env(set(depositor, brokerKeylet.key, iou(40).value()),
+                loan::kInterestRate(TenthBips32(0)),
+                kGracePeriod(60),
+                kPaymentInterval(120),
+                kPaymentTotal(10),
+                Sig(sfCounterpartySignature, owner),
+                Fee(env.current()->fees().base * 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            {
+                auto const sle = env.le(vaultKeylet);
+                BEAST_EXPECT(sle->at(sfAssetsAvailable) == iou(60).value());
+                BEAST_EXPECT(sle->at(sfAssetsTotal) == iou(100).value());
+            }
+
+            auto const sharesBefore = env.balance(depositor, shares);
+
+            // Legacy: zero-amount clawback tries to recover the full
+            // share value (100) without clamping to assetsAvailable (60).
+            // This causes the vault balance to go negative, triggering
+            // the sanity check in doApply → tefINTERNAL.
+            env(vault.clawback({
+                    .issuer = issuer,
+                    .id = vaultKeylet.key,
+                    .holder = depositor,
+                }),
+                Ter(tefINTERNAL));
+            env.close();
+
+            {
+                // Transaction rolled back — vault and shares unchanged
+                auto const sle = env.le(vaultKeylet);
+                BEAST_EXPECT(sle != nullptr);
+                BEAST_EXPECT(sle->at(sfAssetsAvailable) == iou(60).value());
+                BEAST_EXPECT(sle->at(sfAssetsTotal) == iou(100).value());
+                auto const sharesAfter = env.balance(depositor, shares);
+                BEAST_EXPECT(sharesAfter == sharesBefore);
+            }
+
+            env.enableFeature(fixCleanup3_1_3);
+        }
+    }
+
+    void
+    testVaultEscrowedMPT()
+    {
+        using namespace test::jtx;
+        using namespace std::literals;
+
+        // Verify vault deposit/withdraw/clawback respect sfLockedAmount.
+        // When MPT tokens are escrowed, sfMPTAmount is reduced and
+        // sfLockedAmount is increased. Vault operations go through
+        // accountSend/accountHolds which read sfMPTAmount, so escrowed
+        // tokens are naturally excluded.
+
+        {
+            testcase("Vault deposit fails when MPT asset is escrowed");
+
+            Env env{*this, testableAmendments()};
+            auto const baseFee = env.current()->fees().base;
+            Account const owner{"owner"};
+            Account const depositor{"depositor"};
+            Account const issuer{"issuer"};
+            Account const bob{"bob"};
+
+            env.fund(XRP(10000), issuer, owner, depositor, bob);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create(
+                {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow});
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = depositor});
+            mptt.authorize({.account = bob});
+            PrettyAsset const asset = mptt.issuanceID();
+            env(pay(issuer, depositor, asset(100)));
+            env.close();
+
+            // Escrow 60 of 100 MPT tokens: sfMPTAmount drops to 40
+            auto const escrowSeq = env.seq(depositor);
+            env(escrow::create(depositor, bob, asset(60)),
+                escrow::kCondition(escrow::kCb1),
+                escrow::kFinishTime(env.now() + 1s),
+                Fee(baseFee * 150),
+                Ter(tesSUCCESS));
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx, Ter(tesSUCCESS));
+            env.close();
+
+            // Deposit 100 should fail — only 40 spendable
+            env(vault.deposit(
+                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
+                Ter(tecINSUFFICIENT_FUNDS));
+            env.close();
+
+            // Deposit 40 (the unlocked balance) should succeed
+            env(vault.deposit({.depositor = depositor, .id = vaultKeylet.key, .amount = asset(40)}),
+                Ter(tesSUCCESS));
+            env.close();
+
+            {
+                auto const sle = env.le(vaultKeylet);
+                BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value());
+            }
+
+            // Clean up escrow
+            env(escrow::finish(bob, depositor, escrowSeq),
+                escrow::kCondition(escrow::kCb1),
+                escrow::kFulfillment(escrow::kFb1),
+                Fee(baseFee * 150),
+                Ter(tesSUCCESS));
+            env.close();
+        }
+
+        {
+            testcase("Vault withdraw respects escrowed shares");
+
+            Env env{*this, testableAmendments()};
+            auto const baseFee = env.current()->fees().base;
+            Account const owner{"owner"};
+            Account const depositor{"depositor"};
+            Account const issuer{"issuer"};
+            Account const bob{"bob"};
+
+            env.fund(XRP(10000), issuer, owner, depositor, bob);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create(
+                {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow});
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = depositor});
+            PrettyAsset const asset = mptt.issuanceID();
+            env(pay(issuer, depositor, asset(100)));
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx, Ter(tesSUCCESS));
+            env.close();
+
+            // Deposit 100 → get shares
+            env(vault.deposit(
+                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
+                Ter(tesSUCCESS));
+            env.close();
+
+            auto const vaultSle = env.le(vaultKeylet);
+            if (!BEAST_EXPECT(vaultSle))
+                return;
+            env.memoize(Account("vault", vaultSle->at(sfAccount)));
+            PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
+
+            // Authorize bob for share MPT so he can receive escrowed shares
+            auto const shareMPTID = vaultSle->at(sfShareMPTID);
+            {
+                json::Value jv;
+                jv[jss::Account] = bob.human();
+                jv[sfMPTokenIssuanceID] = to_string(shareMPTID);
+                jv[jss::TransactionType] = jss::MPTokenAuthorize;
+                env(jv, Ter(tesSUCCESS));
+                env.close();
+            }
+
+            // Escrow 60% of shares
+            auto const escrowAmount = shares(Number{6, vaultSle->at(sfScale) + 1});
+            env(escrow::create(depositor, bob, escrowAmount),
+                escrow::kCondition(escrow::kCb1),
+                escrow::kFinishTime(env.now() + 1s),
+                Fee(baseFee * 150),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Withdraw all 100 should fail — only 40% of shares are unlocked
+            env(vault.withdraw(
+                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
+                Ter(tecINSUFFICIENT_FUNDS));
+            env.close();
+
+            // Withdraw 40 (matching unlocked shares) should succeed
+            env(vault.withdraw(
+                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(40)}),
+                Ter(tesSUCCESS));
+            env.close();
+
+            {
+                auto const sle = env.le(vaultKeylet);
+                BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(60).value());
+            }
+        }
+
+        {
+            testcase("Vault clawback only recovers unlocked shares");
+
+            Env env{*this, testableAmendments() | fixCleanup3_1_3};
+            auto const baseFee = env.current()->fees().base;
+            Account const owner{"owner"};
+            Account const depositor{"depositor"};
+            Account const issuer{"issuer"};
+            Account const bob{"bob"};
+
+            env.fund(XRP(10000), issuer, owner, depositor, bob);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create(
+                {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow});
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = depositor});
+            PrettyAsset const asset = mptt.issuanceID();
+            env(pay(issuer, depositor, asset(100)));
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx, Ter(tesSUCCESS));
+            env.close();
+
+            // Deposit 100 → get shares
+            env(vault.deposit(
+                    {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}),
+                Ter(tesSUCCESS));
+            env.close();
+
+            auto const vaultSle = env.le(vaultKeylet);
+            if (!BEAST_EXPECT(vaultSle))
+                return;
+            env.memoize(Account("vault", vaultSle->at(sfAccount)));
+            PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID));
+
+            // Authorize bob for share MPT so he can receive escrowed shares
+            auto const shareMPTID = vaultSle->at(sfShareMPTID);
+            {
+                json::Value jv;
+                jv[jss::Account] = bob.human();
+                jv[sfMPTokenIssuanceID] = to_string(shareMPTID);
+                jv[jss::TransactionType] = jss::MPTokenAuthorize;
+                env(jv, Ter(tesSUCCESS));
+                env.close();
+            }
+
+            // Escrow 60% of shares
+            auto const escrowAmount = shares(Number{6, vaultSle->at(sfScale) + 1});
+            env(escrow::create(depositor, bob, escrowAmount),
+                escrow::kCondition(escrow::kCb1),
+                escrow::kFinishTime(env.now() + 1s),
+                Fee(baseFee * 150),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Zero-amount clawback ("all") — should only recover assets
+            // corresponding to unlocked shares (40%)
+            env(vault.clawback({
+                    .issuer = issuer,
+                    .id = vaultKeylet.key,
+                    .holder = depositor,
+                }),
+                Ter(tesSUCCESS));
+            env.close();
+
+            {
+                auto const sle = env.le(vaultKeylet);
+                BEAST_EXPECT(sle != nullptr);
+                // Only 40 of 100 assets recovered (matching 40% unlocked shares)
+                BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(60).value());
+                BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value());
+
+                // Depositor's unlocked shares are now 0
+                auto const sharesAfter = env.balance(depositor, shares);
+                BEAST_EXPECT(sharesAfter == shares(0));
+            }
+        }
+    }
+
+    // The vault's pseudo-account issues the shares, so it never holds any, and naming it as Holder
+    // asks for a clawback that cannot move anything. Before the rule an implicit amount resolved to
+    // zero shares and ended in tecPRECISION_LOSS, while an explicit one debited the vault first and
+    // was caught by the invariant that shares must move.
+    void
+    testClawbackPseudoAccountHolder()
+    {
+        using namespace test::jtx;
+
+        auto const runScenario = [this](FeatureBitset features, std::string const& prefix) {
+            bool const guarded = features[fixCleanup3_4_0];
+            Env env{*this, features};
+
+            Account const owner{"owner"};
+            Account const depositor{"depositor"};
+            Account const issuer{"issuer"};
+
+            env.fund(XRP(1'000), owner, depositor, issuer);
+            env.close();
+
+            env(fset(issuer, asfAllowTrustLineClawback));
+            env.close();
+
+            PrettyAsset const asset = issuer["IOU"];
+            env.trust(asset(1'000), owner);
+            env.trust(asset(1'000), depositor);
+            env(pay(issuer, depositor, asset(200)));
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            auto const vaultSle = env.le(keylet);
+            if (!BEAST_EXPECT(vaultSle))
+                return;
+            Account const pseudo{"vault pseudo-account", vaultSle->at(sfAccount)};
+            env.memoize(pseudo);
+
+            env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}));
+            env.close();
+
+            auto const assetsBefore = [&]() -> Number {
+                auto const sle = env.le(keylet);
+                if (!BEAST_EXPECT(sle))
+                    return Number{};
+                return sle->at(sfAssetsTotal);
+            }();
+
+            {
+                testcase("VaultClawback - " + prefix + " pseudo-account holder, implicit amount");
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = keylet.key,
+                        .holder = pseudo,
+                    }),
+                    Ter(guarded ? TER{tecPSEUDO_ACCOUNT} : TER{tecPRECISION_LOSS}));
+                env.close();
+            }
+
+            {
+                testcase("VaultClawback - " + prefix + " pseudo-account holder, explicit amount");
+                env(vault.clawback({
+                        .issuer = issuer,
+                        .id = keylet.key,
+                        .holder = pseudo,
+                        .amount = asset(10).value(),
+                    }),
+                    Ter(guarded ? TER{tecPSEUDO_ACCOUNT} : TER{tecINVARIANT_FAILED}));
+                env.close();
+            }
+
+            // Neither attempt may touch the vault, whichever way it was refused.
+            auto const sleAfter = env.le(keylet);
+            BEAST_EXPECT(sleAfter && sleAfter->at(sfAssetsTotal) == assetsBefore);
+        };
+
+        runScenario(all_, "post-rule");
+        runScenario(all_ - fixCleanup3_4_0, "pre-rule");
+    }
+
+public:
+    void
+    run() override
+    {
+        testVaultClawbackBurnShares();
+        testVaultClawbackAssets();
+        testClawbackPseudoAccountHolder();
+        testVaultEscrowedMPT();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE_PRIO(VaultClawback, app, xrpl, 1);
+
+}  // namespace xrpl
diff --git a/src/test/app/vault/VaultClosedEnded_test.cpp b/src/test/app/vault/VaultClosedEnded_test.cpp
new file mode 100644
index 0000000000..5ed242f8a4
--- /dev/null
+++ b/src/test/app/vault/VaultClosedEnded_test.cpp
@@ -0,0 +1,1009 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class VaultClosedEnded_test : public VaultTestBase
+{
+private:
+    // VaultCreate malformation and happy paths for closed-ended vaults, plus the
+    // featureLendingProtocolV1_1 gate.
+    void
+    testVaultCreateClosedEnded()
+    {
+        testcase("closed-ended VaultCreate");
+        using namespace test::jtx;
+
+        auto const withEnv = [this](FeatureBitset features, auto&& body) {
+            Env env{*this, features};
+            Account const owner{"owner"};
+            env.fund(XRP(1000), owner);
+            env.close();
+            Vault vault{env};
+            body(env, owner, vault);
+        };
+
+        Asset const asset = xrpIssue();
+        auto const minPeriod = kMinInvestmentPeriod;
+        auto const maxPeriod = kMaxInvestmentPeriod;
+        auto const closedEnded = std::to_underlying(VaultKind::ClosedEnded);
+
+        // Gate: the three new fields require featureLendingProtocolV1_1.
+        withEnv(
+            testableAmendments() - featureLendingProtocolV1_1,
+            [&](Env& env, Account const& owner, Vault& vault) {
+                auto const sub = env.now().time_since_epoch().count() + 60;
+                auto [tx, keylet] = vault.create(
+                    {.owner = owner,
+                     .asset = asset,
+                     .vaultKind = closedEnded,
+                     .subscriptionDate = sub,
+                     .redemptionDate = sub + minPeriod});
+                env(tx, Ter{temDISABLED});
+            });
+
+        /*
+         * Valid closed-ended creation with a comfortably interior gap (well above
+         * kMinInvestmentPeriod and well below kMaxInvestmentPeriod).
+         */
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto const red = sub + 86400;
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .subscriptionDate = sub,
+                 .redemptionDate = red});
+            env(tx);
+            env.close();
+            auto const sle = env.le(keylet);
+            if (BEAST_EXPECT(sle))
+            {
+                BEAST_EXPECT(sle->at(sfVaultKind) == closedEnded);
+                BEAST_EXPECT(sle->at(sfSubscriptionDate) == sub);
+                BEAST_EXPECT(sle->at(sfRedemptionDate) == red);
+            }
+        });
+
+        // ClosedEnded missing one of SubscriptionDate / RedemptionDate => temMALFORMED.
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .redemptionDate = sub + minPeriod});
+            env(tx, Ter{temMALFORMED});
+        });
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .subscriptionDate = sub});
+            env(tx, Ter{temMALFORMED});
+        });
+
+        /*
+         * SubscriptionDate not strictly after parent close time (preclaim, state-dependent -
+         * returns tecEXPIRED). This is the only reachable path to tecEXPIRED in VaultCreate; see
+         * the note below the next case. Note: there is no separate "expired RedemptionDate" test
+         * case here. preflight enforces red >= sub + kMinInvestmentPeriod, so any past
+         * RedemptionDate implies a strictly-earlier, equally-past SubscriptionDate; the
+         * SubscriptionDate check above short-circuits first. The RedemptionDate arm of the
+         * hasExpired check in VaultCreate::preclaim is defensive and unreachable as the sole cause
+         * of tecEXPIRED.
+         */
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const nowSec = env.now().time_since_epoch().count();
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .subscriptionDate = nowSec,
+                 .redemptionDate = nowSec + minPeriod});
+            env(tx, Ter{tecEXPIRED});
+        });
+
+        /*
+         * Gap smaller than kMinInvestmentPeriod => temMALFORMED. Includes the SubscriptionDate >=
+         * RedemptionDate degenerate cases: the red == sub boundary and the strictly-reversed red <
+         * sub case, the latter yielding a negative signed int64 gap that is caught by the
+         * sub-minimum branch of the gap check.
+         */
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .subscriptionDate = sub,
+                 .redemptionDate = sub + minPeriod - 1});
+            env(tx, Ter{temMALFORMED});
+        });
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .subscriptionDate = sub,
+                 .redemptionDate = sub});
+            env(tx, Ter{temMALFORMED});
+        });
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .subscriptionDate = sub,
+                 .redemptionDate = sub - 1});
+            env(tx, Ter{temMALFORMED});
+        });
+
+        // Gap equal to MAX_INVESTMENT_PERIOD => temMALFORMED (bound is half-open on the right).
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .subscriptionDate = sub,
+                 .redemptionDate = sub + maxPeriod});
+            env(tx, Ter{temMALFORMED});
+        });
+
+        // Gap strictly greater than MAX_INVESTMENT_PERIOD => temMALFORMED. Same code path as
+        // gap == MAX_INVESTMENT_PERIOD above, but covers the "gap >= MAX" bullet fully.
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .subscriptionDate = sub,
+                 .redemptionDate = sub + maxPeriod + 1});
+            env(tx, Ter{temMALFORMED});
+        });
+
+        // Happy path: gap exactly equal to kMinInvestmentPeriod is accepted (lower bound is
+        // inclusive). A min-gap vault can originate a minimum-interval loan; see
+        // LoanSet_test::testLoanSetClosedEnded.
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto const red = sub + minPeriod;
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .subscriptionDate = sub,
+                 .redemptionDate = red});
+            env(tx);
+            env.close();
+            auto const sle = env.le(keylet);
+            if (BEAST_EXPECT(sle))
+            {
+                BEAST_EXPECT(sle->at(sfRedemptionDate) == red);
+            }
+        });
+
+        // Happy path: gap one second less than MAX_INVESTMENT_PERIOD is
+        // accepted (upper bound is exclusive).
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto const red = sub + maxPeriod - 1;
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .subscriptionDate = sub,
+                 .redemptionDate = red});
+            env(tx);
+            env.close();
+            auto const sle = env.le(keylet);
+            if (BEAST_EXPECT(sle))
+            {
+                BEAST_EXPECT(sle->at(sfRedemptionDate) == red);
+            }
+        });
+
+        // OpenEnded (absent/0) with SubscriptionDate or RedemptionDate present
+        // => temMALFORMED.
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto [tx, keylet] =
+                vault.create({.owner = owner, .asset = asset, .subscriptionDate = sub});
+            env(tx, Ter{temMALFORMED});
+        });
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto const sub = env.now().time_since_epoch().count() + 60;
+            auto [tx, keylet] =
+                vault.create({.owner = owner, .asset = asset, .redemptionDate = sub + minPeriod});
+            env(tx, Ter{temMALFORMED});
+        });
+
+        // Unrecognised VaultKind => temMALFORMED.
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = static_cast(closedEnded + 1)});
+            env(tx, Ter{temMALFORMED});
+        });
+
+        // Happy path: open-ended vault (no new fields present) is unaffected.
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+            auto const sle = env.le(keylet);
+            if (BEAST_EXPECT(sle))
+            {
+                BEAST_EXPECT(!sle->isFieldPresent(sfVaultKind));
+                BEAST_EXPECT(!sle->isFieldPresent(sfSubscriptionDate));
+                BEAST_EXPECT(!sle->isFieldPresent(sfRedemptionDate));
+            }
+        });
+
+        // Happy path: explicit `VaultKind = 0` (OpenEnded) behaves the same
+        // as absent. Per spec, absent and OpenEnded are equivalent.
+        withEnv(testableAmendments(), [&](Env& env, Account const& owner, Vault& vault) {
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = std::to_underlying(VaultKind::OpenEnded)});
+            env(tx);
+            env.close();
+            auto const sle = env.le(keylet);
+            if (BEAST_EXPECT(sle))
+            {
+                // OpenEnded is sfVaultKind's default; SoeDefault fields
+                // aren't serialized when they hold the default value.
+                BEAST_EXPECT(!sle->isFieldPresent(sfVaultKind));
+                BEAST_EXPECT(!sle->isFieldPresent(sfSubscriptionDate));
+                BEAST_EXPECT(!sle->isFieldPresent(sfRedemptionDate));
+            }
+        });
+    }
+
+    // SubscriptionDate boundary cases at the top of the UINT32 range.
+    // (1) The largest legal sub picks red = UINT32_MAX exactly, which hits
+    // the inclusive lower bound of the kMinInvestmentPeriod gap check.
+    // (2) sub = UINT32_MAX must be rejected: sub + kMinInvestmentPeriod is
+    // unrepresentable as the tx's UINT32 sfRedemptionDate, so no red value
+    // can satisfy the gap check.
+    void
+    testVaultCreateSubscriptionDateBoundary()
+    {
+        testcase("closed-ended VaultCreate SubscriptionDate near UINT32_MAX");
+        using namespace test::jtx;
+
+        auto const closedEnded = std::to_underlying(VaultKind::ClosedEnded);
+        Asset const asset = xrpIssue();
+
+        {
+            Env env{*this, testableAmendments()};
+            Account const owner{"owner"};
+            env.fund(XRP(1000), owner);
+            env.close();
+
+            Vault const vault{env};
+            auto const sub = std::numeric_limits::max() - kMinInvestmentPeriod;
+            auto const red = std::numeric_limits::max();
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .subscriptionDate = sub,
+                 .redemptionDate = red});
+            env(tx);
+            env.close();
+            auto const sle = env.le(keylet);
+            if (BEAST_EXPECT(sle))
+            {
+                BEAST_EXPECT(sle->at(sfSubscriptionDate) == sub);
+                BEAST_EXPECT(sle->at(sfRedemptionDate) == red);
+            }
+        }
+
+        // sub = UINT32_MAX: no legal red exists because sub + kMinInvestmentPeriod
+        // wraps in a UINT32. Every candidate red must fall to temMALFORMED via
+        // the gap check in preflight.
+        auto const rejectAtMax = [&, this](std::uint32_t red) {
+            Env env{*this, testableAmendments()};
+            Account const owner{"owner"};
+            env.fund(XRP(1000), owner);
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create(
+                {.owner = owner,
+                 .asset = asset,
+                 .vaultKind = closedEnded,
+                 .subscriptionDate = std::numeric_limits::max(),
+                 .redemptionDate = red});
+            env(tx, Ter{temMALFORMED});
+        };
+        rejectAtMax(std::numeric_limits::max());
+        rejectAtMax(0u);
+        rejectAtMax(kMinInvestmentPeriod - 1u);
+    }
+
+    // Phase derivation across the SubscriptionDate / RedemptionDate boundaries, including the now
+    // == SubscriptionDate case (which must still resolve to Subscription).
+    void
+    testVaultPhaseDerivation()
+    {
+        testcase("closed-ended phase derivation");
+        using namespace test::jtx;
+
+        Env env{*this, testableAmendments()};
+        Account const owner{"owner"};
+        Account const depositor{"depositor"};
+        env.fund(XRP(1000), owner, depositor);
+        env.close();
+
+        Asset const asset = xrpIssue();
+        auto const [vault, keylet, sub, red] =
+            makeClosedEndedVault(env, owner, asset, 60u, kMinInvestmentPeriod);
+
+        // Pre-seed shares during Subscription so the depositor has capital to
+        // withdraw at the Redemption boundary below.
+        env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = XRP(10).value()}));
+        env.close();
+
+        auto const deposit =
+            [&](TER expected, std::source_location const& loc = std::source_location::current()) {
+                env(
+                    WithSourceLocation{
+                        vault.deposit(
+                            {.depositor = depositor, .id = keylet.key, .amount = XRP(1).value()}),
+                        loc},
+                    Ter{expected});
+            };
+        auto const withdraw =
+            [&](TER expected, std::source_location const& loc = std::source_location::current()) {
+                env(
+                    WithSourceLocation{
+                        vault.withdraw(
+                            {.depositor = depositor, .id = keylet.key, .amount = XRP(1).value()}),
+                        loc},
+                    Ter{expected});
+            };
+
+        auto const runTest = [&](TER expectedDeposit,
+                                 TER expectedWithdraw,
+                                 std::source_location const& loc =
+                                     std::source_location::current()) {
+            deposit(expectedDeposit, loc);
+            withdraw(expectedWithdraw, loc);
+        };
+
+        // Assert both deposit and withdraw return codes at each point so the
+        // active phase is uniquely identified:
+        //   Subscription: deposit tesSUCCESS, withdraw tesSUCCESS
+        //   Investment:   deposit tecEXPIRED, withdraw tecTOO_SOON
+        //   Redemption:   deposit tecEXPIRED, withdraw tesSUCCESS
+
+        // Ledger time comfortably before SubscriptionDate: Subscription.
+        runTest(tesSUCCESS, tesSUCCESS);
+
+        // Boundary: parent close time exactly at SubscriptionDate must still
+        // be Subscription.
+        closeToTime(env, tp{d{sub}});
+        runTest(tesSUCCESS, tesSUCCESS);
+
+        // One second past SubscriptionDate: Investment.
+        closeToTime(env, tp{d{sub}} + getLedgerTimeResolution(env));
+        runTest(tecEXPIRED, tecTOO_SOON);
+
+        // Any point strictly before RedemptionDate remains Investment.
+        closeToTime(env, tp{d{red}} - getLedgerTimeResolution(env));
+        runTest(tecEXPIRED, tecTOO_SOON);
+
+        // Boundary: parent close time == RedemptionDate is Redemption (per
+        // spec table: now >= RedemptionDate). Deposits are rejected but
+        // withdrawals succeed.
+        closeToTime(env, tp{d{red}});
+        runTest(tecEXPIRED, tesSUCCESS);
+        env.close();
+    }
+
+    // Open-ended vaults are always in VaultPhase::NoPhase, regardless of the ledger clock or any
+    // dates present on the vault.
+    void
+    testVaultPhaseDerivationOpenEnded()
+    {
+        testcase("open-ended phase derivation");
+        using namespace test::jtx;
+
+        Env env{*this, testableAmendments()};
+        Account const owner{"owner"};
+        env.fund(XRP(1000), owner);
+        env.close();
+
+        Asset const asset = xrpIssue();
+        Vault const vault{env};
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+        env(tx);
+        env.close();
+
+        auto const checkPhaseAt = [&](NetClock::time_point at) {
+            closeToTime(env, at);
+            auto const sle = env.le(keylet);
+            if (!BEAST_EXPECT(sle))
+                return;
+            BEAST_EXPECT(getVaultPhase(*env.current(), sle) == VaultPhase::NoPhase);
+        };
+
+        // Advance the clock through a wide range of ledger times: an open-ended vault's phase
+        // must be NoPhase at every one of them, because the derivation short-circuits on
+        // VaultKind::OpenEnded before it looks at any dates.
+        auto const ledgerTime = tp{d{30}} + env.closed()->header().closeTimeResolution;
+        checkPhaseAt(ledgerTime);
+        checkPhaseAt(ledgerTime + std::chrono::seconds{kMinInvestmentPeriod});
+        checkPhaseAt(
+            ledgerTime + std::chrono::seconds{kMaxInvestmentPeriod} -
+            env.closed()->header().closeTimeResolution);
+    }
+
+    // VaultDeposit is allowed only during Subscription (or NoPhase). Rejected during Investment and
+    // Redemption.
+    void
+    testVaultDepositClosedEnded()
+    {
+        testcase("closed-ended VaultDeposit phase gating");
+        using namespace test::jtx;
+
+        Env env{*this, testableAmendments()};
+        Account const owner{"owner"};
+        Account const depositor{"depositor"};
+        env.fund(XRP(1000), owner, depositor);
+        env.close();
+
+        Asset const asset = xrpIssue();
+        auto const [vault, keylet, sub, red] =
+            makeClosedEndedVault(env, owner, asset, 60u, kMinInvestmentPeriod);
+
+        auto const deposit =
+            [&](TER expected, std::source_location const& loc = std::source_location::current()) {
+                env(
+                    WithSourceLocation{
+                        vault.deposit(
+                            {.depositor = depositor, .id = keylet.key, .amount = XRP(1).value()}),
+                        loc},
+                    Ter{expected});
+                env.close();
+            };
+
+        // Subscription: allowed.
+        deposit(tesSUCCESS);
+
+        // Investment: rejected.
+        env.close(tp{d{sub + 1}});
+        deposit(tecEXPIRED);
+
+        // Redemption: rejected.
+        env.close(tp{d{red}});
+        deposit(tecEXPIRED);
+    }
+
+    // VaultWithdraw is allowed in Subscription and Redemption; rejected in Investment. The
+    // AssetsAvailable cap continues to apply and is exercised in Redemption against a vault with
+    // capital deployed as an outstanding loan.
+    void
+    testVaultWithdrawClosedEnded()
+    {
+        testcase("closed-ended VaultWithdraw phase gating");
+        using namespace test::jtx;
+        using namespace loan_broker;
+        using namespace loan;
+
+        Env env{*this, testableAmendments()};
+        Account const owner{"owner"};
+        Account const depositor{"depositor"};
+        Account const borrower{"borrower"};
+        env.fund(XRP(10'000), owner, depositor, borrower);
+        env.close();
+
+        Asset const asset = xrpIssue();
+        // Widen the Investment window so a single-payment loan (min payment
+        // interval 60s plus kLoanRedemptionBuffer) fits before RedemptionDate.
+        auto const [vault, keylet, sub, red] =
+            makeClosedEndedVault(env, owner, asset, 60u, kMinInvestmentPeriod + 3600u);
+
+        // Deposit XRP(100) in Subscription so the depositor's shares are
+        // worth XRP(100). The vault holds XRP(100) with
+        // AssetsAvailable == AssetsTotal.
+        env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = XRP(100).value()}));
+        env.close();
+
+        // Create a loan broker backed by this vault. LoanBrokerSet has no
+        // phase gate, so this is fine to do in Subscription.
+        auto const brokerKeylet =
+            keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+        env(loan_broker::set(owner, keylet.key));
+        env.close();
+
+        auto const withdraw = [&](STAmount const& amount,
+                                  TER expected,
+                                  std::source_location const& loc =
+                                      std::source_location::current()) {
+            env(
+                WithSourceLocation{
+                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = amount}),
+                    loc},
+                Ter{expected});
+            env.close();
+        };
+
+        // Subscription: allowed (LP cancel).
+        withdraw(XRP(1).value(), tesSUCCESS);
+
+        // Investment: rejected.
+        closeToTime(env, tp{d{sub}} + getLedgerTimeResolution(env));
+        withdraw(XRP(1).value(), tecTOO_SOON);
+
+        // Deploy capital: borrower takes a loan of XRP(60) against the
+        // vault, dropping AssetsAvailable to ~XRP(39) while AssetsTotal
+        // remains ~XRP(99).
+        env(loan::set(borrower, brokerKeylet.key, XRP(60).value()),
+            loan::kInterestRate(TenthBips32(0)),
+            kGracePeriod(60),
+            kPaymentInterval(60),
+            kPaymentTotal(1),
+            Sig(sfCounterpartySignature, owner),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+
+        // Redemption: withdrawals are allowed but subject to the AssetsAvailable cap. A small
+        // withdrawal within AssetsAvailable succeeds. A withdrawal within the depositor's share
+        // value but exceeding the vault's liquid balance fails with tecINSUFFICIENT_FUNDS from the
+        // vault-shortage guard (not the insufficient-shares guard).
+        closeToTime(env, tp{d{red}});
+        withdraw(XRP(10).value(), tesSUCCESS);
+        withdraw(XRP(80).value(), tecINSUFFICIENT_FUNDS);
+    }
+
+    // End-to-end lifecycle of a closed-ended vault (Subscription → Investment → Redemption) with
+    // multiple depositors and a real loan originated through the Investment leg. Exercises every
+    // phase transition and verifies the expected deposit, withdrawal, and lending behaviour in each
+    // phase.
+    void
+    testVaultClosedEndedLifecycle()
+    {
+        testcase("closed-ended vault lifecycle (subscribe → invest → redeem)");
+        using namespace test::jtx;
+        using namespace loan_broker;
+        using namespace loan;
+
+        Env env{*this, testableAmendments()};
+        Account const owner{"owner"};
+        Account const alice{"alice"};
+        Account const bob{"bob"};
+        Account const borrower{"borrower"};
+        env.fund(XRP(10'000), owner, alice, bob, borrower);
+        env.close();
+
+        auto const closedEnded = std::to_underlying(VaultKind::ClosedEnded);
+        Asset const asset = xrpIssue();
+        // Widen the Investment window so a single-payment loan (min payment interval
+        // 60s plus kLoanRedemptionBuffer) fits before RedemptionDate with headroom.
+        auto const [vault, keylet, sub, red] =
+            makeClosedEndedVault(env, owner, asset, 300u, kMinInvestmentPeriod + 3600u);
+
+        auto const sleCreate = env.le(keylet);
+        BEAST_EXPECT(sleCreate);
+        MPTIssue const shares{sleCreate->at(sfShareMPTID)};
+
+        auto const balancesEq = [&](STAmount const& available, STAmount const& total) {
+            auto const sle = env.le(keylet);
+            BEAST_EXPECT(sle->at(sfAssetsAvailable) == available);
+            BEAST_EXPECT(sle->at(sfAssetsTotal) == total);
+        };
+        auto const availableEq = [&](STAmount const& expected) { balancesEq(expected, expected); };
+
+        // env.balance(account, mptIssue) name-resolves the issuer via Env::lookup, but the share
+        // issuer is the vault's pseudo-account and is never registered with the jtx Env. Read the
+        // MPToken SLE directly to avoid the lookup.
+        auto const sharesEq = [&](Account const& holder, std::uint64_t expected) {
+            auto const sle = env.le(keylet::mptoken(shares.getMptID(), holder.id()));
+            std::uint64_t const actual = sle ? sle->getFieldU64(sfMPTAmount) : 0u;
+            BEAST_EXPECT(actual == expected);
+        };
+
+        // ---- Subscription phase ----
+        // A legitimate VaultSet succeeds (positive control for 3.7).
+        {
+            auto tx = vault.set({.owner = owner, .id = keylet.key});
+            tx[sfData] = "AA";
+            env(tx);
+            env.close();
+        }
+
+        // alice deposits 100 XRP.
+        env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(100).value()}));
+        env.close();
+        sharesEq(alice, 100'000'000);
+        availableEq(XRP(100).value());
+
+        // bob deposits 200 XRP.
+        env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = XRP(200).value()}));
+        env.close();
+        sharesEq(bob, 200'000'000);
+        availableEq(XRP(300).value());
+
+        // alice cancels 25 XRP (LP cancel is permitted in Subscription).
+        env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(25).value()}));
+        env.close();
+        sharesEq(alice, 75'000'000);
+        availableEq(XRP(275).value());
+
+        // Create a loan broker backed by this vault. LoanBrokerSet has no phase gate, so it is
+        // fine to do in Subscription.
+        auto const brokerKeylet =
+            keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+        env(loan_broker::set(owner, keylet.key));
+        env.close();
+
+        // ---- Investment phase (now == sub + 1) ----
+        env.close(tp{d{sub + 1}});
+
+        // Deposits into a closed-ended vault past SubscriptionDate return tecEXPIRED.
+        env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(10).value()}),
+            Ter{tecEXPIRED});
+        env.close();
+        // Withdrawals from a closed-ended vault during the Investment phase return tecTOO_SOON.
+        env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(10).value()}),
+            Ter{tecTOO_SOON});
+        env.close();
+
+        // A real loan is originated during Investment (permitted only in this phase). Zero-interest
+        // one-payment schedule keeps AssetsTotal unchanged (both accrual and cash-basis
+        // accounting recognise no interest at origination); AssetsAvailable drops by the loan
+        // principal.
+        env(loan::set(borrower, brokerKeylet.key, XRP(60).value()),
+            loan::kInterestRate(TenthBips32(0)),
+            kGracePeriod(60),
+            kPaymentInterval(60),
+            kPaymentTotal(1),
+            Sig(sfCounterpartySignature, owner),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+        auto const sleBroker = env.le(keylet::loanBroker(brokerKeylet.key));
+        BEAST_EXPECT(sleBroker);
+        auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1u));
+        BEAST_EXPECT(env.le(loanKeylet));
+        balancesEq(XRP(215).value(), XRP(275).value());
+
+        // Non-immutable VaultSet still works in Investment (positive control).
+        {
+            auto tx = vault.set({.owner = owner, .id = keylet.key});
+            tx[sfData] = "BB";
+            env(tx);
+            env.close();
+        }
+
+        // Depositor share balances unchanged by the loan origination; only AssetsAvailable moved.
+        sharesEq(alice, 75'000'000);
+        sharesEq(bob, 200'000'000);
+
+        // ---- Redemption phase (now == red) ----
+        env.close(tp{d{red}});
+
+        // Deposits into a closed-ended vault past SubscriptionDate return tecEXPIRED, in both
+        // Investment and Redemption.
+        env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(10).value()}),
+            Ter{tecEXPIRED});
+        env.close();
+
+        // alice redeems her remaining 75 XRP (fits within AssetsAvailable = 215).
+        env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(75).value()}));
+        env.close();
+        sharesEq(alice, 0);
+        balancesEq(XRP(140).value(), XRP(200).value());
+
+        // bob has 200 XRP-worth of shares but only 140 XRP is available (the remaining 60 XRP
+        // sits in the outstanding loan). A full 200 XRP withdrawal fails against the
+        // AssetsAvailable cap; bob redeems 140 XRP instead and is left holding 60M shares backed
+        // by the loan receivable — the realistic outcome when capital is still deployed at
+        // Redemption.
+        env(vault.withdraw({.depositor = bob, .id = keylet.key, .amount = XRP(200).value()}),
+            Ter{tecINSUFFICIENT_FUNDS});
+        env.close();
+        env(vault.withdraw({.depositor = bob, .id = keylet.key, .amount = XRP(140).value()}));
+        env.close();
+        sharesEq(bob, 60'000'000);
+        balancesEq(XRP(0).value(), XRP(60).value());
+
+        // Defensive spot-check that the three immutable fields have not changed across the entire
+        // lifecycle. Direct immutability coverage lives with the invariant tests.
+        auto const sleFinal = env.le(keylet);
+        if (BEAST_EXPECT(sleFinal))
+        {
+            BEAST_EXPECT(sleFinal->at(sfVaultKind) == closedEnded);
+            BEAST_EXPECT(sleFinal->at(sfSubscriptionDate) == sub);
+            BEAST_EXPECT(sleFinal->at(sfRedemptionDate) == red);
+        }
+    }
+
+    // A loan whose payment is made after the Investment phase has ended
+    // (well past its next-due-date and grace period, into Redemption) must
+    // still be repayable. The vault phase must not gate LoanPay.
+    void
+    testVaultLoanLatePaymentAfterInvestment()
+    {
+        testcase("closed-ended vault: late loan payment during Redemption succeeds");
+        using namespace test::jtx;
+        using namespace loan_broker;
+        using namespace loan;
+
+        Env env{*this, testableAmendments()};
+        Account const owner{"owner"};
+        Account const alice{"alice"};
+        Account const borrower{"borrower"};
+        env.fund(XRP(10'000), owner, alice, borrower);
+        env.close();
+
+        Asset const asset = xrpIssue();
+        auto const [vault, keylet, sub, red] =
+            makeClosedEndedVault(env, owner, asset, 300u, kMinInvestmentPeriod + 3600u);
+
+        env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(100).value()}));
+        env.close();
+
+        auto const brokerKeylet =
+            keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+        env(loan_broker::set(owner, keylet.key));
+        env.close();
+
+        // Investment phase: originate a zero-interest, single-payment loan
+        // with a 300s payment interval and 60s grace. The payment is due
+        // shortly after origination and well before RedemptionDate.
+        env.close(tp{d{sub + 1}});
+        env(loan::set(borrower, brokerKeylet.key, XRP(60).value()),
+            loan::kInterestRate(TenthBips32(0)),
+            kGracePeriod(60),
+            kPaymentInterval(300),
+            kPaymentTotal(1),
+            Sig(sfCounterpartySignature, owner),
+            Fee(env.current()->fees().base * 2));
+        env.close();
+        auto const loanKeylet = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1u));
+        BEAST_EXPECT(env.le(loanKeylet));
+
+        // Advance to Redemption. The payment is now past its due date and
+        // grace, and the vault is no longer in Investment.
+        closeToTime(env, tp{d{red}});
+
+        env(loan::pay(borrower, loanKeylet.key, XRP(60).value(), tfLoanLatePayment));
+        env.close();
+
+        // Loan principal returned to the vault; assetsAvailable == assetsTotal.
+        auto const sleAfter = env.le(keylet);
+        if (BEAST_EXPECT(sleAfter))
+        {
+            BEAST_EXPECT(sleAfter->at(sfAssetsAvailable) == sleAfter->at(sfAssetsTotal));
+            BEAST_EXPECT(sleAfter->at(sfAssetsAvailable) == XRP(100).value());
+        }
+
+        env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(100).value()}));
+        env.close();
+    }
+
+    // Two concurrent loans against the same closed-ended vault in Investment
+    // must coexist: both loan SLEs are created, AssetsAvailable reflects the
+    // sum of the two outstanding principals, and each can be repaid
+    // independently.
+    void
+    testVaultClosedEndedMultipleLoans()
+    {
+        testcase("closed-ended vault: multiple concurrent loans in Investment");
+        using namespace test::jtx;
+        using namespace loan_broker;
+        using namespace loan;
+
+        Env env{*this, testableAmendments()};
+        Account const owner{"owner"};
+        Account const alice{"alice"};
+        Account const bob{"bob"};
+        Account const borrower1{"borrower1"};
+        Account const borrower2{"borrower2"};
+        env.fund(XRP(10'000), owner, alice, bob, borrower1, borrower2);
+        env.close();
+
+        Asset const asset = xrpIssue();
+        auto const [vault, keylet, sub, red] =
+            makeClosedEndedVault(env, owner, asset, 300u, kMinInvestmentPeriod + 3600u);
+
+        env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(100).value()}));
+        env.close();
+        env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = XRP(100).value()}));
+        env.close();
+
+        auto const brokerKeylet =
+            keylet::loanBroker(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+        env(loan_broker::set(owner, keylet.key));
+        env.close();
+
+        env.close(tp{d{sub + 1}});
+
+        auto const originate = [&](Account const& b, STAmount const& principal) {
+            env(loan::set(b, brokerKeylet.key, principal),
+                loan::kInterestRate(TenthBips32(0)),
+                kGracePeriod(60),
+                kPaymentInterval(300),
+                kPaymentTotal(1),
+                Sig(sfCounterpartySignature, owner),
+                Fee(env.current()->fees().base * 2));
+            env.close();
+        };
+        originate(borrower1, XRP(50).value());
+        originate(borrower2, XRP(70).value());
+
+        auto const loan1 = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(1u));
+        auto const loan2 = keylet::loan(brokerKeylet.key, SeqProxy::rawSequence(2u));
+        BEAST_EXPECT(env.le(loan1));
+        BEAST_EXPECT(env.le(loan2));
+
+        // Zero-interest at origination: AssetsTotal unchanged, AssetsAvailable
+        // drops by the sum of the two loan principals.
+        {
+            auto const sle = env.le(keylet);
+            if (BEAST_EXPECT(sle))
+            {
+                BEAST_EXPECT(sle->at(sfAssetsTotal) == XRP(200).value());
+                BEAST_EXPECT(sle->at(sfAssetsAvailable) == XRP(80).value());
+            }
+        }
+
+        // Repay the first loan; the second remains outstanding.
+        env(loan::pay(borrower1, loan1.key, XRP(50).value()));
+        env.close();
+        {
+            auto const sle = env.le(keylet);
+            if (BEAST_EXPECT(sle))
+            {
+                BEAST_EXPECT(sle->at(sfAssetsTotal) == XRP(200).value());
+                BEAST_EXPECT(sle->at(sfAssetsAvailable) == XRP(130).value());
+            }
+        }
+
+        // Repay the second loan; vault is fully liquid again.
+        env(loan::pay(borrower2, loan2.key, XRP(70).value()));
+        env.close();
+        {
+            auto const sle = env.le(keylet);
+            if (BEAST_EXPECT(sle))
+            {
+                BEAST_EXPECT(sle->at(sfAssetsAvailable) == sle->at(sfAssetsTotal));
+                BEAST_EXPECT(sle->at(sfAssetsAvailable) == XRP(200).value());
+            }
+        }
+
+        // Redemption: both depositors withdraw in full.
+        env.close(tp{d{red}});
+        env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(100).value()}));
+        env.close();
+        env(vault.withdraw({.depositor = bob, .id = keylet.key, .amount = XRP(100).value()}));
+        env.close();
+    }
+
+    // VaultClawback has no phase gate: an issuer must be able to reclaim
+    // asset from a depositor in Subscription, Investment and Redemption
+    // alike. Uses an IOU with asfAllowTrustLineClawback so the issuer path
+    // is exercised (XRP clawback with an explicit amount is temMALFORMED).
+    void
+    testVaultClawbackClosedEndedPhases()
+    {
+        testcase("closed-ended vault: VaultClawback succeeds in each phase");
+        using namespace test::jtx;
+
+        Env env{*this, testableAmendments()};
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const alice{"alice"};
+        env.fund(XRP(10'000), issuer, owner, alice);
+        env.close();
+
+        env(fset(issuer, asfAllowTrustLineClawback));
+        env.close();
+
+        PrettyAsset const iou = issuer["IOU"];
+        env.trust(iou(10'000), alice);
+        env(pay(issuer, alice, iou(1'000)));
+        env.close();
+
+        auto const [vault, keylet, sub, red] =
+            makeClosedEndedVault(env, owner, iou, 300u, kMinInvestmentPeriod + 3600u);
+
+        env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = iou(300).value()}));
+        env.close();
+
+        auto const totalsEq = [&](STAmount const& expected) {
+            auto const sle = env.le(keylet);
+            if (BEAST_EXPECT(sle))
+                BEAST_EXPECT(sle->at(sfAssetsTotal) == expected);
+        };
+
+        // Subscription phase clawback.
+        env(vault.clawback(
+            {.issuer = issuer, .id = keylet.key, .holder = alice, .amount = iou(10).value()}));
+        env.close();
+        totalsEq(iou(290).value());
+
+        // Investment phase clawback.
+        env.close(tp{d{sub + 1}});
+        env(vault.clawback(
+            {.issuer = issuer, .id = keylet.key, .holder = alice, .amount = iou(10).value()}));
+        env.close();
+        totalsEq(iou(280).value());
+
+        // Redemption phase clawback.
+        env.close(tp{d{red}});
+        env(vault.clawback(
+            {.issuer = issuer, .id = keylet.key, .holder = alice, .amount = iou(10).value()}));
+        env.close();
+        totalsEq(iou(270).value());
+    }
+
+public:
+    void
+    run() override
+    {
+        testVaultCreateClosedEnded();
+        testVaultCreateSubscriptionDateBoundary();
+        testVaultPhaseDerivation();
+        testVaultPhaseDerivationOpenEnded();
+        testVaultDepositClosedEnded();
+        testVaultWithdrawClosedEnded();
+        testVaultClosedEndedLifecycle();
+        testVaultLoanLatePaymentAfterInvestment();
+        testVaultClosedEndedMultipleLoans();
+        testVaultClawbackClosedEndedPhases();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE_PRIO(VaultClosedEnded, app, xrpl, 1);
+
+}  // namespace xrpl
diff --git a/src/test/app/vault/VaultDomain_test.cpp b/src/test/app/vault/VaultDomain_test.cpp
new file mode 100644
index 0000000000..5e058a13a8
--- /dev/null
+++ b/src/test/app/vault/VaultDomain_test.cpp
@@ -0,0 +1,887 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class VaultDomain_test : public VaultTestBase
+{
+private:
+    void
+    testWithDomainCheck()
+    {
+        using namespace test::jtx;
+
+        testcase("private vault");
+
+        Env env{*this, testableAmendments()};
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const depositor{"depositor"};
+        Account const charlie{"charlie"};
+        Account const pdOwner{"pdOwner"};
+        Account const credIssuer1{"credIssuer1"};
+        Account const credIssuer2{"credIssuer2"};
+        std::string const credType = "credential";
+        Vault const vault{env};
+        env.fund(XRP(1000), issuer, owner, depositor, charlie, pdOwner, credIssuer1, credIssuer2);
+        env.close();
+        env(fset(issuer, asfAllowTrustLineClawback));
+        env.close();
+        env.require(Flags(issuer, asfAllowTrustLineClawback));
+
+        PrettyAsset const asset = issuer["IOU"];
+        env.trust(asset(1000), owner);
+        env(pay(issuer, owner, asset(500)));
+        env.trust(asset(1000), depositor);
+        env(pay(issuer, depositor, asset(500)));
+        env.trust(asset(1000), charlie);
+        env(pay(issuer, charlie, asset(5)));
+        env.close();
+
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
+        env(tx);
+        env.close();
+        BEAST_EXPECT(env.le(keylet));
+
+        {
+            testcase("private vault owner can deposit");
+            auto tx = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(50)});
+            env(tx);
+        }
+
+        {
+            testcase("private vault depositor not authorized yet");
+            auto tx =
+                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+            env(tx, Ter{tecNO_AUTH});
+        }
+
+        {
+            testcase("private vault cannot set non-existing domain");
+            auto tx = vault.set({.owner = owner, .id = keylet.key});
+            tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
+            env(tx, Ter{tecOBJECT_NOT_FOUND});
+        }
+
+        {
+            testcase("private vault set domainId");
+
+            {
+                pdomain::Credentials const credentials1{
+                    {.issuer = credIssuer1, .credType = credType}};
+
+                env(pdomain::setTx(pdOwner, credentials1));
+                auto const domainId1 = [&]() {
+                    auto tx = env.tx()->getJson(JsonOptions::Values::None);
+                    return pdomain::getNewDomain(env.meta());
+                }();
+
+                auto tx = vault.set({.owner = owner, .id = keylet.key});
+                tx[sfDomainID] = to_string(domainId1);
+                env(tx);
+                env.close();
+
+                // Update domain second time, should be harmless
+                env(tx);
+                env.close();
+            }
+
+            {
+                pdomain::Credentials const credentials{
+                    {.issuer = credIssuer1, .credType = credType},
+                    {.issuer = credIssuer2, .credType = credType}};
+
+                env(pdomain::setTx(pdOwner, credentials));
+                auto const domainId = [&]() {
+                    auto tx = env.tx()->getJson(JsonOptions::Values::None);
+                    return pdomain::getNewDomain(env.meta());
+                }();
+
+                auto tx = vault.set({.owner = owner, .id = keylet.key});
+                tx[sfDomainID] = to_string(domainId);
+                env(tx);
+                env.close();
+
+                // Should be idempotent
+                tx = vault.set({.owner = owner, .id = keylet.key});
+                tx[sfDomainID] = to_string(domainId);
+                env(tx);
+                env.close();
+            }
+        }
+
+        {
+            testcase("private vault depositor still not authorized");
+            auto tx =
+                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+            env(tx, Ter{tecNO_AUTH});
+            env.close();
+        }
+
+        auto const credKeylet = credentials::keylet(depositor, credIssuer1, credType);
+        {
+            testcase("private vault depositor now authorized");
+            env(credentials::create(depositor, credIssuer1, credType));
+            env(credentials::accept(depositor, credIssuer1, credType));
+            env(credentials::create(charlie, credIssuer1, credType));
+            // charlie's credential not accepted
+            env.close();
+            auto credSle = env.le(credKeylet);
+            BEAST_EXPECT(credSle != nullptr);
+
+            auto tx =
+                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+            env(tx);
+            env.close();
+
+            tx = vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(50)});
+            env(tx, Ter{tecNO_AUTH});
+            env.close();
+        }
+
+        {
+            testcase("private vault depositor lost authorization");
+            env(credentials::deleteCred(credIssuer1, depositor, credIssuer1, credType));
+            env(credentials::deleteCred(credIssuer1, charlie, credIssuer1, credType));
+            env.close();
+            auto credSle = env.le(credKeylet);
+            BEAST_EXPECT(credSle == nullptr);
+
+            auto tx =
+                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+            env(tx, Ter{tecNO_AUTH});
+            env.close();
+        }
+
+        auto const shares = [&env, keylet = keylet, this]() -> Asset {
+            auto const vault = env.le(keylet);
+            BEAST_EXPECT(vault != nullptr);
+            return MPTIssue(vault->at(sfShareMPTID));
+        }();
+
+        {
+            testcase("private vault expired authorization");
+            uint32_t const closeTime =
+                env.current()->header().parentCloseTime.time_since_epoch().count();
+            {
+                auto tx0 = credentials::create(depositor, credIssuer2, credType);
+                tx0[sfExpiration] = closeTime + 20;
+                env(tx0);
+                tx0 = credentials::create(charlie, credIssuer2, credType);
+                tx0[sfExpiration] = closeTime + 20;
+                env(tx0);
+                env.close();
+
+                env(credentials::accept(depositor, credIssuer2, credType));
+                env(credentials::accept(charlie, credIssuer2, credType));
+                env.close();
+            }
+
+            {
+                auto tx1 =
+                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+                env(tx1);
+                env.close();
+
+                auto const tokenKeylet =
+                    keylet::mptoken(shares.get().getMptID(), depositor.id());
+                BEAST_EXPECT(env.le(tokenKeylet) != nullptr);
+            }
+
+            {
+                // time advance
+                env.close();
+                env.close();
+                env.close();
+
+                auto const credsKeylet = credentials::keylet(depositor, credIssuer2, credType);
+                BEAST_EXPECT(env.le(credsKeylet) != nullptr);
+
+                auto tx2 =
+                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1)});
+                env(tx2, Ter{tecEXPIRED});
+                env.close();
+
+                BEAST_EXPECT(env.le(credsKeylet) == nullptr);
+            }
+
+            {
+                auto const credsKeylet = credentials::keylet(charlie, credIssuer2, credType);
+                BEAST_EXPECT(env.le(credsKeylet) != nullptr);
+                auto const tokenKeylet =
+                    keylet::mptoken(shares.get().getMptID(), charlie.id());
+                BEAST_EXPECT(env.le(tokenKeylet) == nullptr);
+
+                auto tx3 =
+                    vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(2)});
+                env(tx3, Ter{tecEXPIRED});
+
+                env.close();
+                BEAST_EXPECT(env.le(credsKeylet) == nullptr);
+                BEAST_EXPECT(env.le(tokenKeylet) == nullptr);
+            }
+        }
+
+        {
+            testcase("private vault reset domainId");
+            auto tx = vault.set({.owner = owner, .id = keylet.key});
+            tx[sfDomainID] = "0";
+            env(tx);
+            env.close();
+
+            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+            env(tx, Ter{tecNO_AUTH});
+            env.close();
+
+            tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+            env(tx);
+            env.close();
+
+            tx = vault.clawback(
+                {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
+            env(tx);
+
+            tx = vault.clawback(
+                {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)});
+            env(tx);
+            env.close();
+
+            tx = vault.del({
+                .owner = owner,
+                .id = keylet.key,
+            });
+            env(tx);
+        }
+    }
+
+    void
+    testDomainLossAfterAcquisition()
+    {
+        using namespace test::jtx;
+
+        testcase("private vault share transfer after depositor loses domain");
+
+        // The "Private Vault - Access Control Rules" spec requires that a holder who
+        // loses Layer 2 (Permissioned Domain membership) after acquiring shares be
+        // blocked from sending them onward, by P2P transfer or DEX offer, the same
+        // way a brand-new never-authorized holder is blocked. Only withdrawal to
+        // self is meant to stay open.
+        //
+        // For a domain-gated share MPToken, requireAuth()'s escape hatch for
+        // holders who already have an MPToken (MPTokenHelpers.cpp) only applies to
+        // the classic explicit-issuer-authorization flag, which
+        // enforceMPTokenAuthorization documents as "meaningless" for
+        // domain-authorized holders and never sets. So a stale MPToken does not
+        // carry authorization forward once the account's domain credential is
+        // gone, and both actions below are correctly blocked.
+
+        Env env{*this, testableAmendments()};
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const depositor{"depositor"};
+        Account const bob{"bob"};
+        Account const pdOwner{"pdOwner"};
+        Account const credIssuer{"credIssuer"};
+        std::string const credType = "credential";
+        Vault const vault{env};
+        env.fund(XRP(1000), issuer, owner, depositor, bob, pdOwner, credIssuer);
+        env.close();
+
+        PrettyAsset const asset = issuer["IOU"];
+        env.trust(asset(1000), owner);
+        env(pay(issuer, owner, asset(500)));
+        env.trust(asset(1000), depositor);
+        env(pay(issuer, depositor, asset(500)));
+        env.trust(asset(1000), bob);
+        env(pay(issuer, bob, asset(500)));
+        env.close();
+
+        // Transferable shares (no tfVaultShareNonTransferable): sections 3.3/3.4 of
+        // the spec (DEX trading / P2P transfer) only apply to transferable shares.
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
+        env(tx);
+        env.close();
+
+        pdomain::Credentials const credentials{{.issuer = credIssuer, .credType = credType}};
+        env(pdomain::setTx(pdOwner, credentials));
+        auto const domainId = [&]() {
+            auto tx = env.tx()->getJson(JsonOptions::Values::None);
+            return pdomain::getNewDomain(env.meta());
+        }();
+        {
+            auto domainTx = vault.set({.owner = owner, .id = keylet.key});
+            domainTx[sfDomainID] = to_string(domainId);
+            env(domainTx);
+            env.close();
+        }
+
+        // Both depositor and bob acquire domain membership and deposit, so each
+        // ends up with an authorized share MPToken.
+        env(credentials::create(depositor, credIssuer, credType));
+        env(credentials::accept(depositor, credIssuer, credType));
+        env(credentials::create(bob, credIssuer, credType));
+        env(credentials::accept(bob, credIssuer, credType));
+        env.close();
+
+        env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}));
+        env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)}));
+        env.close();
+
+        auto const shares = [&env, keylet = keylet, this]() -> PrettyAsset {
+            auto const sle = env.le(keylet);
+            BEAST_EXPECT(sle != nullptr);
+            return MPTIssue(sle->at(sfShareMPTID));
+        }();
+
+        // Depositor loses Layer 2: their Permissioned Domain credential is revoked.
+        auto const credKeylet = credentials::keylet(depositor, credIssuer, credType);
+        env(credentials::deleteCred(credIssuer, depositor, credIssuer, credType));
+        env.close();
+        BEAST_EXPECT(env.le(credKeylet) == nullptr);
+
+        // Sanity check, mirrors testWithDomainCheck's "not authorized yet" case: a
+        // brand-new depositor with no MPToken yet is still correctly blocked. The
+        // gap below is specific to holders who already hold shares.
+        {
+            Account const charlie{"charlie"};
+            env.fund(XRP(1000), charlie);
+            env.close();
+            auto depTx =
+                vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(1)});
+            env(depTx, Ter{tecNO_AUTH});
+        }
+
+        // P2P transfer: spec section 3.4 requires this blocked once Layer 2 is
+        // lost, and it is.
+        env(pay(depositor, bob, shares(1)), Ter{tecNO_AUTH});
+        env.close();
+
+        // DEX/CLOB: spec section 3.3 requires the seller leg blocked the same way.
+        // The offer can't even be created: preclaim treats the seller as
+        // unfunded once their share balance reads as zero for auth purposes.
+        env(offer(depositor, XRP(1), shares(1)), Ter{tecUNFUNDED_OFFER});
+        env.close();
+        BEAST_EXPECT(expectOffers(env, depositor, 0));
+    }
+
+    void
+    testDomainCheckBuyerSideOffer()
+    {
+        using namespace test::jtx;
+
+        testcase("private vault share purchase via DEX requires buyer domain membership");
+
+        // The "Private Vault - Access Control Rules" spec requires the buyer leg
+        // of a DEX trade in private-vault shares to hold Layer 1 and Layer 2 as
+        // well, not just the seller.
+
+        Env env{*this, testableAmendments()};
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const bob{"bob"};
+        Account const charlie{"charlie"};
+        Account const pdOwner{"pdOwner"};
+        Account const credIssuer{"credIssuer"};
+        std::string const credType = "credential";
+        Vault const vault{env};
+        env.fund(XRP(1000), issuer, owner, bob, charlie, pdOwner, credIssuer);
+        env.close();
+
+        PrettyAsset const asset = issuer["IOU"];
+        env.trust(asset(1000), owner);
+        env(pay(issuer, owner, asset(500)));
+        env.trust(asset(1000), bob);
+        env(pay(issuer, bob, asset(500)));
+        env.close();
+
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
+        env(tx);
+        env.close();
+
+        pdomain::Credentials const credentials{{.issuer = credIssuer, .credType = credType}};
+        env(pdomain::setTx(pdOwner, credentials));
+        auto const domainId = [&]() {
+            auto tx = env.tx()->getJson(JsonOptions::Values::None);
+            return pdomain::getNewDomain(env.meta());
+        }();
+        {
+            auto domainTx = vault.set({.owner = owner, .id = keylet.key});
+            domainTx[sfDomainID] = to_string(domainId);
+            env(domainTx);
+            env.close();
+        }
+
+        // Only bob joins the domain and deposits; charlie never does.
+        env(credentials::create(bob, credIssuer, credType));
+        env(credentials::accept(bob, credIssuer, credType));
+        env.close();
+        env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(100)}));
+        env.close();
+
+        auto const shares = [&env, keylet = keylet, this]() -> PrettyAsset {
+            auto const sle = env.le(keylet);
+            BEAST_EXPECT(sle != nullptr);
+            return MPTIssue(sle->at(sfShareMPTID));
+        }();
+
+        // Bob (domain member, holds shares) rests a sell offer.
+        env(offer(bob, XRP(1), shares(1)));
+        env.close();
+        BEAST_EXPECT(expectOffers(env, bob, 1));
+
+        // Charlie never held the domain credential. Buying shares via a
+        // crossing offer must be blocked the same way a direct MPTokenAuthorize
+        // + pay attempt already is (see testWithDomainChecXRP's "cannot pay
+        // shares to 3rd party"): checkAcceptAsset() rejects the offer outright
+        // in preclaim, before any funding check is even reached.
+        env(offer(charlie, shares(1), XRP(1)), Ter{tecNO_AUTH});
+        env.close();
+        BEAST_EXPECT(expectOffers(env, bob, 1));
+        BEAST_EXPECT(expectOffers(env, charlie, 0));
+    }
+
+    void
+    testWithDomainChecXRP()
+    {
+        using namespace test::jtx;
+
+        testcase("private XRP vault");
+
+        Env env{*this, testableAmendments()};
+        Account const owner{"owner"};
+        Account const depositor{"depositor"};
+        Account const alice{"charlie"};
+        std::string const credType = "credential";
+        Vault const vault{env};
+        env.fund(XRP(100000), owner, depositor, alice);
+        env.close();
+
+        PrettyAsset const asset = xrpIssue();
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
+        env(tx);
+        env.close();
+
+        auto const [vaultAccount, issuanceId] =
+            [&env, keylet = keylet, this]() -> std::tuple {
+            auto const vault = env.le(keylet);
+            BEAST_EXPECT(vault != nullptr);
+            return {vault->at(sfAccount), vault->at(sfShareMPTID)};
+        }();
+        BEAST_EXPECT(env.le(keylet::account(vaultAccount)));
+        BEAST_EXPECT(env.le(keylet::mptokenIssuance(issuanceId)));
+        PrettyAsset const shares{issuanceId};
+
+        {
+            testcase("private XRP vault owner can deposit");
+            auto tx = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(50)});
+            env(tx);
+            env.close();
+        }
+
+        {
+            testcase("private XRP vault cannot pay shares to depositor yet");
+            env(pay(owner, depositor, shares(1)), Ter{tecNO_AUTH});
+        }
+
+        {
+            testcase("private XRP vault depositor not authorized yet");
+            auto tx =
+                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+            env(tx, Ter{tecNO_AUTH});
+        }
+
+        {
+            testcase("private XRP vault set DomainID");
+            pdomain::Credentials const credentials{{.issuer = owner, .credType = credType}};
+
+            env(pdomain::setTx(owner, credentials));
+            auto const domainId = [&]() {
+                auto tx = env.tx()->getJson(JsonOptions::Values::None);
+                return pdomain::getNewDomain(env.meta());
+            }();
+
+            auto tx = vault.set({.owner = owner, .id = keylet.key});
+            tx[sfDomainID] = to_string(domainId);
+            env(tx);
+            env.close();
+        }
+
+        auto const credKeylet = credentials::keylet(depositor, owner, credType);
+        {
+            testcase("private XRP vault depositor now authorized");
+            env(credentials::create(depositor, owner, credType));
+            env(credentials::accept(depositor, owner, credType));
+            env.close();
+
+            BEAST_EXPECT(env.le(credKeylet));
+            auto tx =
+                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+            env(tx);
+            env.close();
+        }
+
+        {
+            testcase("private XRP vault can pay shares to depositor");
+            env(pay(owner, depositor, shares(1)));
+        }
+
+        {
+            testcase("private XRP vault cannot pay shares to 3rd party");
+            json::Value jv;
+            jv[sfAccount] = alice.human();
+            jv[sfTransactionType] = jss::MPTokenAuthorize;
+            jv[sfMPTokenIssuanceID] = to_string(issuanceId);
+            env(jv);
+            env.close();
+
+            env(pay(owner, alice, shares(1)), Ter{tecNO_AUTH});
+        }
+    }
+
+    // Withdrawing out of a private vault to a third party requires both the
+    // submitter and the destination to be members of the vault's permissioned
+    // domain. Withdrawal to self is exempt: revoking vault access must not
+    // trap already deposited funds. The asset issuer is exempt as a
+    // destination, so that frozen assets can always be returned.
+    void
+    testVaultWithdrawPrivateDestinationDomain(FeatureBitset features)
+    {
+        using namespace test::jtx;
+
+        bool const withFix = features[fixCleanup3_4_0];
+        testcase(
+            std::string{"VaultWithdraw private vault destination domain check"} +
+            (withFix ? " (fixCleanup3_4_0)" : " (pre-fix)"));
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const depositor{"depositor"};
+        Account const beneficiary{"beneficiary"};
+        Account const outsider{"outsider"};
+        Account const pdOwner{"pdOwner"};
+        Account const credIssuer{"credIssuer"};
+        std::string const credType = "credential";
+
+        Env env{*this, features};
+        Vault const vault{env};
+
+        env.fund(
+            XRP(100'000), issuer, owner, depositor, beneficiary, outsider, pdOwner, credIssuer);
+        env.close();
+
+        PrettyAsset const asset = issuer["IOU"];
+        // Everyone holds Layer 1 (asset) permission, so anything blocked below
+        // is blocked by the Layer 2 (vault) check alone.
+        for (auto const& account : {owner, depositor, beneficiary, outsider})
+        {
+            env.trust(asset(1'000'000), account);
+            env(pay(issuer, account, asset(10'000)));
+        }
+        env.close();
+
+        auto const domainId = [&]() {
+            pdomain::Credentials const credentials{{.issuer = credIssuer, .credType = credType}};
+            env(pdomain::setTx(pdOwner, credentials));
+            env.close();
+            return pdomain::getNewDomain(env.meta());
+        }();
+
+        auto const joinDomain = [&](Account const& account) {
+            env(credentials::create(account, credIssuer, credType));
+            env(credentials::accept(account, credIssuer, credType));
+            env.close();
+        };
+        joinDomain(depositor);
+        joinDomain(beneficiary);
+
+        auto [createTx, keylet] =
+            vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
+        env(createTx);
+        env.close();
+
+        {
+            auto tx = vault.set({.owner = owner, .id = keylet.key});
+            tx[sfDomainID] = to_string(domainId);
+            env(tx);
+            env.close();
+        }
+
+        env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1'000)}));
+        env.close();
+
+        auto const withdrawTo = [&, keylet = keylet](Account const& destination) {
+            auto tx =
+                vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)});
+            tx[sfDestination] = destination.human();
+            return tx;
+        };
+
+        {
+            // Destination holds both layers of permission.
+            env(withdrawTo(beneficiary));
+            env.close();
+        }
+
+        {
+            // Destination may hold the asset but was never let into the vault.
+            env(withdrawTo(outsider), Ter(withFix ? TER(tecNO_AUTH) : TER(tesSUCCESS)));
+            env.close();
+        }
+
+        {
+            // The asset issuer can always receive, to keep the recovery path
+            // for frozen assets open.
+            env(withdrawTo(issuer));
+            env.close();
+        }
+
+        {
+            // The vault owner gets no special treatment as a destination: it
+            // is a third party like any other and needs domain membership.
+            env(withdrawTo(owner), Ter(withFix ? TER(tecNO_AUTH) : TER(tesSUCCESS)));
+            env.close();
+        }
+
+        {
+            // Withdrawal to self needs no Destination and stays unaffected.
+            env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)}));
+            env.close();
+        }
+
+        {
+            // Naming yourself as the Destination is still a withdrawal to self.
+            env(withdrawTo(depositor));
+            env.close();
+        }
+
+        {
+            testcase(
+                std::string{"VaultWithdraw private vault submitter lost vault access"} +
+                (withFix ? " (fixCleanup3_4_0)" : " (pre-fix)"));
+
+            env(credentials::deleteCred(credIssuer, depositor, credIssuer, credType));
+            env.close();
+
+            // The exit of last resort: the submitter lost vault access but
+            // must still be able to redeem its own shares.
+            env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)}));
+            env.close();
+
+            // Moving funds to anyone else is not allowed any more, even to a
+            // destination that is itself a domain member.
+            env(withdrawTo(beneficiary), Ter(withFix ? TER(tecNO_AUTH) : TER(tesSUCCESS)));
+            env.close();
+
+            // Returning assets to the issuer stays open regardless.
+            env(withdrawTo(issuer));
+            env.close();
+        }
+
+        {
+            testcase(
+                std::string{"VaultWithdraw private vault with no domain set"} +
+                (withFix ? " (fixCleanup3_4_0)" : " (pre-fix)"));
+
+            // Give the submitter its vault access back first, so that the
+            // vault having no domain is the only reason left to refuse.
+            env(credentials::create(depositor, credIssuer, credType));
+            env(credentials::accept(depositor, credIssuer, credType));
+            env.close();
+
+            auto tx = vault.set({.owner = owner, .id = keylet.key});
+            tx[sfDomainID] = "0";
+            env(tx);
+            env.close();
+
+            // Clearing the domain leaves the vault with nobody it considers
+            // authorized, so a third-party destination cannot qualify even
+            // though both ends of the payout hold a credential.
+            env(withdrawTo(beneficiary), Ter(withFix ? TER(tecNO_AUTH) : TER(tesSUCCESS)));
+            env.close();
+
+            // The two exempt paths survive the domain going away.
+            env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)}));
+            env.close();
+
+            env(withdrawTo(issuer));
+            env.close();
+        }
+
+        {
+            testcase(
+                std::string{"VaultWithdraw public vault destination unaffected"} +
+                (withFix ? " (fixCleanup3_4_0)" : " (pre-fix)"));
+
+            auto [publicTx, publicKeylet] = vault.create({.owner = owner, .asset = asset});
+            env(publicTx);
+            env.close();
+
+            env(vault.deposit({.depositor = owner, .id = publicKeylet.key, .amount = asset(100)}));
+            env.close();
+
+            auto tx =
+                vault.withdraw({.depositor = owner, .id = publicKeylet.key, .amount = asset(1)});
+            tx[sfDestination] = outsider.human();
+            env(tx);
+            env.close();
+        }
+    }
+
+    void
+    testWithdrawCredentialDepositPreauth(FeatureBitset features)
+    {
+        testcase(
+            "withdraw with credential-based deposit preauth " +
+            std::string{features[fixCleanup3_4_0] ? "post-fix" : "pre-fix"});
+        using namespace test::jtx;
+        using namespace std::chrono_literals;
+
+        bool const fixEnabled = features[fixCleanup3_4_0];
+
+        Env env{*this, features};
+
+        Account const owner{"owner"};
+        Account const depositor{"depositor"};
+        Account const dest{"dest"};
+        Account const credIssuer{"credIssuer"};
+        char const credType[] = "abcde";
+
+        env.fund(XRP(1000), owner, depositor, dest, credIssuer);
+        env(fset(dest, asfDepositAuth));
+        env.close();
+
+        PrettyAsset const asset{xrpIssue(), 1'000'000};
+        Vault vault{env};
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+        env(tx);
+        env.close();
+
+        env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)}));
+        env.close();
+
+        auto withdrawToDest = [&]() {
+            auto wtx =
+                vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
+            wtx[sfDestination] = dest.human();
+            return wtx;
+        };
+
+        // Without any preauth, withdraw to dest fails
+        env(withdrawToDest(), Ter{tecNO_PERMISSION});
+        env.close();
+
+        // Issue and accept a credential for the depositor (with expiration)
+        auto jv = credentials::create(depositor, credIssuer, credType);
+        std::uint32_t const expiration =
+            env.current()->header().parentCloseTime.time_since_epoch().count() + 100;
+        jv[sfExpiration.jsonName] = expiration;
+        env(jv);
+        env(credentials::accept(depositor, credIssuer, credType));
+        env.close();
+
+        auto const credKeylet = credentials::keylet(depositor, credIssuer, credType);
+        auto const credIdx =
+            credentials::ledgerEntry(env, depositor, credIssuer, credType)[jss::result][jss::index]
+                .asString();
+
+        // dest authorizes deposits from holders of credentials issued by credIssuer
+        env(deposit::authCredentials(dest, {{.issuer = credIssuer, .credType = credType}}));
+        env.close();
+
+        // Withdraw without supplying credentials still fails
+        env(withdrawToDest(), Ter{tecNO_PERMISSION});
+        env.close();
+
+        if (!fixEnabled)
+        {
+            // Pre-fix: sfCredentialIDs in VaultWithdraw is rejected as disabled
+            env(withdrawToDest(), credentials::Ids({credIdx}), Ter{temDISABLED});
+            env.close();
+            return;
+        }
+
+        // Withdraw with credentials succeeds
+        env(withdrawToDest(), credentials::Ids({credIdx}));
+        env.close();
+
+        // Bad credential id is rejected
+        std::string const invalidIdx =
+            "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E01E034";
+        env(withdrawToDest(), credentials::Ids({invalidIdx}), Ter{tecBAD_CREDENTIALS});
+        env.close();
+
+        // Malformed credential array (duplicates) is rejected by checkFields
+        env(withdrawToDest(), credentials::Ids({credIdx, credIdx}), Ter{temMALFORMED});
+        env.close();
+
+        // Valid credential not authorized by dest hits authorizedDepositPreauth error path
+        char const credType2[] = "fghij";
+        env(credentials::create(depositor, credIssuer, credType2));
+        env(credentials::accept(depositor, credIssuer, credType2));
+        env.close();
+        auto const credIdx2 =
+            credentials::ledgerEntry(env, depositor, credIssuer, credType2)[jss::result][jss::index]
+                .asString();
+        env(withdrawToDest(), credentials::Ids({credIdx2}), Ter{tecNO_PERMISSION});
+        env.close();
+
+        // Advance time past expiration: credentials yield tecEXPIRED and are deleted
+        env.close(150s);
+        BEAST_EXPECT(env.le(credKeylet));
+        env(withdrawToDest(), credentials::Ids({credIdx}), Ter{tecEXPIRED});
+        env.close();
+        BEAST_EXPECT(!env.le(credKeylet));
+    }
+
+public:
+    void
+    run() override
+    {
+        testWithDomainCheck();
+        testDomainLossAfterAcquisition();
+        testDomainCheckBuyerSideOffer();
+        testWithDomainChecXRP();
+        testVaultWithdrawPrivateDestinationDomain(all_ - fixCleanup3_4_0);
+        testVaultWithdrawPrivateDestinationDomain(all_);
+        testWithdrawCredentialDepositPreauth(all_ - fixCleanup3_4_0);
+        testWithdrawCredentialDepositPreauth(all_);
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(VaultDomain, app, xrpl);
+
+}  // namespace xrpl
diff --git a/src/test/app/vault/VaultFreeze_test.cpp b/src/test/app/vault/VaultFreeze_test.cpp
new file mode 100644
index 0000000000..120aabc8f6
--- /dev/null
+++ b/src/test/app/vault/VaultFreeze_test.cpp
@@ -0,0 +1,691 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class VaultFreeze_test : public VaultTestBase
+{
+private:
+    void
+    testVaultDepositFreezeIOU()
+    {
+        using namespace test::jtx;
+        testcase("VaultDeposit IOU freeze checks");
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Env env{*this};
+        Vault vault{env};
+
+        env.fund(XRP(100'000), issuer, owner);
+        env(fset(issuer, asfAllowTrustLineClawback));
+        env.close();
+        PrettyAsset const asset = issuer["IOU"];
+        env.trust(asset(1'000'000), owner);
+        env(pay(issuer, owner, asset(100'000)));
+        env.close();
+
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+        env(tx);
+        env.close();
+        auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
+
+        // Initial deposit so the vault pseudo-account has a trustline
+        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
+        env.close();
+
+        auto runTests = [&]() {
+            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+
+            // Global freeze
+            {
+                testcase("VaultDeposit IOU global freeze");
+                env(fset(issuer, asfGlobalFreeze));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(tecFROZEN));
+                env(fclear(issuer, asfGlobalFreeze));
+            }
+
+            // Depositor freeze
+            {
+                testcase("VaultDeposit IOU depositor freeze");
+                env(trust(issuer, asset(0), owner, tfSetFreeze));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(tecFROZEN));
+                env(trust(issuer, asset(0), owner, tfClearFreeze));
+            }
+
+            // Depositor deep freeze
+            {
+                testcase("VaultDeposit IOU depositor deep freeze");
+                env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(tecFROZEN));
+                env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze));
+            }
+
+            // Vault-account freeze
+            // Post-fix: checkDepositFreeze catches it → tecFROZEN
+            // Pre-fix: not checked directly, but the transitive share
+            //          check triggers → tecLOCKED
+            {
+                testcase("VaultDeposit IOU pseudo-account freeze");
+                auto trustSet = [&]() {
+                    json::Value jv;
+                    jv[jss::Account] = issuer.human();
+                    {
+                        auto& ja = jv[jss::LimitAmount] =
+                            asset(0).value().getJson(JsonOptions::Values::None);
+                        ja[jss::issuer] = toBase58(vaultAcct.id());
+                    }
+                    jv[jss::TransactionType] = jss::TrustSet;
+                    return jv;
+                }();
+
+                trustSet[jss::Flags] = tfSetFreeze;
+                env(trustSet);
+                env.close();
+
+                TER const expected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(expected));
+
+                trustSet[jss::Flags] = tfClearFreeze;
+                env(trustSet);
+                env.close();
+            }
+
+            // Vault-account deep freeze
+            {
+                testcase("VaultDeposit IOU pseudo-account deep freeze");
+                auto trustSet = [&]() {
+                    json::Value jv;
+                    jv[jss::Account] = issuer.human();
+                    {
+                        auto& ja = jv[jss::LimitAmount] =
+                            asset(0).value().getJson(JsonOptions::Values::None);
+                        ja[jss::issuer] = toBase58(vaultAcct.id());
+                    }
+                    jv[jss::TransactionType] = jss::TrustSet;
+                    return jv;
+                }();
+
+                trustSet[jss::Flags] = tfSetFreeze | tfSetDeepFreeze;
+                env(trustSet);
+                env.close();
+
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
+
+                trustSet[jss::Flags] = tfClearFreeze | tfClearDeepFreeze;
+                env(trustSet);
+                env.close();
+            }
+
+            // Clawback works while frozen
+            {
+                testcase("VaultDeposit IOU freeze clawback unaffected");
+                env(fset(issuer, asfGlobalFreeze));
+                env(vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)}));
+                env(fclear(issuer, asfGlobalFreeze));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+                env.close();
+            }
+        };
+
+        runTests();
+        env.disableFeature(fixCleanup3_3_0);
+        runTests();
+        env.enableFeature(fixCleanup3_3_0);
+    }
+
+    void
+    testVaultDepositFreezeMPT()
+    {
+        using namespace test::jtx;
+        testcase("VaultDeposit MPT lock checks");
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Env env{*this};
+        Vault vault{env};
+
+        env.fund(XRP(100'000), issuer, owner);
+        env.close();
+
+        MPTTester mptt{env, issuer, kMptInitNoFund};
+        mptt.create(
+            {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
+        PrettyAsset const mpt{mptt.issuanceID()};
+
+        mptt.authorize({.account = owner});
+        mptt.authorize({.account = issuer, .holder = owner});
+        env.close();
+        env(pay(issuer, owner, mpt(100'000)));
+        env.close();
+
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt});
+        env(tx);
+        env.close();
+        auto const vaultAcctID = env.le(keylet)->at(sfAccount);
+        Account const vaultAcct("vault", vaultAcctID);
+
+        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)}));
+        env.close();
+
+        // For MPT isDeepFrozen == isFrozen, so all locks block in
+        // both pre- and post-fix.
+        auto runTests = [&]() {
+            // Global lock
+            {
+                testcase("VaultDeposit MPT global lock");
+                mptt.set({.flags = tfMPTLock});
+                env.close();
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
+                    Ter(tecLOCKED));
+                mptt.set({.flags = tfMPTUnlock});
+                env.close();
+            }
+
+            // Depositor individual lock
+            {
+                testcase("VaultDeposit MPT depositor lock");
+                mptt.set({.holder = owner, .flags = tfMPTLock});
+                env.close();
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
+                    Ter(tecLOCKED));
+                mptt.set({.holder = owner, .flags = tfMPTUnlock});
+                env.close();
+            }
+
+            // Vault pseudo-account individual lock
+            {
+                testcase("VaultDeposit MPT pseudo-account lock");
+                mptt.set({.holder = vaultAcct, .flags = tfMPTLock});
+                env.close();
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
+                    Ter(tecLOCKED));
+                mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock});
+                env.close();
+            }
+
+            // Clawback works while locked
+            {
+                testcase("VaultDeposit MPT lock clawback unaffected");
+                mptt.set({.flags = tfMPTLock});
+                env.close();
+                env(vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = mpt(1)}));
+                mptt.set({.flags = tfMPTUnlock});
+                env.close();
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
+                env.close();
+            }
+        };
+
+        runTests();
+        env.disableFeature(fixCleanup3_3_0);
+        runTests();
+        env.enableFeature(fixCleanup3_3_0);
+    }
+
+    void
+    testVaultWithdrawFreezeIOU()
+    {
+        using namespace test::jtx;
+        testcase("VaultWithdraw IOU freeze checks");
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Env env{*this};
+        Vault const vault{env};
+
+        env.fund(XRP(100'000), issuer, owner);
+        env(fset(issuer, asfAllowTrustLineClawback));
+        env.close();
+        PrettyAsset const asset = issuer["IOU"];
+        env.trust(asset(1'000'000), owner);
+        env(pay(issuer, owner, asset(100'000)));
+        env.close();
+
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+        env(tx);
+        env.close();
+        auto const vaultAcct = Account("vault", env.le(keylet)->at(sfAccount));
+
+        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
+        env.close();
+
+        Account const charlie{"charlie"};
+        env.fund(XRP(10'000), charlie);
+        env.trust(asset(1'000'000), charlie);
+        env.close();
+
+        auto runTests = [&]() {
+            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+            // Global freeze → self-withdraw
+            {
+                testcase("VaultWithdraw IOU global freeze");
+                env(fset(issuer, asfGlobalFreeze));
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(tecFROZEN));
+                // Global freeze → withdraw to 3rd party
+
+                auto withdrawToCharlie =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                withdrawToCharlie[sfDestination] = charlie.human();
+                env(withdrawToCharlie, Ter(tecFROZEN));
+
+                env(fclear(issuer, asfGlobalFreeze));
+            }
+
+            // Vault-account freeze
+            {
+                testcase("VaultWithdraw IOU pseudo-account freeze");
+                auto trustSet = [&]() {
+                    json::Value jv;
+                    jv[jss::Account] = issuer.human();
+                    {
+                        auto& ja = jv[jss::LimitAmount] =
+                            asset(0).value().getJson(JsonOptions::Values::None);
+                        ja[jss::issuer] = toBase58(vaultAcct.id());
+                    }
+                    jv[jss::TransactionType] = jss::TrustSet;
+                    return jv;
+                }();
+
+                trustSet[jss::Flags] = tfSetFreeze;
+                env(trustSet);
+                env.close();
+
+                TER const terExpected = fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED);
+
+                // Self-withdraw
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(terExpected));
+                // Withdraw to 3rd party
+
+                auto withdrawToCharlie =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                withdrawToCharlie[sfDestination] = charlie.human();
+                env(withdrawToCharlie, Ter(terExpected));
+
+                trustSet[jss::Flags] = tfClearFreeze;
+                env(trustSet);
+                env.close();
+            }
+
+            // Depositor freeze, self-withdraw
+            {
+                testcase("VaultWithdraw IOU self-withdraw freeze check");
+                env(trust(issuer, asset(0), owner, tfSetFreeze));
+
+                // Post-fix: self-withdraw allowed (submitter==dst skip)
+                // Pre-fix: isFrozen(depositor, iou) catches it
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
+
+                // Depositor freeze withdraw to 3rd party
+                auto withdrawTo3rd =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                withdrawTo3rd[sfDestination] = charlie.human();
+
+                // Post-fix: submitter freeze blocks withdraw to 3rd party
+                // Pre-fix: submitter's IOU freeze not checked, but checkFrozen(depositor,
+                // share) triggers tecLOCKED
+                env(withdrawTo3rd, Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
+
+                env(trust(issuer, asset(0), owner, tfClearFreeze));
+                // Replenish what was withdrawn
+                if (fix330Enabled)
+                {
+                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+                }
+                env.close();
+            }
+
+            // Depositor deep freeze → self-withdraw blocked
+            {
+                testcase("VaultWithdraw IOU depositor deep freeze");
+                env(trust(issuer, asset(0), owner, tfSetFreeze | tfSetDeepFreeze));
+
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                    Ter(tecFROZEN));
+
+                env(trust(issuer, asset(0), owner, tfClearFreeze | tfClearDeepFreeze));
+            }
+
+            // Destination freeze → withdraw to 3rd party
+            {
+                testcase("VaultWithdraw IOU freeze withdraw to 3rd party");
+
+                env(trust(issuer, asset(0), charlie, tfSetFreeze));
+
+                // Self-withdraw unaffected by charlie's freeze
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+
+                auto withdrawToCharlie =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                withdrawToCharlie[sfDestination] = charlie.human();
+
+                // Post-fix: freeze on dst allowed
+                // Pre-fix: checkFrozen(dst, iou) catches it
+                env(withdrawToCharlie, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
+
+                env(trust(issuer, asset(0), charlie, tfClearFreeze));
+
+                // Replenish: 1 for self-withdraw + 1 if charlie withdraw succeeded
+                env(vault.deposit(
+                    {.depositor = owner,
+                     .id = keylet.key,
+                     .amount = asset(fix330Enabled ? 2 : 1)}));
+                env.close();
+            }
+
+            // Destination deep freeze → withdraw to 3rd party blocked
+            {
+                testcase("VaultWithdraw IOU deep freeze withdraw to 3rd party");
+
+                env(trust(issuer, asset(0), charlie, tfSetFreeze | tfSetDeepFreeze));
+
+                auto withdrawToCharlie =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                withdrawToCharlie[sfDestination] = charlie.human();
+                env(withdrawToCharlie, Ter(tecFROZEN));
+
+                // Destination deep freeze → self-withdraw unaffected
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+
+                env(trust(issuer, asset(0), charlie, tfClearFreeze | tfClearDeepFreeze));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+                env.close();
+            }
+
+            // Clawback works while frozen
+            {
+                testcase("VaultWithdraw IOU freeze clawback unaffected");
+                env(fset(issuer, asfGlobalFreeze));
+
+                env(vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(1)}));
+
+                env(fclear(issuer, asfGlobalFreeze));
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(1)}));
+                env.close();
+            }
+        };
+
+        runTests();
+        env.disableFeature(fixCleanup3_3_0);
+        runTests();
+        env.enableFeature(fixCleanup3_3_0);
+    }
+
+    void
+    testVaultWithdrawFreezeMPT()
+    {
+        using namespace test::jtx;
+        testcase("VaultWithdraw MPT lock checks");
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Env env{*this};
+        Vault vault{env};
+
+        env.fund(XRP(100'000), issuer, owner);
+        env.close();
+
+        MPTTester mptt{env, issuer, kMptInitNoFund};
+        mptt.create(
+            {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
+        PrettyAsset const mpt{mptt.issuanceID()};
+
+        mptt.authorize({.account = owner});
+        mptt.authorize({.account = issuer, .holder = owner});
+        env.close();
+        env(pay(issuer, owner, mpt(100'000)));
+        env.close();
+
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = mpt});
+        env(tx);
+        env.close();
+        Account const vaultAcct("vault", env.le(keylet)->at(sfAccount));
+
+        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(100)}));
+        env.close();
+
+        Account const charlie{"charlie"};
+        env.fund(XRP(10'000), charlie);
+        env.close();
+        mptt.authorize({.account = charlie});
+        mptt.authorize({.account = issuer, .holder = charlie});
+        env.close();
+
+        auto runTests = [&]() {
+            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+
+            // Global lock
+            {
+                testcase("VaultWithdraw MPT global lock");
+                mptt.set({.flags = tfMPTLock});
+                env.close();
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
+                    Ter(tecLOCKED));
+
+                // Global lock → withdraw to issuer
+                // Post-fix: bypasses freeze checks, but accountHolds
+                //           on the pseudo returns 0 under global lock
+                // Pre-fix: checkFrozen(dst=issuer) catches global lock
+                {
+                    auto withdrawToIssuer =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
+                    withdrawToIssuer[sfDestination] = issuer.human();
+                    env(withdrawToIssuer, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecLOCKED)));
+                }
+                mptt.set({.flags = tfMPTUnlock});
+                env.close();
+                if (fix330Enabled)
+                {
+                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
+                }
+                env.close();
+            }
+
+            // Vault pseudo-account individual lock
+            {
+                testcase("VaultWithdraw MPT pseudo-account lock");
+                mptt.set({.holder = vaultAcct, .flags = tfMPTLock});
+                env.close();
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
+                    Ter(tecLOCKED));
+                mptt.set({.holder = vaultAcct, .flags = tfMPTUnlock});
+                env.close();
+            }
+
+            // Depositor individual lock → self-withdraw blocked
+            // (isDeepFrozen == isFrozen for MPT)
+            {
+                testcase("VaultWithdraw MPT depositor lock");
+                mptt.set({.holder = owner, .flags = tfMPTLock});
+                env.close();
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}),
+                    Ter(tecLOCKED));
+                // Depositor lock → withdraw to 3rd party also blocked
+                {
+                    auto withdrawToCharlie =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
+                    withdrawToCharlie[sfDestination] = charlie.human();
+                    env(withdrawToCharlie, Ter(tecLOCKED));
+                }
+
+                // Depositor lock → withdraw to issuer
+                // Post-fix: issuer bypass in checkWithdrawFreezes
+                // Pre-fix: checkFrozen(depositor, share) blocks transitively
+                {
+                    auto withdrawToIssuer =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
+                    withdrawToIssuer[sfDestination] = issuer.human();
+                    env(withdrawToIssuer, Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecLOCKED)));
+                }
+                mptt.set({.holder = owner, .flags = tfMPTUnlock});
+                env.close();
+                if (fix330Enabled)
+                {
+                    env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
+                }
+                env.close();
+            }
+
+            // 3rd party destination lock → withdraw to 3rd party blocked
+            {
+                testcase("VaultWithdraw MPT 3rd party destination lock");
+                mptt.set({.holder = charlie, .flags = tfMPTLock});
+                env.close();
+                {
+                    auto withdrawToCharlie =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)});
+                    withdrawToCharlie[sfDestination] = charlie.human();
+                    env(withdrawToCharlie, Ter{tecLOCKED});
+                }
+                // 3rd party lock → self-withdraw unaffected
+                env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
+                mptt.set({.holder = charlie, .flags = tfMPTUnlock});
+                env.close();
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
+                env.close();
+            }
+
+            // Clawback works while locked
+            {
+                testcase("VaultWithdraw MPT lock clawback unaffected");
+                mptt.set({.flags = tfMPTLock});
+                env.close();
+                env(vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = mpt(1)}));
+                mptt.set({.flags = tfMPTUnlock});
+                env.close();
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = mpt(1)}));
+                env.close();
+            }
+        };
+
+        runTests();
+        env.disableFeature(fixCleanup3_3_0);
+        runTests();
+        env.enableFeature(fixCleanup3_3_0);
+    }
+
+    // Focused demonstration: a depositor under an individual IOU freeze
+    // can still withdraw to themselves (self-withdrawal), but is blocked from
+    // withdrawing to a third party.
+    //
+    // Pre-fixCleanup3_3_0: both the self-withdrawal AND the third-party
+    // withdrawal were blocked because the old code checked checkFrozen on the
+    // destination regardless of whether it was the submitter.
+    // Post-fixCleanup3_3_0: checkWithdrawFreeze skips the submitter freeze
+    // check when submitter == destination, so self-withdrawal succeeds.
+    void
+    testVaultSelfWithdrawWhileFrozen()
+    {
+        testcase("VaultWithdraw IOU self-withdrawal while individually frozen");
+
+        using namespace test::jtx;
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const charlie{"charlie"};
+        Env env{*this};
+        Vault vault{env};
+
+        env.fund(XRP(100'000), issuer, owner, charlie);
+        env(fset(issuer, asfAllowTrustLineClawback));
+        env.close();
+
+        PrettyAsset const asset = issuer["IOU"];
+        env.trust(asset(1'000'000), owner);
+        env.trust(asset(1'000'000), charlie);
+        env(pay(issuer, owner, asset(100'000)));
+        env.close();
+
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+        env(tx);
+        env.close();
+
+        env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)}));
+        env.close();
+
+        auto runTests = [&]() {
+            auto const fix330Enabled = env.current()->rules().enabled(fixCleanup3_3_0);
+
+            // Set an individual freeze on the owner's IOU trustline.
+            env(trust(issuer, asset(0), owner, tfSetFreeze));
+            env.close();
+
+            // Self-withdrawal: submitter == destination, so the submitter
+            // freeze check is skipped.
+            // Post-fix: tesSUCCESS.  Pre-fix: tecFROZEN.
+            env(vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)}),
+                Ter(fix330Enabled ? TER(tesSUCCESS) : TER(tecFROZEN)));
+
+            // Withdrawal to a third party is blocked: submitter != destination
+            // so the submitter freeze check applies.
+            {
+                auto withdrawToCharlie =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(1)});
+                withdrawToCharlie[sfDestination] = charlie.human();
+                // Post-fix: tecFROZEN (checkIndividualFrozen on submitter).
+                // Pre-fix: tecLOCKED (isFrozen on the vault share).
+                env(withdrawToCharlie, Ter(fix330Enabled ? TER(tecFROZEN) : TER(tecLOCKED)));
+            }
+
+            env(trust(issuer, asset(0), owner, tfClearFreeze));
+            env.close();
+        };
+
+        runTests();
+        env.disableFeature(fixCleanup3_3_0);
+        runTests();
+        env.enableFeature(fixCleanup3_3_0);
+    }
+
+public:
+    void
+    run() override
+    {
+        testVaultDepositFreezeIOU();
+        testVaultDepositFreezeMPT();
+        testVaultWithdrawFreezeIOU();
+        testVaultWithdrawFreezeMPT();
+        testVaultSelfWithdrawWhileFrozen();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(VaultFreeze, app, xrpl);
+
+}  // namespace xrpl
diff --git a/src/test/app/vault/VaultHelpers_test.cpp b/src/test/app/vault/VaultHelpers_test.cpp
new file mode 100644
index 0000000000..d52b732a60
--- /dev/null
+++ b/src/test/app/vault/VaultHelpers_test.cpp
@@ -0,0 +1,484 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include   // IWYU pragma: keep
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+// True unit test of `clampToAssetsTotalScale`. The function under test only
+// reads sfAsset and sfAssetsTotal from the vault SLE and never touches a
+// ledger view or Rules, so a bare in-memory ltVAULT SLE is enough; there is
+// no jtx::Env and no transaction submitted anywhere in this file.
+//
+// Number regime: this suite relies on the default thread_local Number
+// mantissa range, which src/libxrpl/basics/Number.cpp initializes to
+// Large330 (19-digit mantissa, post-fixCleanup3_3_0 cusp-rounding behavior):
+//
+//   thread_local std::reference_wrapper Number::kRange =
+//       MantissaRange::Access::mantissaRange(MantissaRange::MantissaScale::Large330);
+//
+// Unlike transaction processing, this test never constructs a ledger `Rules`
+// object, so `STAmount::operator=(Number const&)` always takes its
+// `!getCurrentTransactionRules()` branch and calls `fromNumber`, independent
+// of amendment state. testProbeLarge330Regime() below asserts directly on a
+// value that only round-trips exactly under Large330, pinning the regime
+// rather than merely asserting it by comment.
+class VaultHelpers_test : public beast::unit_test::Suite
+{
+private:
+    // A single row of the clampToAssetsTotalScale table. `assetsTotal` and
+    // `delta` must already be genuine, on-grid STAmount values for `asset`.
+    struct Case
+    {
+        char const* name = nullptr;
+        Number assetsTotal;
+        Number delta;
+        std::optional expected;  // nullopt means tecPRECISION_LOSS
+    };
+
+    // Builds a bare ltVAULT SLE with only sfAsset and sfAssetsTotal set,
+    // mirroring what a transactor does: set the STNumber field, then call
+    // associateAsset() so it is quantized to the asset's STAmount grid, the
+    // same way VaultDeposit::doApply does for a real vault (see
+    // src/libxrpl/tx/transactors/vault/VaultDeposit.cpp).
+    static std::shared_ptr
+    makeVault(Asset const& asset, Number const& assetsTotal)
+    {
+        auto vault = std::make_shared(keylet::vault(uint256(1)));
+        vault->setFieldIssue(sfAsset, STIssue{sfAsset, asset});
+        vault->at(sfAssetsTotal) = assetsTotal;
+        associateAsset(*vault, asset);
+        return vault;
+    }
+
+    // Runs every case in `cases` against `asset`, once per ambient rounding
+    // mode. The function must give the same answer under all four modes,
+    // and its answer must match the hand-derived `expected` value.
+    template 
+    void
+    runCases(Asset const& asset, std::array const& cases)
+    {
+        std::array const modes{
+            Number::RoundingMode::ToNearest,
+            Number::RoundingMode::Downward,
+            Number::RoundingMode::Upward,
+            Number::RoundingMode::TowardsZero};
+
+        for (auto const& c : cases)
+        {
+            testcase(c.name);
+
+            auto const vault = makeVault(asset, c.assetsTotal);
+            BEAST_EXPECTS(
+                Number(vault->at(sfAssetsTotal)) == c.assetsTotal,
+                std::string(c.name) +
+                    ": assetsTotal is not a genuine on-grid STAmount value (associateAsset "
+                    "changed it)");
+
+            STAmount const delta{asset, c.delta};
+            BEAST_EXPECTS(
+                Number(delta) == c.delta,
+                std::string(c.name) + ": delta is not a genuine on-grid STAmount value");
+
+            std::optional> reference;
+            for (auto const mode : modes)
+            {
+                NumberRoundModeGuard const rg(mode);
+                auto const result = clampToAssetsTotalScale(vault, delta);
+
+                // The function must be insensitive to the caller's ambient
+                // rounding mode: every mode must agree with the first one
+                // tried.
+                if (!reference)
+                {
+                    reference = result;
+                }
+                else
+                {
+                    BEAST_EXPECTS(
+                        result.has_value() == reference->has_value(),
+                        std::string(c.name) + ": result depends on ambient rounding mode");
+                    if (result.has_value() && reference->has_value())
+                    {
+                        BEAST_EXPECTS(
+                            *result == **reference,
+                            std::string(c.name) + ": value depends on ambient rounding mode");
+                    }
+                    else if (!result.has_value() && !reference->has_value())
+                    {
+                        BEAST_EXPECTS(
+                            result.error() == reference->error(),
+                            std::string(c.name) + ": error depends on ambient rounding mode");
+                    }
+                }
+
+                if (!c.expected)
+                {
+                    BEAST_EXPECTS(
+                        !result.has_value(),
+                        std::string(c.name) + ": expected tecPRECISION_LOSS, got success value " +
+                            (result.has_value() ? result->getText() : std::string()));
+                    if (!result.has_value())
+                    {
+                        BEAST_EXPECTS(
+                            result.error() == tecPRECISION_LOSS,
+                            std::string(c.name) + ": expected tecPRECISION_LOSS, got " +
+                                transToken(result.error()));
+                    }
+                    continue;
+                }
+
+                STAmount const expected{asset, *c.expected};
+                if (!BEAST_EXPECTS(
+                        result.has_value(),
+                        std::string(c.name) + ": expected success (" + expected.getText() +
+                            "), got " + transToken(result.error())))
+                {
+                    continue;
+                }
+
+                BEAST_EXPECTS(
+                    *result == expected,
+                    std::string(c.name) + ": expected " + expected.getText() + ", got " +
+                        result->getText());
+
+                // The result must always be positive...
+                BEAST_EXPECT(Number(*result) > Number{0});
+
+                // ...and never larger in magnitude than the requested delta.
+                BEAST_EXPECT(abs(Number(*result)) <= abs(c.delta));
+
+                // For IOU rows, re-flooring the result on the posterior grid
+                // must be a no-op: the result is already exactly
+                // representable at that scale.
+                //
+                // For debits this holds directly at postScale, because the
+                // result IS `roundToScale(magnitude, postScale, Downward)` by
+                // construction. For credits the result is
+                // `roundedPosterior - assetsTotal`, where roundedPosterior
+                // sits exactly on the postScale grid but assetsTotal sits on
+                // its own (possibly finer) natural grid; the difference of a
+                // multiple of 10^postScale and a multiple of 10^assetsScale
+                // is only guaranteed exact at the FINER of the two scales.
+                // Row 7 below ("overcredit fix across a scale boundary") is
+                // exactly this case: assetsTotal's own scale (-15) is finer
+                // than postScale (-14), so checking exactness at postScale
+                // alone fails even though the implementation is correct.
+                if (!asset.integral())
+                {
+                    bool const isDebit = c.delta.mantissa() < 0;
+                    Number const posterior =
+                        isDebit ? c.assetsTotal - Number(*result) : c.assetsTotal + Number(*result);
+                    int const postScale = scale(posterior, asset);
+                    int const checkScale =
+                        isDebit ? postScale : std::min(postScale, scale(c.assetsTotal, asset));
+                    STAmount const reFloored =
+                        roundToScale(*result, checkScale, Number::RoundingMode::Downward);
+                    BEAST_EXPECTS(
+                        reFloored == *result,
+                        std::string(c.name) + ": result " + result->getText() +
+                            " is not exact on the posterior grid (scale " +
+                            std::to_string(checkScale) + ")");
+                }
+            }
+        }
+    }
+
+    // Pins the Number mantissa regime this suite relies on. Under Large330,
+    // a 19-digit mantissa (max 10^19-1) is exact where a legacy 16-digit
+    // ("Small", max 10^16-1) regime would have to round it down to 16
+    // significant digits, changing both mantissa and exponent.
+    void
+    testProbeLarge330Regime()
+    {
+        testcase("probe: default Number regime is Large330 (19-digit mantissa)");
+
+        BEAST_EXPECT(Number::getMantissaScale() == MantissaRange::MantissaScale::Large330);
+
+        // std::numeric_limits::max(), 19 significant digits.
+        // This is already inside Large330's [10^18, 10^19-1] range, so
+        // constructing it is a no-op; under "Small" it would have to lose
+        // its low 3 digits.
+        Number const probe{9'223'372'036'854'775'807LL, 0};
+        BEAST_EXPECT(probe.mantissa() == 9'223'372'036'854'775'807LL);
+        BEAST_EXPECT(probe.exponent() == 0);
+    }
+
+    // -------------------------------------------------------------------
+    // IOU debits (delta negative).
+    // -------------------------------------------------------------------
+    void
+    testIouDebits(Asset const& iou)
+    {
+        std::array const cases{
+            Case{
+                // T = 1000000.000000005, delta = -1e-9.
+                // Posterior = 1000000.000000004, still 16 significant
+                // digits at exponent -9 (no rounding, no decade change).
+                // postScale = -9. magnitude 1e-9 has its own exponent -24
+                // (finer than -9), so it must be actually floored: 1e-9 is
+                // exactly 1 ULP at scale -9, so flooring is a no-op.
+                .name = "IOU debit: on-grid, same decade",
+                .assetsTotal = Number{1'000'000'000'000'005LL, -9},
+                .delta = Number{-1, -9},
+                .expected = Number{1, -9},
+            },
+            Case{
+                // T = 1000000, delta = -7.3e-10.
+                // Posterior = 999999.99999999927 exactly (17 significant
+                // digits: 15 nines, then "27"). Rounding to 16 digits
+                // (ToNearest) rounds the trailing "...92.7" up to
+                // "...93", giving mantissa 9999999999999993 at exponent
+                // -10 -- postScale = -10, ONE DIGIT FINER than the naive
+                // "posterior stays in T's decade at -9" guess, because
+                // subtracting anything positive from an exact power-of-ten
+                // total necessarily drops into the next lower decade
+                // (1000000 has 7 integer digits, 999999.x has 6).
+                // At scale -10 the ULP is 1e-10, and floor(7.3) = 7, so
+                // the debit is NOT sub-ULP: it floors to 7e-10, not to
+                // zero. See discrepancy note in the report.
+                .name = "IOU debit: sub-ULP at the naive scale, but not at the true postScale",
+                .assetsTotal = Number{1'000'000, 0},
+                .delta = Number{-73, -11},
+                .expected = Number{7, -10},
+            },
+            Case{
+                // T = 1000000, delta = -5.3e-9.
+                // Posterior = 999999.9999999947 exactly -- this needs only
+                // 16 significant digits (14 nines, then "47"), so it is
+                // exactly representable with NO rounding at exponent -10.
+                // postScale = -10 (again one digit finer than T's own -9,
+                // for the same power-of-ten-boundary reason as the row
+                // above). At that grid 5.3e-9 is exactly 53 ULPs (integer),
+                // so it floors to itself, unchanged.
+                .name = "IOU debit: exact at the true (finer) postScale",
+                .assetsTotal = Number{1'000'000, 0},
+                .delta = Number{-53, -10},
+                .expected = Number{53, -10},
+            },
+            Case{
+                // T = 1.000000000000000, delta = -7.3e-16.
+                // Posterior = 0.99999999999999927 exactly (17 significant
+                // digits: 15 nines then "27"). Rounding to 16 digits
+                // (ToNearest) gives mantissa 9999999999999993 at exponent
+                // -16 -- postScale = -16. At that grid, 7.3e-16 is 7.3
+                // ULPs (not integral), so it floors to 7e-16, not to
+                // itself. See discrepancy note in the report.
+                .name = "IOU debit: decade-crossing debit, floored (not exact) at finer grid",
+                .assetsTotal = Number{1, 0},
+                .delta = Number{-73, -17},
+                .expected = Number{7, -16},
+            },
+            Case{
+                // T = 1000000, delta = -999999.9999999999 (9.999999999999999e5).
+                // Posterior = 0.0000000001 = 1e-10 exactly. postScale is
+                // the exponent of 1e-10 as a canonical STAmount, i.e. -25 --
+                // far finer than the magnitude's own exponent (-10).
+                // roundToScale short-circuits ("value.exponent() >= scale")
+                // and returns the magnitude unchanged.
+                .name = "IOU debit: near-total debit, unchanged (finer postScale than magnitude)",
+                .assetsTotal = Number{1'000'000, 0},
+                .delta = Number{-9'999'999'999'999'999LL, -10},
+                .expected = Number{9'999'999'999'999'999LL, -10},
+            },
+        };
+
+        runCases(iou, cases);
+    }
+
+    // -------------------------------------------------------------------
+    // IOU credits (delta positive).
+    // -------------------------------------------------------------------
+    void
+    testIouCredits(Asset const& iou)
+    {
+        std::array const cases{
+            Case{
+                // T = 1000000, delta = +2e-9. Posterior = 1000000.000000002,
+                // exactly 16 significant digits at exponent -9
+                // (postScale = -9, unchanged from T -- addition never
+                // crosses below the 1e6 boundary the way subtraction does).
+                // magnitude is already exact at that scale, so it passes
+                // through unchanged.
+                .name = "IOU credit: on-grid",
+                .assetsTotal = Number{1'000'000, 0},
+                .delta = Number{2, -9},
+                .expected = Number{2, -9},
+            },
+            Case{
+                // T = 9.999999999999999, delta = +5.
+                // Exact posterior = 14.999999999999999 (17 significant
+                // digits: "14" then 15 nines). postScale is computed under
+                // ToNearest at the Number (19-digit) level: normalized
+                // mantissa 1499999999999999900 (exponent -17) divided by
+                // 1000 (to reach 16-digit IOU precision) gives
+                // 1499999999999999.9, which rounds UP to 1500000000000000
+                // -- i.e. exactly 15, at exponent -14. postScale = -14.
+                // Downward-guarded posterior (exact, no rounding needed
+                // since 17 digits < 19): 14.999999999999999. Flooring THAT
+                // to 16 digits at scale -14 (Downward) gives
+                // 1499999999999999 * 10^-14 = 14.99999999999999 (postScale
+                // already matches the STAmount's own exponent, so no
+                // further roundToScale is applied).
+                // actualDelta = 14.99999999999999 - 9.999999999999999
+                //             = 4.999999999999991.
+                // This mirrors testBugVaultDepositOvercreditsAcrossScaleBoundary
+                // in VaultBugs_test.cpp (same seed/deposit values), which
+                // asserts post-fix `credited <= paid` rather than an exact
+                // number; this row pins the exact value.
+                .name = "IOU credit: overcredit fix across a scale boundary",
+                .assetsTotal = Number{9'999'999'999'999'999LL, -15},
+                .delta = Number{5, 0},
+                .expected = Number{4'999'999'999'999'991LL, -15},
+            },
+            Case{
+                // Finding-1 regression: T = 1000000, delta = +9.999999999999999e-10.
+                // The exact sum needs ~25 significant digits (1000000 at
+                // position 6, delta's last digit at position -25), far
+                // beyond Number's 19-digit mantissa.
+                //
+                // postScale (computed under ToNearest): the digits of delta
+                // that land within the 19-digit window (positions -10..-12,
+                // "999") plus an all-nines remainder below position -12
+                // round UP under ToNearest, carrying all the way through
+                // the intervening zeros: the sum rounds to exactly
+                // 1000000.000000001, i.e. postScale = -9.
+                //
+                // But the credit branch computes the *posterior* under a
+                // Downward guard, not ToNearest: positions -10..-12 stay
+                // "999" (no carry), giving posterior = 1000000.000000000999
+                // exactly. Flooring that (Downward) to scale -9 truncates
+                // the "999" entirely, landing back on exactly 1000000 --
+                // i.e. the same as T. actualDelta = 0 => tecPRECISION_LOSS.
+                // This is the ambient-rounding leak the Downward guard on
+                // the credit-side sum exists to close; this row is a
+                // regression test that the guard is doing its job.
+                .name = "IOU credit: Finding-1 regression, ToNearest sum would overcredit",
+                .assetsTotal = Number{1'000'000, 0},
+                .delta = Number{9'999'999'999'999'999LL, -25},
+                .expected = std::nullopt,
+            },
+            Case{
+                // Same shape as the row above, but delta = +9.995e-10 is a
+                // 19-digit half-even tie at the position-(-12) cusp: the
+                // remainder below the retained "999" digits is exactly
+                // 0.5 ULP, and ToNearest ties-to-even rounds the (odd) "9"
+                // up, carrying the same way. Downward-guarded posterior
+                // still truncates to "...000999" and floors back to T, so
+                // the outcome is identical: tecPRECISION_LOSS.
+                .name = "IOU credit: Finding-1 regression, 19-digit half-even tie",
+                .assetsTotal = Number{1'000'000, 0},
+                .delta = Number{9'995, -13},
+                .expected = std::nullopt,
+            },
+            Case{
+                // T = 0, delta = +3.7e-5. Posterior grid is delta's own
+                // scale (postScale = -20, the canonical exponent of
+                // 3.7e-5), so the magnitude is trivially unchanged.
+                .name = "IOU credit: zero-total vault",
+                .assetsTotal = Number{0},
+                .delta = Number{37, -6},
+                .expected = Number{37, -6},
+            },
+            Case{
+                // T = 1000000, delta = +4e-10. Exact sum needs 17
+                // significant digits (leading "1" at position 6, trailing
+                // "4" at position -10); rounding to 16 digits drops the "4"
+                // entirely (0.4 ULP at scale -9 rounds down under both
+                // ToNearest and Downward), so postScale = -9 and the
+                // Downward-guarded posterior floors straight back to T.
+                // actualDelta = 0 => tecPRECISION_LOSS.
+                .name = "IOU credit: sub-ULP credit",
+                .assetsTotal = Number{1'000'000, 0},
+                .delta = Number{4, -10},
+                .expected = std::nullopt,
+            },
+        };
+
+        runCases(iou, cases);
+    }
+
+    // -------------------------------------------------------------------
+    // Integral assets (XRP, MPT): rounding is a no-op, magnitude is
+    // returned unchanged and positive regardless of delta's sign. This is
+    // a regression test for a signed-return bug: the function must not
+    // hand back a negative delta for a debit.
+    // -------------------------------------------------------------------
+    void
+    testIntegralAssets(Asset const& mpt, Asset const& xrp)
+    {
+        std::array const mptCases{
+            Case{
+                .name = "MPT debit: magnitude is positive, not the signed delta",
+                .assetsTotal = Number{1'000'000},
+                .delta = Number{-5},
+                .expected = Number{5},
+            },
+            Case{
+                .name = "MPT credit: unchanged",
+                .assetsTotal = Number{1'000'000},
+                .delta = Number{7},
+                .expected = Number{7},
+            },
+        };
+        runCases(mpt, mptCases);
+
+        std::array const xrpCases{
+            Case{
+                .name = "XRP debit: magnitude is positive, not the signed delta",
+                .assetsTotal = Number{100'000},
+                .delta = Number{-3},
+                .expected = Number{3},
+            },
+            Case{
+                .name = "XRP credit: unchanged",
+                .assetsTotal = Number{100'000},
+                .delta = Number{10},
+                .expected = Number{10},
+            },
+        };
+        runCases(xrp, xrpCases);
+    }
+
+public:
+    void
+    run() override
+    {
+        testProbeLarge330Regime();
+
+        test::jtx::Account const issuer{"issuer"};
+        Issue const iou{toCurrency("USD"), issuer.id()};
+        MPTIssue const mpt{makeMptID(1, issuer.id())};
+        Issue const xrp = xrpIssue();
+
+        testIouDebits(iou);
+        testIouCredits(iou);
+        testIntegralAssets(mpt, xrp);
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(VaultHelpers, app, xrpl);
+
+}  // namespace xrpl
diff --git a/src/test/app/vault/VaultInvariantPrecision_test.cpp b/src/test/app/vault/VaultInvariantPrecision_test.cpp
new file mode 100644
index 0000000000..a7eeda34ae
--- /dev/null
+++ b/src/test/app/vault/VaultInvariantPrecision_test.cpp
@@ -0,0 +1,458 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// With fixCleanup3_4_0 disabled the six delta invariants and the
+// lossUnrealized > (assetsTotal - assetsAvailable) gap invariant spuriously
+// fire on legitimate flows; with the amendment enabled the one-unit
+// tolerance absorbs the sub-ULP drift and every one of these transactions
+// must succeed.  Exactness (assetsTotal delta == assetsAvailable delta
+// exactly) is covered by VaultTransactorPrecision_test.
+class VaultInvariantPrecision_test : public VaultPrecisionFixture
+{
+    // Deposit small integer amounts into an A-1 vault.  Pre-amendment,
+    // deposits of 1, 7, and 10'000'000 land on assetsTotal/assetsAvailable
+    // grids that disagree by one ULP and the invariant fires.  Post-
+    // amendment the tolerance-widened check accepts the same states.
+    void
+    testDepositBoundaryInvariant(FeatureBitset features)
+    {
+        using namespace jtx;
+
+        bool const fixEnabled = features[fixCleanup3_4_0];
+        testcase(
+            std::string("A-1 deposit boundary invariant") +
+            (fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
+
+        std::array const kAmounts{1, 7, 10'000'000};
+
+        for (auto const amount : kAmounts)
+        {
+            Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
+            auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
+            if (!f.asset || !f.broker)
+            {
+                BEAST_EXPECT(f.asset && f.broker);
+                continue;
+            }
+            auto const& asset = *f.asset;
+
+            auto const before = read(env, f);
+
+            Vault const v{env};
+            env(v.deposit(
+                    {.depositor = f.depositor,
+                     .id = f.vaultKeylet.key,
+                     .amount = asset(amount).value()}),
+                Ter(std::ignore));
+            env.close();
+
+            TER const actual = env.ter();
+
+            if (fixEnabled)
+            {
+                BEAST_EXPECTS(
+                    actual == tesSUCCESS,
+                    "amount=" + std::to_string(amount) + " expected tesSUCCESS, got " +
+                        transToken(actual));
+
+                auto const after = read(env, f);
+                Number const tDelta = after.assetsTotal - before.assetsTotal;
+                Number const aDelta = after.assetsAvailable - before.assetsAvailable;
+                Number const requested = asset(amount).number();
+
+                BEAST_EXPECT(tDelta <= requested);
+
+                Number const gap = tDelta > aDelta ? tDelta - aDelta : aDelta - tDelta;
+                BEAST_EXPECT(gap <= oneUnit(asset, after.assetsTotal));
+            }
+            else
+            {
+                BEAST_EXPECTS(
+                    actual == tecINVARIANT_FAILED,
+                    "amount=" + std::to_string(amount) + " expected tecINVARIANT_FAILED, got " +
+                        transToken(actual));
+            }
+        }
+    }
+
+    // Withdraw long-mantissa share counts from an A-1 vault.  Pre-fix
+    // some counts trip the withdraw delta invariants; post-fix none does.
+    void
+    testWithdrawBoundaryInvariant(FeatureBitset features)
+    {
+        using namespace jtx;
+
+        bool const fixEnabled = features[fixCleanup3_4_0];
+        testcase(
+            std::string("A-1 withdraw boundary invariant") +
+            (fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
+
+        std::array const kShareCounts{
+            99'999u, 100'001u, 333'333u, 1'234'567u, 142'857'142u, 333'333'333u};
+
+        // Fill the vault with enough shares that every count below is
+        // available to the depositor.
+        Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
+        auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
+        if (!f.asset || !f.broker)
+        {
+            BEAST_EXPECT(f.asset && f.broker);
+            return;
+        }
+        auto const& asset = *f.asset;
+
+        Vault const v{env};
+        // Deposit a large amount so we can afford every withdrawal below.
+        env(v.deposit(
+                {.depositor = f.depositor,
+                 .id = f.vaultKeylet.key,
+                 .amount = asset(1'000'000).value()}),
+            Ter(std::ignore));
+        env.close();
+
+        for (auto const count : kShareCounts)
+        {
+            auto const before = read(env, f);
+            if (before.sharesTotal < count)
+                continue;
+
+            STAmount const shareAmount{MPTIssue{f.share}, Number{static_cast(count)}};
+            env(v.withdraw(
+                    {.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = shareAmount}),
+                Ter(std::ignore));
+            env.close();
+
+            TER const actual = env.ter();
+
+            if (fixEnabled)
+            {
+                BEAST_EXPECTS(
+                    actual != tecINVARIANT_FAILED,
+                    "shares=" + std::to_string(count) + " unexpected invariant failure");
+
+                if (actual == tesSUCCESS)
+                {
+                    auto const after = read(env, f);
+                    Number const tDelta = before.assetsTotal - after.assetsTotal;
+                    Number const pDelta = before.pseudo - after.pseudo;
+                    Number const gap = tDelta > pDelta ? tDelta - pDelta : pDelta - tDelta;
+                    // VaultTransactorPrecision_test tightens this to strict
+                    // equality.
+                    BEAST_EXPECT(gap <= oneUnit(asset, before.assetsTotal));
+                }
+            }
+            // Pre-fix behaviour is fixture-dependent: some share counts may
+            // succeed even without the amendment.  The important property is
+            // that post-fix no legitimate withdrawal is rejected by the
+            // widened invariant.
+        }
+    }
+
+    // Clawback of small IOU amounts against a live-loan vault.  Pre-fix
+    // some amounts trip the clawback delta invariants; post-fix none does.
+    // Also assert the owner force-burn path returns tecNO_PERMISSION
+    // under both amendment states (it never enters assetsToClawback).
+    void
+    testClawbackBoundaryInvariant(FeatureBitset features)
+    {
+        using namespace jtx;
+
+        bool const fixEnabled = features[fixCleanup3_4_0];
+        testcase(
+            std::string("A-1 clawback boundary invariant") +
+            (fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
+
+        std::array const kAmounts{1, 7, 99, 333, 993, 2000};
+
+        Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
+        auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false, /*allowClawback=*/true);
+        if (!f.asset || !f.broker)
+        {
+            BEAST_EXPECT(f.asset && f.broker);
+            return;
+        }
+        auto const& asset = *f.asset;
+
+        Vault const v{env};
+
+        // Give the depositor a stake so that the issuer has something to
+        // claw back.
+        env(v.deposit(
+                {.depositor = f.depositor,
+                 .id = f.vaultKeylet.key,
+                 .amount = asset(2'000).value()}),
+            Ter(std::ignore));
+        env.close();
+
+        for (auto const amount : kAmounts)
+        {
+            auto const before = read(env, f);
+            if (before.sharesTotal == 0)
+                continue;
+
+            env(v.clawback(
+                    {.issuer = f.issuer,
+                     .id = f.vaultKeylet.key,
+                     .holder = f.depositor,
+                     .amount = asset(amount).value()}),
+                Ter(std::ignore));
+            env.close();
+
+            TER const actual = env.ter();
+
+            if (fixEnabled)
+            {
+                BEAST_EXPECTS(
+                    actual != tecINVARIANT_FAILED,
+                    "amount=" + std::to_string(amount) + " unexpected invariant failure");
+            }
+            // Pre-fix behaviour is fixture-dependent: some clawback amounts
+            // may succeed even without the amendment.  The important
+            // property is that post-fix no legitimate clawback is rejected
+            // by the widened invariant.
+        }
+
+        // Owner force-burn only succeeds against an EMPTY vault (see
+        // VaultClawback::preclaim).  Our fixture keeps a live loan, so
+        // this must return tecNO_PERMISSION regardless of the amendment.
+        env(v.clawback({.issuer = f.lender, .id = f.vaultKeylet.key, .holder = f.depositor}),
+            Ter(tecNO_PERMISSION));
+        env.close();
+    }
+
+    // Deposit into an A-3 vault where the impaired-loan gap plus the
+    // interest earned from the sibling repayment lands L > (T - A) by
+    // sub-ULP.  Pre-fix the loss invariant fires; post-fix it does not.
+    void
+    testLossInvariantA3(FeatureBitset features)
+    {
+        using namespace jtx;
+
+        bool const fixEnabled = features[fixCleanup3_4_0];
+        testcase(
+            std::string("A-3 loss invariant sweep") +
+            (fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
+
+        std::array const kAmounts{1, 7, 10'000'000};
+
+        for (auto const amount : kAmounts)
+        {
+            Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
+            auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/true);
+            if (!f.asset || !f.broker)
+            {
+                BEAST_EXPECT(f.asset && f.broker);
+                continue;
+            }
+            auto const& asset = *f.asset;
+
+            Vault const v{env};
+            env(v.deposit(
+                    {.depositor = f.depositor,
+                     .id = f.vaultKeylet.key,
+                     .amount = asset(amount).value()}),
+                Ter(std::ignore));
+            env.close();
+
+            TER const actual = env.ter();
+
+            if (fixEnabled)
+            {
+                BEAST_EXPECTS(
+                    actual == tesSUCCESS,
+                    "amount=" + std::to_string(amount) + " expected tesSUCCESS, got " +
+                        transToken(actual));
+
+                auto const after = read(env, f);
+                BEAST_EXPECT(
+                    after.lossUnrealized <= (after.assetsTotal - after.assetsAvailable) +
+                        oneUnit(asset, after.assetsTotal));
+            }
+            else
+            {
+                BEAST_EXPECTS(
+                    actual == tecINVARIANT_FAILED,
+                    "amount=" + std::to_string(amount) + " expected tecINVARIANT_FAILED, got " +
+                        transToken(actual));
+            }
+        }
+    }
+
+    // Full 17-magnitude A-1 deposit sweep.  Pre-fix {1, 7, 10'000'000}
+    // are the boundary amounts that fail; post-fix every amount succeeds.
+    void
+    testA1DepositMagnitudes(FeatureBitset features)
+    {
+        using namespace jtx;
+
+        bool const fixEnabled = features[fixCleanup3_4_0];
+        testcase(
+            std::string("A-1 deposit magnitude sweep") +
+            (fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
+
+        std::array const kAmounts{
+            1,
+            2,
+            5,
+            7,
+            10,
+            50,
+            100,
+            500,
+            1'000,
+            5'000,
+            10'000,
+            50'000,
+            100'000,
+            500'000,
+            1'000'000,
+            5'000'000,
+            10'000'000};
+        std::array const kPreFixFailures{1, 7, 10'000'000};
+
+        for (auto const amount : kAmounts)
+        {
+            Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
+            auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
+            if (!f.asset || !f.broker)
+            {
+                BEAST_EXPECT(f.asset && f.broker);
+                continue;
+            }
+            auto const& asset = *f.asset;
+
+            Vault const v{env};
+            env(v.deposit(
+                    {.depositor = f.depositor,
+                     .id = f.vaultKeylet.key,
+                     .amount = asset(amount).value()}),
+                Ter(std::ignore));
+            env.close();
+
+            TER const actual = env.ter();
+
+            if (fixEnabled)
+            {
+                BEAST_EXPECTS(
+                    actual == tesSUCCESS,
+                    "amount=" + std::to_string(amount) + " expected tesSUCCESS, got " +
+                        transToken(actual));
+            }
+            else
+            {
+                bool const shouldFail =
+                    std::ranges::find(kPreFixFailures, amount) != kPreFixFailures.end();
+                if (shouldFail)
+                {
+                    BEAST_EXPECTS(
+                        actual == tecINVARIANT_FAILED,
+                        "pre-fix amount=" + std::to_string(amount) +
+                            " expected tecINVARIANT_FAILED, got " + transToken(actual));
+                }
+                // For other amounts pre-fix, we accept any outcome; the
+                // interesting property is only asserted for the known-failing
+                // ones.
+            }
+        }
+    }
+
+    // A-3 deposit sweep.  Pre-fix {1, 7, 10'000, 10'000'000} fail; post-fix
+    // every amount succeeds.  99'999 (delta tolerance) and 10'000'000
+    // (loss tolerance) are the two boundary cases that motivate this PR.
+    void
+    testA3DepositMagnitudes(FeatureBitset features)
+    {
+        using namespace jtx;
+
+        bool const fixEnabled = features[fixCleanup3_4_0];
+        testcase(
+            std::string("A-3 deposit magnitude sweep") +
+            (fixEnabled ? " (fixCleanup3_4_0)" : " (pre-fix)"));
+
+        std::array const kAmounts{
+            1, 7, 100, 1'000, 10'000, 100'000, 1'000'000, 10'000'000, 99'999};
+
+        std::array const kPreFixFailures{1, 7, 10'000, 10'000'000};
+
+        for (auto const amount : kAmounts)
+        {
+            Env env{*this, envconfig(), features, nullptr, beast::Severity::Disabled};
+            auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/true);
+            if (!f.asset || !f.broker)
+            {
+                BEAST_EXPECT(f.asset && f.broker);
+                continue;
+            }
+            auto const& asset = *f.asset;
+
+            Vault const v{env};
+            env(v.deposit(
+                    {.depositor = f.depositor,
+                     .id = f.vaultKeylet.key,
+                     .amount = asset(amount).value()}),
+                Ter(std::ignore));
+            env.close();
+
+            TER const actual = env.ter();
+
+            if (fixEnabled)
+            {
+                BEAST_EXPECTS(
+                    actual == tesSUCCESS,
+                    "amount=" + std::to_string(amount) + " expected tesSUCCESS, got " +
+                        transToken(actual));
+            }
+            else
+            {
+                bool const shouldFail =
+                    std::ranges::find(kPreFixFailures, amount) != kPreFixFailures.end();
+                if (shouldFail)
+                {
+                    BEAST_EXPECTS(
+                        actual == tecINVARIANT_FAILED,
+                        "pre-fix amount=" + std::to_string(amount) +
+                            " expected tecINVARIANT_FAILED, got " + transToken(actual));
+                }
+            }
+        }
+    }
+
+public:
+    void
+    run() override
+    {
+        for (auto const& features : {all_ - fixCleanup3_4_0, all_})
+        {
+            testDepositBoundaryInvariant(features);
+            testWithdrawBoundaryInvariant(features);
+            testClawbackBoundaryInvariant(features);
+            testLossInvariantA3(features);
+            testA1DepositMagnitudes(features);
+            testA3DepositMagnitudes(features);
+        }
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(VaultInvariantPrecision, app, xrpl);
+
+}  // namespace xrpl::test
diff --git a/src/test/app/vault/VaultLifecycle_test.cpp b/src/test/app/vault/VaultLifecycle_test.cpp
new file mode 100644
index 0000000000..ce91ca857a
--- /dev/null
+++ b/src/test/app/vault/VaultLifecycle_test.cpp
@@ -0,0 +1,1776 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class VaultLifecycle_test : public VaultTestBase
+{
+private:
+    void
+    testSequences()
+    {
+        using namespace test::jtx;
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const depositor{"depositor"};
+        Account const charlie{"charlie"};  // authorized 3rd party
+        Account const dave{"dave"};
+
+        auto const testSequence = [&, this](
+                                      std::string const& prefix,
+                                      Env& env,
+                                      Vault& vault,
+                                      PrettyAsset const& asset) {
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            tx[sfData] = "AFEED00E";
+            tx[sfAssetsMaximum] = asset(100).number();
+            env(tx);
+            env.close();
+            BEAST_EXPECT(env.le(keylet));
+            std::uint64_t const scale = asset.raw().holds() ? 1 : 1e6;
+
+            auto const [share, vaultAccount] =
+                [&env, keylet = keylet, asset, this]() -> std::tuple {
+                auto const vault = env.le(keylet);
+                BEAST_EXPECT(vault != nullptr);
+                if (!asset.integral())
+                {
+                    BEAST_EXPECT(vault->at(sfScale) == 6);
+                }
+                else
+                {
+                    BEAST_EXPECT(vault->at(sfScale) == 0);
+                }
+                auto const shares = env.le(keylet::mptokenIssuance(vault->at(sfShareMPTID)));
+                BEAST_EXPECT(shares != nullptr);
+                if (!asset.integral())
+                {
+                    BEAST_EXPECT(shares->at(sfAssetScale) == 6);
+                }
+                else
+                {
+                    BEAST_EXPECT(shares->at(sfAssetScale) == 0);
+                }
+                return {MPTIssue(vault->at(sfShareMPTID)), Account("vault", vault->at(sfAccount))};
+            }();
+            auto const shares = share.raw().get();
+            env.memoize(vaultAccount);
+
+            // Several 3rd party accounts which cannot receive funds
+            Account const alice{"alice"};
+            Account const erin{"erin"};  // not authorized by issuer
+            env.fund(XRP(1000), alice, erin);
+            env(fset(alice, asfDepositAuth));
+            env.close();
+
+            {
+                testcase(prefix + " fail to deposit more than assets held");
+                auto tx = vault.deposit(
+                    {.depositor = depositor, .id = keylet.key, .amount = asset(10000)});
+                env(tx, Ter(tecINSUFFICIENT_FUNDS));
+                env.close();
+            }
+
+            {
+                testcase(prefix + " deposit non-zero amount");
+                auto tx =
+                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(depositor, shares) == share(50 * scale));
+            }
+
+            {
+                testcase(prefix + " deposit non-zero amount again");
+                auto tx =
+                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(depositor, shares) == share(100 * scale));
+            }
+
+            {
+                testcase(prefix + " fail to delete non-empty vault");
+                auto tx = vault.del({.owner = owner, .id = keylet.key});
+                env(tx, Ter(tecHAS_OBLIGATIONS));
+                env.close();
+            }
+
+            {
+                testcase(prefix + " fail to update because wrong owner");
+                auto tx = vault.set({.owner = issuer, .id = keylet.key});
+                tx[sfAssetsMaximum] = asset(50).number();
+                env(tx, Ter(tecNO_PERMISSION));
+                env.close();
+            }
+
+            {
+                testcase(prefix + " fail to set maximum lower than current amount");
+                auto tx = vault.set({.owner = owner, .id = keylet.key});
+                tx[sfAssetsMaximum] = asset(50).number();
+                env(tx, Ter(tecLIMIT_EXCEEDED));
+                env.close();
+            }
+
+            {
+                testcase(prefix + " set maximum higher than current amount");
+                auto tx = vault.set({.owner = owner, .id = keylet.key});
+                tx[sfAssetsMaximum] = asset(150).number();
+                env(tx);
+                env.close();
+            }
+
+            {
+                testcase(prefix + " set maximum is idempotent, set it again");
+                auto tx = vault.set({.owner = owner, .id = keylet.key});
+                tx[sfAssetsMaximum] = asset(150).number();
+                env(tx);
+                env.close();
+            }
+
+            {
+                testcase(prefix + " set data");
+                auto tx = vault.set({.owner = owner, .id = keylet.key});
+                tx[sfData] = "0";
+                env(tx);
+                env.close();
+            }
+
+            {
+                testcase(prefix + " fail to set domain on public vault");
+                auto tx = vault.set({.owner = owner, .id = keylet.key});
+                tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
+                env(tx, Ter{tecNO_PERMISSION});
+                env.close();
+            }
+
+            {
+                testcase(prefix + " fail to deposit more than maximum");
+                auto tx =
+                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+                env(tx, Ter(tecLIMIT_EXCEEDED));
+                env.close();
+            }
+
+            {
+                testcase(prefix + " reset maximum to zero i.e. not enforced");
+                auto tx = vault.set({.owner = owner, .id = keylet.key});
+                tx[sfAssetsMaximum] = asset(0).number();
+                env(tx);
+                env.close();
+            }
+
+            {
+                testcase(prefix + " fail to withdraw more than assets held");
+                auto tx = vault.withdraw(
+                    {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
+                env(tx, Ter(tecINSUFFICIENT_FUNDS));
+                env.close();
+            }
+
+            {
+                testcase(prefix + " deposit some more");
+                auto tx =
+                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(depositor, shares) == share(200 * scale));
+            }
+
+            {
+                testcase(prefix + " clawback some");
+                auto code = asset.raw().native() ? Ter(temMALFORMED) : Ter(tesSUCCESS);
+                auto tx = vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(10)});
+                env(tx, code);
+                env.close();
+                if (!asset.raw().native())
+                {
+                    BEAST_EXPECT(env.balance(depositor, shares) == share(190 * scale));
+                }
+            }
+
+            {
+                testcase(prefix + " clawback all");
+                auto code = asset.raw().native() ? Ter(tecNO_PERMISSION) : Ter(tesSUCCESS);
+                auto tx = vault.clawback({.issuer = issuer, .id = keylet.key, .holder = depositor});
+                env(tx, code);
+                env.close();
+                if (!asset.raw().native())
+                {
+                    BEAST_EXPECT(env.balance(depositor, shares) == share(0));
+
+                    {
+                        auto tx = vault.clawback(
+                            {.issuer = issuer,
+                             .id = keylet.key,
+                             .holder = depositor,
+                             .amount = asset(10)});
+                        env(tx, Ter{tecPRECISION_LOSS});
+                        env.close();
+                    }
+
+                    {
+                        auto tx = vault.withdraw(
+                            {.depositor = depositor, .id = keylet.key, .amount = asset(10)});
+                        env(tx, Ter{tecPRECISION_LOSS});
+                        env.close();
+                    }
+                }
+            }
+
+            if (!asset.raw().native())
+            {
+                testcase(prefix + " deposit again");
+                auto tx =
+                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(200)});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(depositor, shares) == share(200 * scale));
+            }
+            else
+            {
+                testcase(prefix + " deposit/withdrawal same or less than fee");
+                auto const amount = env.current()->fees().base;
+
+                auto tx =
+                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount});
+                env(tx);
+                env.close();
+
+                tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = amount});
+                env(tx);
+                env.close();
+
+                tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount});
+                env(tx);
+                env.close();
+
+                // Withdraw to 3rd party
+                tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = amount});
+                tx[sfDestination] = charlie.human();
+                env(tx);
+                env.close();
+
+                tx =
+                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = amount - 1});
+                env(tx);
+                env.close();
+
+                tx = vault.withdraw(
+                    {.depositor = depositor, .id = keylet.key, .amount = amount - 1});
+                env(tx);
+                env.close();
+            }
+
+            {
+                testcase(prefix + " fail to withdraw to 3rd party lsfDepositAuth");
+                auto tx = vault.withdraw(
+                    {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+                tx[sfDestination] = alice.human();
+                env(tx, Ter{tecNO_PERMISSION});
+                env.close();
+            }
+
+            {
+                testcase(prefix + " fail to withdraw to zero destination");
+                auto tx = vault.withdraw(
+                    {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
+                tx[sfDestination] = "0";
+                env(tx, Ter(temMALFORMED));
+                env.close();
+            }
+
+            if (!asset.raw().native())
+            {
+                testcase(prefix + " fail to withdraw to 3rd party no authorization");
+                auto tx = vault.withdraw(
+                    {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+                tx[sfDestination] = erin.human();
+                env(tx, Ter{asset.raw().holds() ? tecNO_LINE : tecNO_AUTH});
+                env.close();
+            }
+
+            {
+                testcase(prefix + " fail to withdraw to 3rd party lsfRequireDestTag");
+                auto tx = vault.withdraw(
+                    {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+                tx[sfDestination] = dave.human();
+                env(tx, Ter{tecDST_TAG_NEEDED});
+                env.close();
+            }
+
+            {
+                testcase(prefix + " withdraw to 3rd party lsfRequireDestTag");
+                auto tx =
+                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+                tx[sfDestination] = dave.human();
+                tx[sfDestinationTag] = "0";
+                env(tx);
+                env.close();
+            }
+
+            {
+                testcase(prefix + " deposit again");
+                auto tx = vault.deposit({.depositor = dave, .id = keylet.key, .amount = asset(50)});
+                env(tx);
+                env.close();
+            }
+
+            {
+                testcase(prefix + " fail to withdraw lsfRequireDestTag");
+                auto tx =
+                    vault.withdraw({.depositor = dave, .id = keylet.key, .amount = asset(50)});
+                env(tx, Ter{tecDST_TAG_NEEDED});
+                env.close();
+            }
+
+            {
+                testcase(prefix + " withdraw with tag");
+                auto tx =
+                    vault.withdraw({.depositor = dave, .id = keylet.key, .amount = asset(50)});
+                tx[sfDestinationTag] = "0";
+                env(tx);
+                env.close();
+            }
+
+            {
+                testcase(prefix + " withdraw to authorized 3rd party");
+                auto tx =
+                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+                tx[sfDestination] = charlie.human();
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(depositor, shares) == share(100 * scale));
+            }
+
+            {
+                testcase(prefix + " withdraw to issuer");
+                auto tx =
+                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+                tx[sfDestination] = issuer.human();
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(depositor, shares) == share(50 * scale));
+            }
+
+            if (!asset.raw().native())
+            {
+                testcase(prefix + " issuer deposits");
+                auto tx =
+                    vault.deposit({.depositor = issuer, .id = keylet.key, .amount = asset(10)});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(issuer, shares) == share(10 * scale));
+
+                testcase(prefix + " issuer withdraws");
+                tx = vault.withdraw(
+                    {.depositor = issuer, .id = keylet.key, .amount = share(10 * scale)});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(issuer, shares) == share(0 * scale));
+            }
+
+            {
+                testcase(prefix + " withdraw remaining assets");
+                auto tx =
+                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(50)});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(depositor, shares) == share(0));
+
+                if (!asset.raw().native())
+                {
+                    auto tx = vault.clawback(
+                        {.issuer = issuer,
+                         .id = keylet.key,
+                         .holder = depositor,
+                         .amount = asset(0)});
+                    env(tx, Ter{tecPRECISION_LOSS});
+                    env.close();
+                }
+
+                {
+                    auto tx = vault.withdraw(
+                        {.depositor = depositor, .id = keylet.key, .amount = share(10)});
+                    env(tx, Ter{tecINSUFFICIENT_FUNDS});
+                    env.close();
+                }
+            }
+
+            if (!asset.integral())
+            {
+                testcase(prefix + " temporary authorization for 3rd party");
+                env(trust(erin, asset(1000)));
+                env(trust(issuer, asset(0), erin, tfSetfAuth));
+                env(pay(issuer, erin, asset(10)));
+
+                // Erin deposits all in vault, then sends shares to depositor
+                auto tx = vault.deposit({.depositor = erin, .id = keylet.key, .amount = asset(10)});
+                env(tx);
+                env.close();
+                {
+                    auto tx = pay(erin, depositor, share(10 * scale));
+
+                    // depositor no longer has MPToken for shares
+                    env(tx, Ter{tecNO_AUTH});
+                    env.close();
+
+                    // depositor will gain MPToken for shares again
+                    env(vault.deposit(
+                        {.depositor = depositor, .id = keylet.key, .amount = asset(1)}));
+                    env.close();
+
+                    env(tx);
+                    env.close();
+                }
+
+                testcase(prefix + " withdraw to authorized 3rd party");
+                // Depositor withdraws assets, destined to Erin
+                tx =
+                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
+                tx[sfDestination] = erin.human();
+                env(tx);
+                env.close();
+
+                // Erin returns assets to issuer
+                env(pay(erin, issuer, asset(10)));
+                env.close();
+
+                testcase(prefix + " fail to pay to unauthorized 3rd party");
+                env(trust(erin, asset(0)));
+                env.close();
+
+                // Erin has MPToken but is no longer authorized to hold assets
+                env(pay(depositor, erin, share(1)), Ter{tecNO_LINE});
+                env.close();
+
+                // Depositor withdraws remaining single asset
+                tx = vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)});
+                env(tx);
+                env.close();
+            }
+
+            {
+                testcase(prefix + " fail to delete because wrong owner");
+                auto tx = vault.del({.owner = issuer, .id = keylet.key});
+                env(tx, Ter(tecNO_PERMISSION));
+                env.close();
+            }
+
+            {
+                testcase(prefix + " delete empty vault");
+                auto tx = vault.del({.owner = owner, .id = keylet.key});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(!env.le(keylet));
+            }
+        };
+
+        auto testCases = [&, this](
+                             std::string prefix, std::function setup) {
+            Env env{*this, testableAmendments()};
+
+            Vault vault{env};
+            env.fund(XRP(1000), issuer, owner, depositor, charlie, dave);
+            env.close();
+            env(fset(issuer, asfAllowTrustLineClawback));
+            env(fset(issuer, asfRequireAuth));
+            env(fset(dave, asfRequireDest));
+            env.close();
+            env.require(Flags(issuer, asfAllowTrustLineClawback));
+            env.require(Flags(issuer, asfRequireAuth));
+
+            PrettyAsset const asset = setup(env);
+            testSequence(prefix, env, vault, asset);
+        };
+
+        testCases("XRP", [&](Env& env) -> PrettyAsset { return {xrpIssue(), 1'000'000}; });
+
+        testCases("IOU", [&](Env& env) -> Asset {
+            PrettyAsset const asset = issuer["IOU"];
+            env(trust(owner, asset(1000)));
+            env(trust(depositor, asset(1000)));
+            env(trust(charlie, asset(1000)));
+            env(trust(dave, asset(1000)));
+            env(trust(issuer, asset(0), owner, tfSetfAuth));
+            env(trust(issuer, asset(0), depositor, tfSetfAuth));
+            env(trust(issuer, asset(0), charlie, tfSetfAuth));
+            env(trust(issuer, asset(0), dave, tfSetfAuth));
+            env(pay(issuer, depositor, asset(1000)));
+            env.close();
+            return asset;
+        });
+
+        testCases("MPT", [&](Env& env) -> Asset {
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
+            PrettyAsset const asset = mptt.issuanceID();
+            mptt.authorize({.account = depositor});
+            mptt.authorize({.account = charlie});
+            mptt.authorize({.account = dave});
+            env(pay(issuer, depositor, asset(1000)));
+            env.close();
+            return asset;
+        });
+    }
+
+    void
+    testWithMPT()
+    {
+        using namespace test::jtx;
+
+        struct CaseArgs
+        {
+            bool enableClawback = true;
+            bool requireAuth = true;
+            int initialXRP = 1000;
+            FeatureBitset features = testableAmendments();
+        };
+
+        auto testCase = [this](
+                            std::function test,
+                            CaseArgs args = {}) {
+            Env env{*this, args.features};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            Account const depositor{"depositor"};
+            env.fund(XRP(args.initialXRP), issuer, owner, depositor);
+            env.close();
+            Vault vault{env};
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            auto const kNone = LedgerSpecificFlags(0);
+            mptt.create(
+                {.flags = tfMPTCanTransfer | tfMPTCanLock |
+                     (args.enableClawback ? tfMPTCanClawback : kNone) |
+                     (args.requireAuth ? tfMPTRequireAuth : kNone)});
+            PrettyAsset const asset = mptt.issuanceID();
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = depositor});
+            if (args.requireAuth)
+            {
+                mptt.authorize({.account = issuer, .holder = owner});
+                mptt.authorize({.account = issuer, .holder = depositor});
+            }
+
+            env(pay(issuer, depositor, asset(1000)));
+            env.close();
+
+            test(env, issuer, owner, depositor, asset, vault, mptt);
+        };
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     PrettyAsset const& asset,
+                     Vault& vault,
+                     MPTTester& mptt) {
+            testcase("MPT nothing to clawback from");
+            auto tx = vault.clawback(
+                {.issuer = issuer,
+                 .id = keylet::skip().key,
+                 .holder = depositor,
+                 .amount = asset(10)});
+            env(tx, Ter(tecNO_ENTRY));
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault,
+                     MPTTester& mptt) {
+            testcase("MPT global lock blocks create");
+            mptt.set({.account = issuer, .flags = tfMPTLock});
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx, Ter(tecLOCKED));
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     PrettyAsset const& asset,
+                     Vault& vault,
+                     MPTTester& mptt) {
+            testcase("MPT only issuer can clawback");
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+            env(tx);
+            env.close();
+
+            {
+                auto tx = vault.clawback({
+                    .issuer = depositor,
+                    .id = keylet.key,
+                    .holder = depositor,
+                });
+                env(tx, Ter(tecNO_PERMISSION));
+            }
+
+            {
+                auto tx = vault.clawback({
+                    .issuer = owner,
+                    .id = keylet.key,
+                    .holder = depositor,
+                });
+                env(tx, Ter(tecNO_PERMISSION));
+            }
+        });
+
+        testCase(
+            [this](
+                Env& env,
+                Account const& issuer,
+                Account const& owner,
+                Account const& depositor,
+                PrettyAsset const& asset,
+                Vault& vault,
+                MPTTester& mptt) {
+                testcase("MPT depositor without MPToken, auth required");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                env(tx);
+                env.close();
+
+                tx = vault.deposit(
+                    {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
+                env(tx);
+                env.close();
+
+                {
+                    // Remove depositor MPToken and it will not be re-created
+                    mptt.authorize({.account = depositor, .flags = tfMPTUnauthorize});
+                    env.close();
+
+                    auto const mptoken = keylet::mptoken(mptt.issuanceID(), depositor);
+                    auto const sleMPT1 = env.le(mptoken);
+                    BEAST_EXPECT(sleMPT1 == nullptr);
+
+                    tx = vault.withdraw(
+                        {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+                    env(tx, Ter{tecNO_AUTH});
+                    env.close();
+
+                    auto const sleMPT2 = env.le(mptoken);
+                    BEAST_EXPECT(sleMPT2 == nullptr);
+                }
+
+                {
+                    // Set destination to 3rd party without MPToken
+                    Account const charlie{"charlie"};
+                    env.fund(XRP(1000), charlie);
+                    env.close();
+
+                    tx = vault.withdraw(
+                        {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+                    tx[sfDestination] = charlie.human();
+                    env(tx, Ter(tecNO_AUTH));
+                }
+            },
+            {.requireAuth = true});
+
+        testCase(
+            [this](
+                Env& env,
+                Account const& issuer,
+                Account const& owner,
+                Account const& depositor,
+                PrettyAsset const& asset,
+                Vault& vault,
+                MPTTester& mptt) {
+                testcase("MPT depositor without MPToken, no auth required");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                env(tx);
+                env.close();
+                auto v = env.le(keylet);
+                BEAST_EXPECT(v);
+
+                tx = vault.deposit(
+                    {.depositor = depositor,
+                     .id = keylet.key,
+                     .amount = asset(1000)});  // all assets held by depositor
+                env(tx);
+                env.close();
+
+                {
+                    // Remove depositor's MPToken and it will be re-created
+                    mptt.authorize({.account = depositor, .flags = tfMPTUnauthorize});
+                    env.close();
+
+                    auto const mptoken = keylet::mptoken(mptt.issuanceID(), depositor);
+                    auto const sleMPT1 = env.le(mptoken);
+                    BEAST_EXPECT(sleMPT1 == nullptr);
+
+                    tx = vault.withdraw(
+                        {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+                    env(tx);
+                    env.close();
+
+                    auto const sleMPT2 = env.le(mptoken);
+                    BEAST_EXPECT(sleMPT2 != nullptr);
+                    BEAST_EXPECT(sleMPT2->at(sfMPTAmount) == 100);
+                }
+
+                {
+                    // Remove 3rd party MPToken and it will not be re-created
+                    mptt.authorize({.account = owner, .flags = tfMPTUnauthorize});
+                    env.close();
+
+                    auto const mptoken = keylet::mptoken(mptt.issuanceID(), owner);
+                    auto const sleMPT1 = env.le(mptoken);
+                    BEAST_EXPECT(sleMPT1 == nullptr);
+
+                    tx = vault.withdraw(
+                        {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+                    tx[sfDestination] = owner.human();
+                    env(tx, Ter(tecNO_AUTH));
+                    env.close();
+
+                    auto const sleMPT2 = env.le(mptoken);
+                    BEAST_EXPECT(sleMPT2 == nullptr);
+                }
+            },
+            {.requireAuth = false});
+
+        auto const [acctReserve, incReserve] = [this]() -> std::pair {
+            Env const env{*this, testableAmendments()};
+            return {
+                env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(),
+                env.current()->fees().increment.drops() / kDropsPerXrp.drops()};
+        }();
+
+        testCase(
+            [&, this](
+                Env& env,
+                Account const& issuer,
+                Account const& owner,
+                Account const& depositor,
+                PrettyAsset const& asset,
+                Vault& vault,
+                MPTTester& mptt) {
+                testcase("MPT fail reserve to re-create MPToken");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                env(tx);
+                env.close();
+                auto v = env.le(keylet);
+                BEAST_EXPECT(v);
+
+                env(pay(depositor, owner, asset(1000)));
+                env.close();
+
+                tx = vault.deposit(
+                    {.depositor = owner,
+                     .id = keylet.key,
+                     .amount = asset(1000)});  // all assets held by owner
+                env(tx);
+                env.close();
+
+                {
+                    // Remove owners's MPToken and it will not be re-created
+                    mptt.authorize({.account = owner, .flags = tfMPTUnauthorize});
+                    env.close();
+
+                    auto const mptoken = keylet::mptoken(mptt.issuanceID(), owner);
+                    auto const sleMPT = env.le(mptoken);
+                    BEAST_EXPECT(sleMPT == nullptr);
+
+                    // Use one reserve so the next transaction fails
+                    env(ticket::create(owner, 1));
+                    env.close();
+
+                    // No reserve to create MPToken for asset in VaultWithdraw
+                    tx = vault.withdraw(
+                        {.depositor = owner, .id = keylet.key, .amount = asset(100)});
+                    env(tx, Ter{tecINSUFFICIENT_RESERVE});
+                    env.close();
+
+                    env(pay(depositor, owner, XRP(incReserve)));
+                    env.close();
+
+                    // Withdraw can now create asset MPToken, tx will succeed
+                    env(tx);
+                    env.close();
+                }
+            },
+            {.requireAuth = false, .initialXRP = acctReserve + (incReserve * 4) + 1});
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     PrettyAsset const& asset,
+                     Vault& vault,
+                     MPTTester& mptt) {
+            testcase("MPT issuance deleted");
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
+            env(tx);
+            env.close();
+
+            {
+                auto tx = vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
+                env(tx);
+            }
+
+            mptt.destroy({.issuer = issuer, .id = mptt.issuanceID()});
+            env.close();
+
+            {
+                auto [tx, keylet] = vault.create({.owner = depositor, .asset = asset});
+                env(tx, Ter{tecOBJECT_NOT_FOUND});
+            }
+
+            {
+                auto tx =
+                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
+                env(tx, Ter{tecOBJECT_NOT_FOUND});
+            }
+
+            {
+                auto tx =
+                    vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(10)});
+                env(tx, Ter{tecOBJECT_NOT_FOUND});
+            }
+
+            {
+                auto tx = vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
+                env(tx, Ter{tecOBJECT_NOT_FOUND});
+            }
+
+            env(vault.del({.owner = owner, .id = keylet.key}));
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     PrettyAsset const& asset,
+                     Vault& vault,
+                     MPTTester& mptt) {
+            testcase("MPT vault owner can receive shares unless unauthorized");
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
+            env(tx);
+            env.close();
+
+            auto const issuanceId = [&env](xrpl::Keylet keylet) -> MPTID {
+                auto const vault = env.le(keylet);
+                return vault->at(sfShareMPTID);
+            }(keylet);
+            PrettyAsset const shares = MPTIssue(issuanceId);
+
+            {
+                // owner has MPToken for shares they did not explicitly create
+                env(pay(depositor, owner, shares(1)));
+                env.close();
+
+                tx = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = shares(1)});
+                env(tx);
+                env.close();
+
+                // owner's MPToken for vault shares not destroyed by withdraw
+                env(pay(depositor, owner, shares(1)));
+                env.close();
+
+                tx = vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)});
+                env(tx);
+                env.close();
+
+                // owner's MPToken for vault shares not destroyed by clawback
+                env(pay(depositor, owner, shares(1)));
+                env.close();
+
+                // pay back, so we can destroy owner's MPToken now
+                env(pay(owner, depositor, shares(1)));
+                env.close();
+
+                {
+                    // explicitly destroy vault owners MPToken with zero balance
+                    json::Value jv;
+                    jv[sfAccount] = owner.human();
+                    jv[sfMPTokenIssuanceID] = to_string(issuanceId);
+                    jv[sfFlags] = tfMPTUnauthorize;
+                    jv[sfTransactionType] = jss::MPTokenAuthorize;
+                    env(jv);
+                    env.close();
+                }
+
+                // owner no longer has MPToken for vault shares
+                tx = pay(depositor, owner, shares(1));
+                env(tx, Ter{tecNO_AUTH});
+                env.close();
+
+                // destroy all remaining shares, so we can delete vault
+                tx = vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)});
+                env(tx);
+                env.close();
+
+                // will soft fail destroying MPToken for vault owner
+                env(vault.del({.owner = owner, .id = keylet.key}));
+                env.close();
+            }
+        });
+
+        testCase(
+            [this](
+                Env& env,
+                Account const& issuer,
+                Account const& owner,
+                Account const& depositor,
+                PrettyAsset const& asset,
+                Vault& vault,
+                MPTTester& mptt) {
+                testcase("MPT clawback disabled");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                env(tx);
+                env.close();
+
+                tx = vault.deposit(
+                    {.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
+                env(tx);
+                env.close();
+
+                {
+                    auto tx = vault.clawback(
+                        {.issuer = issuer,
+                         .id = keylet.key,
+                         .holder = depositor,
+                         .amount = asset(0)});
+                    env(tx, Ter{tecNO_PERMISSION});
+                }
+            },
+            {.enableClawback = false});
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault,
+                     MPTTester& mptt) {
+            testcase("MPT un-authorization");
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+            tx = vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)});
+            env(tx);
+            env.close();
+
+            mptt.authorize({.account = issuer, .holder = depositor, .flags = tfMPTUnauthorize});
+            env.close();
+
+            {
+                auto tx = vault.withdraw(
+                    {.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+                env(tx, Ter(tecNO_AUTH));
+
+                // Withdrawal to other (authorized) accounts works
+                tx[sfDestination] = issuer.human();
+                env(tx);
+                env.close();
+
+                tx[sfDestination] = owner.human();
+                env(tx);
+                env.close();
+            }
+
+            {
+                // Cannot deposit some more
+                auto tx =
+                    vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(100)});
+                env(tx, Ter(tecNO_AUTH));
+            }
+
+            {
+                // Cannot clawback if issuer is the holder
+                tx = vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = issuer, .amount = asset(800)});
+                env(tx, Ter(tecNO_PERMISSION));
+            }
+            // Clawback works
+            tx = vault.clawback(
+                {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(800)});
+            env(tx);
+            env.close();
+
+            env(vault.del({.owner = owner, .id = keylet.key}));
+        });
+
+        {
+            testcase("MPT shares to a vault");
+
+            Env env{*this, testableAmendments()};
+            Account const owner{"owner"};
+            Account const issuer{"issuer"};
+            env.fund(XRP(1000000), owner, issuer);
+            env.close();
+            Vault const vault{env};
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create(
+                {.flags = tfMPTCanTransfer | tfMPTCanLock | lsfMPTCanClawback | tfMPTRequireAuth});
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = issuer, .holder = owner});
+            PrettyAsset const asset = mptt.issuanceID();
+            env(pay(issuer, owner, asset(100)));
+            auto [tx1, k1] = vault.create({.owner = owner, .asset = asset});
+            env(tx1);
+            env.close();
+
+            auto const shares = [&env, keylet = k1, this]() -> Asset {
+                auto const vault = env.le(keylet);
+                BEAST_EXPECT(vault != nullptr);
+                return MPTIssue(vault->at(sfShareMPTID));
+            }();
+
+            auto [tx2, k2] = vault.create({.owner = owner, .asset = shares});
+            env(tx2, Ter{tecWRONG_ASSET});
+            env.close();
+        }
+
+        {
+            testcase("MPT locked: vault shares inherit underlying lock");
+
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+            Account const carol{"carol"};
+            env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
+            env.close();
+            Vault const vault{env};
+
+            MPTTester asset{
+                {.env = env,
+                 .issuer = issuer,
+                 .holders = {owner, alice, bob, carol},
+                 .flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock}};
+            env(pay(issuer, alice, asset(1'000)));
+            env(pay(issuer, bob, asset(1'000)));
+            env.close();
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
+            // Bob also deposits so he has a share MPToken to receive into.
+            env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
+            env.close();
+
+            auto const shares = [&]() -> PrettyAsset {
+                auto const sle = env.le(keylet);
+                BEAST_EXPECT(sle != nullptr);
+                return MPTIssue(sle->at(sfShareMPTID));
+            }();
+            auto const shareMptID = shares.raw().get().getMptID();
+            auto const shareBalance = [&](Account const& account) {
+                auto const sle = env.le(keylet::mptoken(shareMptID, account));
+                return sle ? sle->at(sfMPTAmount) : 0;
+            };
+
+            // Sanity: before the underlying lock, peer-to-peer share
+            // transfers are allowed.
+            env(pay(alice, bob, shares(1)));
+            env.close();
+
+            // Create the offer while shares are spendable, then lock the
+            // underlying to test whether a stale offer can still be crossed.
+            env(offer(alice, XRP(1), shares(1)));
+            env.close();
+
+            // Lock the underlying after the vault and share balances exist.
+            asset.set({.account = issuer, .flags = tfMPTLock});
+            env.close();
+
+            // Direct vault share payment inherits the underlying lock via
+            // sfReferenceHolding.
+            BEAST_EXPECT(shareBalance(alice) == 499);
+            BEAST_EXPECT(shareBalance(bob) == 501);
+            env(pay(alice, bob, shares(1)), Ter{tecLOCKED});
+            env.close();
+            BEAST_EXPECT(shareBalance(alice) == 499);
+            BEAST_EXPECT(shareBalance(bob) == 501);
+
+            // The same inherited lock must also block DEX payment paths that
+            // would consume an offer selling vault shares.
+            env(pay(carol, bob, shares(1)),
+                Sendmax(XRP(1)),
+                Path(BookSpec{shares.raw()}),
+                Ter{tecPATH_PARTIAL});
+            env.close();
+            BEAST_EXPECT(shareBalance(alice) == 499);
+            BEAST_EXPECT(shareBalance(bob) == 501);
+            BEAST_EXPECT(expectOffers(env, alice, 1));
+        }
+
+        {
+            testcase("MPT CanTrade governance: share inherits underlying on DEX and AMM");
+
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            Account const alice{"alice"};
+            Account const bob{"bob"};
+            env.fund(XRP(100'000), issuer, owner, alice, bob);
+            env.close();
+            Vault const vault{env};
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
+            PrettyAsset const asset = mptt.issuanceID();
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = alice});
+            mptt.authorize({.account = bob});
+            env(pay(issuer, alice, asset(10'000)));
+            env(pay(issuer, bob, asset(10'000)));
+            env.close();
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            // Seed shares so we can later place them on trading venues.
+            env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(5'000)}));
+            env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(5'000)}));
+            env.close();
+
+            auto const shares = [&]() -> PrettyAsset {
+                auto const sle = env.le(keylet);
+                BEAST_EXPECT(sle != nullptr);
+                return MPTIssue(sle->at(sfShareMPTID));
+            }();
+
+            // CanTrade is not set on the underlying, both the asset and
+            // the vault share are blocked on the DEX.
+            env(offer(alice, XRP(1), asset(10)), Ter{tecNO_PERMISSION});
+            env(offer(alice, XRP(1), shares(1)), Ter{tecNO_PERMISSION});
+            env.close();
+
+            // Deposit still works before enabling CanTrade.
+            env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(100)}));
+            env.close();
+
+            // Peer-to-peer share transfers still work (CanTransfer is set on
+            // both layers).
+            env(pay(alice, bob, shares(1)));
+            env.close();
+
+            // Withdraw still works before enabling CanTrade.
+            env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = asset(100)}));
+            env.close();
+
+            // Enable CanTrade on the underlying.
+            mptt.set({.flags = tfMPTSetCanTrade});
+            env.close();
+
+            env(offer(alice, XRP(1), asset(10)));
+            env(offer(alice, XRP(1), shares(1)));
+            env.close();
+
+            AMM const ammUnderlying(env, alice, XRP(1'000), asset(1'000));
+        }
+
+        {
+            testcase("MPT OutstandingAmount > MaximumAmount");
+
+            Env env{*this, testableAmendments() | featureSingleAssetVault};
+            Account const alice{"alice"};
+            Account const issuer{"issuer"};
+            env.fund(XRP(1'000), alice, issuer);
+            env.close();
+            Vault const vault{env};
+
+            MPTTester const btc({.env = env, .issuer = issuer, .holders = {alice}, .maxAmt = 100});
+
+            auto [tx, k] = vault.create({.owner = issuer, .asset = btc});
+            env(tx);
+            env.close();
+
+            tx = vault.deposit({.depositor = issuer, .id = k.key, .amount = btc(110)});
+            // accountHolds is the first check and the issuer has only BTC(100)
+            // available
+            env(tx, Ter{tecINSUFFICIENT_FUNDS});
+            env.close();
+
+            // OutstandingAmount == MaximumAmount
+            env(pay(issuer, alice, btc(100)));
+            env.close();
+
+            tx = vault.deposit({.depositor = issuer, .id = k.key, .amount = btc(100)});
+            // the issuer has BTC(0) available
+            env(tx, Ter{tecINSUFFICIENT_FUNDS});
+            env.close();
+
+            tx = vault.deposit({.depositor = alice, .id = k.key, .amount = btc(100)});
+            // alice transfers BTC(100), OutstandingAmount is 100
+            env(tx);
+            env.close();
+        }
+    }
+
+    void
+    testWithIOU()
+    {
+        using namespace test::jtx;
+
+        struct CaseArgs
+        {
+            int initialXRP = 1000;
+            Number initialIOU = 200;
+            double transferRate = 1.0;
+            bool charlieRipple = true;
+            FeatureBitset features = testableAmendments();
+        };
+
+        auto testCase = [&, this](
+                            std::function vaultAccount,
+                                Vault& vault,
+                                PrettyAsset const& asset,
+                                std::function issuanceId)> test,
+                            CaseArgs args = {}) {
+            Env env{*this, args.features};
+            Account const owner{"owner"};
+            Account const issuer{"issuer"};
+            Account const charlie{"charlie"};
+            Vault vault{env};
+            env.fund(XRP(args.initialXRP), issuer, owner, charlie);
+            env(fset(issuer, asfAllowTrustLineClawback));
+            env.close();
+
+            PrettyAsset const asset = issuer["IOU"];
+            env.trust(asset(1000), owner);
+            env(pay(issuer, owner, asset(args.initialIOU)));
+            env.close();
+            if (!args.charlieRipple)
+            {
+                env(fset(issuer, 0, asfDefaultRipple));
+                env.close();
+                env.trust(asset(1000), charlie);
+                env.close();
+                env(pay(issuer, charlie, asset(args.initialIOU)));
+                env.close();
+                env(fset(issuer, asfDefaultRipple));
+            }
+            else
+            {
+                env.trust(asset(1000), charlie);
+            }
+            env.close();
+            env(rate(issuer, args.transferRate));
+            env.close();
+
+            auto const vaultAccount = [&env](xrpl::Keylet keylet) -> Account {
+                return Account("vault", env.le(keylet)->at(sfAccount));
+            };
+            auto const issuanceId = [&env](xrpl::Keylet keylet) -> MPTID {
+                return env.le(keylet)->at(sfShareMPTID);
+            };
+
+            test(env, owner, issuer, charlie, vaultAccount, vault, asset, issuanceId);
+        };
+
+        testCase([&, this](
+                     Env& env,
+                     Account const& owner,
+                     Account const& issuer,
+                     Account const&,
+                     auto vaultAccount,
+                     Vault& vault,
+                     PrettyAsset const& asset,
+                     auto&&...) {
+            testcase("IOU cannot use different asset");
+            PrettyAsset const foo = issuer["FOO"];
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            {
+                // Cannot create new trustline to a vault
+                auto tx = [&, account = vaultAccount(keylet)]() {
+                    json::Value jv;
+                    jv[jss::Account] = issuer.human();
+                    {
+                        auto& ja = jv[jss::LimitAmount] =
+                            foo(0).value().getJson(JsonOptions::Values::None);
+                        ja[jss::issuer] = toBase58(account);
+                    }
+                    jv[jss::TransactionType] = jss::TrustSet;
+                    jv[jss::Flags] = tfSetFreeze;
+                    return jv;
+                }();
+                env(tx, Ter{tecNO_PERMISSION});
+                env.close();
+            }
+
+            {
+                auto tx = vault.deposit({.depositor = issuer, .id = keylet.key, .amount = foo(20)});
+                env(tx, Ter{tecWRONG_ASSET});
+                env.close();
+            }
+
+            {
+                auto tx =
+                    vault.withdraw({.depositor = issuer, .id = keylet.key, .amount = foo(20)});
+                env(tx, Ter{tecWRONG_ASSET});
+                env.close();
+            }
+
+            env(vault.del({.owner = owner, .id = keylet.key}));
+            env.close();
+        });
+
+        testCase(
+            [&, this](
+                Env& env,
+                Account const& owner,
+                Account const& issuer,
+                Account const& charlie,
+                auto vaultAccount,
+                Vault& vault,
+                PrettyAsset const& asset,
+                auto issuanceId) {
+                testcase("IOU transfer fees not applied");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                env(tx);
+                env.close();
+
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
+                env.close();
+
+                auto const issue = asset.raw().get();
+                Asset const share = Asset(issuanceId(keylet));
+
+                // transfer fees ignored on deposit
+                BEAST_EXPECT(env.balance(owner, issue) == asset(100));
+                BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(100));
+
+                {
+                    auto tx = vault.clawback(
+                        {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)});
+                    env(tx);
+                    env.close();
+                }
+
+                // transfer fees ignored on clawback
+                BEAST_EXPECT(env.balance(owner, issue) == asset(100));
+                BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(50));
+
+                env(vault.withdraw(
+                    {.depositor = owner, .id = keylet.key, .amount = share(20'000'000)}));
+
+                // transfer fees ignored on withdraw
+                BEAST_EXPECT(env.balance(owner, issue) == asset(120));
+                BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(30));
+
+                {
+                    auto tx = vault.withdraw(
+                        {.depositor = owner, .id = keylet.key, .amount = share(30'000'000)});
+                    tx[sfDestination] = charlie.human();
+                    env(tx);
+                }
+
+                // transfer fees ignored on withdraw to 3rd party
+                BEAST_EXPECT(env.balance(owner, issue) == asset(120));
+                BEAST_EXPECT(env.balance(charlie, issue) == asset(30));
+                BEAST_EXPECT(env.balance(vaultAccount(keylet), issue) == asset(0));
+
+                env(vault.del({.owner = owner, .id = keylet.key}));
+                env.close();
+            },
+            CaseArgs{.transferRate = 1.25});
+
+        testCase([&, this](
+                     Env& env,
+                     Account const& owner,
+                     Account const& issuer,
+                     Account const& charlie,
+                     auto,
+                     Vault& vault,
+                     PrettyAsset const& asset,
+                     auto&&...) {
+            testcase("IOU no trust line to 3rd party");
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
+            env.close();
+
+            Account const erin{"erin"};
+            env.fund(XRP(1000), erin);
+            env.close();
+
+            // Withdraw to 3rd party without trust line
+            auto const tx1 = [&](xrpl::Keylet keylet) {
+                auto tx =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
+                tx[sfDestination] = erin.human();
+                return tx;
+            }(keylet);
+            env(tx1, Ter{tecNO_LINE});
+        });
+
+        testCase([&, this](
+                     Env& env,
+                     Account const& owner,
+                     Account const& issuer,
+                     Account const& charlie,
+                     auto,
+                     Vault& vault,
+                     PrettyAsset const& asset,
+                     auto&&...) {
+            testcase("IOU no trust line to depositor");
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            // reset limit, so deposit of all funds will delete the trust line
+            env.trust(asset(0), owner);
+            env.close();
+
+            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(200)}));
+            env.close();
+
+            auto trustline = env.le(keylet::trustLine(owner, asset.raw().get()));
+            BEAST_EXPECT(trustline == nullptr);
+
+            // Withdraw without trust line, will succeed
+            auto const tx1 = [&](xrpl::Keylet keylet) {
+                auto tx =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
+                return tx;
+            }(keylet);
+            env(tx1);
+        });
+
+        testCase(
+            [&, this](
+                Env& env,
+                Account const& owner,
+                Account const& issuer,
+                Account const& charlie,
+                auto vaultAccount,
+                Vault& vault,
+                PrettyAsset const& asset,
+                std::function issuanceId) {
+                testcase("IOU non-transferable");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                tx[sfScale] = 0;
+                env(tx);
+                env.close();
+
+                // Turn on noripple on the pseudo account's trust line.
+                // Charlie's is already set.
+                env(trust(issuer, vaultAccount(keylet)["IOU"], tfSetNoRipple));
+
+                {
+                    // Charlie cannot deposit
+                    auto tx = vault.deposit(
+                        {.depositor = charlie, .id = keylet.key, .amount = asset(100)});
+                    env(tx, Ter{terNO_RIPPLE});
+                    env.close();
+                }
+
+                {
+                    PrettyAsset const shares = issuanceId(keylet);
+                    auto tx1 =
+                        vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)});
+                    env(tx1);
+                    env.close();
+
+                    // Charlie cannot receive funds
+                    auto tx2 = vault.withdraw(
+                        {.depositor = owner, .id = keylet.key, .amount = shares(100)});
+                    tx2[sfDestination] = charlie.human();
+                    env(tx2, Ter{terNO_RIPPLE});
+                    env.close();
+
+                    {
+                        // Create MPToken for shares held by Charlie
+                        json::Value tx{json::ValueType::Object};
+                        tx[sfAccount] = charlie.human();
+                        tx[sfMPTokenIssuanceID] =
+                            to_string(shares.raw().get().getMptID());
+                        tx[sfTransactionType] = jss::MPTokenAuthorize;
+                        env(tx);
+                        env.close();
+                    }
+                    // Behavioral shift introduced by share inheritance:
+                    // before fixCleanup3_2_0 this share Payment succeeded
+                    // and the underlying IOU's NoRipple restriction surfaced
+                    // only later on Charlie's withdrawal (terNO_RIPPLE).
+                    // Post-amendment, canTransfer reads the share's
+                    // sfReferenceHolding and dispatches to the underlying IOU;
+                    // rippling is disabled between owner and charlie so the
+                    // share payment itself is now blocked. tecPATH_DRY is
+                    // the path-find layer's translation of the underlying
+                    // terNO_RIPPLE under featureMPTokensV2.
+                    env(pay(owner, charlie, shares(100)), Ter{tecPATH_DRY});
+                    env.close();
+                }
+
+                tx = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(100)});
+                env(tx);
+                env.close();
+
+                // Delete vault with zero balance
+                env(vault.del({.owner = owner, .id = keylet.key}));
+            },
+            {.charlieRipple = false});
+
+        testCase(
+            [&, this](
+                Env& env,
+                Account const& owner,
+                Account const& issuer,
+                Account const& charlie,
+                auto const& vaultAccount,
+                Vault& vault,
+                PrettyAsset const& asset,
+                auto&&...) {
+                testcase("IOU calculation rounding");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                tx[sfScale] = 1;
+                env(tx);
+                env.close();
+
+                auto const startingOwnerBalance = env.balance(owner, asset);
+                BEAST_EXPECT((startingOwnerBalance.value() == STAmount{asset, 11875, -2}));
+
+                // This operation (first deposit 100, then 3.75 x 5) is known to
+                // have triggered calculation rounding errors in Number
+                // (addition and division), causing the last deposit to be
+                // blocked by Vault invariants.
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(100)}));
+
+                auto const tx1 = vault.deposit(
+                    {.depositor = owner, .id = keylet.key, .amount = asset(Number(375, -2))});
+                for (auto i = 0; i < 5; ++i)
+                {
+                    env(tx1);
+                }
+                env.close();
+
+                {
+                    STAmount const xfer{asset, 1185, -1};
+                    BEAST_EXPECT(env.balance(owner, asset) == startingOwnerBalance.value() - xfer);
+                    BEAST_EXPECT(env.balance(vaultAccount(keylet), asset) == xfer);
+
+                    auto const vault = env.le(keylet);
+                    BEAST_EXPECT(vault->at(sfAssetsAvailable) == xfer);
+                    BEAST_EXPECT(vault->at(sfAssetsTotal) == xfer);
+                }
+
+                // Total vault balance should be 118.5 IOU. Withdraw and delete
+                // the vault to verify this exact amount was deposited and the
+                // owner has matching shares
+                env(vault.withdraw(
+                    {.depositor = owner,
+                     .id = keylet.key,
+                     .amount = asset(Number(1000 + (37 * 5), -1))}));
+
+                {
+                    BEAST_EXPECT(env.balance(owner, asset) == startingOwnerBalance.value());
+                    BEAST_EXPECT(env.balance(vaultAccount(keylet), asset) == beast::kZero);
+                    auto const vault = env.le(keylet);
+                    BEAST_EXPECT(vault->at(sfAssetsAvailable) == beast::kZero);
+                    BEAST_EXPECT(vault->at(sfAssetsTotal) == beast::kZero);
+                }
+
+                env(vault.del({.owner = owner, .id = keylet.key}));
+                env.close();
+            },
+            {.initialIOU = Number(11875, -2)});
+
+        auto const [acctReserve, incReserve] = [this]() -> std::pair {
+            Env const env{*this, testableAmendments()};
+            return {
+                env.current()->fees().accountReserve(0, 1).drops() / kDropsPerXrp.drops(),
+                env.current()->fees().increment.drops() / kDropsPerXrp.drops()};
+        }();
+
+        testCase(
+            [&, this](
+                Env& env,
+                Account const& owner,
+                Account const& issuer,
+                Account const& charlie,
+                auto,
+                Vault& vault,
+                PrettyAsset const& asset,
+                auto&&...) {
+                testcase("IOU no trust line to depositor no reserve");
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                env(tx);
+                env.close();
+
+                // reset limit, so deposit of all funds will delete the trust
+                // line
+                env.trust(asset(0), owner);
+                env.close();
+
+                env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(200)}));
+                env.close();
+
+                auto trustline = env.le(keylet::trustLine(owner, asset.raw().get()));
+                BEAST_EXPECT(trustline == nullptr);
+
+                env(ticket::create(owner, 1));
+                env.close();
+
+                // Fail because not enough reserve to create trust line
+                tx = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
+                env(tx, Ter{tecNO_LINE_INSUF_RESERVE});
+                env.close();
+
+                env(pay(charlie, owner, XRP(incReserve)));
+                env.close();
+
+                // Withdraw can now create trust line, will succeed
+                env(tx);
+                env.close();
+            },
+            CaseArgs{.initialXRP = acctReserve + (incReserve * 4) + 1});
+
+        testCase(
+            [&, this](
+                Env& env,
+                Account const& owner,
+                Account const& issuer,
+                Account const& charlie,
+                auto,
+                Vault& vault,
+                PrettyAsset const& asset,
+                auto&&...) {
+                testcase("IOU no reserve for share MPToken");
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                env(tx);
+                env.close();
+
+                env(pay(owner, charlie, asset(100)));
+                env.close();
+
+                env(ticket::create(charlie, 3));
+                env.close();
+
+                // Fail because not enough reserve to create MPToken for shares
+                tx = vault.deposit({.depositor = charlie, .id = keylet.key, .amount = asset(100)});
+                env(tx, Ter{tecINSUFFICIENT_RESERVE});
+                env.close();
+
+                env(pay(issuer, charlie, XRP(incReserve)));
+                env.close();
+
+                // Deposit can now create MPToken, will succeed
+                env(tx);
+                env.close();
+            },
+            CaseArgs{.initialXRP = acctReserve + (incReserve * 4) + 1});
+    }
+
+public:
+    void
+    run() override
+    {
+        testSequences();
+        testWithMPT();
+        testWithIOU();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE_PRIO(VaultLifecycle, app, xrpl, 1);
+
+}  // namespace xrpl
diff --git a/src/test/app/vault/VaultPrecisionFixture.h b/src/test/app/vault/VaultPrecisionFixture.h
new file mode 100644
index 0000000000..22a3276fdf
--- /dev/null
+++ b/src/test/app/vault/VaultPrecisionFixture.h
@@ -0,0 +1,256 @@
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+// Shared fixture for VaultInvariantPrecision_test and
+// VaultTransactorPrecision_test.
+// impairAndPaySibling=false: 1000 USD vault and one ordinary loan.
+// impairAndPaySibling=true: a second loan is impaired then a sibling is paid
+// off, leaving lossUnrealized at assetsTotal - assetsAvailable.
+class VaultPrecisionFixture : public LoanTestBase
+{
+protected:
+    static constexpr std::uint32_t kFixturePaymentInterval = 86400u * 30u;
+    static constexpr std::uint32_t kFixtureGracePeriod = 86400u * 30u;
+    static constexpr std::uint32_t kFixturePaymentTotal = 120u;
+    // 10% APR, expressed in tenth-bips (1000 = 10.00 %).
+    static constexpr std::uint32_t kFixtureInterestTenthBips = 1000u;
+
+    struct Fixture
+    {
+        // Every account is initialised with a placeholder name because
+        // jtx::Account has no default constructor; setupSingleLoanVault
+        // overwrites them.
+        jtx::Account issuer{"vp_issuer_placeholder"};
+        jtx::Account lender{"vp_lender_placeholder"};
+        jtx::Account borrower{"vp_borrower_placeholder"};
+        // Distinct account used to deposit into the vault. Keeps share
+        // ownership independent of the initial vault seeding.
+        jtx::Account depositor{"vp_depositor_placeholder"};
+        // Optional so callers can BEAST_EXPECT(f.asset && f.broker)
+        // after setup; both are populated in the happy path.
+        std::optional asset;
+        std::optional broker;
+        // Keylet has no default constructor. Fill with an obviously
+        // meaningless placeholder; setupSingleLoanVault overwrites the
+        // fields that matter.
+        Keylet vaultKeylet{ltACCOUNT_ROOT, uint256{}};
+        Keylet loan1Keylet{ltACCOUNT_ROOT, uint256{}};
+        // Only meaningful when impairAndPaySibling == true.
+        Keylet loan2Keylet{ltACCOUNT_ROOT, uint256{}};
+        jtx::Account vaultAccount{"vp_vault_pseudo_placeholder"};
+        MPTID share;
+    };
+
+    // Read-only snapshot of the vault + share issuance at a point in time.
+    // Uses Number for exact arithmetic (no re-quantization).
+    struct Numbers
+    {
+        Asset asset;
+        MPTIssue share;
+        // The {} initializers are not redundant: Number's default constructor is explicit, so
+        // fields omitted from the designated initializer in read() below would otherwise fail
+        // copy-list-initialization.
+        // NOLINTBEGIN(readability-redundant-member-init)
+        Number assetsTotal{};      // sfAssetsTotal
+        Number assetsAvailable{};  // sfAssetsAvailable
+        Number lossUnrealized{};   // sfLossUnrealized
+        Number pseudo{};           // vault pseudo-account balance in the asset
+        Number sharesTotal{};      // sfOutstandingAmount on the share MPT
+        // NOLINTEND(readability-redundant-member-init)
+    };
+
+    static Numbers
+    read(jtx::Env const& env, Fixture const& f)
+    {
+        Numbers n{.asset = f.asset ? f.asset->raw() : Asset{}, .share = MPTIssue{f.share}};
+        if (auto const vaultSle = env.le(f.vaultKeylet))
+        {
+            n.assetsTotal = vaultSle->at(sfAssetsTotal);
+            n.assetsAvailable = vaultSle->at(sfAssetsAvailable);
+            n.lossUnrealized = vaultSle->at(sfLossUnrealized);
+        }
+        if (auto const issuanceSle = env.le(keylet::mptokenIssuance(f.share)))
+        {
+            n.sharesTotal = issuanceSle->at(sfOutstandingAmount);
+        }
+        if (f.asset)
+            n.pseudo = env.balance(f.vaultAccount, *f.asset).number();
+        return n;
+    }
+
+    // One unit at the STAmount scale of `assetsTotalAfter`.  Used as the
+    // tolerance in one-unit-band assertions.
+    static Number
+    oneUnit(Asset const& asset, Number const& assetsTotalAfter)
+    {
+        return Number{1, scale(assetsTotalAfter, asset)};
+    }
+
+    // Build the shared vault + loan(s) layout.  The caller constructs
+    // `env` with whatever FeatureBitset they want to exercise; this helper
+    // just uses it.  If `allowClawback` is true, the issuer's
+    // asfAllowTrustLineClawback flag is set BEFORE any trust line is
+    // established for that issuer.  A separate env.close() runs so the
+    // flag lands in the ledger before the trust lines are set up.
+    static Fixture
+    setupSingleLoanVault(jtx::Env& env, bool impairAndPaySibling, bool allowClawback = false)
+    {
+        using namespace jtx;
+        using namespace jtx::loan;
+        using namespace jtx::loan_broker;
+
+        Fixture f;
+        f.issuer = Account{"vp_issuer"};
+        f.lender = Account{"vp_lender"};
+        f.borrower = Account{"vp_borrower"};
+        f.depositor = Account{"vp_depositor"};
+
+        env.fund(XRP(1'000'000), f.issuer, f.lender, f.borrower, f.depositor);
+        env.close();
+
+        // Must be set BEFORE any trust line to `issuer` is created.
+        if (allowClawback)
+        {
+            env(fset(f.issuer, asfAllowTrustLineClawback));
+            env.close();
+        }
+
+        PrettyAsset const asset = f.issuer["USD"];
+        f.asset = asset;
+
+        env.trust(asset(1'000'000'000), f.lender);
+        env.trust(asset(1'000'000'000), f.borrower);
+        env.trust(asset(1'000'000'000), f.depositor);
+        env(pay(f.issuer, f.lender, asset(100'000'000)));
+        env(pay(f.issuer, f.borrower, asset(100'000'000)));
+        env(pay(f.issuer, f.depositor, asset(100'000'000)));
+        env.close();
+
+        BrokerParameters const brokerParams{
+            .vaultDeposit = 1'000,
+            .debtMax = 0,
+            .coverRateMin = percentageToTenthBips(1),
+            .coverDeposit = 10'000,
+            .managementFeeRate = TenthBips16{100},
+            .coverRateLiquidation = xrpl::lending::kMaxCoverRate};
+
+        // Build the vault + broker manually (rather than calling
+        // createVaultAndBroker) so we can seed only the lender/depositor
+        // trust lines we set up above, and skip the LoanTestBase auto
+        // funding that assumes an XRP asset.
+        Vault const vault{env};
+        auto [createTx, vaultKeylet] = vault.create({.owner = f.lender, .asset = asset});
+        env(createTx);
+        env.close();
+        f.vaultKeylet = vaultKeylet;
+
+        env(vault.deposit(
+            {.depositor = f.lender,
+             .id = vaultKeylet.key,
+             .amount = asset(brokerParams.vaultDeposit)}));
+        env.close();
+
+        auto const brokerKeylet =
+            keylet::loanBroker(f.lender.id(), SeqProxy::rawSequence(env.seq(f.lender)));
+
+        env(set(f.lender, vaultKeylet.key, brokerParams.flags),
+            kManagementFeeRate(brokerParams.managementFeeRate),
+            kDebtMaximum(asset(brokerParams.debtMax).value()),
+            kCoverRateMinimum(brokerParams.coverRateMin),
+            kCoverRateLiquidation(TenthBips32(brokerParams.coverRateLiquidation)));
+        env(coverDeposit(f.lender, brokerKeylet.key, asset(brokerParams.coverDeposit).value()));
+        env.close();
+
+        f.broker = BrokerInfo{asset, brokerKeylet, vaultKeylet, brokerParams};
+
+        auto const vaultSle = env.le(vaultKeylet);
+        f.vaultAccount = Account{"vp_vault_pseudo", vaultSle->at(sfAccount)};
+        f.share = vaultSle->at(sfShareMPTID);
+
+        Fee const bigFee{env.current()->fees().base * 200};
+
+        auto const setLoan = [&](Number const& principal) -> Keylet {
+            auto const brokerSle = env.le(brokerKeylet);
+            auto const loanKeylet = keylet::loan(
+                brokerKeylet.key, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
+            env(loan::set(f.borrower, brokerKeylet.key, asset(principal).number()),
+                Sig(sfCounterpartySignature, f.lender),
+                jtx::loan::kInterestRate(TenthBips32{kFixtureInterestTenthBips}),
+                jtx::loan::kPaymentTotal(kFixturePaymentTotal),
+                jtx::loan::kPaymentInterval(kFixturePaymentInterval),
+                jtx::loan::kGracePeriod(kFixtureGracePeriod),
+                bigFee);
+            env.close();
+            return loanKeylet;
+        };
+
+        // Loan 1: principal 7, the one ordinary loan in both fixtures.
+        // With vault deposit 1000, this leaves A ≈ 993 (see plan).
+        f.loan1Keylet = setLoan(Number{7});
+
+        if (!impairAndPaySibling)
+            return f;
+
+        // Loan 2: sibling loan of principal 11.
+        f.loan2Keylet = setLoan(Number{11});
+
+        // Pay off loan 2 in full so its total value flows into the vault
+        // and pushes T-A upward, meeting the residual loss.  Generous
+        // upper bound; the transactor takes only what is due.
+        //
+        // This happens before the impair below because impair under
+        // fixCleanup3_4_0 requires loan 1 to already be late, and the two
+        // loans are originated close enough together that advancing past
+        // loan 1's due date also makes loan 2 late — which would reject
+        // this full payment with tecEXPIRED.
+        auto const payoff = asset(Number{50}).value();
+        env(pay(f.borrower, f.loan2Keylet.key, payoff, tfLoanFullPayment), bigFee);
+        env.close();
+
+        // Impair loan 1 → drives sfLossUnrealized to loan 1's value.
+        if (env.current()->rules().enabled(fixCleanup3_4_0))
+        {
+            std::uint32_t const dueDate = env.le(f.loan1Keylet)->at(sfNextPaymentDueDate);
+            env.close(NetClock::time_point{NetClock::duration{dueDate}} + std::chrono::seconds{1});
+        }
+
+        env(jtx::loan::manage(f.lender, f.loan1Keylet.key, tfLoanImpair), bigFee);
+        env.close();
+
+        return f;
+    }
+};
+
+}  // namespace xrpl::test
diff --git a/src/test/app/vault/VaultRPC_test.cpp b/src/test/app/vault/VaultRPC_test.cpp
new file mode 100644
index 0000000000..dbceb1cb9c
--- /dev/null
+++ b/src/test/app/vault/VaultRPC_test.cpp
@@ -0,0 +1,620 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class VaultRPC_test : public VaultTestBase
+{
+private:
+    void
+    testRPC()
+    {
+        using namespace test::jtx;
+
+        testcase("RPC");
+        Env env{*this, testableAmendments()};
+        Account const owner{"owner"};
+        Account const issuer{"issuer"};
+        Vault const vault{env};
+        env.fund(XRP(1000), issuer, owner);
+        env.close();
+
+        PrettyAsset const asset = issuer["IOU"];
+        env.trust(asset(1000), owner);
+        env(pay(issuer, owner, asset(200)));
+        env.close();
+
+        auto const sequence = env.seq(owner);
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+        env(tx);
+        env.close();
+
+        // Set some fields
+        {
+            auto tx1 = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(50)});
+            env(tx1);
+
+            auto tx2 = vault.set({.owner = owner, .id = keylet.key});
+            tx2[sfAssetsMaximum] = asset(1000).number();
+            env(tx2);
+            env.close();
+        }
+
+        auto const sleVault = [&env, keylet = keylet, this]() {
+            auto const vault = env.le(keylet);
+            BEAST_EXPECT(vault != nullptr);
+            return vault;
+        }();
+
+        auto const check = [&, keylet = keylet, sle = sleVault, this](
+                               json::Value const& vault,
+                               json::Value const& issuance = json::ValueType::Null) {
+            BEAST_EXPECT(vault.isObject());
+
+            static constexpr auto kCheckString =
+                [](auto& node, SField const& field, std::string v) -> bool {
+                return node.isMember(field.fieldName) && node[field.fieldName].isString() &&
+                    node[field.fieldName] == v;
+            };
+            static constexpr auto kCheckObject =
+                [](auto& node, SField const& field, json::Value v) -> bool {
+                return node.isMember(field.fieldName) && node[field.fieldName].isObject() &&
+                    node[field.fieldName] == v;
+            };
+            static constexpr auto kCheckInt = [](auto& node, SField const& field, int v) -> bool {
+                return node.isMember(field.fieldName) &&
+                    ((node[field.fieldName].isInt() && node[field.fieldName] == json::Int(v)) ||
+                     (node[field.fieldName].isUInt() && node[field.fieldName] == json::UInt(v)));
+            };
+
+            BEAST_EXPECT(vault["LedgerEntryType"].asString() == "Vault");
+            BEAST_EXPECT(vault[jss::index].asString() == strHex(keylet.key));
+            BEAST_EXPECT(kCheckInt(vault, sfFlags, 0));
+            // Ignore all other standard fields, this test doesn't care
+
+            BEAST_EXPECT(kCheckString(vault, sfAccount, toBase58(sle->at(sfAccount))));
+            BEAST_EXPECT(kCheckObject(vault, sfAsset, toJson(sle->at(sfAsset))));
+            BEAST_EXPECT(kCheckString(vault, sfAssetsAvailable, "50"));
+            BEAST_EXPECT(kCheckString(vault, sfAssetsMaximum, "1000"));
+            BEAST_EXPECT(kCheckString(vault, sfAssetsTotal, "50"));
+            BEAST_EXPECT(!vault.isMember(sfLossUnrealized.getJsonName()));
+
+            auto const strShareID = strHex(sle->at(sfShareMPTID));
+            BEAST_EXPECT(kCheckString(vault, sfShareMPTID, strShareID));
+            BEAST_EXPECT(kCheckString(vault, sfOwner, toBase58(owner.id())));
+            BEAST_EXPECT(kCheckInt(vault, sfSequence, sequence));
+            BEAST_EXPECT(kCheckInt(vault, sfWithdrawalPolicy, kVaultStrategyFirstComeFirstServe));
+
+            if (issuance.isObject())
+            {
+                BEAST_EXPECT(issuance["LedgerEntryType"].asString() == "MPTokenIssuance");
+                BEAST_EXPECT(issuance[jss::mpt_issuance_id].asString() == strShareID);
+                BEAST_EXPECT(kCheckInt(issuance, sfSequence, 1));
+                BEAST_EXPECT(kCheckInt(
+                    issuance, sfFlags, int(lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer)));
+                BEAST_EXPECT(kCheckString(issuance, sfOutstandingAmount, "50000000"));
+            }
+        };
+
+        // An error response must carry a registered token together with the matching code and
+        // message, so that clients dispatching on either of them reach the same conclusion.
+        auto const checkError = [this](
+                                    json::Value const& result,
+                                    std::string const& token,
+                                    ErrorCodeI const code,
+                                    std::string const& message) {
+            BEAST_EXPECT(result[jss::error].asString() == token);
+            BEAST_EXPECT(result[jss::error_code].asInt() == code);
+            BEAST_EXPECT(result[jss::error_message].asString() == message);
+        };
+
+        std::string const badSeqMessage = "Invalid field 'seq', not a positive 32-bit integer.";
+        std::string const badFieldsMessage =
+            "Must specify either 'vault_id' or both 'owner' and 'seq'.";
+
+        {
+            testcase("RPC ledger_entry selected by key");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault] = strHex(keylet.key);
+            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
+
+            BEAST_EXPECT(!jvVault[jss::result].isMember(jss::error));
+            BEAST_EXPECT(jvVault[jss::result].isMember(jss::node));
+            check(jvVault[jss::result][jss::node]);
+        }
+
+        {
+            testcase("RPC ledger_entry selected by owner and seq");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault][jss::owner] = owner.human();
+            jvParams[jss::vault][jss::seq] = sequence;
+            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
+
+            BEAST_EXPECT(!jvVault[jss::result].isMember(jss::error));
+            BEAST_EXPECT(jvVault[jss::result].isMember(jss::node));
+            check(jvVault[jss::result][jss::node]);
+        }
+
+        {
+            testcase("RPC ledger_entry cannot find vault by key");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault] = to_string(uint256(42));
+            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
+            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "entryNotFound");
+        }
+
+        {
+            testcase("RPC ledger_entry cannot find vault by owner and seq");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault][jss::owner] = issuer.human();
+            jvParams[jss::vault][jss::seq] = 1'000'000;
+            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
+            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "entryNotFound");
+        }
+
+        {
+            testcase("RPC ledger_entry malformed key");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault] = 42;
+            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
+            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
+        }
+
+        {
+            testcase("RPC ledger_entry malformed owner");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault][jss::owner] = 42;
+            jvParams[jss::vault][jss::seq] = sequence;
+            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
+            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedOwner");
+        }
+
+        {
+            testcase("RPC ledger_entry malformed seq");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault][jss::owner] = issuer.human();
+            jvParams[jss::vault][jss::seq] = "foo";
+            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
+            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
+        }
+
+        {
+            testcase("RPC ledger_entry negative seq");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault][jss::owner] = issuer.human();
+            jvParams[jss::vault][jss::seq] = -1;
+            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
+            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
+        }
+
+        {
+            testcase("RPC ledger_entry oversized seq");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault][jss::owner] = issuer.human();
+            jvParams[jss::vault][jss::seq] = 1e20;
+            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
+            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
+        }
+
+        {
+            testcase("RPC ledger_entry bool seq");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault][jss::owner] = issuer.human();
+            jvParams[jss::vault][jss::seq] = true;
+            auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams));
+            BEAST_EXPECT(jvVault[jss::result][jss::error].asString() == "malformedRequest");
+        }
+
+        {
+            testcase("RPC account_objects");
+
+            json::Value jvParams;
+            jvParams[jss::account] = owner.human();
+            jvParams[jss::type] = jss::vault;
+            auto jv = env.rpc("json", "account_objects", to_string(jvParams))[jss::result];
+
+            BEAST_EXPECT(jv[jss::account_objects].size() == 1);
+            check(jv[jss::account_objects][0u]);
+        }
+
+        {
+            testcase("RPC ledger_data");
+
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::binary] = false;
+            jvParams[jss::type] = jss::vault;
+            json::Value jv = env.rpc("json", "ledger_data", to_string(jvParams));
+            BEAST_EXPECT(jv[jss::result][jss::state].size() == 1);
+            check(jv[jss::result][jss::state][0u]);
+        }
+
+        {
+            testcase("RPC vault_info command line");
+            json::Value jv = env.rpc("vault_info", strHex(keylet.key), "validated");
+
+            BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
+            BEAST_EXPECT(jv[jss::result].isMember(jss::vault));
+            check(jv[jss::result][jss::vault], jv[jss::result][jss::vault][jss::shares]);
+        }
+
+        {
+            testcase("RPC vault_info json");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault_id] = strHex(keylet.key);
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+
+            BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
+            BEAST_EXPECT(jv[jss::result].isMember(jss::vault));
+            check(jv[jss::result][jss::vault], jv[jss::result][jss::vault][jss::shares]);
+        }
+
+        {
+            testcase("RPC vault_info invalid vault_id");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault_id] = "foobar";
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(
+                jv[jss::result],
+                "invalidParams",
+                RpcInvalidParams,
+                "Invalid field 'vault_id', not hex string.");
+        }
+
+        {
+            testcase("RPC vault_info json numeric vault_id");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault_id] = 0;
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(
+                jv[jss::result],
+                "invalidParams",
+                RpcInvalidParams,
+                "Invalid field 'vault_id', not hex string.");
+        }
+
+        {
+            testcase("RPC vault_info json object vault_id");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault_id] = json::Value(json::ValueType::Object);
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(
+                jv[jss::result],
+                "invalidParams",
+                RpcInvalidParams,
+                "Invalid field 'vault_id', not hex string.");
+        }
+
+        {
+            // An all-zero key is a well-formed request for a vault that cannot exist, not a
+            // malformed one. parseHex accepts both the padded form and the short "0".
+            testcase("RPC vault_info json all zero vault_id");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault_id] = strHex(uint256(beast::kZero));
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "entryNotFound", RpcEntryNotFound, "Entry not found.");
+        }
+
+        {
+            testcase("RPC vault_info json short zero vault_id");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault_id] = "0";
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "entryNotFound", RpcEntryNotFound, "Entry not found.");
+        }
+
+        {
+            testcase("RPC vault_info json by owner and sequence");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::owner] = owner.human();
+            jvParams[jss::seq] = sequence;
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+
+            BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
+            BEAST_EXPECT(jv[jss::result].isMember(jss::vault));
+            check(jv[jss::result][jss::vault], jv[jss::result][jss::vault][jss::shares]);
+        }
+
+        {
+            testcase("RPC vault_info json malformed sequence");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::owner] = owner.human();
+            jvParams[jss::seq] = "foobar";
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "invalidParams", RpcInvalidParams, badSeqMessage);
+        }
+
+        {
+            testcase("RPC vault_info json invalid sequence");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::owner] = owner.human();
+            jvParams[jss::seq] = 0;
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "invalidParams", RpcInvalidParams, badSeqMessage);
+        }
+
+        {
+            testcase("RPC vault_info json negative sequence");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::owner] = owner.human();
+            jvParams[jss::seq] = -1;
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "invalidParams", RpcInvalidParams, badSeqMessage);
+        }
+
+        {
+            testcase("RPC vault_info json oversized sequence");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::owner] = owner.human();
+            jvParams[jss::seq] = 1e20;
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "invalidParams", RpcInvalidParams, badSeqMessage);
+        }
+
+        {
+            testcase("RPC vault_info json bool sequence");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::owner] = owner.human();
+            jvParams[jss::seq] = true;
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "invalidParams", RpcInvalidParams, badSeqMessage);
+        }
+
+        {
+            testcase("RPC vault_info json malformed owner");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::owner] = "foobar";
+            jvParams[jss::seq] = sequence;
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(
+                jv[jss::result],
+                "actMalformed",
+                RpcActMalformed,
+                "Invalid field 'owner', not AccountID.");
+        }
+
+        {
+            testcase("RPC vault_info json array owner");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::owner] = json::Value(json::ValueType::Array);
+            jvParams[jss::seq] = sequence;
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(
+                jv[jss::result],
+                "actMalformed",
+                RpcActMalformed,
+                "Invalid field 'owner', not AccountID.");
+        }
+
+        {
+            testcase("RPC vault_info json invalid combination only owner");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::owner] = owner.human();
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "invalidParams", RpcInvalidParams, badFieldsMessage);
+        }
+
+        {
+            testcase("RPC vault_info json invalid combination only seq");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::seq] = sequence;
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "invalidParams", RpcInvalidParams, badFieldsMessage);
+        }
+
+        {
+            testcase("RPC vault_info json invalid combination seq vault_id");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault_id] = strHex(keylet.key);
+            jvParams[jss::seq] = sequence;
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "invalidParams", RpcInvalidParams, badFieldsMessage);
+        }
+
+        {
+            testcase("RPC vault_info json invalid combination owner vault_id");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault_id] = strHex(keylet.key);
+            jvParams[jss::owner] = owner.human();
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "invalidParams", RpcInvalidParams, badFieldsMessage);
+        }
+
+        {
+            testcase(
+                "RPC vault_info json invalid combination owner seq "
+                "vault_id");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault_id] = strHex(keylet.key);
+            jvParams[jss::seq] = sequence;
+            jvParams[jss::owner] = owner.human();
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "invalidParams", RpcInvalidParams, badFieldsMessage);
+        }
+
+        {
+            testcase("RPC vault_info json no input");
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            checkError(jv[jss::result], "invalidParams", RpcInvalidParams, badFieldsMessage);
+        }
+
+        {
+            testcase("RPC vault_info command line invalid index");
+            json::Value jv = env.rpc("vault_info", "foobar", "validated");
+            BEAST_EXPECT(jv[jss::error].asString() == "invalidParams");
+        }
+
+        {
+            testcase("RPC vault_info command line zero index");
+            json::Value jv = env.rpc("vault_info", "0", "validated");
+            checkError(jv[jss::result], "entryNotFound", RpcEntryNotFound, "Entry not found.");
+        }
+
+        {
+            testcase("RPC vault_info command line unknown index");
+            json::Value jv = env.rpc("vault_info", strHex(uint256(42)), "validated");
+            checkError(jv[jss::result], "entryNotFound", RpcEntryNotFound, "Entry not found.");
+        }
+
+        {
+            testcase("RPC vault_info command line invalid ledger");
+            json::Value jv = env.rpc("vault_info", strHex(keylet.key), "0");
+            BEAST_EXPECT(jv[jss::result][jss::error].asString() == "lgrNotFound");
+        }
+    }
+
+    // RPC coverage: closed-ended vaults must return VaultKind, SubscriptionDate and RedemptionDate
+    // in both vault_info and ledger_entry responses. Open-ended vaults must not.
+    void
+    testRPCClosedEnded()
+    {
+        using namespace test::jtx;
+
+        testcase("RPC closed-ended vault fields");
+        Env env{*this, testableAmendments()};
+        Account const owner{"owner"};
+        Account const owner2{"owner2"};
+        env.fund(XRP(1000), owner, owner2);
+        env.close();
+
+        auto const closedEnded = std::to_underlying(VaultKind::ClosedEnded);
+        Asset const asset = xrpIssue();
+        auto const sub = env.now().time_since_epoch().count() + 60;
+        auto const red = sub + kMinInvestmentPeriod;
+
+        Vault const vault{env};
+        auto [tx, keylet] = vault.create(
+            {.owner = owner,
+             .asset = asset,
+             .vaultKind = closedEnded,
+             .subscriptionDate = sub,
+             .redemptionDate = red});
+        env(tx);
+        env.close();
+
+        auto [tx2, keylet2] = vault.create({.owner = owner2, .asset = asset});
+        env(tx2);
+        env.close();
+
+        auto const asUInt = [](json::Value const& jv) -> json::UInt {
+            return jv.isUInt() ? jv.asUInt() : json::UInt(jv.asInt());
+        };
+        auto const checkClosedEnded = [&](json::Value const& v) {
+            BEAST_EXPECT(v.isObject());
+            BEAST_EXPECT(v.isMember(sfVaultKind.fieldName));
+            BEAST_EXPECT(asUInt(v[sfVaultKind.fieldName]) == json::UInt(closedEnded));
+            BEAST_EXPECT(v.isMember(sfSubscriptionDate.fieldName));
+            BEAST_EXPECT(asUInt(v[sfSubscriptionDate.fieldName]) == json::UInt(sub));
+            BEAST_EXPECT(v.isMember(sfRedemptionDate.fieldName));
+            BEAST_EXPECT(asUInt(v[sfRedemptionDate.fieldName]) == json::UInt(red));
+        };
+        auto const checkOpenEnded = [&](json::Value const& v) {
+            BEAST_EXPECT(v.isObject());
+            BEAST_EXPECT(!v.isMember(sfVaultKind.fieldName));
+            BEAST_EXPECT(!v.isMember(sfSubscriptionDate.fieldName));
+            BEAST_EXPECT(!v.isMember(sfRedemptionDate.fieldName));
+        };
+
+        {
+            json::Value jvParams;
+            jvParams[jss::vault_id] = strHex(keylet.key);
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
+            checkClosedEnded(jv[jss::result][jss::vault]);
+        }
+        {
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault] = strHex(keylet.key);
+            auto jv = env.rpc("json", "ledger_entry", to_string(jvParams));
+            BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
+            checkClosedEnded(jv[jss::result][jss::node]);
+        }
+        {
+            json::Value jvParams;
+            jvParams[jss::vault_id] = strHex(keylet2.key);
+            auto jv = env.rpc("json", "vault_info", to_string(jvParams));
+            BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
+            checkOpenEnded(jv[jss::result][jss::vault]);
+        }
+        {
+            json::Value jvParams;
+            jvParams[jss::ledger_index] = jss::validated;
+            jvParams[jss::vault] = strHex(keylet2.key);
+            auto jv = env.rpc("json", "ledger_entry", to_string(jvParams));
+            BEAST_EXPECT(!jv[jss::result].isMember(jss::error));
+            checkOpenEnded(jv[jss::result][jss::node]);
+        }
+    }
+
+public:
+    void
+    run() override
+    {
+        testRPC();
+        testRPCClosedEnded();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(VaultRPC, app, xrpl);
+
+}  // namespace xrpl
diff --git a/src/test/app/vault/VaultScale_test.cpp b/src/test/app/vault/VaultScale_test.cpp
new file mode 100644
index 0000000000..c2858a204d
--- /dev/null
+++ b/src/test/app/vault/VaultScale_test.cpp
@@ -0,0 +1,1340 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class VaultScale_test : public VaultTestBase
+{
+private:
+    void
+    testScaleIOU()
+    {
+        using namespace test::jtx;
+
+        struct Data
+        {
+            Account const& owner;
+            Account const& issuer;
+            Account const& depositor;
+            Account const& vaultAccount;
+            MPTIssue shares;
+            PrettyAsset const& share;
+            Vault& vault;
+            xrpl::Keylet keylet;
+            Issue assets;
+            PrettyAsset const& asset;
+            std::function)> peek;
+        };
+
+        auto testCase = [&, this](
+                            std::uint8_t scale, std::function test) {
+            // These scale-focused tests build an open-ended vault and
+            // exercise deposit/withdraw/clawback (with one test also
+            // attaching a loan broker). featureLendingProtocolV1_1 adds a
+            // closed-ended vault gate on LoanBrokerSet::preclaim and is
+            // orthogonal to what this suite asserts, so strip it here.
+            Env env{*this, testableAmendments() - featureLendingProtocolV1_1};
+            Account const owner{"owner"};
+            Account const issuer{"issuer"};
+            Account const depositor{"depositor"};
+            Vault vault{env};
+            env.fund(XRP(1000), issuer, owner, depositor);
+            env(fset(issuer, asfAllowTrustLineClawback));
+            env.close();
+
+            PrettyAsset const asset = issuer["IOU"];
+            env.trust(asset(1000), owner);
+            env.trust(asset(1000), depositor);
+            env(pay(issuer, owner, asset(200)));
+            env(pay(issuer, depositor, asset(200)));
+            env.close();
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            tx[sfScale] = scale;
+            env(tx);
+
+            auto const [vaultAccount, issuanceId] =
+                [&env](xrpl::Keylet keylet) -> std::tuple {
+                auto const vault = env.le(keylet);
+                return {Account("vault", vault->at(sfAccount)), vault->at(sfShareMPTID)};
+            }(keylet);
+            MPTIssue const shares(issuanceId);
+            env.memoize(vaultAccount);
+
+            auto const peek = [keylet, &env, this](std::function fn) -> bool {
+                return env.app().getOpenLedger().modify(
+                    [&](OpenView& view, beast::Journal j) -> bool {
+                        Sandbox sb(&view, TapNone);
+                        auto vault = sb.peek(keylet::vault(keylet.key));
+                        if (!BEAST_EXPECT(vault))
+                            return false;
+                        auto shares = sb.peek(keylet::mptokenIssuance(vault->at(sfShareMPTID)));
+                        if (!BEAST_EXPECT(shares))
+                            return false;
+                        if (fn(*vault, *shares))
+                        {
+                            sb.update(vault);
+                            sb.update(shares);
+                            sb.apply(view);
+                            return true;
+                        }
+                        return false;
+                    });
+            };
+
+            test(
+                env,
+                {.owner = owner,
+                 .issuer = issuer,
+                 .depositor = depositor,
+                 .vaultAccount = vaultAccount,
+                 .shares = shares,
+                 .share = PrettyAsset(shares),
+                 .vault = vault,
+                 .keylet = keylet,
+                 .assets = asset.raw().get(),
+                 .asset = asset,
+                 .peek = peek});
+        };
+
+        testCase(18, [&, this](Env& env, Data d) {
+            testcase("Scale deposit overflow on first deposit");
+            auto tx = d.vault.deposit(
+                {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(10)});
+            env(tx, Ter{tecPATH_DRY});
+            env.close();
+        });
+
+        testCase(18, [&, this](Env& env, Data d) {
+            testcase("Scale deposit overflow on second deposit");
+
+            {
+                auto tx = d.vault.deposit(
+                    {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
+                env(tx);
+                env.close();
+            }
+
+            {
+                auto tx = d.vault.deposit(
+                    {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(10)});
+                env(tx, Ter{tecPATH_DRY});
+                env.close();
+            }
+        });
+
+        testCase(18, [&, this](Env& env, Data d) {
+            testcase("Scale deposit overflow on total shares");
+
+            {
+                auto tx = d.vault.deposit(
+                    {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
+                env(tx);
+                env.close();
+            }
+
+            {
+                auto tx = d.vault.deposit(
+                    {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
+                env(tx, Ter{tecPATH_DRY});
+                env.close();
+            }
+        });
+
+        testCase(1, [&, this](Env& env, Data d) {
+            testcase("Scale deposit exact");
+
+            auto const start = env.balance(d.depositor, d.assets).number();
+            auto tx = d.vault.deposit(
+                {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(1)});
+            env(tx);
+            env.close();
+            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(10));
+            BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start - 1));
+        });
+
+        testCase(1, [&, this](Env& env, Data d) {
+            testcase("Scale deposit insignificant amount");
+
+            auto tx = d.vault.deposit(
+                {.depositor = d.depositor,
+                 .id = d.keylet.key,
+                 .amount = STAmount(d.asset, Number(9, -2))});
+            env(tx, Ter{tecPRECISION_LOSS});
+        });
+
+        testCase(1, [&, this](Env& env, Data d) {
+            testcase("Scale deposit exact, using full precision");
+
+            auto const start = env.balance(d.depositor, d.assets).number();
+            auto tx = d.vault.deposit(
+                {.depositor = d.depositor,
+                 .id = d.keylet.key,
+                 .amount = STAmount(d.asset, Number(15, -1))});
+            env(tx);
+            env.close();
+            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(15));
+            BEAST_EXPECT(
+                env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(15, -1)));
+        });
+
+        testCase(1, [&, this](Env& env, Data d) {
+            testcase("Scale deposit exact, truncating from .5");
+
+            auto const start = env.balance(d.depositor, d.assets).number();
+            // Each of the cases below will transfer exactly 1.2 IOU to the
+            // vault and receive 12 shares in exchange
+            {
+                auto tx = d.vault.deposit(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, Number(125, -2))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(12));
+                BEAST_EXPECT(
+                    env.balance(d.depositor, d.assets) ==
+                    STAmount(d.asset, start - Number(12, -1)));
+            }
+
+            {
+                auto tx = d.vault.deposit(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, Number(1201, -3))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(24));
+                BEAST_EXPECT(
+                    env.balance(d.depositor, d.assets) ==
+                    STAmount(d.asset, start - Number(24, -1)));
+            }
+
+            {
+                auto tx = d.vault.deposit(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, Number(1299, -3))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(36));
+                BEAST_EXPECT(
+                    env.balance(d.depositor, d.assets) ==
+                    STAmount(d.asset, start - Number(36, -1)));
+            }
+        });
+
+        testCase(1, [&, this](Env& env, Data d) {
+            testcase("Scale deposit exact, truncating from .01");
+
+            auto const start = env.balance(d.depositor, d.assets).number();
+            // round to 12
+            auto tx = d.vault.deposit(
+                {.depositor = d.depositor,
+                 .id = d.keylet.key,
+                 .amount = STAmount(d.asset, Number(1201, -3))});
+            env(tx);
+            env.close();
+            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(12));
+            BEAST_EXPECT(
+                env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(12, -1)));
+
+            {
+                // round to 6
+                auto tx = d.vault.deposit(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, Number(69, -2))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(18));
+                BEAST_EXPECT(
+                    env.balance(d.depositor, d.assets) ==
+                    STAmount(d.asset, start - Number(18, -1)));
+            }
+        });
+
+        testCase(1, [&, this](Env& env, Data d) {
+            testcase("Scale deposit exact, truncating from .99");
+
+            auto const start = env.balance(d.depositor, d.assets).number();
+            // round to 12
+            auto tx = d.vault.deposit(
+                {.depositor = d.depositor,
+                 .id = d.keylet.key,
+                 .amount = STAmount(d.asset, Number(1299, -3))});
+            env(tx);
+            env.close();
+            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(12));
+            BEAST_EXPECT(
+                env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(12, -1)));
+
+            {
+                // round to 6
+                auto tx = d.vault.deposit(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, Number(62, -2))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(18));
+                BEAST_EXPECT(
+                    env.balance(d.depositor, d.assets) ==
+                    STAmount(d.asset, start - Number(18, -1)));
+            }
+        });
+
+        testCase(1, [&, this](Env& env, Data d) {
+            // initial setup: deposit 100 IOU, receive 1000 shares
+            auto const start = env.balance(d.depositor, d.assets).number();
+            auto tx = d.vault.deposit(
+                {.depositor = d.depositor,
+                 .id = d.keylet.key,
+                 .amount = STAmount(d.asset, Number(100, 0))});
+            env(tx);
+            env.close();
+            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
+            BEAST_EXPECT(
+                env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(100, 0)));
+            BEAST_EXPECT(
+                env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(100, 0)));
+            BEAST_EXPECT(
+                env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-1000, 0)));
+
+            {
+                testcase("Scale redeem exact");
+                // sharesToAssetsWithdraw:
+                //  assets = assetsTotal * (shares / sharesTotal)
+                //  assets = 100 * 100 / 1000 = 100 * 0.1 = 10
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+                auto tx = d.vault.withdraw(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.share, Number(100, 0))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900));
+                BEAST_EXPECT(
+                    env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(10, 0)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(90, 0)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-900, 0)));
+            }
+
+            {
+                testcase("Scale redeem with rounding");
+                // sharesToAssetsWithdraw:
+                //  assets = assetsTotal * (shares / sharesTotal)
+                //  assets = 90 * 25 / 900 = 90 * 0.02777... = 2.5
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+                d.peek([](SLE& vault, auto&) -> bool {
+                    vault[sfAssetsAvailable] = Number(1);
+                    return true;
+                });
+
+                // Note, this transaction fails first (because of above change
+                // in the open ledger) but then succeeds when the ledger is
+                // closed (because a modification like above is not persistent),
+                // which is why the checks below are expected to pass.
+                auto tx = d.vault.withdraw(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.share, Number(25, 0))});
+                env(tx, Ter{tecINSUFFICIENT_FUNDS});
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900 - 25));
+                BEAST_EXPECT(
+                    env.balance(d.depositor, d.assets) ==
+                    STAmount(d.asset, start + Number(25, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) ==
+                    STAmount(d.asset, Number(900 - 25, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) ==
+                    STAmount(d.share, -Number(900 - 25, 0)));
+            }
+
+            {
+                testcase("Scale redeem exact");
+                // sharesToAssetsWithdraw:
+                //  assets = assetsTotal * (shares / sharesTotal)
+                //  assets = 87.5 * 21 / 875 = 87.5 * 0.024 = 2.1
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+
+                tx = d.vault.withdraw(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.share, Number(21, 0))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(875 - 21));
+                BEAST_EXPECT(
+                    env.balance(d.depositor, d.assets) ==
+                    STAmount(d.asset, start + Number(21, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) ==
+                    STAmount(d.asset, Number(875 - 21, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) ==
+                    STAmount(d.share, -Number(875 - 21, 0)));
+            }
+
+            {
+                testcase("Scale redeem rest");
+                auto const rest = env.balance(d.depositor, d.shares).number();
+
+                tx = d.vault.withdraw(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.share, rest)});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares).number() == 0);
+                BEAST_EXPECT(env.balance(d.vaultAccount, d.assets).number() == 0);
+                BEAST_EXPECT(env.balance(d.vaultAccount, d.shares).number() == 0);
+            }
+        });
+
+        testCase(18, [&, this](Env& env, Data d) {
+            testcase("Scale withdraw overflow");
+
+            {
+                auto tx = d.vault.deposit(
+                    {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
+                env(tx);
+                env.close();
+            }
+
+            {
+                auto tx = d.vault.withdraw(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, Number(10, 0))});
+                env(tx, Ter{tecPATH_DRY});
+                env.close();
+            }
+        });
+
+        testCase(1, [&, this](Env& env, Data d) {
+            // initial setup: deposit 100 IOU, receive 1000 shares
+            auto const start = env.balance(d.depositor, d.assets).number();
+            auto tx = d.vault.deposit(
+                {.depositor = d.depositor,
+                 .id = d.keylet.key,
+                 .amount = STAmount(d.asset, Number(100, 0))});
+            env(tx);
+            env.close();
+            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
+            BEAST_EXPECT(
+                env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(100, 0)));
+            BEAST_EXPECT(
+                env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(100, 0)));
+            BEAST_EXPECT(
+                env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-1000, 0)));
+
+            {
+                testcase("Scale withdraw exact");
+                // assetsToSharesWithdraw:
+                //  shares = sharesTotal * (assets / assetsTotal)
+                //  shares = 1000 * 10 / 100 = 1000 * 0.1 = 100
+                // sharesToAssetsWithdraw:
+                //  assets = assetsTotal * (shares / sharesTotal)
+                //  assets = 100 * 100 / 1000 = 100 * 0.1 = 10
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+                auto tx = d.vault.withdraw(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, Number(10, 0))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900));
+                BEAST_EXPECT(
+                    env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(10, 0)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(90, 0)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-900, 0)));
+            }
+
+            {
+                testcase("Scale withdraw insignificant amount");
+                auto tx = d.vault.withdraw(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, Number(4, -2))});
+                env(tx, Ter{tecPRECISION_LOSS});
+            }
+
+            {
+                testcase("Scale withdraw with rounding assets");
+                // assetsToSharesWithdraw:
+                //  shares = sharesTotal * (assets / assetsTotal)
+                //  shares = 900 * 2.5 / 90 = 900 * 0.02777... = 25
+                // sharesToAssetsWithdraw:
+                //  assets = assetsTotal * (shares / sharesTotal)
+                //  assets = 90 * 25 / 900 = 90 * 0.02777... = 2.5
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+                d.peek([](SLE& vault, auto&) -> bool {
+                    vault[sfAssetsAvailable] = Number(1);
+                    return true;
+                });
+
+                // Note, this transaction fails first (because of above change
+                // in the open ledger) but then succeeds when the ledger is
+                // closed (because a modification like above is not persistent),
+                // which is why the checks below are expected to pass.
+                auto tx = d.vault.withdraw(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, Number(25, -1))});
+                env(tx, Ter{tecINSUFFICIENT_FUNDS});
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900 - 25));
+                BEAST_EXPECT(
+                    env.balance(d.depositor, d.assets) ==
+                    STAmount(d.asset, start + Number(25, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) ==
+                    STAmount(d.asset, Number(900 - 25, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) ==
+                    STAmount(d.share, -Number(900 - 25, 0)));
+            }
+
+            {
+                testcase("Scale withdraw with rounding shares up (truncated post-fixCleanup3_4_0)");
+                // Pre-fixCleanup3_4_0:
+                //   shares = round(875 * 3.75 / 87.5) = 38
+                //   assets = 87.5 * 38 / 875 = 3.8 > 3.75 requested.
+                // Post-fixCleanup3_4_0:
+                //   shares = floor(37.5) = 37
+                //   assets = 87.5 * 37 / 875 = 3.7 <= 3.75 requested.
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+                auto tx = d.vault.withdraw(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, Number(375, -2))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(875 - 37));
+                BEAST_EXPECT(
+                    env.balance(d.depositor, d.assets) ==
+                    STAmount(d.asset, start + Number(37, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) ==
+                    STAmount(d.asset, Number(875 - 37, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) ==
+                    STAmount(d.share, -Number(875 - 37, 0)));
+            }
+
+            {
+                testcase("Scale withdraw with rounding shares down");
+                // Chained state: 838 shares outstanding, 83.8 assets.
+                //   shares = floor(838 * 3.72 / 83.8) = floor(37.199...) = 37
+                //   assets = 83.8 * 37 / 838 = 3.7 <= 3.72 requested.
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+                auto tx = d.vault.withdraw(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, Number(372, -2))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(838 - 37));
+                BEAST_EXPECT(
+                    env.balance(d.depositor, d.assets) ==
+                    STAmount(d.asset, start + Number(37, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) ==
+                    STAmount(d.asset, Number(838 - 37, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) ==
+                    STAmount(d.share, -Number(838 - 37, 0)));
+            }
+
+            {
+                testcase("Scale withdraw tiny amount rejected post-fixCleanup3_4_0");
+                // Chained state: 801 shares outstanding, 80.1 assets.
+                //   shares = floor(801 * 0.09 / 80.1) = floor(0.9) = 0
+                // Zero shares => tecPRECISION_LOSS. State is unchanged.
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+                auto tx = d.vault.withdraw(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, Number(9, -2))});
+                env(tx, Ter{tecPRECISION_LOSS});
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(801));
+                BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(801, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(801, 0)));
+            }
+
+            {
+                testcase("Scale withdraw rest");
+                auto const rest = env.balance(d.vaultAccount, d.assets).number();
+
+                tx = d.vault.withdraw(
+                    {.depositor = d.depositor,
+                     .id = d.keylet.key,
+                     .amount = STAmount(d.asset, rest)});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares).number() == 0);
+                BEAST_EXPECT(env.balance(d.vaultAccount, d.assets).number() == 0);
+                BEAST_EXPECT(env.balance(d.vaultAccount, d.shares).number() == 0);
+            }
+        });
+
+        testCase(18, [&, this](Env& env, Data d) {
+            testcase("Scale clawback overflow");
+
+            {
+                auto tx = d.vault.deposit(
+                    {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)});
+                env(tx);
+                env.close();
+            }
+
+            {
+                auto tx = d.vault.clawback(
+                    {.issuer = d.issuer,
+                     .id = d.keylet.key,
+                     .holder = d.depositor,
+                     .amount = STAmount(d.asset, Number(10, 0))});
+                env(tx, Ter{tecPATH_DRY});
+                env.close();
+            }
+        });
+
+        testCase(1, [&, this](Env& env, Data d) {
+            // initial setup: deposit 100 IOU, receive 1000 shares
+            auto const start = env.balance(d.depositor, d.assets).number();
+            auto tx = d.vault.deposit(
+                {.depositor = d.depositor,
+                 .id = d.keylet.key,
+                 .amount = STAmount(d.asset, Number(100, 0))});
+            env(tx);
+            env.close();
+            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
+            BEAST_EXPECT(
+                env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(100, 0)));
+            BEAST_EXPECT(
+                env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(100, 0)));
+            BEAST_EXPECT(
+                env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(1000, 0)));
+            {
+                testcase("Scale clawback exact");
+                // assetsToSharesWithdraw:
+                //  shares = sharesTotal * (assets / assetsTotal)
+                //  shares = 1000 * 10 / 100 = 1000 * 0.1 = 100
+                // sharesToAssetsWithdraw:
+                //  assets = assetsTotal * (shares / sharesTotal)
+                //  assets = 100 * 100 / 1000 = 100 * 0.1 = 10
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+                auto tx = d.vault.clawback(
+                    {.issuer = d.issuer,
+                     .id = d.keylet.key,
+                     .holder = d.depositor,
+                     .amount = STAmount(d.asset, Number(10, 0))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900));
+                BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(90, 0)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(900, 0)));
+            }
+
+            {
+                testcase("Scale clawback insignificant amount");
+                auto tx = d.vault.clawback(
+                    {.issuer = d.issuer,
+                     .id = d.keylet.key,
+                     .holder = d.depositor,
+                     .amount = STAmount(d.asset, Number(4, -2))});
+                env(tx, Ter{tecPRECISION_LOSS});
+            }
+
+            {
+                testcase("Scale clawback with rounding assets");
+                // assetsToSharesWithdraw:
+                //  shares = sharesTotal * (assets / assetsTotal)
+                //  shares = 900 * 2.5 / 90 = 900 * 0.02777... = 25
+                // sharesToAssetsWithdraw:
+                //  assets = assetsTotal * (shares / sharesTotal)
+                //  assets = 90 * 25 / 900 = 90 * 0.02777... = 2.5
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+                auto tx = d.vault.clawback(
+                    {.issuer = d.issuer,
+                     .id = d.keylet.key,
+                     .holder = d.depositor,
+                     .amount = STAmount(d.asset, Number(25, -1))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(900 - 25));
+                BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) ==
+                    STAmount(d.asset, Number(900 - 25, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) ==
+                    STAmount(d.share, -Number(900 - 25, 0)));
+            }
+
+            {
+                testcase("Scale clawback with rounding shares up (truncated post-fixCleanup3_4_0)");
+                // Pre-fixCleanup3_4_0:
+                //   shares = round(875 * 3.75 / 87.5) = 38
+                //   assets = 87.5 * 38 / 875 = 3.8 > 3.75 requested.
+                // Post-fixCleanup3_4_0:
+                //   shares = floor(37.5) = 37
+                //   assets = 87.5 * 37 / 875 = 3.7 <= 3.75 requested.
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+                auto tx = d.vault.clawback(
+                    {.issuer = d.issuer,
+                     .id = d.keylet.key,
+                     .holder = d.depositor,
+                     .amount = STAmount(d.asset, Number(375, -2))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(875 - 37));
+                BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) ==
+                    STAmount(d.asset, Number(875 - 37, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) ==
+                    STAmount(d.share, -Number(875 - 37, 0)));
+            }
+
+            {
+                testcase("Scale clawback with rounding shares down");
+                // Chained state: 838 shares outstanding, 83.8 assets.
+                //   shares = floor(838 * 3.72 / 83.8) = floor(37.199...) = 37
+                //   assets = 83.8 * 37 / 838 = 3.7 <= 3.72 requested.
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+                auto tx = d.vault.clawback(
+                    {.issuer = d.issuer,
+                     .id = d.keylet.key,
+                     .holder = d.depositor,
+                     .amount = STAmount(d.asset, Number(372, -2))});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(838 - 37));
+                BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) ==
+                    STAmount(d.asset, Number(838 - 37, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) ==
+                    STAmount(d.share, -Number(838 - 37, 0)));
+            }
+
+            {
+                testcase("Scale clawback tiny amount rejected post-fixCleanup3_4_0");
+                // Chained state: 801 shares outstanding, 80.1 assets.
+                //   shares = floor(801 * 0.09 / 80.1) = floor(0.9) = 0
+                // Zero shares => tecPRECISION_LOSS. State is unchanged.
+
+                auto const start = env.balance(d.depositor, d.assets).number();
+                auto tx = d.vault.clawback(
+                    {.issuer = d.issuer,
+                     .id = d.keylet.key,
+                     .holder = d.depositor,
+                     .amount = STAmount(d.asset, Number(9, -2))});
+                env(tx, Ter{tecPRECISION_LOSS});
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(801));
+                BEAST_EXPECT(env.balance(d.depositor, d.assets) == STAmount(d.asset, start));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(801, -1)));
+                BEAST_EXPECT(
+                    env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(801, 0)));
+            }
+
+            {
+                testcase("Scale clawback rest");
+                auto const rest = env.balance(d.vaultAccount, d.assets).number();
+                d.peek([](SLE& vault, auto&) -> bool {
+                    vault[sfAssetsAvailable] = Number(5);
+                    return true;
+                });
+
+                // Note, this transaction yields two different results:
+                // * in the open ledger, with AssetsAvailable = 5
+                // * when the ledger is closed with unmodified AssetsAvailable
+                //   because a modification like above is not persistent.
+                tx = d.vault.clawback(
+                    {.issuer = d.issuer,
+                     .id = d.keylet.key,
+                     .holder = d.depositor,
+                     .amount = STAmount(d.asset, rest)});
+                env(tx);
+                env.close();
+                BEAST_EXPECT(env.balance(d.depositor, d.shares).number() == 0);
+                BEAST_EXPECT(env.balance(d.vaultAccount, d.assets).number() == 0);
+                BEAST_EXPECT(env.balance(d.vaultAccount, d.shares).number() == 0);
+            }
+        });
+
+        // Non-1:1 ratio (scale=1, 10:1 shares:assets) with an outstanding loan.
+        // Deposit 100 IOU → 1000 shares. Borrow 40 → assetsAvailable=60.
+        // Clawback 80 IOU → clamped to 60, then share math uses truncation.
+        testCase(1, [&, this](Env& env, Data d) {
+            using namespace loan_broker;
+            using namespace loan;
+
+            testcase("Scale clawback clamped with outstanding loan");
+
+            auto tx = d.vault.deposit(
+                {.depositor = d.depositor,
+                 .id = d.keylet.key,
+                 .amount = STAmount(d.asset, Number(100, 0))});
+            env(tx);
+            env.close();
+            BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000));
+
+            // Create a loan broker backed by this vault
+            auto const brokerKeylet =
+                keylet::loanBroker(d.owner.id(), SeqProxy::rawSequence(env.seq(d.owner)));
+            env(set(d.owner, d.keylet.key));
+            env.close();
+
+            // Borrow 40: assetsAvailable=60, assetsTotal=100
+            env(set(d.depositor, brokerKeylet.key, STAmount(d.asset, Number(40, 0))),
+                loan::kInterestRate(TenthBips32(0)),
+                kGracePeriod(60),
+                kPaymentInterval(120),
+                kPaymentTotal(10),
+                Sig(sfCounterpartySignature, d.owner),
+                Fee(env.current()->fees().base * 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            {
+                auto const sle = env.le(d.keylet);
+                BEAST_EXPECT(sle->at(sfAssetsAvailable) == STAmount(d.asset, Number(60, 0)));
+                BEAST_EXPECT(sle->at(sfAssetsTotal) == STAmount(d.asset, Number(100, 0)));
+            }
+
+            // Request 80 IOU clawback — clamped to assetsAvailable (60)
+            // With scale=1 (10:1), 60 assets = 600 shares destroyed
+            tx = d.vault.clawback(
+                {.issuer = d.issuer,
+                 .id = d.keylet.key,
+                 .holder = d.depositor,
+                 .amount = STAmount(d.asset, Number(80, 0))});
+            env(tx, Ter(tesSUCCESS));
+            env.close();
+
+            {
+                auto const sle = env.le(d.keylet);
+                BEAST_EXPECT(sle != nullptr);
+                BEAST_EXPECT(sle->at(sfAssetsAvailable) == STAmount(d.asset, Number(0, 0)));
+                BEAST_EXPECT(sle->at(sfAssetsTotal) == STAmount(d.asset, Number(40, 0)));
+
+                // 600 of 1000 shares destroyed, 400 remain
+                BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(400));
+            }
+        });
+
+        // peek() writes the open ledger only; do not close() before le().
+        auto seedLargeTotal = [](Env& env,
+                                 Data& d,
+                                 Number const& total,
+                                 Number const& available,
+                                 std::uint64_t outstanding) {
+            auto tx = d.vault.deposit(
+                {.depositor = d.depositor,
+                 .id = d.keylet.key,
+                 .amount = STAmount(d.asset, Number(100, 0))});
+            env(tx);
+            env.close();
+            d.peek([&](SLE& vault, SLE& shares) -> bool {
+                vault[sfAssetsTotal] = total;
+                vault[sfAssetsAvailable] = available;
+                shares[sfOutstandingAmount] = outstanding;
+                return true;
+            });
+        };
+
+        auto expectVault = [this](
+                               Env& env,
+                               Data const& d,
+                               Number const& total,
+                               Number const& available,
+                               STAmount const& shareBalance) {
+            auto const sle = env.le(d.keylet);
+            BEAST_EXPECT(sle != nullptr);
+            BEAST_EXPECT(sle->at(sfAssetsTotal) == total);
+            BEAST_EXPECT(sle->at(sfAssetsAvailable) == available);
+            BEAST_EXPECT(env.balance(d.depositor, d.shares) == shareBalance);
+        };
+
+        // T-6 is exact after the decade; recover 6.
+        testCase(0, [&, this](Env& env, Data d) {
+            testcase("Scale clawback uses posterior scale across decade boundary");
+
+            Number const midGridTotal{10000000000000005ll};
+            Number const available{6};
+            seedLargeTotal(env, d, midGridTotal, available, 10000000000000005ull);
+
+            auto tx =
+                d.vault.clawback({.issuer = d.issuer, .id = d.keylet.key, .holder = d.depositor});
+            env(tx, Ter(tesSUCCESS));
+            expectVault(env, d, midGridTotal - available, Number(0), d.share(94));
+        });
+
+        // T stays on the 10-asset grid; 6 is unrepresentable.
+        testCase(0, [&, this](Env& env, Data d) {
+            testcase("Scale clawback rejects amount below posterior scale");
+
+            Number const midGridTotal{12345678901234567ll};
+            Number const available{6};
+            seedLargeTotal(env, d, midGridTotal, available, 12345678901234567ull);
+
+            auto tx =
+                d.vault.clawback({.issuer = d.issuer, .id = d.keylet.key, .holder = d.depositor});
+            env(tx, Ter(tecPRECISION_LOSS));
+            expectVault(env, d, midGridTotal, available, d.share(100));
+        });
+
+        // A recovery larger than the anterior ULP also lands exactly on the finer posterior grid.
+        testCase(0, [&, this](Env& env, Data d) {
+            testcase("Scale clawback preserves exact posterior amount");
+
+            Number const midGridTotal{10000000000000005ll};
+            Number const available{15};
+            seedLargeTotal(env, d, midGridTotal, available, 10000000000000005ull);
+
+            auto tx =
+                d.vault.clawback({.issuer = d.issuer, .id = d.keylet.key, .holder = d.depositor});
+            env(tx, Ter(tesSUCCESS));
+            expectVault(env, d, midGridTotal - available, Number(0), d.share(85));
+        });
+
+        testCase(0, [&, this](Env& env, Data d) {
+            testcase("Scale deposit rejects amount below posterior scale");
+
+            Number const midGridTotal{10000000000000005ll};
+            Number const available{100};
+            seedLargeTotal(env, d, midGridTotal, available, 10000000000000005ull);
+
+            auto const assetsBefore = env.balance(d.depositor, d.assets);
+            auto tx = d.vault.deposit(
+                {.depositor = d.depositor,
+                 .id = d.keylet.key,
+                 .amount = STAmount(d.asset, Number(6))});
+            env(tx, Ter(tecPRECISION_LOSS));
+            expectVault(env, d, midGridTotal, available, d.share(100));
+            BEAST_EXPECT(env.balance(d.depositor, d.assets) == assetsBefore);
+        });
+
+        testCase(0, [&, this](Env& env, Data d) {
+            testcase("Scale withdraw uses posterior scale across decade boundary");
+
+            Number const midGridTotal{10000000000000005ll};
+            Number const available{100};
+            seedLargeTotal(env, d, midGridTotal, available, 10000000000000005ull);
+
+            auto const assetsBefore = env.balance(d.depositor, d.assets);
+            auto tx = d.vault.withdraw(
+                {.depositor = d.depositor,
+                 .id = d.keylet.key,
+                 .amount = STAmount(d.share, Number(15))});
+            env(tx, Ter(tesSUCCESS));
+            expectVault(env, d, midGridTotal - Number(15), Number(85), d.share(85));
+            BEAST_EXPECT(
+                env.balance(d.depositor, d.assets) ==
+                STAmount(d.asset, assetsBefore.number() + Number(15)));
+        });
+    }
+
+    void
+    testAssetsMaximum()
+    {
+        testcase("Assets Maximum");
+
+        using namespace test::jtx;
+
+        Env env{*this, testableAmendments()};
+        Account const owner{"owner"};
+        Account const issuer{"issuer"};
+
+        Vault const vault{env};
+        env.fund(XRP(1'000'000), issuer, owner);
+        env.close();
+
+        auto const maxInt64 = std::to_string(std::numeric_limits::max());
+        BEAST_EXPECT(maxInt64 == "9223372036854775807");
+
+        auto const maxInt64Plus1 = std::to_string(
+            static_cast(std::numeric_limits::max()) + 1);
+        BEAST_EXPECT(maxInt64Plus1 == "9223372036854775808");
+
+        // Naming things is hard
+        auto const maxInt64Plus2 = std::to_string(
+            static_cast(std::numeric_limits::max()) + 2);
+        BEAST_EXPECT(maxInt64Plus2 == "9223372036854775809");
+
+        auto const initialXRP = to_string(kInitialXrp);
+        BEAST_EXPECT(initialXRP == "100000000000000000");
+
+        auto const initialXRPPlus1 = to_string(kInitialXrp + 1);
+        BEAST_EXPECT(initialXRPPlus1 == "100000000000000001");
+
+        {
+            testcase("Assets Maximum: XRP");
+
+            PrettyAsset const xrpAsset = xrpIssue();
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
+            tx[sfData] = "4D65746144617461";
+
+            tx[sfAssetsMaximum] = maxInt64;
+            env(tx, Ter(tefEXCEPTION));
+            env.close();
+
+            tx[sfAssetsMaximum] = initialXRPPlus1;
+            env(tx, Ter(tefEXCEPTION));
+            env.close();
+
+            tx[sfAssetsMaximum] = initialXRP;
+            env(tx);
+            env.close();
+
+            // There are several parse failures expected in this function, so just disable it once.
+            env.setParseFailureExpected(true);
+            try
+            {
+                tx[sfAssetsMaximum] = maxInt64Plus1;
+                env(tx, Ter(tefEXCEPTION));
+                env.close();
+                // should throw in parser
+                fail();
+            }
+            catch (std::exception const& e)
+            {
+                BEAST_EXPECT(
+                    std::string(e.what()) ==
+                    "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
+            }
+
+            try
+            {
+                tx[sfAssetsMaximum] = maxInt64Plus2;
+                env(tx, Ter(tefEXCEPTION));
+                // should throw in parser
+                fail();
+            }
+            catch (std::exception const& e)
+            {
+                BEAST_EXPECT(
+                    std::string(e.what()) ==
+                    "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
+            }
+
+            auto const newKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+            try
+            {
+                auto const insertAt = maxInt64Plus2.size() - 3;
+                auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." +
+                    maxInt64Plus2.substr(insertAt);  // (max int64+2) / 1000
+                BEAST_EXPECT(decimalTest == "9223372036854775.809");
+                tx[sfAssetsMaximum] = decimalTest;
+                env(tx);
+                // should throw in parser
+                fail();
+            }
+            catch (std::exception const& e)
+            {
+                BEAST_EXPECT(
+                    std::string(e.what()) ==
+                    "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
+            }
+
+            auto const vaultSle = env.le(newKeylet);
+            BEAST_EXPECT(!vaultSle);
+        }
+
+        {
+            testcase("Assets Maximum: MPT");
+
+            PrettyAsset const mptAsset = [&]() {
+                MPTTester mptt{env, issuer, kMptInitNoFund};
+                mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
+                env.close();
+                PrettyAsset const mptAsset = mptt["MPT"];
+                mptt.authorize({.account = owner});
+                env.close();
+                return mptAsset;
+            }();
+
+            env(pay(issuer, owner, mptAsset(100'000)));
+            env.close();
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = mptAsset});
+            tx[sfData] = "4D65746144617461";
+
+            tx[sfAssetsMaximum] = maxInt64;
+            env(tx);
+            env.close();
+
+            tx[sfAssetsMaximum] = initialXRPPlus1;
+            env(tx);
+            env.close();
+
+            tx[sfAssetsMaximum] = initialXRP;
+            env(tx);
+            env.close();
+
+            try
+            {
+                tx[sfAssetsMaximum] = maxInt64Plus2;
+                env(tx, Ter(tefEXCEPTION));
+                // should throw in parser
+                fail();
+            }
+            catch (std::exception const& e)
+            {
+                BEAST_EXPECT(
+                    std::string(e.what()) ==
+                    "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
+            }
+
+            auto const newKeylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+            try
+            {
+                auto const insertAt = maxInt64Plus2.size() - 1;
+                auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." +
+                    maxInt64Plus2.substr(insertAt);  // (max int64+2) / 10
+                BEAST_EXPECT(decimalTest == "922337203685477580.9");
+                tx[sfAssetsMaximum] = decimalTest;
+                env(tx);
+                // should throw in parser
+                fail();
+            }
+            catch (std::exception const& e)
+            {
+                BEAST_EXPECT(
+                    std::string(e.what()) ==
+                    "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
+            }
+
+            auto const vaultSle = env.le(newKeylet);
+            BEAST_EXPECT(!vaultSle);
+        }
+
+        {
+            testcase("Assets Maximum: IOU");
+
+            // Almost anything goes with IOUs
+            PrettyAsset const iouAsset = issuer["IOU"];
+            env.trust(iouAsset(1000), owner);
+            env(pay(issuer, owner, iouAsset(200)));
+            env.close();
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = iouAsset});
+            tx[sfData] = "4D65746144617461";
+
+            tx[sfAssetsMaximum] = maxInt64;
+            env(tx);
+            env.close();
+
+            tx[sfAssetsMaximum] = initialXRPPlus1;
+            env(tx);
+            env.close();
+
+            tx[sfAssetsMaximum] = initialXRP;
+            env(tx);
+            env.close();
+
+            // Since several tests are expected to have parser failures, leave this flag set for the
+            // remainder of this function.
+            env.setParseFailureExpected(true);
+            try
+            {
+                tx[sfAssetsMaximum] = maxInt64Plus2;
+                env(tx);
+                // should throw in parser
+                fail();
+            }
+            catch (std::exception const& e)
+            {
+                BEAST_EXPECT(
+                    std::string(e.what()) ==
+                    "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
+            }
+
+            tx[sfAssetsMaximum] = "1000000000000000e80";
+            env.close();
+
+            tx[sfAssetsMaximum] = "1000000000000000e-96";
+            env.close();
+
+            // These values will be rounded to 15 significant digits
+            {
+                auto const newKeylet =
+                    keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+                try
+                {
+                    auto const insertAt = maxInt64Plus2.size() - 1;
+                    auto const decimalTest = maxInt64Plus2.substr(0, insertAt) + "." +
+                        maxInt64Plus2.substr(insertAt);  // (max int64+2) / 10
+                    BEAST_EXPECT(decimalTest == "922337203685477580.9");
+                    tx[sfAssetsMaximum] = decimalTest;
+                    env(tx);
+                    // should throw in parser
+                    fail();
+                }
+                catch (std::exception const& e)
+                {
+                    BEAST_EXPECT(
+                        std::string(e.what()) ==
+                        "invalidParamsField 'tx_json.AssetsMaximum' has invalid data.");
+                }
+
+                auto const vaultSle = env.le(newKeylet);
+                BEAST_EXPECT(!vaultSle);
+            }
+            {
+                tx[sfAssetsMaximum] = "9223372036854775807e40";  // max int64 * 10^40
+                auto const newKeylet =
+                    keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+                env(tx);
+                env.close();
+
+                auto const vaultSle = env.le(newKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+
+                BEAST_EXPECT(
+                    (vaultSle->at(sfAssetsMaximum) ==
+                     Number{9223372036854776, 43, Number::Normalized{}}));
+            }
+            {
+                tx[sfAssetsMaximum] = "9223372036854775807e-40";  // max int64 * 10^-40
+                auto const newKeylet =
+                    keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+                env(tx);
+                env.close();
+
+                auto const vaultSle = env.le(newKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+
+                BEAST_EXPECT(
+                    (vaultSle->at(sfAssetsMaximum) ==
+                     Number{9223372036854776, -37, Number::Normalized{}}));
+            }
+            {
+                tx[sfAssetsMaximum] = "9223372036854775807e-100";  // max int64 * 10^-100
+                auto const newKeylet =
+                    keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+                env(tx);
+                env.close();
+
+                // Field 'AssetsMaximum' may not be explicitly set to default.
+                auto const vaultSle = env.le(newKeylet);
+                if (!BEAST_EXPECT(vaultSle))
+                    return;
+
+                BEAST_EXPECT(vaultSle->at(sfAssetsMaximum) == kNumZero);
+            }
+
+            // What _can't_ IOUs do?
+            // 1. Exceed maximum exponent / offset
+            tx[sfAssetsMaximum] = "1000000000000000e81";
+            env(tx, Ter(tefEXCEPTION));
+            env.close();
+
+            // 2. Mantissa larger than uint64 max
+            try
+            {
+                auto const g = env.getParseFailureGuard(true);
+                tx[sfAssetsMaximum] = "18446744073709551617e5";  // uint64 max + 1
+                env(tx);
+                BEAST_EXPECTS(false, "Expected parse_error for mantissa larger than uint64 max");
+            }
+            catch (ParseError const& e)
+            {
+                using namespace std::string_literals;
+                BEAST_EXPECT(
+                    e.what() == "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."s);
+            }
+        }
+    }
+
+public:
+    void
+    run() override
+    {
+        testScaleIOU();
+        testAssetsMaximum();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE_PRIO(VaultScale, app, xrpl, 1);
+
+}  // namespace xrpl
diff --git a/src/test/app/vault/VaultShares_test.cpp b/src/test/app/vault/VaultShares_test.cpp
new file mode 100644
index 0000000000..037ee3e057
--- /dev/null
+++ b/src/test/app/vault/VaultShares_test.cpp
@@ -0,0 +1,736 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class VaultShares_test : public VaultTestBase
+{
+private:
+    void
+    testNonTransferableShares()
+    {
+        using namespace test::jtx;
+
+        Env env{*this, testableAmendments()};
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const depositor{"depositor"};
+        env.fund(XRP(1000), issuer, owner, depositor);
+        env.close();
+
+        Vault const vault{env};
+        PrettyAsset const asset = issuer["IOU"];
+        env.trust(asset(1000), owner);
+        env(pay(issuer, owner, asset(100)));
+        env.trust(asset(1000), depositor);
+        env(pay(issuer, depositor, asset(100)));
+        env.close();
+
+        auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+        tx[sfFlags] = tfVaultShareNonTransferable;
+        env(tx);
+        env.close();
+
+        {
+            testcase("nontransferable deposits");
+            auto tx1 =
+                vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(40)});
+            env(tx1);
+
+            auto tx2 = vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(60)});
+            env(tx2);
+            env.close();
+        }
+
+        auto const vaultAccount =  //
+            [&env, key = keylet.key, this]() -> AccountID {
+            auto jvVault = env.rpc("vault_info", strHex(key));
+
+            BEAST_EXPECT(jvVault[jss::result][jss::vault][sfAssetsTotal] == "100");
+            BEAST_EXPECT(
+                jvVault[jss::result][jss::vault][jss::shares][sfOutstandingAmount] == "100000000");
+
+            // Vault pseudo-account
+            return parseBase58(jvVault[jss::result][jss::vault][jss::Account].asString())
+                .value();
+        }();
+
+        auto const mptId = makeMptID(1, vaultAccount);
+        Asset const shares = mptId;
+
+        {
+            testcase("nontransferable shares cannot be moved");
+            env(pay(owner, depositor, shares(10)), Ter{tecNO_AUTH});
+            env(pay(depositor, owner, shares(10)), Ter{tecNO_AUTH});
+        }
+
+        {
+            testcase("nontransferable shares can be used to withdraw");
+            auto tx1 =
+                vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(20)});
+            env(tx1);
+
+            auto tx2 = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(30)});
+            env(tx2);
+            env.close();
+        }
+
+        {
+            testcase("nontransferable shares balance check");
+            auto jvVault = env.rpc("vault_info", strHex(keylet.key));
+            BEAST_EXPECT(jvVault[jss::result][jss::vault][sfAssetsTotal] == "50");
+            BEAST_EXPECT(
+                jvVault[jss::result][jss::vault][jss::shares][sfOutstandingAmount] == "50000000");
+        }
+
+        {
+            testcase("nontransferable shares withdraw rest");
+            auto tx1 =
+                vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(20)});
+            env(tx1);
+
+            auto tx2 = vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(30)});
+            env(tx2);
+            env.close();
+        }
+
+        {
+            testcase("nontransferable shares delete empty vault");
+            auto tx = vault.del({.owner = owner, .id = keylet.key});
+            env(tx);
+            BEAST_EXPECT(!env.le(keylet));
+        }
+    }
+
+    void
+    testFailedPseudoAccount()
+    {
+        using namespace test::jtx;
+
+        testcase("fail pseudo-account allocation");
+        Env env{*this, testableAmendments()};
+        Account const owner{"owner"};
+        Vault const vault{env};
+        env.fund(XRP(1000), owner);
+
+        auto const keylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+        for (int i = 0; i < 256; ++i)
+        {
+            AccountID const accountId = xrpl::pseudoAccountAddress(*env.current(), keylet.key);
+
+            env(pay(env.master.id(), accountId, XRP(1000)),
+                Seq(kAutofill),
+                Fee(kAutofill),
+                Sig(kAutofill));
+        }
+
+        auto [tx, keylet1] = vault.create({.owner = owner, .asset = xrpIssue()});
+        BEAST_EXPECT(keylet.key == keylet1.key);
+        env(tx, Ter{terADDRESS_COLLISION});
+    }
+
+    void
+    testRemoveEmptyHoldingLockedAmount()
+    {
+        testcase("removeEmptyHolding deletes MPToken with sfLockedAmount");
+        using namespace test::jtx;
+        using namespace std::literals;
+
+        auto const amendments = testableAmendments();
+        auto runTest = [&](FeatureBitset f) {
+            Env env{*this, f};
+            auto const baseFee = env.current()->fees().base;
+
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            Account const depositor{"depositor"};
+            Account const bob{"bob"};
+
+            env.fund(XRP(100000), issuer, owner, depositor, bob);
+            env.close();
+
+            Vault const vault{env};
+
+            // Create an MPT asset for the vault
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
+            PrettyAsset const asset = mptt.issuanceID();
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = depositor});
+            env(pay(issuer, depositor, asset(1000)));
+            env.close();
+
+            // Create vault
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            auto const vaultSle = env.le(keylet);
+            BEAST_EXPECT(vaultSle != nullptr);
+            auto const shareMptID = vaultSle->at(sfShareMPTID);
+            MPTIssue const shareIssue{shareMptID};
+
+            // Depositor deposits 1000 asset units into vault, receiving shares
+            env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)}));
+            env.close();
+
+            // Check depositor has shares
+            {
+                auto const sleMpt = env.le(keylet::mptoken(shareMptID, depositor));
+                BEAST_EXPECT(sleMpt != nullptr);
+                BEAST_EXPECT(sleMpt->at(sfMPTAmount) == 1000);
+            }
+
+            // Escrow 500 of those shares
+            env(escrow::create(depositor, bob, STAmount{shareIssue, 500}),
+                escrow::kCondition(escrow::kCb1),
+                escrow::kFinishTime(env.now() + 1s),
+                Fee(baseFee * 150),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Verify: sfMPTAmount=500, sfLockedAmount=500
+            {
+                auto const sleMpt = env.le(keylet::mptoken(shareMptID, depositor));
+                BEAST_EXPECT(sleMpt != nullptr);
+                BEAST_EXPECT(sleMpt->at(sfLockedAmount) == 500);
+                BEAST_EXPECT(sleMpt->at(sfMPTAmount) == 500);
+            }
+
+            // Withdraw remaining spendable shares — triggers removeEmptyHolding
+            env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(500)}),
+                Ter(tesSUCCESS));
+            env.close();
+
+            auto const sleMptAfter = env.le(keylet::mptoken(shareMptID, depositor));
+            if (!f[fixCleanup3_1_3])
+            {
+                // Without the fix, removeEmptyHolding deletes the MPToken
+                // even though sfLockedAmount > 0, leaving the escrow's locked
+                // amount untracked.
+                BEAST_EXPECT(sleMptAfter == nullptr);
+            }
+            else
+            {
+                // With the fix, MPToken must still exist with sfLockedAmount > 0
+                // and sfMPTAmount == 0 (all spendable shares withdrawn).
+                BEAST_EXPECT(sleMptAfter != nullptr);
+                if (sleMptAfter)
+                {
+                    BEAST_EXPECT(sleMptAfter->at(sfLockedAmount) == 500);
+                    BEAST_EXPECT(sleMptAfter->at(sfMPTAmount) == 0);
+                }
+            }
+        };
+
+        runTest(amendments - fixCleanup3_1_3);
+        runTest(amendments);
+    }
+
+    void
+    testRemoveEmptyHoldingConfidentialBalances()
+    {
+        testcase("removeEmptyHolding keeps MPToken with confidential balances");
+        using namespace test::jtx;
+
+        Env env{*this, testableAmendments()};
+
+        Account const issuer{"issuer"};
+        Account const holder{"holder"};
+        MPTTester mpt{env, issuer, {.holders = {holder}}};
+        mpt.create({.authorize = MPTCreate::allHolders});
+
+        auto const tokenKeylet = keylet::mptoken(mpt.issuanceID(), holder.id());
+        auto const encryptedBalanceFields = {
+            &sfConfidentialBalanceInbox,
+            &sfConfidentialBalanceSpending,
+            &sfIssuerEncryptedBalance,
+            &sfAuditorEncryptedBalance};
+
+        env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal j) {
+            for (auto const field : encryptedBalanceFields)
+            {
+                Sandbox sb(&view, TapNone);
+                auto const token = sb.peek(tokenKeylet);
+                if (!BEAST_EXPECT(token))
+                    return false;
+
+                token->setFieldVL(*field, gMakeZeroBuffer(kEcGamalEncryptedTotalLength));
+                sb.update(token);
+
+                auto const dummyTx = *env.jt(noop(holder)).stx;
+                BEAST_EXPECT(
+                    removeEmptyHolding({sb, dummyTx}, holder.id(), MPTIssue(mpt.issuanceID()), j) ==
+                    tecHAS_OBLIGATIONS);
+                BEAST_EXPECT(sb.peek(tokenKeylet) != nullptr);
+            }
+            return true;
+        });
+    }
+
+    void
+    testReferenceHolding()
+    {
+        using namespace test::jtx;
+
+        auto readReferenceHolding = [&](Env const& env,
+                                        Keylet const& vaultKeylet) -> std::optional {
+            auto const sleVault = env.le(vaultKeylet);
+            if (!sleVault)
+                return std::nullopt;
+            auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
+            if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
+                return std::nullopt;
+            return sleIssuance->getFieldH256(sfReferenceHolding);
+        };
+
+        // Post-fixCleanup3_2_0: vault share carries sfReferenceHolding
+        // pointing to the vault pseudo's MPToken (for MPT-backed vaults)
+        // or RippleState (for IOU-backed vaults).
+        {
+            testcase("sfReferenceHolding: MPT-backed vault, post-amendment");
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            env.fund(XRP(10'000), issuer, owner);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
+            PrettyAsset const asset = mptt.issuanceID();
+            mptt.authorize({.account = owner});
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            auto const sleVault = env.le(keylet);
+            BEAST_EXPECT(sleVault != nullptr);
+            auto const pseudoId = sleVault->at(sfAccount);
+            auto const expected = keylet::mptoken(mptt.issuanceID(), pseudoId).key;
+
+            auto const stored = readReferenceHolding(env, keylet);
+            BEAST_EXPECT(stored.has_value());
+            BEAST_EXPECT(stored && *stored == expected);
+            // The pointed-to MPToken must actually exist.
+            BEAST_EXPECT(env.le(keylet::mptoken(mptt.issuanceID(), pseudoId)) != nullptr);
+        }
+
+        {
+            testcase("sfReferenceHolding: IOU-backed vault, post-amendment");
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            env.fund(XRP(10'000), issuer, owner);
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const asset = issuer["IOU"];
+            env.trust(asset(1'000'000), owner);
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            auto const sleVault = env.le(keylet);
+            BEAST_EXPECT(sleVault != nullptr);
+            auto const pseudoId = sleVault->at(sfAccount);
+            auto const expected = keylet::trustLine(pseudoId, asset.raw().get()).key;
+
+            auto const stored = readReferenceHolding(env, keylet);
+            BEAST_EXPECT(stored.has_value());
+            BEAST_EXPECT(stored && *stored == expected);
+            // The pointed-to RippleState must actually exist.
+            BEAST_EXPECT(env.le(keylet::trustLine(pseudoId, asset.raw().get())) != nullptr);
+        }
+
+        // XRP-backed vaults leave the field absent: XRP has no separate
+        // holding ledger entry and no transferability concept to inherit.
+        {
+            testcase("sfReferenceHolding: XRP-backed vault, field absent");
+            Env env{*this, testableAmendments()};
+            Account const owner{"owner"};
+            env.fund(XRP(10'000), owner);
+            env.close();
+
+            PrettyAsset const asset{xrpIssue(), 1'000'000};
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            BEAST_EXPECT(!readReferenceHolding(env, keylet).has_value());
+        }
+
+        // Pre-fixCleanup3_2_0: vault share has the field absent regardless
+        // of underlying type.
+        {
+            testcase("sfReferenceHolding: vault share, pre-amendment");
+            Env env{*this, testableAmendments() - fixCleanup3_2_0};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            env.fund(XRP(10'000), issuer, owner);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
+            PrettyAsset const asset = mptt.issuanceID();
+            mptt.authorize({.account = owner});
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            BEAST_EXPECT(!readReferenceHolding(env, keylet).has_value());
+        }
+
+        // Plain MPTokenIssuanceCreate (not a vault share) must never
+        // populate the field. Only the post-amendment case is
+        // interesting; pre-amendment nothing writes the field at all.
+        {
+            testcase("sfReferenceHolding: plain MPT issuance never set");
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            env.fund(XRP(10'000), issuer);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
+            env.close();
+
+            auto const sleIssuance = env.le(keylet::mptokenIssuance(mptt.issuanceID()));
+            if (BEAST_EXPECT(sleIssuance))
+                BEAST_EXPECT(!sleIssuance->isFieldPresent(sfReferenceHolding));
+        }
+    }
+
+    // Probe every transactor surface that might delete the vault pseudo-
+    // account's underlying holding (the MPToken or RippleState pointed to
+    // by sfReferenceHolding). Each scenario asserts either that the
+    // existing pseudo-account guards stop the deletion at preclaim, or
+    // that the ledger leaves the holding intact afterwards. This is a
+    // regression guard: if any of these guards regresses, the share's
+    // sfReferenceHolding pointer would dangle and the new ValidMPTIssuance
+    // invariant would catch it - but we want to fail much earlier, at
+    // the transactor's preclaim / doApply, not at invariant time.
+    void
+    testHoldingDeletionBlocked()
+    {
+        using namespace test::jtx;
+
+        // Helper: read the share's referenced holding and confirm the
+        // pointed-to SLE still exists after the probe.
+        auto referencedHoldingExists = [&](Env const& env, Keylet const& vaultKeylet) -> bool {
+            auto const sleVault = env.le(vaultKeylet);
+            if (!sleVault)
+                return false;
+            auto const sleIssuance = env.le(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
+            if (!sleIssuance || !sleIssuance->isFieldPresent(sfReferenceHolding))
+                return false;
+            auto const holdingKey = sleIssuance->getFieldH256(sfReferenceHolding);
+            return env.le(keylet::unchecked(holdingKey)) != nullptr;
+        };
+
+        // ---- MPT-backed vault ----------------------------------------
+        {
+            testcase("vault pseudo MPToken: Clawback blocked by tecPSEUDO_ACCOUNT");
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            Account const depositor{"depositor"};
+            env.fund(XRP(10'000), issuer, owner, depositor);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback});
+            PrettyAsset const asset = mptt.issuanceID();
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = depositor});
+            env(pay(issuer, depositor, asset(1'000)));
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(500)}));
+            env.close();
+
+            BEAST_EXPECT(referencedHoldingExists(env, keylet));
+
+            Account const pseudoAccount{"vault-pseudo", env.le(keylet)->at(sfAccount)};
+            // Issuer attempts to claw back the FULL underlying balance
+            // (500) directly from the vault pseudo-account. With the
+            // full amount, the doApply path would drain the pseudo's
+            // MPToken to zero and removeEmptyHolding would erase it -
+            // if doApply ever ran. SAV's pseudo-account guard at
+            // Clawback.cpp:201 refuses at preclaim with
+            // tecPSEUDO_ACCOUNT before any state change.
+            env(claw(issuer, asset(500), pseudoAccount), Ter{tecPSEUDO_ACCOUNT});
+            env.close();
+            BEAST_EXPECT(referencedHoldingExists(env, keylet));
+            // Sanity: pseudo's full balance is intact.
+            BEAST_EXPECT(env.balance(pseudoAccount, asset).number() == 500);
+        }
+
+        {
+            testcase("vault pseudo MPToken: Issuer cannot Unauthorize pseudo");
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            env.fund(XRP(10'000), issuer, owner);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTRequireAuth});
+            PrettyAsset const asset = mptt.issuanceID();
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = issuer, .holder = owner});
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            BEAST_EXPECT(referencedHoldingExists(env, keylet));
+
+            auto const pseudoId = env.le(keylet)->at(sfAccount);
+            // Issuer attempts MPTokenAuthorize against the pseudo with
+            // tfMPTUnauthorize. MPTokenAuthorize.cpp blocks pseudo
+            // accounts via isPseudoAccount; the pseudo's MPToken is
+            // preserved. Construct the tx manually since the pseudo
+            // lacks a signing key, and the issuer-driven flavour is
+            // expressed via sfHolder.
+            json::Value jv;
+            jv[sfAccount] = issuer.human();
+            jv[sfHolder] = toBase58(pseudoId);
+            jv[sfMPTokenIssuanceID] = to_string(mptt.issuanceID());
+            jv[sfFlags] = tfMPTUnauthorize;
+            jv[sfTransactionType] = jss::MPTokenAuthorize;
+            env(jv, Ter{tecNO_PERMISSION});
+            env.close();
+            BEAST_EXPECT(referencedHoldingExists(env, keylet));
+        }
+
+        {
+            testcase("vault pseudo MPToken: MPTokenIssuanceDestroy blocked while vault holds");
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            Account const depositor{"depositor"};
+            env.fund(XRP(10'000), issuer, owner, depositor);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
+            PrettyAsset const asset = mptt.issuanceID();
+            mptt.authorize({.account = owner});
+            mptt.authorize({.account = depositor});
+            env(pay(issuer, depositor, asset(1'000)));
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(500)}));
+            env.close();
+
+            BEAST_EXPECT(referencedHoldingExists(env, keylet));
+
+            // While the vault holds outstanding underlying, the issuer
+            // cannot destroy the issuance. tecHAS_OBLIGATIONS confirms
+            // the protection - and as a side effect, the share's
+            // sfReferenceHolding pointer cannot be left pointing at a
+            // ghost issuance.
+            mptt.destroy({.id = mptt.issuanceID(), .err = tecHAS_OBLIGATIONS});
+            env.close();
+            BEAST_EXPECT(referencedHoldingExists(env, keylet));
+        }
+
+        // ---- IOU-backed vault ----------------------------------------
+        {
+            testcase("vault pseudo trust line: Clawback blocked by tecPSEUDO_ACCOUNT");
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            env.fund(XRP(10'000), issuer, owner);
+            env(fset(issuer, asfAllowTrustLineClawback));
+            env.close();
+
+            PrettyAsset const asset = issuer["IOU"];
+            env.trust(asset(1'000'000), owner);
+            env(pay(issuer, owner, asset(1'000)));
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(500)}));
+            env.close();
+
+            BEAST_EXPECT(referencedHoldingExists(env, keylet));
+
+            Account const pseudoAccount{"vault-pseudo", env.le(keylet)->at(sfAccount)};
+            // Issuer attempts to claw back the FULL IOU balance (500)
+            // directly from the vault pseudo. With the full amount, the
+            // doApply path would drain the trust line to zero and (if
+            // both reserve flags clear) trustDelete would erase it - if
+            // doApply ever ran. The same SAV pseudo-account guard
+            // refuses at preclaim with tecPSEUDO_ACCOUNT. The amount's
+            // STAmount issuer field is the holder, per IOU clawback
+            // convention.
+            env(claw(issuer, pseudoAccount["IOU"](500)), Ter{tecPSEUDO_ACCOUNT});
+            env.close();
+            BEAST_EXPECT(referencedHoldingExists(env, keylet));
+            // Sanity: pseudo's full balance is intact.
+            BEAST_EXPECT(env.balance(pseudoAccount, asset).number() == 500);
+        }
+
+        {
+            testcase("vault pseudo trust line: TrustSet limit=0 from issuer preserves line");
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            env.fund(XRP(10'000), issuer, owner);
+            env(fset(issuer, asfDefaultRipple));
+            env.close();
+
+            PrettyAsset const asset = issuer["IOU"];
+            env.trust(asset(1'000'000), owner);
+            env(pay(issuer, owner, asset(1'000)));
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            env(vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(500)}));
+            env.close();
+
+            BEAST_EXPECT(referencedHoldingExists(env, keylet));
+
+            // Issuer submits TrustSet with limit=0 against the vault
+            // pseudo. The pseudo's side of the line still has the
+            // original (non-zero) limit and a non-zero balance, so the
+            // line is preserved - even though the issuer cleared its
+            // own side. trustDelete only fires when both limits clear
+            // and the balance is zero.
+            Account const pseudoAccount{"vault-pseudo", env.le(keylet)->at(sfAccount)};
+            env(trust(issuer, pseudoAccount["IOU"](0)));
+            env.close();
+            BEAST_EXPECT(referencedHoldingExists(env, keylet));
+        }
+
+        // ---- Positive control: VaultDelete is the only legitimate path
+        {
+            testcase("vault pseudo holding: VaultDelete is the legitimate cleanup path");
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            env.fund(XRP(10'000), issuer, owner);
+            env.close();
+
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock});
+            PrettyAsset const asset = mptt.issuanceID();
+            mptt.authorize({.account = owner});
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+
+            BEAST_EXPECT(referencedHoldingExists(env, keylet));
+            auto const pseudoId = env.le(keylet)->at(sfAccount);
+            auto const sharedMptId = env.le(keylet)->at(sfShareMPTID);
+            auto const holdingKeylet = keylet::mptoken(mptt.issuanceID(), pseudoId);
+
+            // VaultDelete tears down the vault pseudo's holding, the
+            // share issuance, and the pseudo-account itself. Invariant
+            // permits this because the tx is ttVAULT_DELETE.
+            env(vault.del({.owner = owner, .id = keylet.key}));
+            env.close();
+
+            BEAST_EXPECT(env.le(keylet) == nullptr);
+            BEAST_EXPECT(env.le(holdingKeylet) == nullptr);
+            BEAST_EXPECT(env.le(keylet::mptokenIssuance(sharedMptId)) == nullptr);
+        }
+    }
+
+public:
+    void
+    run() override
+    {
+        testNonTransferableShares();
+        testFailedPseudoAccount();
+        testRemoveEmptyHoldingLockedAmount();
+        testRemoveEmptyHoldingConfidentialBalances();
+        testReferenceHolding();
+        testHoldingDeletionBlocked();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(VaultShares, app, xrpl);
+
+}  // namespace xrpl
diff --git a/src/test/app/vault/VaultSoleShareholder_test.cpp b/src/test/app/vault/VaultSoleShareholder_test.cpp
new file mode 100644
index 0000000000..92d5dd04d4
--- /dev/null
+++ b/src/test/app/vault/VaultSoleShareholder_test.cpp
@@ -0,0 +1,685 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class VaultSoleShareholder_test : public VaultTestBase
+{
+private:
+    // design doc:
+    //     AssetsAvailable ≈ 3,333.50
+    //     AssetsTotal     ≈ 6,666.50  (3,333.50 cash + 3,333 receivable)
+    //     LossUnrealized  =  3,333
+    //     OutstandingShares = sharesLender   (5e9 at IOU scale 1e6)
+    struct StuckDepositorFixture
+    {
+        test::jtx::Account issuer{"issuer"};
+        test::jtx::Account lender{"lender"};
+        test::jtx::Account bob{"bob"};
+        test::jtx::Account borrower{"borrower"};
+        std::optional asset;
+        std::optional vaultKeylet;
+        uint256 brokerID;
+        std::optional loanKeylet;
+        MPTID shareAsset;
+        std::uint64_t sharesLender = 0;
+    };
+
+    static constexpr std::int64_t kStuckFunding = 1'000'000;
+    static constexpr std::int64_t kStuckDepositorIOU = 1'000'000;
+    static constexpr std::int64_t kStuckBorrowerIOU = 100'000;
+    static constexpr std::int64_t kStuckDeposit = 5'000;
+    static constexpr std::int64_t kStuckPrincipal = 3'333;
+    static constexpr std::uint32_t kStuckPayInterval = 600;
+    static constexpr std::uint32_t kStuckPayTotal = 2;
+
+    [[nodiscard]] StuckDepositorFixture
+    setupStuckDepositor(test::jtx::Env& env)
+    {
+        using namespace test::jtx;
+
+        StuckDepositorFixture f;
+        f.asset = f.issuer[iouCurrency_];
+
+        env.fund(XRP(kStuckFunding), f.issuer, f.lender, f.bob, f.borrower);
+        env.close();
+
+        env(trust(f.lender, (*f.asset)(10'000'000)));
+        env(trust(f.bob, (*f.asset)(10'000'000)));
+        env(trust(f.borrower, (*f.asset)(10'000'000)));
+        env.close();
+
+        env(pay(f.issuer, f.lender, (*f.asset)(kStuckDepositorIOU)));
+        env(pay(f.issuer, f.bob, (*f.asset)(kStuckDepositorIOU)));
+        env(pay(f.issuer, f.borrower, (*f.asset)(kStuckBorrowerIOU)));
+        env.close();
+
+        // Vault: Lender creates and seeds it; Bob matches the deposit for a
+        // clean 50/50 split.
+        Vault const v{env};
+        auto [createTx, vaultKeylet] = v.create({.owner = f.lender, .asset = *f.asset});
+        env(createTx);
+        env.close();
+        if (!BEAST_EXPECT(env.le(vaultKeylet)))
+            return f;
+        f.vaultKeylet = vaultKeylet;
+
+        env(v.deposit({
+                .depositor = f.lender,
+                .id = vaultKeylet.key,
+                .amount = (*f.asset)(kStuckDeposit),
+            }),
+            Ter(tesSUCCESS));
+        env(v.deposit({
+                .depositor = f.bob,
+                .id = vaultKeylet.key,
+                .amount = (*f.asset)(kStuckDeposit),
+            }),
+            Ter(tesSUCCESS));
+        env.close();
+
+        // Loan broker: no cover, no management fee, debt cap 10x principal.
+        f.brokerID =
+            keylet::loanBroker(f.lender.id(), SeqProxy::rawSequence(env.seq(f.lender))).key;
+        {
+            using namespace loan_broker;
+            env(set(f.lender, vaultKeylet.key),
+                kDebtMaximum((*f.asset)(kStuckPrincipal * 10).value()));
+            env.close();
+        }
+
+        // Loan: 3,333 USD principal, impaired immediately.
+        auto const sleBroker = env.le(keylet::loanBroker(f.brokerID));
+        if (!BEAST_EXPECT(sleBroker))
+            return f;
+        f.loanKeylet =
+            keylet::loan(f.brokerID, SeqProxy::rawSequence(sleBroker->at(sfLoanSequence)));
+
+        {
+            using namespace loan;
+            using namespace std::chrono_literals;
+            env(set(f.borrower, f.brokerID, kStuckPrincipal),
+                Sig(sfCounterpartySignature, f.lender),
+                kPaymentTotal(kStuckPayTotal),
+                kPaymentInterval(kStuckPayInterval),
+                Fee(env.current()->fees().base * 2),
+                Ter(tesSUCCESS));
+            env.close();
+
+            // Impairment requires the payment to be late, so advance past
+            // the due date before impairing.
+            auto const loanSle = env.le(*f.loanKeylet);
+            if (!BEAST_EXPECT(loanSle))
+                return f;
+            std::uint32_t const dueDate = loanSle->at(sfNextPaymentDueDate);
+            env.close(NetClock::time_point{NetClock::duration{dueDate}} + 1s);
+
+            env(manage(f.lender, f.loanKeylet->key, tfLoanImpair), Ter(tesSUCCESS));
+            env.close();
+        }
+
+        auto const vaultSle = env.le(vaultKeylet);
+        if (!BEAST_EXPECT(vaultSle))
+            return f;
+        BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == (*f.asset)(kStuckPrincipal).value());
+
+        f.shareAsset = vaultSle->at(sfShareMPTID);
+
+        auto const tokenBob = env.le(keylet::mptoken(f.shareAsset, f.bob.id()));
+        if (!BEAST_EXPECT(tokenBob))
+            return f;
+        std::uint64_t const sharesBob = tokenBob->getFieldU64(sfMPTAmount);
+
+        // Bob (non-sole) exits at the discounted rate. Always succeeds.
+        STAmount const bobShareAmt{MPTIssue{f.shareAsset}, Number(sharesBob)};
+        env(v.withdraw({
+                .depositor = f.bob,
+                .id = vaultKeylet.key,
+                .amount = bobShareAmt,
+            }),
+            Ter(tesSUCCESS));
+        env.close();
+
+        auto const tokenLender = env.le(keylet::mptoken(f.shareAsset, f.lender.id()));
+        if (!BEAST_EXPECT(tokenLender))
+            return f;
+        f.sharesLender = tokenLender->getFieldU64(sfMPTAmount);
+
+        auto const sleIssuance = env.le(keylet::mptokenIssuance(f.shareAsset));
+        if (!BEAST_EXPECT(sleIssuance))
+            return f;
+        BEAST_EXPECT(sleIssuance->getFieldU64(sfOutstandingAmount) == f.sharesLender);
+
+        auto const vaultAfterBob = env.le(vaultKeylet);
+        if (!BEAST_EXPECT(vaultAfterBob))
+            return f;
+        // After Bob's exit: loss is unchanged (3,333 receivable), and the
+        // gap between assetsTotal and assetsAvailable equals exactly that
+        // receivable.
+        BEAST_EXPECT(vaultAfterBob->at(sfLossUnrealized) == (*f.asset)(kStuckPrincipal).value());
+        BEAST_EXPECT(
+            vaultAfterBob->at(sfAssetsTotal) - vaultAfterBob->at(sfAssetsAvailable) ==
+            vaultAfterBob->at(sfLossUnrealized));
+
+        return f;
+    }
+
+    // Reproduces the worked example from the XLS-0065 design doc. The sole
+    // remaining shareholder asks (via fixed-asset input) for the vault's
+    // entire AssetsAvailable. Pre-fix this fails with the zero-sized-vault
+    // invariant violation. Post-fix the full-price exchange rate burns
+    // only a portion of the shares, the depositor receives all of
+    // AssetsAvailable, and the residual shares remain backed by the
+    // impaired-loan receivable.
+    void
+    testWithdrawSoleShareholderFixedAssetExit(FeatureBitset features)
+    {
+        using namespace test::jtx;
+
+        bool const withFix = features[fixCleanup3_2_0];
+        testcase(
+            std::string{"Vault withdraw: sole shareholder exits via "
+                        "fixed-asset amount with impaired loan"} +
+            (withFix ? " (fixCleanup3_2_0)" : " (pre-fix)"));
+
+        std::string logs;
+        Env env(*this, features, std::make_unique(&logs));
+        auto const f = setupStuckDepositor(env);
+        if (!f.vaultKeylet || !f.asset || f.sharesLender == 0)
+        {
+            BEAST_EXPECT(false);
+            return;
+        }
+        Keylet const& vaultKey = *f.vaultKeylet;
+        PrettyAsset const& asset = *f.asset;
+
+        auto const vaultBefore = env.le(vaultKey);
+        if (!BEAST_EXPECT(vaultBefore))
+            return;
+        Number const availableBefore = vaultBefore->at(sfAssetsAvailable);
+        Number const totalBefore = vaultBefore->at(sfAssetsTotal);
+        Number const lossBefore = vaultBefore->at(sfLossUnrealized);
+
+        STAmount const lenderBalanceBefore = env.balance(f.lender, asset);
+
+        // The requested amount differs between feature regimes because
+        // the two regimes are testing different behaviors:
+        //
+        // - Pre-fix: request the full AssetsAvailable (3,333.50). Under
+        //   the discounted formula this would burn every outstanding
+        //   share, hitting the zero-sized-vault invariant. The
+        //   transaction is rejected with tecINVARIANT_FAILED — the
+        //   stuck-depositor bug.
+        //
+        // - Post-fix: request a strictly smaller amount (1,000 USD).
+        //   The full-price formula burns only ~30% of the outstanding
+        //   shares; the vault retains the rest, backed by the impaired
+        //   receivable. Requesting *exactly* AssetsAvailable post-fix
+        //   would currently fail with tecINSUFFICIENT_FUNDS due to the
+        //   round-to-nearest used by assetsToSharesWithdraw (the
+        //   recomputed payout can overshoot the request by a few ULPs).
+        //   The "force payout to AssetsAvailable" branch in doApply
+        //   only triggers when every share is burned, which is covered
+        //   by the loan-repayment test.
+        STAmount const requestAssets =
+            withFix ? asset(1000).value() : STAmount{asset.raw(), availableBefore};
+        Vault const v{env};
+        env(v.withdraw({
+                .depositor = f.lender,
+                .id = vaultKey.key,
+                .amount = requestAssets,
+            }),
+            Ter(withFix ? TER{tesSUCCESS} : TER{tecINVARIANT_FAILED}));
+        env.close();
+
+        auto const vaultAfter = env.le(vaultKey);
+        if (!BEAST_EXPECT(vaultAfter))
+            return;
+        auto const issuanceAfter = env.le(keylet::mptokenIssuance(f.shareAsset));
+        if (!BEAST_EXPECT(issuanceAfter))
+            return;
+
+        std::uint64_t const sharesAfter = issuanceAfter->getFieldU64(sfOutstandingAmount);
+        Number const availableAfter = vaultAfter->at(sfAssetsAvailable);
+        Number const totalAfter = vaultAfter->at(sfAssetsTotal);
+        Number const lossAfter = vaultAfter->at(sfLossUnrealized);
+
+        if (!withFix)
+        {
+            // Pre-fix: rejected — vault state unchanged.
+            BEAST_EXPECT(sharesAfter == f.sharesLender);
+            BEAST_EXPECT(availableAfter == availableBefore);
+            BEAST_EXPECT(totalAfter == totalBefore);
+            BEAST_EXPECT(lossAfter == lossBefore);
+            return;
+        }
+
+        // Post-fix exact-value derivation (fixture: sharesLender=5e9,
+        // totalBefore=6666.5, request=1000):
+        //   sharesRedeemed = round(sharesLender * request / totalBefore)
+        //                  = round(750,018,750.469) = 750,018,750
+        //   received       = totalBefore * sharesRedeemed / sharesLender
+        //                  = 999.999999375  (slightly under 1,000 due to
+        //                                    integer-share rounding)
+        constexpr std::uint64_t kExpectedSharesRedeemed = 750'018'750;
+        Number const expectedReceived =
+            totalBefore * Number(kExpectedSharesRedeemed) / Number(f.sharesLender);
+
+        BEAST_EXPECT(sharesAfter == f.sharesLender - kExpectedSharesRedeemed);
+
+        // LossUnrealized is unchanged: the loan-protocol side is untouched.
+        BEAST_EXPECT(lossAfter == lossBefore);
+
+        // The entire (total - available) gap is the impaired receivable,
+        // i.e. equal to lossUnrealized.
+        BEAST_EXPECT(totalAfter - availableAfter == lossAfter);
+
+        STAmount const lenderBalanceAfter = env.balance(f.lender, asset);
+        Number const received{lenderBalanceAfter - lenderBalanceBefore};
+        BEAST_EXPECT(received == expectedReceived);
+
+        // Conservation: assets removed from the vault equal what the
+        // depositor received.
+        BEAST_EXPECT(totalBefore - totalAfter == received);
+        BEAST_EXPECT(availableBefore - availableAfter == received);
+    }
+
+    // Sole shareholder attempts to burn ALL outstanding shares via
+    // fixed-shares input while the vault still holds an impaired
+    // receivable. Pre-fix this fails with the zero-sized-vault invariant
+    // violation. Post-fix the full-price rate causes assetsWithdrawn to
+    // equal assetsTotal, which exceeds assetsAvailable, so the transaction
+    // is rejected with tecINSUFFICIENT_FUNDS.
+    void
+    testWithdrawSoleShareholderFullSharesRejected(FeatureBitset features)
+    {
+        using namespace test::jtx;
+
+        bool const withFix = features[fixCleanup3_2_0];
+        testcase(
+            std::string{"Vault withdraw: sole shareholder full-shares "
+                        "burn is rejected while loss outstanding"} +
+            (withFix ? " (fixCleanup3_2_0)" : " (pre-fix)"));
+
+        std::string logs;
+        Env env(*this, features, std::make_unique(&logs));
+        auto const f = setupStuckDepositor(env);
+        if (!f.vaultKeylet || f.sharesLender == 0)
+        {
+            BEAST_EXPECT(false);
+            return;
+        }
+        Keylet const& vaultKey = *f.vaultKeylet;
+
+        auto const vaultBefore = env.le(vaultKey);
+        if (!BEAST_EXPECT(vaultBefore))
+            return;
+        Number const availableBefore = vaultBefore->at(sfAssetsAvailable);
+        Number const totalBefore = vaultBefore->at(sfAssetsTotal);
+        Number const lossBefore = vaultBefore->at(sfLossUnrealized);
+
+        // Fixed-shares input: ask for ALL outstanding shares.
+        STAmount const shareAmt{MPTIssue{f.shareAsset}, Number(f.sharesLender)};
+        Vault const v{env};
+        env(v.withdraw({
+                .depositor = f.lender,
+                .id = vaultKey.key,
+                .amount = shareAmt,
+            }),
+            Ter(withFix ? TER{tecINSUFFICIENT_FUNDS} : TER{tecINVARIANT_FAILED}));
+        env.close();
+
+        // Either way the transaction was rejected; vault state unchanged.
+        auto const vaultAfter = env.le(vaultKey);
+        if (!BEAST_EXPECT(vaultAfter))
+            return;
+        auto const issuanceAfter = env.le(keylet::mptokenIssuance(f.shareAsset));
+        if (!BEAST_EXPECT(issuanceAfter))
+            return;
+        BEAST_EXPECT(issuanceAfter->getFieldU64(sfOutstandingAmount) == f.sharesLender);
+        BEAST_EXPECT(vaultAfter->at(sfAssetsAvailable) == availableBefore);
+        BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore);
+        BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
+    }
+
+    // Clean-state regression: with no impaired loan, a sole shareholder
+    // burning all their shares fully empties the vault under both the
+    // pre-fix and post-fix code paths. Confirms the new logic doesn't
+    // break the existing happy-path close-out.
+    void
+    testWithdrawSoleShareholderCleanVaultUnaffected(FeatureBitset features)
+    {
+        using namespace test::jtx;
+
+        bool const withFix = features[fixCleanup3_2_0];
+        testcase(
+            std::string{"Vault withdraw: sole shareholder clean-state "
+                        "close-out unchanged"} +
+            (withFix ? " (fixCleanup3_2_0)" : " (pre-fix)"));
+
+        Env env(*this, features);
+
+        Account const issuer{"issuer"};
+        Account const lender{"lender"};
+
+        env.fund(XRP(kStuckFunding), issuer, lender);
+        env.close();
+
+        PrettyAsset const asset = issuer[iouCurrency_];
+        env(trust(lender, asset(10'000'000)));
+        env.close();
+        env(pay(issuer, lender, asset(kStuckDepositorIOU)));
+        env.close();
+
+        // Sole shareholder of a clean vault — no loan broker needed.
+        Vault const v{env};
+        auto [createTx, vaultKeylet] = v.create({.owner = lender, .asset = asset});
+        env(createTx);
+        env.close();
+
+        env(v.deposit({
+                .depositor = lender,
+                .id = vaultKeylet.key,
+                .amount = asset(kStuckDeposit),
+            }),
+            Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultBefore = env.le(vaultKeylet);
+        if (!BEAST_EXPECT(vaultBefore))
+            return;
+        auto const shareAsset = vaultBefore->at(sfShareMPTID);
+        auto const tokenLender = env.le(keylet::mptoken(shareAsset, lender.id()));
+        if (!BEAST_EXPECT(tokenLender))
+            return;
+        std::uint64_t const sharesLender = tokenLender->getFieldU64(sfMPTAmount);
+
+        // Sole shareholder, no loans, no loss. Burn everything.
+        STAmount const allShares{MPTIssue{shareAsset}, Number(sharesLender)};
+        env(v.withdraw({
+                .depositor = lender,
+                .id = vaultKeylet.key,
+                .amount = allShares,
+            }),
+            Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultFinal = env.le(vaultKeylet);
+        if (!BEAST_EXPECT(vaultFinal))
+            return;
+        auto const issuanceFinal = env.le(keylet::mptokenIssuance(shareAsset));
+        if (!BEAST_EXPECT(issuanceFinal))
+            return;
+        BEAST_EXPECT(issuanceFinal->getFieldU64(sfOutstandingAmount) == 0);
+        BEAST_EXPECT(vaultFinal->at(sfAssetsTotal) == beast::kZero);
+        BEAST_EXPECT(vaultFinal->at(sfAssetsAvailable) == beast::kZero);
+        BEAST_EXPECT(vaultFinal->at(sfLossUnrealized) == beast::kZero);
+
+        // (Pre-fix path takes the regular code path; post-fix path enters
+        // the new final-withdrawal guard, which forces payout to exactly
+        // assetsAvailable. Either way the result is identical for a clean
+        // vault.)
+        (void)withFix;
+    }
+
+    // Sole shareholder in an impaired vault redeems a *partial* count of
+    // shares via fixed-shares input. Pre-fix the discounted formula is
+    // used; post-fix the full-price formula is used (waiveUnrealizedLoss
+    // = Yes). The relative payout therefore differs, and post-fix the
+    // depositor recovers proportionally more of the residual cash for
+    // the shares burned. In both cases the vault is left in a valid
+    // (non-empty) state.
+    void
+    testWithdrawSoleShareholderPartialFixedSharesUsesFullPrice()
+    {
+        using namespace test::jtx;
+
+        testcase(
+            "Vault withdraw: sole-shareholder partial fixed-shares uses "
+            "full-price rate (fixCleanup3_2_0)");
+
+        // Strip featureLendingProtocolV1_1: setupStuckDepositor builds an
+        // open-ended vault and this test asserts amendment-independent
+        // withdrawal invariants (see the note on run()).
+        Env env(*this, (all_ - featureLendingProtocolV1_1) | fixCleanup3_2_0);
+        auto const f = setupStuckDepositor(env);
+        if (!f.vaultKeylet || !f.asset || f.sharesLender == 0)
+        {
+            BEAST_EXPECT(false);
+            return;
+        }
+        Keylet const& vaultKey = *f.vaultKeylet;
+        PrettyAsset const& asset = *f.asset;
+
+        auto const vaultBefore = env.le(vaultKey);
+        if (!BEAST_EXPECT(vaultBefore))
+            return;
+        Number const totalBefore = vaultBefore->at(sfAssetsTotal);
+        Number const availableBefore = vaultBefore->at(sfAssetsAvailable);
+        Number const lossBefore = vaultBefore->at(sfLossUnrealized);
+
+        // Burn exactly half of the outstanding shares.
+        std::uint64_t const halfShares = f.sharesLender / 2;
+        STAmount const halfAmt{MPTIssue{f.shareAsset}, Number(halfShares)};
+
+        STAmount const lenderBalanceBefore = env.balance(f.lender, asset);
+
+        Vault const v{env};
+        env(v.withdraw({
+                .depositor = f.lender,
+                .id = vaultKey.key,
+                .amount = halfAmt,
+            }),
+            Ter(tesSUCCESS));
+        env.close();
+
+        // Expected payout under the full-price formula:
+        //   assets = totalBefore * halfShares / sharesLender
+        // which (with halfShares == sharesLender/2) is roughly
+        //   totalBefore / 2.
+        STAmount const lenderBalanceAfter = env.balance(f.lender, asset);
+        Number const received{lenderBalanceAfter - lenderBalanceBefore};
+        Number const expected = totalBefore * Number(halfShares) / Number(f.sharesLender);
+        BEAST_EXPECT(received == expected);
+
+        // The full-price payout exceeds the discounted formula by exactly
+        // lossBefore * halfShares / sharesLender — that's the whole point
+        // of the waive.
+        Number const discounted =
+            (totalBefore - lossBefore) * Number(halfShares) / Number(f.sharesLender);
+        Number const expectedDelta = lossBefore * Number(halfShares) / Number(f.sharesLender);
+        BEAST_EXPECT(received - discounted == expectedDelta);
+
+        auto const vaultAfter = env.le(vaultKey);
+        if (!BEAST_EXPECT(vaultAfter))
+            return;
+        auto const issuanceAfter = env.le(keylet::mptokenIssuance(f.shareAsset));
+        if (!BEAST_EXPECT(issuanceAfter))
+            return;
+
+        // Vault remains valid: half the shares remain, lossUnrealized
+        // is untouched, and the entire (total - available) gap is still
+        // the impaired receivable.
+        BEAST_EXPECT(
+            issuanceAfter->getFieldU64(sfOutstandingAmount) == f.sharesLender - halfShares);
+        BEAST_EXPECT(vaultAfter->at(sfAssetsTotal) == totalBefore - received);
+        BEAST_EXPECT(vaultAfter->at(sfLossUnrealized) == lossBefore);
+        BEAST_EXPECT(
+            vaultAfter->at(sfAssetsTotal) - vaultAfter->at(sfAssetsAvailable) ==
+            vaultAfter->at(sfLossUnrealized));
+
+        // Conservation: vault delta matches the depositor's gain.
+        BEAST_EXPECT(totalBefore - vaultAfter->at(sfAssetsTotal) == received);
+        BEAST_EXPECT(availableBefore - vaultAfter->at(sfAssetsAvailable) == received);
+    }
+
+    // Post-fix end-to-end resolution: after the sole-shareholder partial
+    // exit, the loan is repaid in full. With unrealized loss cleared and
+    // all assets back as cash, the depositor can burn all remaining
+    // shares and fully exit the vault. The final withdrawal hits the
+    // "force payout to assetsAvailable" branch in doApply.
+    void
+    testWithdrawSoleShareholderLoanRepaymentExit()
+    {
+        using namespace test::jtx;
+        using namespace loan;
+
+        testcase(
+            "Vault withdraw: sole shareholder fully exits after impaired "
+            "loan is repaid (fixCleanup3_2_0)");
+
+        // Strip featureLendingProtocolV1_1 as above.
+        Env env(*this, (all_ - featureLendingProtocolV1_1) | fixCleanup3_2_0);
+        auto const f = setupStuckDepositor(env);
+        if (!f.vaultKeylet || !f.asset || !f.loanKeylet || f.sharesLender == 0)
+        {
+            BEAST_EXPECT(false);
+            return;
+        }
+        Keylet const& vaultKey = *f.vaultKeylet;
+        Keylet const& loanKey = *f.loanKeylet;
+        PrettyAsset const& asset = *f.asset;
+
+        Vault const v{env};
+
+        // Sole-shareholder partial exit (see comment in
+        // testWithdrawSoleShareholderFixedAssetExit for why we request
+        // less than full AssetsAvailable).
+        {
+            STAmount const requestAssets = asset(1000).value();
+            env(v.withdraw({
+                    .depositor = f.lender,
+                    .id = vaultKey.key,
+                    .amount = requestAssets,
+                }),
+                Ter(tesSUCCESS));
+            env.close();
+        }
+
+        // Confirm the "dormant-but-alive" state from the design doc. The
+        // partial exit burned exactly 750,018,750 shares (see derivation
+        // in testWithdrawSoleShareholderFixedAssetExit).
+        auto const tokenAfterExit = env.le(keylet::mptoken(f.shareAsset, f.lender.id()));
+        if (!BEAST_EXPECT(tokenAfterExit))
+            return;
+        std::uint64_t const retainedShares = tokenAfterExit->getFieldU64(sfMPTAmount);
+        BEAST_EXPECT(retainedShares == f.sharesLender - 750'018'750);
+
+        // Borrower repays the loan in full (pays more than the outstanding
+        // total each time; the loan transactor caps the receivable). The
+        // loan is still overdue from the impairment setup, so the first
+        // (and only remaining, since kStuckPayTotal == 2) outstanding
+        // installment must be caught up with a late payment before the
+        // final regular payment can close the loan out.
+        env(pay(f.borrower, loanKey.key, asset(kStuckPrincipal * 2), tfLoanLatePayment),
+            Ter(tesSUCCESS));
+        env.close();
+        env(pay(f.borrower, loanKey.key, asset(kStuckPrincipal * 2)), Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultAfterRepay = env.le(vaultKey);
+        if (!BEAST_EXPECT(vaultAfterRepay))
+            return;
+        // Repayment converts the 3,333 receivable back to cash; assetsTotal
+        // is unchanged but assetsAvailable jumps by exactly the same amount,
+        // and lossUnrealized clears to zero.
+        BEAST_EXPECT(vaultAfterRepay->at(sfLossUnrealized) == beast::kZero);
+        BEAST_EXPECT(vaultAfterRepay->at(sfAssetsAvailable) == vaultAfterRepay->at(sfAssetsTotal));
+
+        STAmount const lenderBalanceBeforeFinal = env.balance(f.lender, asset);
+        Number const availableBeforeFinal = vaultAfterRepay->at(sfAssetsAvailable);
+
+        // Burn all remaining shares — the clean-state preconditions of
+        // the "final withdrawal" guard are now satisfied.
+        STAmount const allShares{MPTIssue{f.shareAsset}, Number(retainedShares)};
+        env(v.withdraw({
+                .depositor = f.lender,
+                .id = vaultKey.key,
+                .amount = allShares,
+            }),
+            Ter(tesSUCCESS));
+        env.close();
+
+        auto const vaultFinal = env.le(vaultKey);
+        if (!BEAST_EXPECT(vaultFinal))
+            return;
+        auto const issuanceFinal = env.le(keylet::mptokenIssuance(f.shareAsset));
+        if (!BEAST_EXPECT(issuanceFinal))
+            return;
+
+        // Zero-sized vault invariant satisfied: 0 shares, 0 assets.
+        BEAST_EXPECT(issuanceFinal->getFieldU64(sfOutstandingAmount) == 0);
+        BEAST_EXPECT(vaultFinal->at(sfAssetsTotal) == beast::kZero);
+        BEAST_EXPECT(vaultFinal->at(sfAssetsAvailable) == beast::kZero);
+        BEAST_EXPECT(vaultFinal->at(sfLossUnrealized) == beast::kZero);
+
+        // The final payout equals exactly the AssetsAvailable that
+        // existed before the call (the "force payout" branch).
+        STAmount const lenderBalanceAfter = env.balance(f.lender, asset);
+        Number const finalReceived{lenderBalanceAfter - lenderBalanceBeforeFinal};
+        BEAST_EXPECT(finalReceived == availableBeforeFinal);
+    }
+
+public:
+    void
+    run() override
+    {
+        // These sole-shareholder exit scenarios build an open-ended vault
+        // and drive it through deposits, a loan broker, an impaired loan
+        // and finally a withdrawal by the last shareholder. Under
+        // featureLendingProtocolV1_1 LoanBrokerSet::preclaim rejects
+        // brokers attached to open-ended vaults, so this suite runs with
+        // the amendment stripped; the invariants asserted here are
+        // amendment-independent.
+        auto const legacy = all_ - featureLendingProtocolV1_1;
+        testWithdrawSoleShareholderFixedAssetExit(legacy - fixCleanup3_2_0);
+        testWithdrawSoleShareholderFixedAssetExit(legacy);
+        testWithdrawSoleShareholderFullSharesRejected(legacy - fixCleanup3_2_0);
+        testWithdrawSoleShareholderFullSharesRejected(legacy);
+        testWithdrawSoleShareholderCleanVaultUnaffected(legacy - fixCleanup3_2_0);
+        testWithdrawSoleShareholderCleanVaultUnaffected(legacy);
+        testWithdrawSoleShareholderPartialFixedSharesUsesFullPrice();
+        testWithdrawSoleShareholderLoanRepaymentExit();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(VaultSoleShareholder, app, xrpl);
+
+}  // namespace xrpl
diff --git a/src/test/app/vault/VaultTestBase.h b/src/test/app/vault/VaultTestBase.h
new file mode 100644
index 0000000000..538f3b72d8
--- /dev/null
+++ b/src/test/app/vault/VaultTestBase.h
@@ -0,0 +1,120 @@
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+/**
+ * Shared base for the Vault*_test family under src/test/app/vault/.
+ *
+ * Owns the class-level helpers (type aliases, closed-ended vault
+ * scaffolding, standard feature bitset, IOU currency string) that every
+ * topical Vault*_test suite depends on. Mirrors
+ * src/test/app/lending/LoanTestBase.h.
+ *
+ * Run all suites in this family with `xrpld -u Vault` (the "Vault" prefix
+ * is matched against every suite name via
+ * beast::unit_test::Selector::ModeT::Automatch).
+ */
+class VaultTestBase : public beast::unit_test::Suite
+{
+protected:
+    using PrettyAsset = test::jtx::PrettyAsset;
+    using PrettyAmount = test::jtx::PrettyAmount;
+
+    static constexpr auto kNegativeAmount = [](PrettyAsset const& asset) -> PrettyAmount {
+        return {STAmount{asset.raw(), 1ul, 0, true, STAmount::Unchecked{}}, ""};
+    };
+
+    /**
+     * Get the current ledger's close time resolution.
+     * @param env The test environment.
+     */
+    static NetClock::duration
+    getLedgerTimeResolution(test::jtx::Env& env)
+    {
+        return env.current()->header().closeTimeResolution;
+    }
+
+    void
+    closeToTime(
+        test::jtx::Env& env,
+        NetClock::time_point time,
+        std::source_location const& loc = std::source_location::current())
+    {
+        using namespace std::chrono_literals;
+        env.close(time - env.closed()->header().closeTimeResolution + 1s);
+        expect(
+            env.closed()->header().closeTime == time,
+            std::format(
+                "current ledger time {} is not equal to the target ledger time {}",
+                env.closed()->header().closeTime.time_since_epoch(),
+                time.time_since_epoch()),
+            loc.file_name(),
+            loc.line());
+    }
+
+    using d = NetClock::duration;
+    using tp = NetClock::time_point;
+
+    // Vault holds an Env& so no default initializer is possible; the
+    // struct is always aggregate-initialized by makeClosedEndedVault.
+    // NOLINTBEGIN(cppcoreguidelines-pro-type-member-init)
+    struct ClosedEndedSetup
+    {
+        test::jtx::Vault vault;
+        Keylet keylet;
+        std::uint32_t sub = 0;
+        std::uint32_t red = 0;
+    };
+    // NOLINTEND(cppcoreguidelines-pro-type-member-init)
+
+    // Submit a VaultCreate for a closed-ended vault with SubscriptionDate at
+    // env.now() + subOffset and RedemptionDate at SubscriptionDate + gap, then
+    // close the ledger. Returns the Vault helper, the vault's keylet and the
+    // resolved sub/red timestamps.
+    static ClosedEndedSetup
+    makeClosedEndedVault(
+        test::jtx::Env& env,
+        test::jtx::Account const& owner,
+        Asset const& asset,
+        std::uint32_t subOffset,
+        std::uint32_t gap)
+    {
+        auto const sub = env.now().time_since_epoch().count() + subOffset;
+        auto const red = sub + gap;
+        test::jtx::Vault const vault{env};
+        auto [tx, keylet] = vault.create(
+            {.owner = owner,
+             .asset = asset,
+             .vaultKind = std::to_underlying(VaultKind::ClosedEnded),
+             .subscriptionDate = sub,
+             .redemptionDate = red});
+        env(tx);
+        env.close();
+        return {.vault = vault, .keylet = keylet, .sub = sub, .red = red};
+    }
+
+    FeatureBitset const all_{test::jtx::testableAmendments()};
+    std::string const iouCurrency_{"IOU"};
+};
+
+}  // namespace xrpl
diff --git a/src/test/app/vault/VaultTransactorPrecision_test.cpp b/src/test/app/vault/VaultTransactorPrecision_test.cpp
new file mode 100644
index 0000000000..8e8ee2d629
--- /dev/null
+++ b/src/test/app/vault/VaultTransactorPrecision_test.cpp
@@ -0,0 +1,362 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// With fixCleanup3_4_0, deposit/withdraw/clawback apply one amount on the
+// sfAssetsTotal grid. These tests require T, A, and the pseudo-account to
+// change by the same Number; the invariant suite still allows a one-unit gap.
+class VaultTransactorPrecision_test : public VaultPrecisionFixture
+{
+    jtx::Env
+    makeEnv()
+    {
+        return jtx::Env{*this, jtx::envconfig(), all_, nullptr, beast::Severity::Disabled};
+    }
+
+    bool
+    ready(Fixture const& f)
+    {
+        return BEAST_EXPECT(f.asset && f.broker) && f.asset;
+    }
+
+    void
+    assertEqualDeltas(Numbers const& before, Numbers const& after, std::string const& tag)
+    {
+        Number const tDelta = before.assetsTotal - after.assetsTotal;
+        Number const aDelta = before.assetsAvailable - after.assetsAvailable;
+        Number const pDelta = before.pseudo - after.pseudo;
+        BEAST_EXPECTS(tDelta == aDelta, tag + " tDelta != aDelta");
+        BEAST_EXPECTS(tDelta == pDelta, tag + " tDelta != pDelta");
+    }
+
+    void
+    testDeposit()
+    {
+        using namespace jtx;
+
+        testcase("deposit clamp does not over-credit");
+
+        std::array const kAmounts{1, 7, 1'000, 10'000'000};
+
+        for (auto const amount : kAmounts)
+        {
+            Env env = makeEnv();
+            auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
+            if (!ready(f))
+                continue;
+            // ready() above guarantees f.asset is engaged; the guard is opaque to clang-tidy.
+            // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+            jtx::PrettyAsset const& asset = f.asset.value();
+
+            auto const before = read(env, f);
+
+            Vault const v{env};
+            env(v.deposit(
+                    {.depositor = f.depositor,
+                     .id = f.vaultKeylet.key,
+                     .amount = asset(amount).value()}),
+                Ter(std::ignore));
+            env.close();
+
+            if (env.ter() != tesSUCCESS)
+                continue;
+
+            auto const after = read(env, f);
+            Number const tDelta = after.assetsTotal - before.assetsTotal;
+            Number const requested = asset(amount).number();
+            BEAST_EXPECTS(
+                tDelta <= requested,
+                "amount=" + std::to_string(amount) + " tDelta exceeds requested");
+
+            Number const sharesMinted = after.sharesTotal - before.sharesTotal;
+            if (before.sharesTotal == Number{0})
+                continue;
+            Number const shareValue = (before.assetsTotal * sharesMinted) / before.sharesTotal;
+            // The depositor is never charged more than the shares they received are worth.
+            BEAST_EXPECTS(
+                tDelta <= shareValue,
+                "amount=" + std::to_string(amount) + " assetsTaken > shareValue");
+            // Discount is strictly less than one ULP of the new AssetsTotal
+            BEAST_EXPECTS(
+                shareValue - tDelta < oneUnit(asset.raw(), after.assetsTotal),
+                "amount=" + std::to_string(amount) + " discount is not below one unit");
+        }
+
+        {
+            Env env = makeEnv();
+            auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
+            if (!ready(f))
+                return;
+            // ready() above guarantees f.asset is engaged; the guard is opaque to clang-tidy.
+            // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+            jtx::PrettyAsset const& asset = f.asset.value();
+
+            Vault const v{env};
+            env(v.deposit(
+                    {.depositor = f.depositor,
+                     .id = f.vaultKeylet.key,
+                     .amount = asset(99'000'000).value()}),
+                Ter(std::ignore));
+            env.close();
+
+            auto const before = read(env, f);
+            Number const kLowerBound{1, 6};
+            BEAST_EXPECT(before.assetsTotal > kLowerBound);
+
+            auto const tinyAmount = asset(Number{1, -10}).value();
+            env(v.deposit(
+                    {.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = tinyAmount}),
+                Ter(std::ignore));
+            env.close();
+
+            BEAST_EXPECTS(
+                env.ter() == tecPRECISION_LOSS,
+                std::string{"expected tecPRECISION_LOSS, got "} + transToken(env.ter()));
+
+            auto const after = read(env, f);
+            BEAST_EXPECT(after.assetsTotal == before.assetsTotal);
+            BEAST_EXPECT(after.assetsAvailable == before.assetsAvailable);
+            BEAST_EXPECT(after.sharesTotal == before.sharesTotal);
+        }
+    }
+
+    void
+    testWithdraw()
+    {
+        using namespace jtx;
+
+        testcase("withdraw deltas are equal");
+
+        Env env = makeEnv();
+        auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
+        if (!ready(f))
+            return;
+        // ready() above guarantees f.asset is engaged; the guard is opaque to clang-tidy.
+        // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+        jtx::PrettyAsset const& asset = f.asset.value();
+
+        Vault const v{env};
+        env(v.deposit(
+                {.depositor = f.depositor,
+                 .id = f.vaultKeylet.key,
+                 .amount = asset(1'000'000).value()}),
+            Ter(std::ignore));
+        env.close();
+
+        auto checkSuccess = [&](STAmount const& amount, std::string const& tag) {
+            auto const before = read(env, f);
+            env(v.withdraw({.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = amount}),
+                Ter(std::ignore));
+            env.close();
+            if (env.ter() != tesSUCCESS)
+                return;
+
+            auto const after = read(env, f);
+            assertEqualDeltas(before, after, tag);
+
+            Number const sharesBurned = before.sharesTotal - after.sharesTotal;
+            if (before.sharesTotal == Number{0})
+                return;
+            Number const shareValue = (before.assetsTotal * sharesBurned) / before.sharesTotal;
+            Number const tDelta = before.assetsTotal - after.assetsTotal;
+            BEAST_EXPECTS(tDelta <= shareValue, tag + " payout > shareValue");
+        };
+
+        std::array const kShareCounts{99'999u, 333'333u, 1'234'567u};
+        for (auto const count : kShareCounts)
+        {
+            auto const before = read(env, f);
+            if (before.sharesTotal < count)
+                continue;
+            STAmount const shareAmount{MPTIssue{f.share}, Number{static_cast(count)}};
+            checkSuccess(shareAmount, "shares=" + std::to_string(count));
+        }
+
+        std::array const kAssetAmounts{1, 7, 99};
+        for (auto const amount : kAssetAmounts)
+            checkSuccess(asset(amount).value(), "assets=" + std::to_string(amount));
+    }
+
+    // Withdraw more than sfAssetsAvailable must return tecINSUFFICIENT_FUNDS,
+    // not tecPRECISION_LOSS.
+    void
+    testWithdrawInsufficientFundsPrecedence()
+    {
+        using namespace jtx;
+
+        testcase("withdraw over available returns insufficient funds, not precision loss");
+
+        Env env = makeEnv();
+        auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/false);
+        if (!ready(f))
+            return;
+        // ready() above guarantees f.asset is engaged; the guard is opaque to clang-tidy.
+        // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+        jtx::PrettyAsset const& asset = f.asset.value();
+
+        Vault const v{env};
+        env(v.deposit(
+                {.depositor = f.depositor,
+                 .id = f.vaultKeylet.key,
+                 .amount = asset(1'000'000).value()}),
+            Ter(std::ignore));
+        env.close();
+
+        auto const before = read(env, f);
+        if (!BEAST_EXPECT(before.assetsAvailable > Number{0}))
+            return;
+
+        STAmount const request = asset(before.assetsAvailable + Number{1}).value();
+        env(v.withdraw({.depositor = f.depositor, .id = f.vaultKeylet.key, .amount = request}),
+            Ter(std::ignore));
+        env.close();
+
+        BEAST_EXPECTS(
+            env.ter() == tecINSUFFICIENT_FUNDS,
+            std::string{"expected tecINSUFFICIENT_FUNDS, got "} + transToken(env.ter()));
+    }
+
+    void
+    testClawback()
+    {
+        using namespace jtx;
+
+        testcase("clawback deltas are equal");
+
+        Env env = makeEnv();
+        auto f = setupSingleLoanVault(
+            env,
+            /*impairAndPaySibling=*/false,
+            /*allowClawback=*/true);
+        if (!ready(f))
+            return;
+        // ready() above guarantees f.asset is engaged; the guard is opaque to clang-tidy.
+        // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+        jtx::PrettyAsset const& asset = f.asset.value();
+
+        Vault const v{env};
+        env(v.deposit(
+                {.depositor = f.depositor,
+                 .id = f.vaultKeylet.key,
+                 .amount = asset(2'000).value()}),
+            Ter(std::ignore));
+        env.close();
+
+        auto checkSuccess = [&](std::optional const& amount, std::string const& tag) {
+            auto const before = read(env, f);
+            if (before.sharesTotal == Number{0})
+                return;
+
+            env(v.clawback(
+                    {.issuer = f.issuer,
+                     .id = f.vaultKeylet.key,
+                     .holder = f.depositor,
+                     .amount = amount}),
+                Ter(std::ignore));
+            env.close();
+            if (env.ter() != tesSUCCESS)
+                return;
+
+            assertEqualDeltas(before, read(env, f), tag);
+        };
+
+        std::array const kAmounts{1, 7, 99};
+        for (auto const amount : kAmounts)
+            checkSuccess(asset(amount).value(), "amount=" + std::to_string(amount));
+
+        checkSuccess(std::nullopt, "sfAmount absent");
+    }
+
+    void
+    testImpairedVault()
+    {
+        using namespace jtx;
+
+        testcase("impaired vault loss stays within assetsTotal - assetsAvailable");
+
+        Env env = makeEnv();
+        auto f = setupSingleLoanVault(env, /*impairAndPaySibling=*/true);
+        if (!ready(f))
+            return;
+        // ready() above guarantees f.asset is engaged; the guard is opaque to clang-tidy.
+        // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+        jtx::PrettyAsset const& asset = f.asset.value();
+
+        Vault const v{env};
+        env(v.deposit(
+                {.depositor = f.depositor,
+                 .id = f.vaultKeylet.key,
+                 .amount = asset(5'000).value()}),
+            Ter(std::ignore));
+        env.close();
+
+        auto checkInvariant = [&](std::string const& tag) {
+            TER const actual = env.ter();
+            BEAST_EXPECTS(actual != tecINVARIANT_FAILED, tag + " unexpected invariant failure");
+            if (actual != tesSUCCESS)
+                return;
+            auto const after = read(env, f);
+            BEAST_EXPECTS(
+                after.lossUnrealized <= after.assetsTotal - after.assetsAvailable,
+                tag + " lossUnrealized exceeds assetsTotal - assetsAvailable");
+        };
+
+        std::array const kAmounts{1, 7, 51, 137};
+        for (std::size_t i = 0; i + 1 < kAmounts.size(); i += 2)
+        {
+            int const depositAmount = kAmounts[i];
+            int const withdrawAmount = kAmounts[i + 1];
+
+            env(v.deposit(
+                    {.depositor = f.depositor,
+                     .id = f.vaultKeylet.key,
+                     .amount = asset(depositAmount).value()}),
+                Ter(std::ignore));
+            env.close();
+            checkInvariant("deposit=" + std::to_string(depositAmount));
+
+            env(v.withdraw(
+                    {.depositor = f.depositor,
+                     .id = f.vaultKeylet.key,
+                     .amount = asset(withdrawAmount).value()}),
+                Ter(std::ignore));
+            env.close();
+            checkInvariant("withdraw=" + std::to_string(withdrawAmount));
+        }
+    }
+
+public:
+    void
+    run() override
+    {
+        testDeposit();
+        testWithdraw();
+        testWithdrawInsufficientFundsPrecedence();
+        testClawback();
+        testImpairedVault();
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(VaultTransactorPrecision, app, xrpl);
+
+}  // namespace xrpl::test
diff --git a/src/test/app/vault/VaultValidation_test.cpp b/src/test/app/vault/VaultValidation_test.cpp
new file mode 100644
index 0000000000..45f6d1deaf
--- /dev/null
+++ b/src/test/app/vault/VaultValidation_test.cpp
@@ -0,0 +1,1198 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl {
+
+class VaultValidation_test : public VaultTestBase
+{
+private:
+    void
+    testPreflight()
+    {
+        using namespace test::jtx;
+
+        struct CaseArgs
+        {
+            FeatureBitset features = testableAmendments();
+        };
+
+        auto testCase = [&, this](
+                            std::function test,
+                            CaseArgs args = {}) {
+            Env env{*this, args.features};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            Vault vault{env};
+            env.fund(XRP(1000), issuer, owner);
+            env.close();
+
+            env(fset(issuer, asfAllowTrustLineClawback));
+            env(fset(issuer, asfRequireAuth));
+            env.close();
+
+            PrettyAsset const asset = issuer["IOU"];
+            env(trust(owner, asset(1000)));
+            env(trust(issuer, asset(0), owner, tfSetfAuth));
+            env(pay(issuer, owner, asset(1000)));
+            env.close();
+
+            test(env, issuer, owner, asset, vault);
+        };
+
+        auto testDisabled = [&](TER resultAfterCreate = temDISABLED) {
+            return [&, resultAfterCreate](
+                       Env& env,
+                       Account const& issuer,
+                       Account const& owner,
+                       Asset const& asset,
+                       Vault& vault) {
+                testcase("disabled single asset vault");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                env(tx, Ter{temDISABLED});
+
+                {
+                    auto tx = vault.set({.owner = owner, .id = keylet.key});
+                    env(tx, kData("test"), Ter{resultAfterCreate});
+                }
+
+                {
+                    auto tx =
+                        vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)});
+                    env(tx, Ter{resultAfterCreate});
+                }
+
+                {
+                    auto tx =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
+                    env(tx, Ter{resultAfterCreate});
+                }
+
+                {
+                    auto tx = vault.clawback(
+                        {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(10)});
+                    env(tx, Ter{resultAfterCreate});
+                }
+
+                {
+                    auto tx = vault.del({.owner = owner, .id = keylet.key});
+                    env(tx, Ter{resultAfterCreate});
+                }
+            };
+        };
+
+        testCase(testDisabled(), {.features = testableAmendments() - featureSingleAssetVault});
+
+        testCase(testDisabled(tecNO_ENTRY), {.features = testableAmendments() - featureMPTokensV1});
+
+        testCase(
+            [&](Env& env,
+                Account const& issuer,
+                Account const& owner,
+                Asset const& asset,
+                Vault& vault) {
+                testcase("disabled permissioned domains");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                env(tx);
+
+                tx[sfFlags] = tx[sfFlags].asUInt() | tfVaultPrivate;
+                tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
+                env(tx, Ter{temDISABLED});
+
+                {
+                    auto tx = vault.set({.owner = owner, .id = keylet.key});
+                    env(tx, kData("Test"));
+
+                    tx[sfDomainID] = to_string(BaseUInt<256>(13ul));
+                    env(tx, Ter{temDISABLED});
+                }
+            },
+            {.features = testableAmendments() - featurePermissionedDomains});
+
+        testCase([&](Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Asset const& asset,
+                     Vault& vault) {
+            testcase("invalid flags");
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            tx[sfFlags] = tfClearDeepFreeze;
+            env(tx, Ter{temINVALID_FLAG});
+
+            {
+                auto tx = vault.set({.owner = owner, .id = keylet.key});
+                tx[sfFlags] = tfClearDeepFreeze;
+                env(tx, Ter{temINVALID_FLAG});
+            }
+
+            {
+                auto tx =
+                    vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)});
+                tx[sfFlags] = tfClearDeepFreeze;
+                env(tx, Ter{temINVALID_FLAG});
+            }
+
+            {
+                auto tx =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
+                tx[sfFlags] = tfClearDeepFreeze;
+                env(tx, Ter{temINVALID_FLAG});
+            }
+
+            {
+                auto tx = vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(10)});
+                tx[sfFlags] = tfClearDeepFreeze;
+                env(tx, Ter{temINVALID_FLAG});
+            }
+
+            {
+                auto tx = vault.del({.owner = owner, .id = keylet.key});
+                tx[sfFlags] = tfClearDeepFreeze;
+                env(tx, Ter{temINVALID_FLAG});
+            }
+        });
+
+        testCase([&](Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Asset const& asset,
+                     Vault& vault) {
+            testcase("invalid fee");
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            tx[jss::Fee] = "-1";
+            env(tx, Ter{temBAD_FEE});
+
+            {
+                auto tx = vault.set({.owner = owner, .id = keylet.key});
+                tx[jss::Fee] = "-1";
+                env(tx, Ter{temBAD_FEE});
+            }
+
+            {
+                auto tx =
+                    vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(10)});
+                tx[jss::Fee] = "-1";
+                env(tx, Ter{temBAD_FEE});
+            }
+
+            {
+                auto tx =
+                    vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
+                tx[jss::Fee] = "-1";
+                env(tx, Ter{temBAD_FEE});
+            }
+
+            {
+                auto tx = vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(10)});
+                tx[jss::Fee] = "-1";
+                env(tx, Ter{temBAD_FEE});
+            }
+
+            {
+                auto tx = vault.del({.owner = owner, .id = keylet.key});
+                tx[jss::Fee] = "-1";
+                env(tx, Ter{temBAD_FEE});
+            }
+        });
+
+        testCase(
+            [&](Env& env, Account const&, Account const& owner, Asset const&, Vault& vault) {
+                testcase("disabled permissioned domain");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpIssue()});
+                tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
+                env(tx, Ter{temDISABLED});
+
+                {
+                    auto tx = vault.set({.owner = owner, .id = keylet.key});
+                    tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
+                    env(tx, Ter{temDISABLED});
+                }
+
+                {
+                    auto tx = vault.set({.owner = owner, .id = keylet.key});
+                    tx[sfDomainID] = "0";
+                    env(tx, Ter{temDISABLED});
+                }
+            },
+            {.features = (testableAmendments()) - featurePermissionedDomains});
+
+        testCase([&](Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Asset const& asset,
+                     Vault& vault) {
+            testcase("use zero vault");
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpIssue()});
+
+            {
+                auto tx = vault.set({
+                    .owner = owner,
+                    .id = beast::kZero,
+                });
+                env(tx, Ter{temMALFORMED});
+            }
+
+            {
+                auto tx =
+                    vault.deposit({.depositor = owner, .id = beast::kZero, .amount = asset(10)});
+                env(tx, Ter(temMALFORMED));
+            }
+
+            {
+                auto tx =
+                    vault.withdraw({.depositor = owner, .id = beast::kZero, .amount = asset(10)});
+                env(tx, Ter{temMALFORMED});
+            }
+
+            {
+                auto tx = vault.clawback(
+                    {.issuer = issuer, .id = beast::kZero, .holder = owner, .amount = asset(10)});
+                env(tx, Ter{temMALFORMED});
+            }
+
+            {
+                auto tx = vault.del({
+                    .owner = owner,
+                    .id = beast::kZero,
+                });
+                env(tx, Ter{temMALFORMED});
+            }
+        });
+
+        testCase(
+            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
+                testcase("withdraw to bad destination");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+
+                {
+                    auto tx =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(10)});
+                    tx[jss::Destination] = "0";
+                    env(tx, Ter{temMALFORMED});
+                }
+            });
+
+        testCase(
+            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
+                testcase("create with Scale");
+
+                {
+                    auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                    tx[sfScale] = 255;
+                    env(tx, Ter(temMALFORMED));
+                }
+
+                {
+                    auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                    tx[sfScale] = 19;
+                    env(tx, Ter(temMALFORMED));
+                }
+
+                // accepted range from 0 to 18
+                {
+                    auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                    tx[sfScale] = 18;
+                    env(tx);
+                    env.close();
+                    auto const sleVault = env.le(keylet);
+                    BEAST_EXPECT(sleVault);
+                    BEAST_EXPECT((*sleVault)[sfScale] == 18);
+                }
+
+                {
+                    auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                    tx[sfScale] = 0;
+                    env(tx);
+                    env.close();
+                    auto const sleVault = env.le(keylet);
+                    BEAST_EXPECT(sleVault);
+                    BEAST_EXPECT((*sleVault)[sfScale] == 0);
+                }
+
+                {
+                    auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                    env(tx);
+                    env.close();
+                    auto const sleVault = env.le(keylet);
+                    BEAST_EXPECT(sleVault);
+                    BEAST_EXPECT((*sleVault)[sfScale] == 6);
+                }
+            });
+
+        testCase(
+            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
+                testcase("create or set invalid data");
+
+                auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
+
+                {
+                    auto tx = tx1;
+                    tx[sfData] = "";
+                    env(tx, Ter(temMALFORMED));
+                }
+
+                {
+                    auto tx = tx1;
+                    // A hexadecimal string of 257 bytes.
+                    tx[sfData] = std::string(514, 'A');
+                    env(tx, Ter(temMALFORMED));
+                }
+
+                {
+                    auto tx = vault.set({.owner = owner, .id = keylet.key});
+                    tx[sfData] = "";
+                    env(tx, Ter{temMALFORMED});
+                }
+
+                {
+                    auto tx = vault.set({.owner = owner, .id = keylet.key});
+                    // A hexadecimal string of 257 bytes.
+                    tx[sfData] = std::string(514, 'A');
+                    env(tx, Ter{temMALFORMED});
+                }
+            });
+
+        testCase(
+            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
+                testcase("set nothing updated");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+
+                {
+                    auto tx = vault.set({.owner = owner, .id = keylet.key});
+                    env(tx, Ter{temMALFORMED});
+                }
+            });
+
+        testCase(
+            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
+                testcase("create with invalid metadata");
+
+                auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
+
+                {
+                    auto tx = tx1;
+                    tx[sfMPTokenMetadata] = "";
+                    env(tx, Ter(temMALFORMED));
+                }
+
+                {
+                    auto tx = tx1;
+                    // This metadata is for the share token.
+                    // A hexadecimal string of 1025 bytes.
+                    tx[sfMPTokenMetadata] = std::string(2050, 'B');
+                    env(tx, Ter(temMALFORMED));
+                }
+            });
+
+        testCase(
+            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
+                testcase("set negative maximum");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+
+                {
+                    auto tx = vault.set({.owner = owner, .id = keylet.key});
+                    tx[sfAssetsMaximum] = kNegativeAmount(asset).number();
+                    env(tx, Ter{temMALFORMED});
+                }
+            });
+
+        testCase(
+            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
+                testcase("invalid deposit amount");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+
+                {
+                    auto tx = vault.deposit(
+                        {.depositor = owner, .id = keylet.key, .amount = kNegativeAmount(asset)});
+                    env(tx, Ter(temBAD_AMOUNT));
+                }
+
+                {
+                    auto tx =
+                        vault.deposit({.depositor = owner, .id = keylet.key, .amount = asset(0)});
+                    env(tx, Ter(temBAD_AMOUNT));
+                }
+            });
+
+        testCase(
+            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
+                testcase("invalid set immutable flag");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+
+                {
+                    auto tx = vault.set({.owner = owner, .id = keylet.key});
+                    tx[sfFlags] = tfVaultPrivate;
+                    env(tx, Ter(temINVALID_FLAG));
+                }
+            });
+
+        testCase(
+            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
+                testcase("invalid withdraw amount");
+
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+
+                {
+                    auto tx = vault.withdraw(
+                        {.depositor = owner, .id = keylet.key, .amount = kNegativeAmount(asset)});
+                    env(tx, Ter(temBAD_AMOUNT));
+                }
+
+                {
+                    auto tx =
+                        vault.withdraw({.depositor = owner, .id = keylet.key, .amount = asset(0)});
+                    env(tx, Ter(temBAD_AMOUNT));
+                }
+            });
+
+        testCase([&](Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Asset const& asset,
+                     Vault& vault) {
+            testcase("invalid clawback");
+
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+
+            // Preclaim only checks for native assets.
+            if (asset.native())
+            {
+                auto tx = vault.clawback(
+                    {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)});
+                env(tx, Ter(temMALFORMED));
+            }
+
+            {
+                auto tx = vault.clawback(
+                    {.issuer = issuer,
+                     .id = keylet.key,
+                     .holder = owner,
+                     .amount = kNegativeAmount(asset)});
+                env(tx, Ter(temBAD_AMOUNT));
+            }
+        });
+
+        testCase(
+            [&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) {
+                testcase("invalid create");
+
+                auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset});
+
+                {
+                    auto tx = tx1;
+                    tx[sfWithdrawalPolicy] = 0;
+                    env(tx, Ter(temMALFORMED));
+                }
+
+                {
+                    auto tx = tx1;
+                    tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
+                    env(tx, Ter{temMALFORMED});
+                }
+
+                {
+                    auto tx = tx1;
+                    tx[sfAssetsMaximum] = kNegativeAmount(asset).number();
+                    env(tx, Ter{temMALFORMED});
+                }
+
+                {
+                    auto tx = tx1;
+                    tx[sfFlags] = tfVaultPrivate;
+                    tx[sfDomainID] = "0";
+                    env(tx, Ter{temMALFORMED});
+                }
+            });
+    }
+
+    // Test for non-asset specific behaviors.
+    void
+    testCreateFailXRP()
+    {
+        using namespace test::jtx;
+
+        auto testCase = [this](
+                            std::function test) {
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            Account const depositor{"depositor"};
+
+            env.fund(XRP(1000), issuer, owner, depositor);
+            env.close();
+            Vault vault{env};
+            Asset const asset = xrpIssue();
+
+            test(env, issuer, owner, depositor, asset, vault);
+        };
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     PrettyAsset const& asset,
+                     Vault& vault) {
+            testcase("nothing to set");
+            auto tx = vault.set({.owner = owner, .id = keylet::skip().key});
+            tx[sfAssetsMaximum] = asset(0).number();
+            env(tx, Ter(tecNO_ENTRY));
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     PrettyAsset const& asset,
+                     Vault& vault) {
+            testcase("nothing to deposit to");
+            auto tx = vault.deposit(
+                {.depositor = depositor, .id = keylet::skip().key, .amount = asset(10)});
+            env(tx, Ter(tecNO_ENTRY));
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     PrettyAsset const& asset,
+                     Vault& vault) {
+            testcase("nothing to withdraw from");
+            auto tx = vault.withdraw(
+                {.depositor = depositor, .id = keylet::skip().key, .amount = asset(10)});
+            env(tx, Ter(tecNO_ENTRY));
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault) {
+            testcase("nothing to delete");
+            auto tx = vault.del({.owner = owner, .id = keylet::skip().key});
+            env(tx, Ter(tecNO_ENTRY));
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault) {
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            testcase("transaction is good");
+            env(tx);
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault) {
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            tx[sfWithdrawalPolicy] = 1;
+            testcase("explicitly select withdrawal policy");
+            env(tx);
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault) {
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            testcase("insufficient fee");
+            env(tx, Fee(env.current()->fees().base - 1), Ter(telINSUF_FEE_P));
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault) {
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            testcase("insufficient reserve");
+            // It is possible to construct a complicated mathematical
+            // expression for this amount, but it is sadly not easy.
+            env(pay(owner, issuer, XRP(775)));
+            env.close();
+            env(tx, Ter(tecINSUFFICIENT_RESERVE));
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault) {
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            tx[sfFlags] = tfVaultPrivate;
+            tx[sfDomainID] = to_string(BaseUInt<256>(42ul));
+            testcase("non-existing domain");
+            env(tx, Ter{tecOBJECT_NOT_FOUND});
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault) {
+            testcase("cannot set Scale=0");
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            tx[sfScale] = 0;
+            env(tx, Ter{temMALFORMED});
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault) {
+            testcase("cannot set Scale=1");
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            tx[sfScale] = 1;
+            env(tx, Ter{temMALFORMED});
+        });
+    }
+
+    void
+    testCreateFailIOU()
+    {
+        using namespace test::jtx;
+        {
+            {
+                testcase("IOU fail because MPT is disabled");
+                Env env{*this, (testableAmendments() - featureMPTokensV1)};
+                Account const issuer{"issuer"};
+                Account const owner{"owner"};
+                env.fund(XRP(1000), issuer, owner);
+                env.close();
+
+                Vault const vault{env};
+                Asset const asset = issuer["IOU"].asset();
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+
+                env(tx, Ter(temDISABLED));
+                env.close();
+            }
+
+            {
+                testcase("IOU fail create frozen");
+                Env env{*this, testableAmendments()};
+                Account const issuer{"issuer"};
+                Account const owner{"owner"};
+                env.fund(XRP(1000), issuer, owner);
+                env.close();
+                env(fset(issuer, asfGlobalFreeze));
+                env.close();
+
+                Vault const vault{env};
+                Asset const asset = issuer["IOU"].asset();
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+
+                env(tx, Ter(tecFROZEN));
+                env.close();
+            }
+
+            {
+                testcase("IOU fail create no ripling");
+                Env env{*this, testableAmendments()};
+                Account const issuer{"issuer"};
+                Account const owner{"owner"};
+                env.fund(XRP(1000), issuer, owner);
+                env.close();
+                env(fclear(issuer, asfDefaultRipple));
+                env.close();
+
+                Vault const vault{env};
+                Asset const asset = issuer["IOU"].asset();
+                auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                env(tx, Ter(terNO_RIPPLE));
+                env.close();
+            }
+
+            {
+                testcase("IOU no issuer");
+                Env env{*this, testableAmendments()};
+                Account const issuer{"issuer"};
+                Account const owner{"owner"};
+                env.fund(XRP(1000), owner);
+                env.close();
+
+                Vault const vault{env};
+                Asset const asset = issuer["IOU"].asset();
+                {
+                    auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+                    env(tx, Ter(terNO_ACCOUNT));
+                    env.close();
+                }
+            }
+        }
+
+        {
+            testcase("IOU fail create vault for AMM LPToken");
+            Env env{*this, testableAmendments()};
+            Account const gw("gateway");
+            Account const alice("alice");
+            Account const carol("carol");
+            IOU const usd = gw["USD"];
+
+            auto const [asset1, asset2] = std::pair(XRP(10000), usd(10000));
+            auto toFund = [&](STAmount const& a) -> STAmount {
+                if (a.native())
+                {
+                    auto const defXRP = XRP(30000);
+                    if (a <= defXRP)
+                        return defXRP;
+                    return a + XRP(1000);
+                }
+                auto defIOU = STAmount{a.asset(), 30000};
+                if (a <= defIOU)
+                    return defIOU;
+                return a + STAmount{a.asset(), 1000};
+            };
+            auto const toFund1 = toFund(asset1);
+            auto const toFund2 = toFund(asset2);
+            BEAST_EXPECT(asset1 <= toFund1 && asset2 <= toFund2);
+
+            if (!asset1.native() && !asset2.native())
+            {
+                fund(env, gw, {alice, carol}, {toFund1, toFund2}, Fund::All);
+            }
+            else if (asset1.native())
+            {
+                fund(env, gw, {alice, carol}, toFund1, {toFund2}, Fund::All);
+            }
+            else if (asset2.native())
+            {
+                fund(env, gw, {alice, carol}, toFund2, {toFund1}, Fund::All);
+            }
+
+            AMM const ammAlice(env, alice, asset1, asset2, CreateArg{.log = false, .tfee = 0});
+
+            Account const owner{"owner"};
+            env.fund(XRP(1000000), owner);
+
+            Vault const vault{env};
+            auto [tx, k] = vault.create({.owner = owner, .asset = ammAlice.lptIssue()});
+            env(tx, Ter{tecWRONG_ASSET});
+            env.close();
+        }
+    }
+
+    void
+    testCreateFailMPT()
+    {
+        using namespace test::jtx;
+
+        auto testCase = [this](
+                            std::function test) {
+            Env env{*this, testableAmendments()};
+            Account const issuer{"issuer"};
+            Account const owner{"owner"};
+            Account const depositor{"depositor"};
+            env.fund(XRP(1000), issuer, owner, depositor);
+            env.close();
+            Vault vault{env};
+            MPTTester mptt{env, issuer, kMptInitNoFund};
+            // Locked because that is the default flag.
+            mptt.create();
+            Asset const asset = mptt.issuanceID();
+
+            test(env, issuer, owner, depositor, asset, vault);
+        };
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault) {
+            testcase("MPT no authorization");
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx, Ter(tecNO_AUTH));
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault) {
+            testcase("MPT cannot set Scale=0");
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            tx[sfScale] = 0;
+            env(tx, Ter{temMALFORMED});
+        });
+
+        testCase([this](
+                     Env& env,
+                     Account const& issuer,
+                     Account const& owner,
+                     Account const& depositor,
+                     Asset const& asset,
+                     Vault& vault) {
+            testcase("MPT cannot set Scale=1");
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            tx[sfScale] = 1;
+            env(tx, Ter{temMALFORMED});
+        });
+    }
+
+    void
+    testVaultDeleteMemoData()
+    {
+        using namespace test::jtx;
+
+        Env env{*this};
+
+        Account const owner{"owner"};
+        env.fund(XRP(1'000'000), owner);
+        env.close();
+
+        Vault const vault{env};
+
+        auto const keylet = keylet::vault(owner.id(), SeqProxy::rawSequence(1));
+        auto delTx = vault.del({.owner = owner, .id = keylet.key});
+
+        // Test VaultDelete with featureLendingProtocolV1_1 disabled
+        // Transaction fails if the data field is provided
+        {
+            testcase("VaultDelete memo data featureLendingProtocolV1_1 disabled");
+            env.disableFeature(featureLendingProtocolV1_1);
+            delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
+            env(delTx, Ter(temDISABLED));
+            env.enableFeature(featureLendingProtocolV1_1);
+            env.close();
+        }
+
+        // Transaction fails if the data field is too large
+        {
+            testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data too large");
+            delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength + 1, 'A'));
+            env(delTx, Ter(temMALFORMED));
+            env.close();
+        }
+
+        // Transaction fails if the data field is set, but is empty
+        {
+            testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data empty");
+            delTx[sfMemoData] = strHex(std::string());
+            env(delTx, Ter(temMALFORMED));
+            env.close();
+        }
+
+        {
+            testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled no vault");
+            auto const keylet = keylet::vault(owner.id(), SeqProxy::rawSequence(env.seq(owner)));
+
+            // Recreate the transaction as the vault keylet changed
+            auto delTx = vault.del({.owner = owner, .id = keylet.key});
+            delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
+            env(delTx, Ter(tecNO_ENTRY));
+            env.close();
+        }
+
+        {
+            testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data valid");
+            PrettyAsset const xrpAsset = xrpIssue();
+            auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
+            env(tx, Ter(tesSUCCESS));
+            env.close();
+            // Recreate the transaction as the vault keylet changed
+            auto delTx = vault.del({.owner = owner, .id = keylet.key});
+            delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
+            env(delTx, Ter(tesSUCCESS));
+            env.close();
+        }
+    }
+
+    void
+    testVaultCreateLEVersion()
+    {
+        using namespace test::jtx;
+
+        Account const owner{"owner"};
+        PrettyAsset const xrpAsset = xrpIssue();
+
+        {
+            testcase("VaultCreate LEVersion: featureLendingProtocolV1_1 disabled, field absent");
+            Env env{*this};
+            env.disableFeature(featureLendingProtocolV1_1);
+            env.fund(XRP(1'000'000), owner);
+            env.close();
+
+            Vault const vault{env};
+            auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
+            env(tx, Ter(tesSUCCESS));
+            env.close();
+
+            auto const sleVault = env.le(keylet);
+            BEAST_EXPECT(sleVault);
+            BEAST_EXPECT(!sleVault->isFieldPresent(sfLEVersion));
+        }
+
+        {
+            testcase(
+                "VaultCreate LEVersion: featureLendingProtocolV1_1 enabled, LEVersion == "
+                "VaultVersion::CashBasis");
+            Env env{*this};
+            env.fund(XRP(1'000'000), owner);
+            env.close();
+
+            Vault const vault{env};
+            auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
+            env(tx, Ter(tesSUCCESS));
+            env.close();
+
+            auto const sleVault = env.le(keylet);
+            BEAST_EXPECT(sleVault);
+            BEAST_EXPECT(sleVault->isFieldPresent(sfLEVersion));
+            BEAST_EXPECT(sleVault->at(sfLEVersion) == std::to_underlying(VaultVersion::CashBasis));
+        }
+
+        {
+            testcase("VaultCreate rejects LEVersion set in the transaction");
+            Env env{*this};
+            env.fund(XRP(1'000'000), owner);
+            env.close();
+
+            Vault const vault{env};
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
+            tx[sfLEVersion] = 2;
+            env(tx, Ter(temMALFORMED));
+            env.close();
+
+            BEAST_EXPECT(!env.le(keylet));
+        }
+
+        {
+            testcase("VaultSet rejects LEVersion set in the transaction");
+            Env env{*this};
+            env.fund(XRP(1'000'000), owner);
+            env.close();
+
+            Vault const vault{env};
+            auto const [createTx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
+            env(createTx, Ter(tesSUCCESS));
+            env.close();
+
+            auto setTx = vault.set({.owner = owner, .id = keylet.key});
+            setTx[sfLEVersion] = 2;
+            env(setTx, Ter(temMALFORMED));
+            env.close();
+        }
+    }
+
+    // A pseudo-account belongs to a ledger object, so it must never be the
+    // destination of a withdrawal. The payout is refused either way, by the
+    // deposit authorization every pseudo-account carries, so the only change
+    // is a misleading tecNO_PERMISSION becoming tecPSEUDO_ACCOUNT. The check
+    // runs ahead of the private-vault domain check, which would otherwise
+    // report a domain problem against an account that can never join one.
+    void
+    testVaultWithdrawPseudoAccountDestination(FeatureBitset features)
+    {
+        using namespace test::jtx;
+
+        bool const withFix = features[fixCleanup3_4_0];
+        testcase(
+            std::string{"VaultWithdraw pseudo-account destination"} +
+            (withFix ? " (fixCleanup3_4_0)" : " (pre-fix)"));
+
+        Account const issuer{"issuer"};
+        Account const owner{"owner"};
+        Account const depositor{"depositor"};
+        Account const pdOwner{"pdOwner"};
+        Account const credIssuer{"credIssuer"};
+        std::string const credType = "credential";
+
+        Env env{*this, features};
+        Vault const vault{env};
+
+        env.fund(XRP(100'000), issuer, owner, depositor, pdOwner, credIssuer);
+        // Rippling plays no part in what is being tested here, and would
+        // otherwise stop the payout before it reaches the check under test.
+        env(fset(issuer, asfDefaultRipple));
+        env.close();
+
+        PrettyAsset const asset = issuer["IOU"];
+        for (auto const& account : {owner, depositor})
+        {
+            env.trust(asset(1'000'000), account);
+            env(pay(issuer, account, asset(10'000)));
+        }
+        env.close();
+
+        // Another vault over the same asset supplies the destination. Its
+        // pseudo-account holds a trust line for the asset from creation, so
+        // the payout is refused for being a pseudo-account and nothing else.
+        auto const pseudoDestination = [&]() {
+            auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(tx);
+            env.close();
+            return Account("otherVault", env.le(keylet)->at(sfAccount));
+        }();
+
+        TER const expected = withFix ? TER(tecPSEUDO_ACCOUNT) : TER(tecNO_PERMISSION);
+
+        auto const withdrawToPseudo = [&](uint256 const& vaultId) {
+            auto tx = vault.withdraw({.depositor = depositor, .id = vaultId, .amount = asset(1)});
+            tx[sfDestination] = pseudoDestination.human();
+            return tx;
+        };
+
+        {
+            auto [createTx, keylet] = vault.create({.owner = owner, .asset = asset});
+            env(createTx);
+            env.close();
+
+            env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1'000)}));
+            env.close();
+
+            env(withdrawToPseudo(keylet.key), Ter(expected));
+            env.close();
+
+            // Withdrawing to self out of the same vault stays unaffected.
+            env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(1)}));
+            env.close();
+        }
+
+        {
+            auto const domainId = [&]() {
+                pdomain::Credentials const credentials{
+                    {.issuer = credIssuer, .credType = credType}};
+                env(pdomain::setTx(pdOwner, credentials));
+                env.close();
+                return pdomain::getNewDomain(env.meta());
+            }();
+
+            env(credentials::create(depositor, credIssuer, credType));
+            env(credentials::accept(depositor, credIssuer, credType));
+            env.close();
+
+            auto [createTx, keylet] =
+                vault.create({.owner = owner, .asset = asset, .flags = tfVaultPrivate});
+            env(createTx);
+            env.close();
+
+            auto setTx = vault.set({.owner = owner, .id = keylet.key});
+            setTx[sfDomainID] = to_string(domainId);
+            env(setTx);
+            env.close();
+
+            env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1'000)}));
+            env.close();
+
+            // The domain check never gets a say: the destination is rejected
+            // for what it is, not for the domain it is missing.
+            env(withdrawToPseudo(keylet.key), Ter(expected));
+            env.close();
+        }
+    }
+
+public:
+    void
+    run() override
+    {
+        testPreflight();
+        testCreateFailXRP();
+        testCreateFailIOU();
+        testCreateFailMPT();
+        testVaultDeleteMemoData();
+        testVaultCreateLEVersion();
+
+        testVaultWithdrawPseudoAccountDestination(all_ - fixCleanup3_4_0);
+        testVaultWithdrawPseudoAccountDestination(all_);
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(VaultValidation, app, xrpl);
+
+}  // namespace xrpl
diff --git a/src/test/app/wasm_fixtures/.gitignore b/src/test/app/wasm_fixtures/.gitignore
deleted file mode 100644
index 08b2e8a256..0000000000
--- a/src/test/app/wasm_fixtures/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-**/target
-**/debug
-*.wasm
diff --git a/src/test/app/wasm_fixtures/all_host_functions/Cargo.lock b/src/test/app/wasm_fixtures/all_host_functions/Cargo.lock
deleted file mode 100644
index 5240e9b0f0..0000000000
--- a/src/test/app/wasm_fixtures/all_host_functions/Cargo.lock
+++ /dev/null
@@ -1,180 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "all_host_functions"
-version = "0.1.0"
-dependencies = [
- "xrpl-common-stdlib",
- "xrpl-escrow-stdlib",
-]
-
-[[package]]
-name = "block-buffer"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
-dependencies = [
- "hybrid-array",
-]
-
-[[package]]
-name = "bs58"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
-dependencies = [
- "tinyvec",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "const-oid"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
-
-[[package]]
-name = "cpufeatures"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "crypto-common"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
-dependencies = [
- "hybrid-array",
-]
-
-[[package]]
-name = "digest"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
-dependencies = [
- "block-buffer",
- "const-oid",
- "crypto-common",
-]
-
-[[package]]
-name = "hybrid-array"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
-dependencies = [
- "typenum",
-]
-
-[[package]]
-name = "libc"
-version = "0.2.186"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.106"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "sha2"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
-[[package]]
-name = "syn"
-version = "3.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "tinyvec"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
-dependencies = [
- "tinyvec_macros",
-]
-
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
-
-[[package]]
-name = "typenum"
-version = "1.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
-
-[[package]]
-name = "xrpl-common-stdlib"
-version = "0.8.0"
-source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9"
-dependencies = [
- "xrpl-macros",
-]
-
-[[package]]
-name = "xrpl-escrow-stdlib"
-version = "0.1.0"
-source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9"
-dependencies = [
- "xrpl-common-stdlib",
-]
-
-[[package]]
-name = "xrpl-macros"
-version = "0.1.0"
-source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9"
-dependencies = [
- "bs58",
- "proc-macro2",
- "quote",
- "sha2",
- "syn",
-]
diff --git a/src/test/app/wasm_fixtures/all_host_functions/Cargo.toml b/src/test/app/wasm_fixtures/all_host_functions/Cargo.toml
deleted file mode 100644
index 71ad3ba9c6..0000000000
--- a/src/test/app/wasm_fixtures/all_host_functions/Cargo.toml
+++ /dev/null
@@ -1,22 +0,0 @@
-[package]
-name = "all_host_functions"
-version = "0.1.0"
-edition = "2024"
-
-# This empty workspace definition keeps this project independent of the parent workspace
-[workspace]
-
-[lib]
-crate-type = ["cdylib"]
-
-[dependencies]
-xrpl-std = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-common-stdlib", branch = "error-and-trace" }
-xrpl-escrow = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-escrow-stdlib", branch = "error-and-trace" }
-
-[profile.dev]
-panic = "abort"
-
-[profile.release]
-panic = "abort"
-opt-level = "z"
-lto = true
diff --git a/src/test/app/wasm_fixtures/all_host_functions/src/lib.rs b/src/test/app/wasm_fixtures/all_host_functions/src/lib.rs
deleted file mode 100644
index a593f3cb83..0000000000
--- a/src/test/app/wasm_fixtures/all_host_functions/src/lib.rs
+++ /dev/null
@@ -1,760 +0,0 @@
-#![cfg_attr(target_arch = "wasm32", no_std)]
-
-#[cfg(not(target_arch = "wasm32"))]
-extern crate std;
-
-//
-// Host Functions Test
-// Tests 26 host functions (across 7 categories)
-//
-// With craft you can run this test with:
-//   craft test --project host_functions_test --test-case host_functions_test
-//
-// Amount Format Update:
-// - XRP amounts now return as 8-byte serialized rippled objects
-// - IOU and MPT amounts return in variable-length serialized format
-// - Format details: https://xrpl.org/docs/references/protocol/binary-format#amount-fields
-//
-// Error Code Ranges:
-// -100 to -199: Ledger Header Functions (3 functions)
-// -200 to -299: Transaction Data Functions (5 functions)
-// -300 to -399: Current Ledger Object Functions (4 functions)
-// -400 to -499: Any Ledger Object Functions (5 functions)
-// -500 to -599: Keylet Generation Functions (4 functions)
-// -600 to -699: Utility Functions (4 functions)
-// -700 to -799: Data Update Functions (1 function)
-//
-
-use xrpl_escrow::current_tx::escrow_finish::EscrowFinish;
-use xrpl_std::current_tx::traits::TransactionCommonFields;
-use xrpl_std::host;
-use xrpl_std::host::trace::TraceDataType;
-use xrpl_std::host::trace::{trace, trace_acct_buf, trace_hex, trace_num};
-use xrpl_std::sfield;
-
-#[unsafe(no_mangle)]
-pub extern "C" fn escrow_finish() -> i32 {
-    let _ = trace("=== HOST FUNCTIONS TEST ===");
-    let _ = trace("Testing 26 host functions");
-
-    // Category 1: Ledger Header Data Functions (3 functions)
-    // Error range: -100 to -199
-    match test_ledger_header_functions() {
-        0 => (),
-        err => return err,
-    }
-
-    // Category 2: Transaction Data Functions (5 functions)
-    // Error range: -200 to -299
-    match test_transaction_data_functions() {
-        0 => (),
-        err => return err,
-    }
-
-    // Category 3: Current Ledger Object Functions (4 functions)
-    // Error range: -300 to -399
-    match test_current_ledger_object_functions() {
-        0 => (),
-        err => return err,
-    }
-
-    // Category 4: Any Ledger Object Functions (5 functions)
-    // Error range: -400 to -499
-    match test_any_ledger_object_functions() {
-        0 => (),
-        err => return err,
-    }
-
-    // Category 5: Keylet Generation Functions (4 functions)
-    // Error range: -500 to -599
-    match test_keylet_generation_functions() {
-        0 => (),
-        err => return err,
-    }
-
-    // Category 6: Utility Functions (4 functions)
-    // Error range: -600 to -699
-    match test_utility_functions() {
-        0 => (),
-        err => return err,
-    }
-
-    // Category 7: Data Update Functions (1 function)
-    // Error range: -700 to -799
-    match test_data_update_functions() {
-        0 => (),
-        err => return err,
-    }
-
-    let _ = trace("SUCCESS: All host function tests passed!");
-    1 // Success return code for WASM finish function
-}
-
-/// Test Category 1: Ledger Header Data Functions (3 functions)
-/// - get_ledger_sqn() - Get ledger sequence number
-/// - get_parent_ledger_time() - Get parent ledger timestamp
-/// - get_parent_ledger_hash() - Get parent ledger hash
-fn test_ledger_header_functions() -> i32 {
-    let _ = trace("--- Category 1: Ledger Header Functions ---");
-
-    // Test 1.1: get_ledger_sqn() - should return current ledger sequence number
-    let mut sqn_buffer = [0u8; 4];
-    let sqn_result = unsafe { host::ldgr_index(sqn_buffer.as_mut_ptr(), sqn_buffer.len()) };
-
-    if sqn_result <= 0 {
-        let _ = trace_num("ERROR: get_ledger_sqn failed:", sqn_result as i64);
-        return -101; // Ledger sequence number test failed
-    }
-    let ledger_sqn = u32::from_be_bytes(sqn_buffer);
-    let _ = trace_num("Ledger sequence number:", ledger_sqn as i64);
-
-    // Test 1.2: get_parent_ledger_time() - should return parent ledger timestamp
-    let mut time_buffer = [0u8; 4];
-    let time_result =
-        unsafe { host::parent_ldgr_time(time_buffer.as_mut_ptr(), time_buffer.len()) };
-
-    if time_result <= 0 {
-        let _ = trace_num("ERROR: get_parent_ledger_time failed:", time_result as i64);
-        return -102; // Parent ledger time test failed
-    }
-    let parent_ledger_time = u32::from_be_bytes(time_buffer);
-    let _ = trace_num("Parent ledger time:", parent_ledger_time as i64);
-
-    // Test 1.3: get_parent_ledger_hash() - should return parent ledger hash (32 bytes)
-    let mut hash_buffer = [0u8; 32];
-    let hash_result =
-        unsafe { host::parent_ldgr_hash(hash_buffer.as_mut_ptr(), hash_buffer.len()) };
-
-    if hash_result != 32 {
-        let _ = trace_num(
-            "ERROR: get_parent_ledger_hash wrong length:",
-            hash_result as i64,
-        );
-        return -103; // Parent ledger hash test failed - should be exactly 32 bytes
-    }
-    let _ = trace_hex("Parent ledger hash:", &hash_buffer);
-
-    let _ = trace("SUCCESS: Ledger header functions");
-    0
-}
-
-/// Test Category 2: Transaction Data Functions (5 functions)
-/// Tests all functions for accessing current transaction data
-fn test_transaction_data_functions() -> i32 {
-    let _ = trace("--- Category 2: Transaction Data Functions ---");
-
-    // Test 2.1: get_tx_field() - Basic transaction field access
-    // Test with Account field (required, 20 bytes)
-    let mut account_buffer = [0u8; 20];
-    let account_len = unsafe {
-        host::tx_field(
-            sfield::Account.into(),
-            account_buffer.as_mut_ptr(),
-            account_buffer.len(),
-        )
-    };
-
-    if account_len != 20 {
-        let _ = trace_num(
-            "ERROR: get_tx_field(Account) wrong length:",
-            account_len as i64,
-        );
-        return -201; // Basic transaction field test failed
-    }
-    let _ = trace_acct_buf("Transaction Account:", &account_buffer);
-
-    // Test with Fee field (XRP amount - 8 bytes in new serialized format)
-    // New format: XRP amounts are always 8 bytes (positive: value | cPositive flag, negative: just value)
-    let mut fee_buffer = [0u8; 8];
-    let fee_len = unsafe {
-        host::tx_field(
-            sfield::Fee.into(),
-            fee_buffer.as_mut_ptr(),
-            fee_buffer.len(),
-        )
-    };
-
-    if fee_len != 8 {
-        let _ = trace_num(
-            "ERROR: get_tx_field(Fee) wrong length (expected 8 bytes for XRP):",
-            fee_len as i64,
-        );
-        return -202; // Fee field test failed - XRP amounts should be exactly 8 bytes
-    }
-    let _ = trace_num("Transaction Fee length:", fee_len as i64);
-    let _ = trace_hex("Transaction Fee (serialized XRP amount):", &fee_buffer);
-
-    // Test with Sequence field (required, 4 bytes uint32)
-    let mut seq_buffer = [0u8; 4];
-    let seq_len = unsafe {
-        host::tx_field(
-            sfield::Sequence.into(),
-            seq_buffer.as_mut_ptr(),
-            seq_buffer.len(),
-        )
-    };
-
-    if seq_len != 4 {
-        let _ = trace_num(
-            "ERROR: get_tx_field(Sequence) wrong length:",
-            seq_len as i64,
-        );
-        return -203; // Sequence field test failed
-    }
-    let _ = trace_hex("Transaction Sequence:", &seq_buffer);
-
-    // NOTE: get_tx_field2() through get_tx_field6() have been deprecated.
-    // Use get_tx_field() with appropriate parameters for all transaction field access.
-
-    // Test 2.2: get_tx_nested_field() - Nested field access with locator
-    let locator = [
-        0x01_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8,
-    ]; // Two int32s in little-endian: [1, 0]
-    let mut nested_buffer = [0u8; 32];
-    let nested_result = unsafe {
-        host::tx_inner(
-            locator.as_ptr(),
-            locator.len(),
-            nested_buffer.as_mut_ptr(),
-            nested_buffer.len(),
-        )
-    };
-
-    if nested_result < 0 {
-        let _ = trace_num(
-            "INFO: get_tx_nested_field not applicable:",
-            nested_result as i64,
-        );
-        // Expected - locator may not match transaction structure
-    } else {
-        let _ = trace_num("Nested field length:", nested_result as i64);
-        let _ = trace_hex("Nested field:", &nested_buffer[..nested_result as usize]);
-    }
-
-    // Test 2.3: get_tx_array_len() - Get array length
-    let signers_len = unsafe { host::tx_arr_len(sfield::Signers.into()) };
-    let _ = trace_num("Signers array length:", signers_len as i64);
-
-    let memos_len = unsafe { host::tx_arr_len(sfield::Memos.into()) };
-    let _ = trace_num("Memos array length:", memos_len as i64);
-
-    // Test 2.4: get_tx_nested_array_len() - Get nested array length with locator
-    let nested_array_len = unsafe { host::tx_inner_arr_len(locator.as_ptr(), locator.len()) };
-
-    if nested_array_len < 0 {
-        let _ = trace_num(
-            "INFO: get_tx_nested_array_len not applicable:",
-            nested_array_len as i64,
-        );
-    } else {
-        let _ = trace_num("Nested array length:", nested_array_len as i64);
-    }
-
-    let _ = trace("SUCCESS: Transaction data functions");
-    0
-}
-
-/// Test Category 3: Current Ledger Object Functions (4 functions)
-/// Tests functions that access the current ledger object being processed
-fn test_current_ledger_object_functions() -> i32 {
-    let _ = trace("--- Category 3: Current Ledger Object Functions ---");
-
-    // Test 3.1: get_current_ledger_obj_field() - Access field from current ledger object
-    // Test with Balance field (XRP amount - 8 bytes in new serialized format)
-    let mut balance_buffer = [0u8; 8];
-    let balance_result = unsafe {
-        host::home_le_field(
-            sfield::Balance.into(),
-            balance_buffer.as_mut_ptr(),
-            balance_buffer.len(),
-        )
-    };
-
-    if balance_result <= 0 {
-        let _ = trace_num(
-            "INFO: get_current_ledger_obj_field(Balance) failed (may be expected):",
-            balance_result as i64,
-        );
-        // This might fail if current ledger object doesn't have balance field
-    } else if balance_result == 8 {
-        let _ = trace_num(
-            "Current object balance length (XRP amount):",
-            balance_result as i64,
-        );
-        let _ = trace_hex(
-            "Current object balance (serialized XRP amount):",
-            &balance_buffer,
-        );
-    } else {
-        let _ = trace_num(
-            "Current object balance length (non-XRP amount):",
-            balance_result as i64,
-        );
-        let _ = trace_hex(
-            "Current object balance:",
-            &balance_buffer[..balance_result as usize],
-        );
-    }
-
-    // Test with Account field
-    let mut current_account_buffer = [0u8; 20];
-    let current_account_result = unsafe {
-        host::home_le_field(
-            sfield::Account.into(),
-            current_account_buffer.as_mut_ptr(),
-            current_account_buffer.len(),
-        )
-    };
-
-    if current_account_result <= 0 {
-        let _ = trace_num(
-            "INFO: get_current_ledger_obj_field(Account) failed:",
-            current_account_result as i64,
-        );
-    } else {
-        let _ = trace_acct_buf("Current ledger object account:", ¤t_account_buffer);
-    }
-
-    // Test 3.2: get_current_ledger_obj_nested_field() - Nested field access
-    let locator = [
-        0x01_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8,
-    ]; // Two int32s in little-endian: [1, 0]
-    let mut current_nested_buffer = [0u8; 32];
-    let current_nested_result = unsafe {
-        host::home_le_inner(
-            locator.as_ptr(),
-            locator.len(),
-            current_nested_buffer.as_mut_ptr(),
-            current_nested_buffer.len(),
-        )
-    };
-
-    if current_nested_result < 0 {
-        let _ = trace_num(
-            "INFO: get_current_ledger_obj_nested_field not applicable:",
-            current_nested_result as i64,
-        );
-    } else {
-        let _ = trace_num("Current nested field length:", current_nested_result as i64);
-        let _ = trace_hex(
-            "Current nested field:",
-            ¤t_nested_buffer[..current_nested_result as usize],
-        );
-    }
-
-    // Test 3.3: get_current_ledger_obj_array_len() - Array length in current object
-    let current_array_len = unsafe { host::home_le_arr_len(sfield::Signers.into()) };
-    let _ = trace_num(
-        "Current object Signers array length:",
-        current_array_len as i64,
-    );
-
-    // Test 3.4: get_current_ledger_obj_nested_array_len() - Nested array length
-    let current_nested_array_len =
-        unsafe { host::home_le_inner_arr_len(locator.as_ptr(), locator.len()) };
-
-    if current_nested_array_len < 0 {
-        let _ = trace_num(
-            "INFO: get_current_ledger_obj_nested_array_len not applicable:",
-            current_nested_array_len as i64,
-        );
-    } else {
-        let _ = trace_num(
-            "Current nested array length:",
-            current_nested_array_len as i64,
-        );
-    }
-
-    let _ = trace("SUCCESS: Current ledger object functions");
-    0
-}
-
-/// Test Category 4: Any Ledger Object Functions (5 functions)
-/// Tests functions that work with cached ledger objects
-fn test_any_ledger_object_functions() -> i32 {
-    let _ = trace("--- Category 4: Any Ledger Object Functions ---");
-
-    // First we need to cache a ledger object to test the other functions
-    // Get the account from transaction and generate its keylet
-    let escrow_finish = EscrowFinish;
-    let account_id = escrow_finish.get_account().unwrap();
-
-    // Test 4.1: cache_le() - Cache a ledger object
-    let mut keylet_buffer = [0u8; 32];
-    let keylet_result = unsafe {
-        host::accountroot_id(
-            account_id.0.as_ptr(),
-            account_id.0.len(),
-            keylet_buffer.as_mut_ptr(),
-            keylet_buffer.len(),
-        )
-    };
-
-    if keylet_result != 32 {
-        let _ = trace_num(
-            "ERROR: accountroot_id failed for caching test:",
-            keylet_result as i64,
-        );
-        return -401; // Keylet generation failed for caching test
-    }
-
-    let cache_result = unsafe { host::cache_le(keylet_buffer.as_ptr(), keylet_result as usize, 0) };
-
-    if cache_result <= 0 {
-        let _ = trace_num(
-            "INFO: cache_le failed (expected with test fixtures):",
-            cache_result as i64,
-        );
-        // Test fixtures may not contain the account object - this is expected
-        // We'll test the interface but expect failures
-
-        // Test 4.2-4.5 with invalid slot (should fail gracefully)
-        let mut test_buffer = [0u8; 32];
-
-        // Test le_field with invalid slot
-        let field_result = unsafe {
-            host::le_field(
-                1,
-                sfield::Balance.into(),
-                test_buffer.as_mut_ptr(),
-                test_buffer.len(),
-            )
-        };
-        if field_result < 0 {
-            let _ = trace_num(
-                "INFO: le_field failed as expected (no cached object):",
-                field_result as i64,
-            );
-        }
-
-        // Test le_inner_field with invalid slot
-        let locator = [
-            0x01_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8,
-        ]; // Two int32s in little-endian: [1, 0]
-        let nested_result = unsafe {
-            host::le_inner(
-                1,
-                locator.as_ptr(),
-                locator.len(),
-                test_buffer.as_mut_ptr(),
-                test_buffer.len(),
-            )
-        };
-        if nested_result < 0 {
-            let _ = trace_num(
-                "INFO: le_inner_field failed as expected:",
-                nested_result as i64,
-            );
-        }
-
-        // Test le_inner_arr_len with invalid slot
-        let array_result = unsafe { host::le_arr_len(1, sfield::Signers.into()) };
-        if array_result < 0 {
-            let _ = trace_num(
-                "INFO: le_inner_arr_len failed as expected:",
-                array_result as i64,
-            );
-        }
-
-        // Test get_ledger_obj_nested_array_len with invalid slot
-        let nested_array_result =
-            unsafe { host::le_inner_arr_len(1, locator.as_ptr(), locator.len()) };
-        if nested_array_result < 0 {
-            let _ = trace_num(
-                "INFO: get_ledger_obj_nested_array_len failed as expected:",
-                nested_array_result as i64,
-            );
-        }
-
-        let _ = trace("SUCCESS: Any ledger object functions (interface tested)");
-        return 0;
-    }
-
-    // If we successfully cached an object, test the access functions
-    let slot = cache_result;
-    let _ = trace_num("Successfully cached object in slot:", slot as i64);
-
-    // Test 4.2: le_field() - Access field from cached object
-    let mut cached_balance_buffer = [0u8; 8];
-    let cached_balance_result = unsafe {
-        host::le_field(
-            slot,
-            sfield::Balance.into(),
-            cached_balance_buffer.as_mut_ptr(),
-            cached_balance_buffer.len(),
-        )
-    };
-
-    if cached_balance_result <= 0 {
-        let _ = trace_num(
-            "INFO: le_field(Balance) failed:",
-            cached_balance_result as i64,
-        );
-    } else if cached_balance_result == 8 {
-        let _ = trace_num(
-            "Cached object balance length (XRP amount):",
-            cached_balance_result as i64,
-        );
-        let _ = trace_hex(
-            "Cached object balance (serialized XRP amount):",
-            &cached_balance_buffer,
-        );
-    } else {
-        let _ = trace_num(
-            "Cached object balance length (non-XRP amount):",
-            cached_balance_result as i64,
-        );
-        let _ = trace_hex(
-            "Cached object balance:",
-            &cached_balance_buffer[..cached_balance_result as usize],
-        );
-    }
-
-    // Test 4.3: le_inner_field() - Nested field from cached object
-    let locator = [
-        0x01_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8, 0x00_u8,
-    ]; // Two int32s in little-endian: [1, 0]
-    let mut cached_nested_buffer = [0u8; 32];
-    let cached_nested_result = unsafe {
-        host::le_inner(
-            slot,
-            locator.as_ptr(),
-            locator.len(),
-            cached_nested_buffer.as_mut_ptr(),
-            cached_nested_buffer.len(),
-        )
-    };
-
-    if cached_nested_result < 0 {
-        let _ = trace_num(
-            "INFO: le_inner_field not applicable:",
-            cached_nested_result as i64,
-        );
-    } else {
-        let _ = trace_num("Cached nested field length:", cached_nested_result as i64);
-        let _ = trace_hex(
-            "Cached nested field:",
-            &cached_nested_buffer[..cached_nested_result as usize],
-        );
-    }
-
-    // Test 4.4: le_inner_arr_len() - Array length from cached object
-    let cached_array_len = unsafe { host::le_arr_len(slot, sfield::Signers.into()) };
-    let _ = trace_num(
-        "Cached object Signers array length:",
-        cached_array_len as i64,
-    );
-
-    // Test 4.5: get_ledger_obj_nested_array_len() - Nested array length from cached object
-    let cached_nested_array_len =
-        unsafe { host::le_inner_arr_len(slot, locator.as_ptr(), locator.len()) };
-
-    if cached_nested_array_len < 0 {
-        let _ = trace_num(
-            "INFO: get_ledger_obj_nested_array_len not applicable:",
-            cached_nested_array_len as i64,
-        );
-    } else {
-        let _ = trace_num(
-            "Cached nested array length:",
-            cached_nested_array_len as i64,
-        );
-    }
-
-    let _ = trace("SUCCESS: Any ledger object functions");
-    0
-}
-
-/// Test Category 5: Keylet Generation Functions (4 functions)
-/// Tests keylet generation functions for different ledger entry types
-fn test_keylet_generation_functions() -> i32 {
-    let _ = trace("--- Category 5: Keylet Generation Functions ---");
-
-    let escrow_finish = EscrowFinish;
-    let account_id = escrow_finish.get_account().unwrap();
-
-    // Test 5.1: accountroot_id() - Generate keylet for account
-    let mut accountroot_id_buffer = [0u8; 32];
-    let accountroot_id_result = unsafe {
-        host::accountroot_id(
-            account_id.0.as_ptr(),
-            account_id.0.len(),
-            accountroot_id_buffer.as_mut_ptr(),
-            accountroot_id_buffer.len(),
-        )
-    };
-
-    if accountroot_id_result != 32 {
-        let _ = trace_num(
-            "ERROR: accountroot_id failed:",
-            accountroot_id_result as i64,
-        );
-        return -501; // Account keylet generation failed
-    }
-    let _ = trace_hex("Account keylet:", &accountroot_id_buffer);
-
-    // Test 5.2: credential_keylet() - Generate keylet for credential
-    let mut credential_keylet_buffer = [0u8; 32];
-    let credential_keylet_result = unsafe {
-        host::credential_id(
-            account_id.0.as_ptr(), // Subject
-            account_id.0.len(),
-            account_id.0.as_ptr(), // Issuer - same account for test
-            account_id.0.len(),
-            b"TestType".as_ptr(), // Credential type
-            9usize,               // Length of "TestType"
-            credential_keylet_buffer.as_mut_ptr(),
-            credential_keylet_buffer.len(),
-        )
-    };
-
-    if credential_keylet_result <= 0 {
-        let _ = trace_num(
-            "INFO: credential_keylet failed (expected - interface issue):",
-            credential_keylet_result as i64,
-        );
-        // This is expected to fail due to unusual parameter types
-    } else {
-        let _ = trace_hex(
-            "Credential keylet:",
-            &credential_keylet_buffer[..credential_keylet_result as usize],
-        );
-    }
-
-    // Test 5.3: escrow_keylet() - Generate keylet for escrow
-    let mut escrow_keylet_buffer = [0u8; 32];
-    let sequence_number: i32 = 1000;
-    let sequence_number_bytes = sequence_number.to_be_bytes();
-    let escrow_keylet_result = unsafe {
-        host::escrow_id(
-            account_id.0.as_ptr(),
-            account_id.0.len(),
-            sequence_number_bytes.as_ptr(),
-            sequence_number_bytes.len(),
-            escrow_keylet_buffer.as_mut_ptr(),
-            escrow_keylet_buffer.len(),
-        )
-    };
-
-    if escrow_keylet_result != 32 {
-        let _ = trace_num("ERROR: escrow_keylet failed:", escrow_keylet_result as i64);
-        return -503; // Escrow keylet generation failed
-    }
-    let _ = trace_hex("Escrow keylet:", &escrow_keylet_buffer);
-
-    // Test 5.4: oracle_keylet() - Generate keylet for oracle
-    let mut oracle_keylet_buffer = [0u8; 32];
-    let document_id: i32 = 42;
-    let document_id_bytes = document_id.to_be_bytes();
-    let oracle_keylet_result = unsafe {
-        host::oracle_id(
-            account_id.0.as_ptr(),
-            account_id.0.len(),
-            document_id_bytes.as_ptr(),
-            document_id_bytes.len(),
-            oracle_keylet_buffer.as_mut_ptr(),
-            oracle_keylet_buffer.len(),
-        )
-    };
-
-    if oracle_keylet_result != 32 {
-        let _ = trace_num("ERROR: oracle_keylet failed:", oracle_keylet_result as i64);
-        return -504; // Oracle keylet generation failed
-    }
-    let _ = trace_hex("Oracle keylet:", &oracle_keylet_buffer);
-
-    let _ = trace("SUCCESS: Keylet generation functions");
-    0
-}
-
-/// Test Category 6: Utility Functions (4 functions)
-/// Tests utility functions for hashing, NFT access, and tracing
-fn test_utility_functions() -> i32 {
-    let _ = trace("--- Category 6: Utility Functions ---");
-
-    // Test 6.1: compute_sha512_half() - SHA512 hash computation (first 32 bytes)
-    let test_data = b"Hello, XRPL WASM world!";
-    let mut hash_output = [0u8; 32];
-    let hash_result = unsafe {
-        host::sha512_half(
-            test_data.as_ptr(),
-            test_data.len(),
-            hash_output.as_mut_ptr(),
-            hash_output.len(),
-        )
-    };
-
-    if hash_result != 32 {
-        let _ = trace_num("ERROR: compute_sha512_half failed:", hash_result as i64);
-        return -601; // SHA512 half computation failed
-    }
-    let _ = trace_hex("Input data:", test_data);
-    let _ = trace_hex("SHA512 half hash:", &hash_output);
-
-    // Test 6.2: get_nft() - NFT data retrieval
-    let escrow_finish = EscrowFinish;
-    let account_id = escrow_finish.get_account().unwrap();
-    let nft_id = [0u8; 32]; // Dummy NFT ID for testing
-    let mut nft_buffer = [0u8; 256];
-    let nft_result = unsafe {
-        host::nft_uri(
-            account_id.0.as_ptr(),
-            account_id.0.len(),
-            nft_id.as_ptr(),
-            nft_id.len(),
-            nft_buffer.as_mut_ptr(),
-            nft_buffer.len(),
-        )
-    };
-
-    if nft_result <= 0 {
-        let _ = trace_num(
-            "INFO: get_nft failed (expected - no such NFT):",
-            nft_result as i64,
-        );
-        // This is expected - test account likely doesn't own the dummy NFT
-    } else {
-        let _ = trace_num("NFT data length:", nft_result as i64);
-        let _ = trace_hex("NFT data:", &nft_buffer[..nft_result as usize]);
-    }
-
-    // Test 6.3: trace() - Debug logging with data
-    let trace_message = b"Test trace message";
-    let trace_data_payload = b"payload";
-    unsafe {
-        host::trace(
-            trace_message.as_ptr(),
-            trace_message.len(),
-            TraceDataType::AsHex as i32,
-            trace_data_payload.as_ptr(),
-            trace_data_payload.len(),
-        )
-    };
-
-    // Test 6.4: trace_num() - Debug logging with number
-    let test_number = 42i64;
-    trace_num("Test number trace", test_number);
-
-    let _ = trace("SUCCESS: Utility functions");
-    0
-}
-
-/// Test Category 7: Data Update Functions (1 function)
-/// Tests the function for modifying the current ledger entry
-fn test_data_update_functions() -> i32 {
-    let _ = trace("--- Category 7: Data Update Functions ---");
-
-    // Test 7.1: update_data() - Update current ledger entry data
-    let update_payload = b"Updated ledger entry data from WASM test";
-
-    let update_result = unsafe { host::set_data(update_payload.as_ptr(), update_payload.len()) };
-
-    if update_result != update_payload.len() as i32 {
-        let _ = trace_num("ERROR: update_data failed:", update_result as i64);
-        return -701; // Data update failed
-    }
-
-    let _ = trace_hex("Successfully updated ledger entry with:", update_payload);
-    let _ = trace("SUCCESS: Data update functions");
-    0
-}
diff --git a/src/test/app/wasm_fixtures/all_keylets/Cargo.lock b/src/test/app/wasm_fixtures/all_keylets/Cargo.lock
deleted file mode 100644
index 5da5b26f66..0000000000
--- a/src/test/app/wasm_fixtures/all_keylets/Cargo.lock
+++ /dev/null
@@ -1,171 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "all_keylets"
-version = "0.0.1"
-dependencies = [
- "xrpl-wasm-stdlib",
-]
-
-[[package]]
-name = "block-buffer"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
-dependencies = [
- "generic-array",
-]
-
-[[package]]
-name = "bs58"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
-dependencies = [
- "tinyvec",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "cpufeatures"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "crypto-common"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
-dependencies = [
- "generic-array",
- "typenum",
-]
-
-[[package]]
-name = "digest"
-version = "0.10.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
-dependencies = [
- "block-buffer",
- "crypto-common",
-]
-
-[[package]]
-name = "generic-array"
-version = "0.14.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
-dependencies = [
- "typenum",
- "version_check",
-]
-
-[[package]]
-name = "libc"
-version = "0.2.186"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.106"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "sha2"
-version = "0.10.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
-[[package]]
-name = "syn"
-version = "2.0.117"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "tinyvec"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
-dependencies = [
- "tinyvec_macros",
-]
-
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
-
-[[package]]
-name = "typenum"
-version = "1.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
-
-[[package]]
-name = "version_check"
-version = "0.9.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
-
-[[package]]
-name = "xrpl-macros"
-version = "0.1.0"
-source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=renames#21c522f34a24b460297ebb6be1822680459bf37e"
-dependencies = [
- "bs58",
- "quote",
- "sha2",
- "syn",
-]
-
-[[package]]
-name = "xrpl-wasm-stdlib"
-version = "0.8.0"
-source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=renames#21c522f34a24b460297ebb6be1822680459bf37e"
-dependencies = [
- "xrpl-macros",
-]
diff --git a/src/test/app/wasm_fixtures/all_keylets/Cargo.toml b/src/test/app/wasm_fixtures/all_keylets/Cargo.toml
deleted file mode 100644
index ad53fd62b1..0000000000
--- a/src/test/app/wasm_fixtures/all_keylets/Cargo.toml
+++ /dev/null
@@ -1,21 +0,0 @@
-[package]
-edition = "2024"
-name = "all_keylets"
-version = "0.0.1"
-
-# This empty workspace definition keeps this project independent of the parent workspace
-[workspace]
-
-[lib]
-crate-type = ["cdylib"]
-
-[profile.release]
-lto = true
-opt-level = 's'
-panic = "abort"
-
-[dependencies]
-xrpl-std = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-wasm-stdlib", branch = "renames" }
-
-[profile.dev]
-panic = "abort"
diff --git a/src/test/app/wasm_fixtures/all_keylets/src/lib.rs b/src/test/app/wasm_fixtures/all_keylets/src/lib.rs
deleted file mode 100644
index f0a4e5abb5..0000000000
--- a/src/test/app/wasm_fixtures/all_keylets/src/lib.rs
+++ /dev/null
@@ -1,176 +0,0 @@
-#![cfg_attr(target_arch = "wasm32", no_std)]
-
-#[cfg(not(target_arch = "wasm32"))]
-extern crate std;
-
-use crate::host::{Error, Result, Result::Err, Result::Ok};
-use xrpl_std::core::keylets;
-use xrpl_std::core::ledger_objects::current_escrow::get_current_escrow;
-use xrpl_std::core::ledger_objects::current_escrow::CurrentEscrow;
-use xrpl_std::core::ledger_objects::ledger_object;
-use xrpl_std::core::ledger_objects::traits::CurrentEscrowFields;
-use xrpl_std::core::ledger_objects::LedgerObjectFieldGetter;
-use xrpl_std::core::types::currency::Currency;
-use xrpl_std::core::types::issue::{IouIssue, Issue, XrpIssue};
-use xrpl_std::core::types::mpt_id::MptId;
-use xrpl_std::host;
-use xrpl_std::host::trace::{trace, trace_acct, trace_data, trace_num, DataRepr};
-use xrpl_std::sfield;
-
-pub fn object_exists(
-    keylet_result: Result,
-    keylet_type: &str,
-    sfield: sfield::SField,
-) -> Result {
-    let field = CODE;
-    match keylet_result {
-        Ok(keylet) => {
-            let _ = trace_data(keylet_type, &keylet, DataRepr::AsHex);
-
-            let slot = unsafe { host::cache_le(keylet.as_ptr(), keylet.len(), 0) };
-            if slot <= 0 {
-                let _ = trace_num("Error: ", slot.into());
-                return Err(Error::from_code(slot));
-            }
-            if field == 0 {
-                let new_field = sfield::PreviousTxnID;
-                let _ = trace_num("Getting field: ", new_field.clone().into());
-                match ledger_object::get_field(slot, new_field) {
-                    Ok(data) => {
-                        let _ = trace_data("Field data: ", &data.0, DataRepr::AsHex);
-                    }
-                    Err(result_code) => {
-                        let _ = trace_num("Error getting field: ", result_code.into());
-                        return Err(result_code);
-                    }
-                }
-            } else {
-                let _ = trace_num("Getting field: ", field.into());
-                match ledger_object::get_field(slot, sfield) {
-                    Ok(_data) => {
-                        let _ = trace("Field data: retrieved");
-                    }
-                    Err(result_code) => {
-                        let _ = trace_num("Error getting field: ", result_code.into());
-                        return Err(result_code);
-                    }
-                }
-            }
-
-            Ok(true)
-        }
-        Err(error) => {
-            let _ = trace_num("Error getting keylet: ", error.into());
-            Err(error)
-        }
-    }
-}
-
-#[unsafe(no_mangle)]
-pub extern "C" fn escrow_finish() -> i32 {
-    let _ = trace("$$$$$ STARTING WASM EXECUTION $$$$$");
-
-    let escrow: CurrentEscrow = get_current_escrow();
-
-    let account = escrow.get_account().unwrap_or_panic();
-    let _ = trace_acct("Account:", &account);
-
-    let destination = escrow.get_destination().unwrap_or_panic();
-    let _ = trace_acct("Destination:", &destination);
-
-    let mut seq = 5;
-
-    macro_rules! check_object_exists {
-        ($keylet:expr, $type:expr, $field:expr) => {
-            match object_exists($keylet, $type, $field) {
-                Ok(_exists) => {
-                    // false isn't returned
-                    let _ = trace(concat!(
-                        $type,
-                        " object exists, proceeding with escrow finish."
-                    ));
-                }
-                Err(error) => {
-                    let _ = trace_num("Current seq value:", seq.try_into().unwrap());
-                    return error.code();
-                }
-            }
-        };
-    }
-
-    let accountroot_id = keylets::accountroot_id(&account);
-    check_object_exists!(accountroot_id, "Account", sfield::Account);
-
-    let currency_code: &[u8; 3] = b"USD";
-    let currency: Currency = Currency::from(*currency_code);
-    let trustline_id = keylets::trustline_id(&account, &destination, ¤cy);
-    check_object_exists!(trustline_id, "Trustline", sfield::Generic);
-    seq += 1;
-
-    let asset1 = Issue::XRP(XrpIssue {});
-    let asset2 = Issue::IOU(IouIssue::new(destination, currency));
-    check_object_exists!(keylets::amm_id(&asset1, &asset2), "AMM", sfield::Account);
-
-    let check_id = keylets::check_id(&account, seq);
-    check_object_exists!(check_id, "Check", sfield::Account);
-    seq += 1;
-
-    let cred_type: &[u8] = b"termsandconditions";
-    let credential_id = keylets::credential_id(&account, &account, cred_type);
-    check_object_exists!(credential_id, "Credential", sfield::Subject);
-    seq += 1;
-
-    let delegate_id = keylets::delegate_id(&account, &destination);
-    check_object_exists!(delegate_id, "Delegate", sfield::Account);
-    seq += 1;
-
-    let deposit_preauth_id = keylets::deposit_preauth_id(&account, &destination);
-    check_object_exists!(deposit_preauth_id, "DepositPreauth", sfield::Account);
-    seq += 1;
-
-    let did_id = keylets::did_id(&account);
-    check_object_exists!(did_id, "DID", sfield::Account);
-    seq += 1;
-
-    let escrow_id = keylets::escrow_id(&account, seq);
-    check_object_exists!(escrow_id, "Escrow", sfield::Account);
-    seq += 1;
-
-    let mpt_issuance_id = keylets::mpt_issuance_id(&account, seq);
-    let mpt_id = MptId::new(seq.try_into().unwrap(), account);
-    check_object_exists!(mpt_issuance_id, "MPTIssuance", sfield::Issuer);
-    seq += 1;
-
-    let mptoken_id = keylets::mptoken_id(&mpt_id, &destination);
-    check_object_exists!(mptoken_id, "MPToken", sfield::Account);
-
-    let nft_offer_id = keylets::nft_offer_id(&destination, 6);
-    check_object_exists!(nft_offer_id, "NFTokenOffer", sfield::Owner);
-
-    let offer_id = keylets::offer_id(&account, seq);
-    check_object_exists!(offer_id, "Offer", sfield::Account);
-    seq += 1;
-
-    let paychan_id = keylets::paychan_id(&account, &destination, seq);
-    check_object_exists!(paychan_id, "PayChannel", sfield::Account);
-    seq += 1;
-
-    let pd_id = keylets::permissioned_domain_id(&account, seq);
-    check_object_exists!(pd_id, "PermissionedDomain", sfield::Owner);
-    seq += 1;
-
-    let signers_id = keylets::signers_id(&account);
-    check_object_exists!(signers_id, "SignerList", sfield::Generic);
-    seq += 1;
-
-    seq += 1; // ticket sequence number is one greater
-    let ticket_id = keylets::ticket_id(&account, seq);
-    check_object_exists!(ticket_id, "Ticket", sfield::Account);
-    seq += 1;
-
-    let vault_id = keylets::vault_id(&account, seq);
-    check_object_exists!(vault_id, "Vault", sfield::Account);
-    // seq += 1;
-
-    1 // All keylets exist, finish the escrow.
-}
diff --git a/src/test/app/wasm_fixtures/bad_align.c b/src/test/app/wasm_fixtures/bad_align.c
deleted file mode 100644
index 560245e762..0000000000
--- a/src/test/app/wasm_fixtures/bad_align.c
+++ /dev/null
@@ -1,42 +0,0 @@
-#include 
-
-int32_t float_from_uint(uint8_t const *, int32_t, uint8_t *, int32_t, int32_t);
-int32_t check_id(uint8_t const *, int32_t, uint8_t const *, int32_t, uint8_t *,
-                 int32_t);
-
-uint8_t e_data1[32 * 1024];
-uint8_t e_data2[32 * 1024];
-
-int32_t test1()
-{
-  e_data1[1] = 0xFF;
-  e_data1[2] = 0xFF;
-  e_data1[3] = 0xFF;
-  e_data1[4] = 0xFF;
-  e_data1[5] = 0xFF;
-  e_data1[6] = 0xFF;
-  e_data1[7] = 0xFF;
-  e_data1[8] = 0xFF;
-  int32_t result = float_from_uint(&e_data1[1], 8, &e_data1[35], 12, 0);
-  return result >= 0 ? *((int32_t *)(&e_data1[36])) : result;
-}
-
-int32_t test2()
-{
-  // Set up misaligned uint32 (seq) at offset 1
-  e_data2[1] = 0xFF;
-  e_data2[2] = 0xFF;
-  e_data2[3] = 0xFF;
-  e_data2[4] = 0xFF;
-  // Set up valid non-zero AccountID (20 bytes) at offset 10
-  for (int i = 0; i < 20; i++)
-    e_data2[10 + i] = i + 1;
-  // Call check_id with misaligned uint32 at &e_data2[1] to hit line 72 in
-  // HostFuncWrapper.cpp
-  int32_t result = check_id(&e_data2[10], 20, &e_data2[1], 4, &e_data2[35], 32);
-  // Return the misaligned value directly to validate it was read correctly (-1
-  // if all 0xFF)
-  return result >= 0 ? *((int32_t *)(&e_data2[36])) : result;
-}
-
-int32_t test() { return test1() + test2(); }
diff --git a/src/test/app/wasm_fixtures/codecov_tests/Cargo.lock b/src/test/app/wasm_fixtures/codecov_tests/Cargo.lock
deleted file mode 100644
index 899f278196..0000000000
--- a/src/test/app/wasm_fixtures/codecov_tests/Cargo.lock
+++ /dev/null
@@ -1,180 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "block-buffer"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
-dependencies = [
- "hybrid-array",
-]
-
-[[package]]
-name = "bs58"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
-dependencies = [
- "tinyvec",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "codecov_tests"
-version = "0.0.1"
-dependencies = [
- "xrpl-common-stdlib",
- "xrpl-escrow-stdlib",
-]
-
-[[package]]
-name = "const-oid"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
-
-[[package]]
-name = "cpufeatures"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "crypto-common"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
-dependencies = [
- "hybrid-array",
-]
-
-[[package]]
-name = "digest"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
-dependencies = [
- "block-buffer",
- "const-oid",
- "crypto-common",
-]
-
-[[package]]
-name = "hybrid-array"
-version = "0.4.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
-dependencies = [
- "typenum",
-]
-
-[[package]]
-name = "libc"
-version = "0.2.186"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.106"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "sha2"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
-[[package]]
-name = "syn"
-version = "3.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "tinyvec"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
-dependencies = [
- "tinyvec_macros",
-]
-
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
-
-[[package]]
-name = "typenum"
-version = "1.20.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
-
-[[package]]
-name = "xrpl-common-stdlib"
-version = "0.8.0"
-source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9"
-dependencies = [
- "xrpl-macros",
-]
-
-[[package]]
-name = "xrpl-escrow-stdlib"
-version = "0.1.0"
-source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9"
-dependencies = [
- "xrpl-common-stdlib",
-]
-
-[[package]]
-name = "xrpl-macros"
-version = "0.1.0"
-source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=error-and-trace#b008b097237ce0d1a2dffc72ba39dd9fc50020a9"
-dependencies = [
- "bs58",
- "proc-macro2",
- "quote",
- "sha2",
- "syn",
-]
diff --git a/src/test/app/wasm_fixtures/codecov_tests/Cargo.toml b/src/test/app/wasm_fixtures/codecov_tests/Cargo.toml
deleted file mode 100644
index 1e388a5154..0000000000
--- a/src/test/app/wasm_fixtures/codecov_tests/Cargo.toml
+++ /dev/null
@@ -1,19 +0,0 @@
-[package]
-edition = "2024"
-name = "codecov_tests"
-version = "0.0.1"
-
-# This empty workspace definition keeps this project independent of the parent workspace
-[workspace]
-
-[lib]
-crate-type = ["cdylib"]
-
-[profile.release]
-lto = true
-opt-level = 's'
-panic = "abort"
-
-[dependencies]
-xrpl-std = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-common-stdlib", branch = "error-and-trace" }
-xrpl-escrow = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-escrow-stdlib", branch = "error-and-trace" }
diff --git a/src/test/app/wasm_fixtures/codecov_tests/src/host_bindings_loose.rs b/src/test/app/wasm_fixtures/codecov_tests/src/host_bindings_loose.rs
deleted file mode 100644
index 7b42f747a8..0000000000
--- a/src/test/app/wasm_fixtures/codecov_tests/src/host_bindings_loose.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-//TODO add docs after discussing the interface
-//Note that Craft currently does not honor the rounding modes
-#[allow(unused)]
-pub const FLOAT_ROUNDING_MODES_TO_NEAREST: i32 = 0;
-#[allow(unused)]
-pub const FLOAT_ROUNDING_MODES_TOWARDS_ZERO: i32 = 1;
-#[allow(unused)]
-pub const FLOAT_ROUNDING_MODES_DOWNWARD: i32 = 2;
-#[allow(unused)]
-pub const FLOAT_ROUNDING_MODES_UPWARD: i32 = 3;
-
-// pub enum RippledRoundingModes{
-//     ToNearest = 0,
-//     TowardsZero = 1,
-//     DOWNWARD = 2,
-//     UPWARD = 3
-// }
-
-#[allow(unused)]
-#[link(wasm_import_module = "host_lib")]
-unsafe extern "C" {
-    pub fn parent_ldgr_hash(out_buff_ptr: i32, out_buff_len: i32) -> i32;
-
-    pub fn cache_le(keylet_ptr: i32, keylet_len: i32, cache_num: i32) -> i32;
-
-    pub fn tx_inner_arr_len(locator_ptr: i32, locator_len: i32) -> i32;
-
-    pub fn accountroot_id(
-        account_ptr: i32,
-        account_len: i32,
-        out_buff_ptr: *mut u8,
-        out_buff_len: usize,
-    ) -> i32;
-
-    pub fn trustline_id(
-        account1_ptr: *const u8,
-        account1_len: usize,
-        account2_ptr: *const u8,
-        account2_len: usize,
-        currency_ptr: i32,
-        currency_len: i32,
-        out_buff_ptr: *mut u8,
-        out_buff_len: usize,
-    ) -> i32;
-
-    // Same wasm functype as the real binding, so this is not a second import of
-    // host_lib.trace. Loose i32 pointers exercise the out-of-bounds path.
-    #[link_name = "trace"]
-    pub fn trace_loose(
-        msg_read_ptr: i32,
-        msg_read_len: i32,
-        data_type: i32,
-        data_read_ptr: i32,
-        data_read_len: i32,
-    );
-}
diff --git a/src/test/app/wasm_fixtures/codecov_tests/src/lib.rs b/src/test/app/wasm_fixtures/codecov_tests/src/lib.rs
deleted file mode 100644
index 02b38f633e..0000000000
--- a/src/test/app/wasm_fixtures/codecov_tests/src/lib.rs
+++ /dev/null
@@ -1,1744 +0,0 @@
-#![cfg_attr(target_arch = "wasm32", no_std)]
-
-#[cfg(not(target_arch = "wasm32"))]
-extern crate std;
-
-use core::panic;
-use xrpl_escrow::current_tx::escrow_finish::{EscrowFinish, get_current_escrow_finish};
-use xrpl_std::current_tx::traits::TransactionCommonFields;
-use xrpl_std::fields::locator::Locator;
-use xrpl_std::host;
-use xrpl_std::host::error_codes;
-use xrpl_std::host::trace::TraceDataType;
-use xrpl_std::host::trace::{trace, trace_num as trace_number};
-use xrpl_std::ledger_entry_ids;
-use xrpl_std::sfield;
-use xrpl_std::types::blob::DEFAULT_BLOB_SIZE;
-use xrpl_std::types::contract_data::XRPL_CONTRACT_DATA_SIZE;
-use xrpl_std::types::issue::Issue;
-use xrpl_std::types::issue::XrpIssue;
-use xrpl_std::types::mpt_id::MptId;
-
-mod host_bindings_loose;
-include!("host_bindings_loose.rs");
-
-fn check_result(result: i32, expected: i32, test_name: &'static str) {
-    match result {
-        code if code == expected => {
-            let _ = trace_number(test_name, code.into());
-        }
-        code if code >= 0 => {
-            let _ = trace(test_name);
-            let _ = trace_number("TEST FAILED", code.into());
-            panic!("Unexpected success code: {}", code);
-        }
-        code => {
-            let _ = trace(test_name);
-            let _ = trace_number("TEST FAILED", code.into());
-            panic!("Error code: {}", code);
-        }
-    }
-}
-
-fn with_buffer(mut f: F) -> R
-where
-    F: FnMut(*mut u8, usize) -> R,
-{
-    let mut buf = [0u8; N];
-    f(buf.as_mut_ptr(), buf.len())
-}
-
-#[unsafe(no_mangle)]
-pub extern "C" fn escrow_finish() -> i32 {
-    let _ = trace("$$$$$ STARTING WASM EXECUTION $$$$$");
-
-    // ########################################
-    // Step #1: Test all host function happy paths
-    // Note: not testing all the keylet functions,
-    // that's in a separate test file (all_keylets).
-    // The float tests are also in a separate file (float_tests).
-    // ########################################
-    with_buffer::<4, _, _>(|ptr, len| {
-        check_result(unsafe { host::ldgr_index(ptr, len) }, 4, "ldgr_index");
-    });
-    with_buffer::<4, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::parent_ldgr_time(ptr, len) },
-            4,
-            "parent_ldgr_time",
-        );
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::parent_ldgr_hash(ptr, len) },
-            32,
-            "parent_ldgr_hash",
-        );
-    });
-    with_buffer::<4, _, _>(|ptr, len| {
-        check_result(unsafe { host::base_fee(ptr, len) }, 4, "base_fee");
-    });
-    let amendment_name: &[u8] = b"test_amendment";
-    let amendment_id: [u8; 32] = [1; 32];
-    check_result(
-        unsafe { host::amendment_enabled(amendment_name.as_ptr(), amendment_name.len()) },
-        1,
-        "amendment_enabled",
-    );
-    check_result(
-        unsafe { host::amendment_enabled(amendment_id.as_ptr(), amendment_id.len()) },
-        1,
-        "amendment_enabled",
-    );
-    let tx: EscrowFinish = get_current_escrow_finish();
-    let account = tx.get_account().unwrap_or_panic(); // get_tx_field under the hood
-    let keylet = ledger_entry_ids::accountroot_id(&account).unwrap_or_panic(); // accountroot_id under the hood
-    check_result(
-        unsafe { host::cache_le(keylet.as_ptr(), keylet.len(), 0) },
-        1,
-        "cache_le",
-    );
-    with_buffer::<20, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::home_le_field(sfield::Account.into(), ptr, len) },
-            20,
-            "home_le_field",
-        );
-    });
-    with_buffer::<20, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::le_field(1, sfield::Account.into(), ptr, len) },
-            20,
-            "le_field",
-        );
-    });
-    let mut locator = Locator::new();
-    locator.pack(sfield::Account);
-    with_buffer::<20, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::tx_inner(locator.as_ptr(), locator.len(), ptr, len) },
-            20,
-            "tx_inner",
-        );
-    });
-    with_buffer::<20, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::home_le_inner(locator.as_ptr(), locator.len(), ptr, len) },
-            20,
-            "home_le_inner",
-        );
-    });
-    with_buffer::<20, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::le_inner(1, locator.as_ptr(), locator.len(), ptr, len) },
-            20,
-            "le_inner",
-        );
-    });
-    check_result(
-        unsafe { host::tx_arr_len(sfield::Memos.into()) },
-        32,
-        "tx_arr_len",
-    );
-    check_result(
-        unsafe { host::home_le_arr_len(sfield::Memos.into()) },
-        32,
-        "home_le_arr_len",
-    );
-    check_result(
-        unsafe { host::le_arr_len(1, sfield::Memos.into()) },
-        32,
-        "le_arr_len",
-    );
-    check_result(
-        unsafe { host::tx_inner_arr_len(locator.as_ptr(), locator.len()) },
-        32,
-        "tx_inner_arr_len",
-    );
-    check_result(
-        unsafe { host::home_le_inner_arr_len(locator.as_ptr(), locator.len()) },
-        32,
-        "home_le_inner_arr_len",
-    );
-    check_result(
-        unsafe { host::le_inner_arr_len(1, locator.as_ptr(), locator.len()) },
-        32,
-        "le_inner_arr_len",
-    );
-    check_result(
-        unsafe { host::set_data(account.0.as_ptr(), account.0.len()) },
-        20,
-        "set_data",
-    );
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::sha512_half(locator.as_ptr(), locator.len(), ptr, len) },
-            32,
-            "sha512_half",
-        );
-    });
-    let message: &[u8] = b"test message";
-    let pubkey: &[u8] = b"test pubkey"; //tx.get_public_key().unwrap_or_panic();
-    let signature: &[u8] = b"test signature";
-    check_result(
-        unsafe {
-            host::check_sig(
-                message.as_ptr(),
-                message.len(),
-                pubkey.as_ptr(),
-                pubkey.len(),
-                signature.as_ptr(),
-                signature.len(),
-            )
-        },
-        1,
-        "check_sig",
-    );
-
-    let nft_id: [u8; 32] = amendment_id;
-    with_buffer::<18, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::nft_uri(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    nft_id.as_ptr(),
-                    nft_id.len(),
-                    ptr,
-                    len,
-                )
-            },
-            18,
-            "nft_uri",
-        )
-    });
-    with_buffer::<20, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::nft_issuer(nft_id.as_ptr(), nft_id.len(), ptr, len) },
-            20,
-            "nft_issuer",
-        )
-    });
-    with_buffer::<4, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::nft_taxon(nft_id.as_ptr(), nft_id.len(), ptr, len) },
-            4,
-            "nft_taxon",
-        )
-    });
-    check_result(
-        unsafe { host::nft_flags(nft_id.as_ptr(), nft_id.len()) },
-        8,
-        "nft_flags",
-    );
-    check_result(
-        unsafe { host::nft_xfer_fee(nft_id.as_ptr(), nft_id.len()) },
-        10,
-        "nft_xfer_fee",
-    );
-    with_buffer::<4, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::nft_serial(nft_id.as_ptr(), nft_id.len(), ptr, len) },
-            4,
-            "nft_serial",
-        )
-    });
-    let message = "testing trace";
-    unsafe {
-        host::trace(
-            message.as_ptr(),
-            message.len(),
-            TraceDataType::Account as i32,
-            account.0.as_ptr(),
-            account.0.len(),
-        )
-    };
-    let amount = &[0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5F]; // 95 drops of XRP
-    unsafe {
-        host::trace(
-            message.as_ptr(),
-            message.len(),
-            TraceDataType::Amount as i32,
-            amount.as_ptr(),
-            amount.len(),
-        )
-    };
-    let amount = &[0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; // 0 drops of XRP
-    unsafe {
-        host::trace(
-            message.as_ptr(),
-            message.len(),
-            TraceDataType::Amount as i32,
-            amount.as_ptr(),
-            amount.len(),
-        )
-    };
-
-    // ########################################
-    // Step #2: Test set_data edge cases
-    // ########################################
-    check_result(
-        unsafe { host_bindings_loose::parent_ldgr_hash(-1, 4) },
-        error_codes::INVALID_PARAMS,
-        "parent_ldgr_hash_neg_ptr",
-    );
-    with_buffer::<4, _, _>(|ptr, _len| {
-        check_result(
-            unsafe { host_bindings_loose::parent_ldgr_hash(ptr as i32, -1) },
-            error_codes::INVALID_PARAMS,
-            "parent_ldgr_hash_neg_len",
-        )
-    });
-    with_buffer::<3, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host_bindings_loose::parent_ldgr_hash(ptr as i32, len as i32) },
-            error_codes::BUFFER_TOO_SMALL,
-            "parent_ldgr_hash_buf_too_small",
-        )
-    });
-    with_buffer::<4, _, _>(|ptr, _len| {
-        check_result(
-            unsafe { host_bindings_loose::parent_ldgr_hash(ptr as i32, 1_000_000_000) },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "parent_ldgr_hash_len_too_long",
-        )
-    });
-
-    // ########################################
-    // Step #3: Test getData[Type] edge cases
-    // ########################################
-
-    // SField
-    check_result(
-        unsafe { host::tx_arr_len(2) }, // not a valid SField value
-        error_codes::INVALID_FIELD,
-        "tx_arr_len_invalid_sfield",
-    );
-
-    // Slice
-    check_result(
-        unsafe { host_bindings_loose::tx_inner_arr_len(-1, locator.len() as i32) },
-        error_codes::INVALID_PARAMS,
-        "tx_inner_arr_len_neg_ptr",
-    );
-    check_result(
-        unsafe { host_bindings_loose::tx_inner_arr_len(locator.as_ptr() as i32, -1) },
-        error_codes::INVALID_PARAMS,
-        "tx_inner_arr_len_neg_len",
-    );
-    let long_len = DEFAULT_BLOB_SIZE + 1;
-    check_result(
-        unsafe { host_bindings_loose::tx_inner_arr_len(locator.as_ptr() as i32, long_len as i32) },
-        error_codes::DATA_FIELD_TOO_LARGE,
-        "tx_inner_arr_len_too_long",
-    );
-    check_result(
-        unsafe {
-            host_bindings_loose::tx_inner_arr_len(
-                locator.as_ptr() as i32 + 1_000_000_000,
-                locator.len() as i32,
-            )
-        },
-        error_codes::POINTER_OUT_OF_BOUNDS,
-        "tx_inner_arr_len_ptr_oob",
-    );
-
-    // uint32
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::check_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    locator.as_ptr().wrapping_add(1_000_000_000),
-                    8,
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "check_id_oob_len_u32",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::check_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "check_id_wrong_len_u32",
-        )
-    });
-
-    // uint64
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::float_from_uint(
-                    locator.as_ptr().wrapping_add(1_000_000_000),
-                    8,
-                    ptr,
-                    len,
-                    FLOAT_ROUNDING_MODES_TO_NEAREST,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "float_from_uint_len_oob",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::float_from_uint(
-                    locator.as_ptr(),
-                    locator.len(),
-                    ptr,
-                    len,
-                    FLOAT_ROUNDING_MODES_TO_NEAREST,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "float_from_uint_wrong_len_uint64",
-        )
-    });
-
-    // uint256
-    check_result(
-        unsafe {
-            host_bindings_loose::cache_le(
-                locator.as_ptr() as i32 + 1_000_000_000,
-                locator.len() as i32,
-                1,
-            )
-        },
-        error_codes::POINTER_OUT_OF_BOUNDS,
-        "cache_le_ptr_oob",
-    );
-    check_result(
-        unsafe { host_bindings_loose::cache_le(locator.as_ptr() as i32, locator.len() as i32, 1) },
-        error_codes::INVALID_PARAMS,
-        "cache_le_wrong_len",
-    );
-
-    // AccountID
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host_bindings_loose::accountroot_id(
-                    locator.as_ptr() as i32 + 1_000_000_000,
-                    locator.len() as i32,
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "accountroot_id_len_oob",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host_bindings_loose::accountroot_id(
-                    locator.as_ptr() as i32,
-                    locator.len() as i32,
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "accountroot_id_wrong_len",
-        )
-    });
-
-    // Currency
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host_bindings_loose::trustline_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    locator.as_ptr() as i32 + 1_000_000_000,
-                    locator.len() as i32,
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "trustline_id_len_oob_currency",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host_bindings_loose::trustline_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    locator.as_ptr() as i32,
-                    locator.len() as i32,
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "trustline_id_wrong_len_currency",
-        )
-    });
-
-    // Issue
-    let asset1_bytes = Issue::XRP(XrpIssue {}).as_bytes();
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::amm_id(
-                    asset1_bytes.as_ptr(),
-                    asset1_bytes.len(),
-                    locator.as_ptr().wrapping_add(1_000_000_000),
-                    locator.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "amm_id_len_oob_asset2",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::amm_id(
-                    asset1_bytes.as_ptr(),
-                    asset1_bytes.len(),
-                    locator.as_ptr(),
-                    locator.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "amm_id_len_wrong_len_asset2",
-        )
-    });
-    let currency: &[u8] = b"USD00000000000000000"; // 20 bytes
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::amm_id(
-                    asset1_bytes.as_ptr(),
-                    asset1_bytes.len(),
-                    currency.as_ptr(),
-                    currency.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "amm_id_len_wrong_non_xrp_currency_len",
-        )
-    });
-    let xrp_issue: &[u8] = &[0; 40]; // 40 bytes
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::amm_id(
-                    xrp_issue.as_ptr(),
-                    xrp_issue.len(),
-                    asset1_bytes.as_ptr(),
-                    asset1_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "amm_id_len_wrong_xrp_currency_len",
-        )
-    });
-    let mptid = MptId::new(1, account);
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::amm_id(
-                    mptid.as_ptr(),
-                    mptid.len(),
-                    asset1_bytes.as_ptr(),
-                    asset1_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "amm_id_mpt",
-        )
-    });
-
-    // Out-of-bounds message pointer; nothing to assert on now that trace is void.
-    let num_bytes = 42i64.to_le_bytes();
-    unsafe {
-        host_bindings_loose::trace_loose(
-            locator.as_ptr() as i32 + 1_000_000_000,
-            locator.len() as i32,
-            TraceDataType::Int64 as i32,
-            num_bytes.as_ptr() as i32,
-            num_bytes.len() as i32,
-        )
-    };
-
-    // ########################################
-    // Step #4: Test other host function edge cases
-    // ########################################
-
-    // invalid SFields
-
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::tx_field(2, ptr, len) },
-            error_codes::INVALID_FIELD,
-            "tx_field_invalid_sfield",
-        );
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::home_le_field(2, ptr, len) },
-            error_codes::INVALID_FIELD,
-            "home_le_field_invalid_sfield",
-        );
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::le_field(1, 2, ptr, len) },
-            error_codes::INVALID_FIELD,
-            "le_field_invalid_sfield",
-        );
-    });
-    check_result(
-        unsafe { host::tx_arr_len(2) },
-        error_codes::INVALID_FIELD,
-        "tx_arr_len_invalid_sfield",
-    );
-    check_result(
-        unsafe { host::home_le_arr_len(2) },
-        error_codes::INVALID_FIELD,
-        "home_le_arr_len_invalid_sfield",
-    );
-    check_result(
-        unsafe { host::le_arr_len(1, 2) },
-        error_codes::INVALID_FIELD,
-        "le_arr_len_invalid_sfield",
-    );
-
-    // invalid Slice
-
-    check_result(
-        unsafe { host::amendment_enabled(amendment_name.as_ptr(), long_len) },
-        error_codes::DATA_FIELD_TOO_LARGE,
-        "amendment_enabled_too_big_slice",
-    );
-    check_result(
-        unsafe { host::amendment_enabled(amendment_name.as_ptr(), 65) },
-        error_codes::DATA_FIELD_TOO_LARGE,
-        "amendment_enabled_too_long",
-    );
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::tx_inner(locator.as_ptr(), long_len, ptr, len) },
-            error_codes::DATA_FIELD_TOO_LARGE,
-            "tx_inner_too_big_slice",
-        );
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::home_le_inner(locator.as_ptr(), long_len, ptr, len) },
-            error_codes::DATA_FIELD_TOO_LARGE,
-            "home_le_inner_too_big_slice",
-        );
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::le_inner(1, locator.as_ptr(), long_len, ptr, len) },
-            error_codes::DATA_FIELD_TOO_LARGE,
-            "le_inner_too_big_slice",
-        );
-    });
-    check_result(
-        unsafe { host::tx_inner_arr_len(locator.as_ptr(), long_len) },
-        error_codes::DATA_FIELD_TOO_LARGE,
-        "tx_inner_arr_len_too_big_slice",
-    );
-    check_result(
-        unsafe { host::home_le_inner_arr_len(locator.as_ptr(), long_len) },
-        error_codes::DATA_FIELD_TOO_LARGE,
-        "home_le_inner_arr_len_too_big_slice",
-    );
-    check_result(
-        unsafe { host::le_inner_arr_len(1, locator.as_ptr(), long_len) },
-        error_codes::DATA_FIELD_TOO_LARGE,
-        "le_inner_arr_len_too_big_slice",
-    );
-    let too_big_data_len = XRPL_CONTRACT_DATA_SIZE + 1;
-    check_result(
-        unsafe { host::set_data(locator.as_ptr(), too_big_data_len) },
-        error_codes::DATA_FIELD_TOO_LARGE,
-        "set_data_too_big_slice",
-    );
-    check_result(
-        unsafe {
-            host::check_sig(
-                message.as_ptr(),
-                long_len,
-                pubkey.as_ptr(),
-                pubkey.len(),
-                signature.as_ptr(),
-                signature.len(),
-            )
-        },
-        error_codes::DATA_FIELD_TOO_LARGE,
-        "check_sig",
-    );
-    check_result(
-        unsafe {
-            host::check_sig(
-                message.as_ptr(),
-                message.len(),
-                pubkey.as_ptr(),
-                long_len,
-                signature.as_ptr(),
-                signature.len(),
-            )
-        },
-        error_codes::DATA_FIELD_TOO_LARGE,
-        "check_sig",
-    );
-    check_result(
-        unsafe {
-            host::check_sig(
-                message.as_ptr(),
-                message.len(),
-                pubkey.as_ptr(),
-                pubkey.len(),
-                signature.as_ptr(),
-                long_len,
-            )
-        },
-        error_codes::DATA_FIELD_TOO_LARGE,
-        "check_sig",
-    );
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::sha512_half(locator.as_ptr(), long_len, ptr, len) },
-            error_codes::DATA_FIELD_TOO_LARGE,
-            "sha512_half_too_big_slice",
-        );
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::amm_id(
-                    asset1_bytes.as_ptr(),
-                    long_len,
-                    asset1_bytes.as_ptr(),
-                    asset1_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::DATA_FIELD_TOO_LARGE,
-            "amm_id_too_big_slice",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::credential_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    locator.as_ptr(),
-                    long_len,
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::DATA_FIELD_TOO_LARGE,
-            "credential_id_too_big_slice",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::mptoken_id(
-                    mptid.as_ptr(),
-                    long_len,
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::DATA_FIELD_TOO_LARGE,
-            "mptoken_id_too_big_slice_mptid",
-        )
-    });
-    unsafe {
-        host::trace(
-            message.as_ptr(),
-            message.len(),
-            TraceDataType::AsText as i32,
-            locator.as_ptr().wrapping_add(1_000_000_000),
-            locator.len(),
-        )
-    };
-    let float: [u8; 8] = [0xD4, 0x83, 0x8D, 0x7E, 0xA4, 0xC6, 0x80, 0x00];
-    unsafe {
-        host::trace(
-            message.as_ptr(),
-            message.len(),
-            TraceDataType::Xfloat as i32,
-            float.as_ptr().wrapping_add(1_000_000_000),
-            float.len(),
-        )
-    };
-    unsafe {
-        host::trace(
-            message.as_ptr(),
-            message.len(),
-            TraceDataType::Amount as i32,
-            locator.as_ptr().wrapping_add(1_000_000_000),
-            locator.len(),
-        )
-    };
-    check_result(
-        unsafe {
-            host::float_cmp(
-                float.as_ptr().wrapping_add(1_000_000_000),
-                float.len(),
-                float.as_ptr(),
-                float.len(),
-            )
-        },
-        error_codes::POINTER_OUT_OF_BOUNDS,
-        "float_cmp_oob_slice1",
-    );
-    check_result(
-        unsafe {
-            host::float_cmp(
-                float.as_ptr(),
-                float.len(),
-                float.as_ptr().wrapping_add(1_000_000_000),
-                float.len(),
-            )
-        },
-        error_codes::POINTER_OUT_OF_BOUNDS,
-        "float_cmp_oob_slice2",
-    );
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::float_add(
-                    float.as_ptr().wrapping_add(1_000_000_000),
-                    float.len(),
-                    float.as_ptr(),
-                    float.len(),
-                    ptr,
-                    len,
-                    FLOAT_ROUNDING_MODES_TO_NEAREST,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "float_add_oob_slice1",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::float_add(
-                    float.as_ptr(),
-                    float.len(),
-                    float.as_ptr().wrapping_add(1_000_000_000),
-                    float.len(),
-                    ptr,
-                    len,
-                    FLOAT_ROUNDING_MODES_TO_NEAREST,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "float_add_oob_slice2",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::float_sub(
-                    float.as_ptr().wrapping_add(1_000_000_000),
-                    float.len(),
-                    float.as_ptr(),
-                    float.len(),
-                    ptr,
-                    len,
-                    FLOAT_ROUNDING_MODES_TO_NEAREST,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "float_sub_oob_slice1",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::float_sub(
-                    float.as_ptr(),
-                    float.len(),
-                    float.as_ptr().wrapping_add(1_000_000_000),
-                    float.len(),
-                    ptr,
-                    len,
-                    FLOAT_ROUNDING_MODES_TO_NEAREST,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "float_sub_oob_slice2",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::float_mult(
-                    float.as_ptr().wrapping_add(1_000_000_000),
-                    float.len(),
-                    float.as_ptr(),
-                    float.len(),
-                    ptr,
-                    len,
-                    FLOAT_ROUNDING_MODES_TO_NEAREST,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "float_mult_oob_slice1",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::float_mult(
-                    float.as_ptr(),
-                    float.len(),
-                    float.as_ptr().wrapping_add(1_000_000_000),
-                    float.len(),
-                    ptr,
-                    len,
-                    FLOAT_ROUNDING_MODES_TO_NEAREST,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "float_mult_oob_slice2",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::float_div(
-                    float.as_ptr().wrapping_add(1_000_000_000),
-                    float.len(),
-                    float.as_ptr(),
-                    float.len(),
-                    ptr,
-                    len,
-                    FLOAT_ROUNDING_MODES_TO_NEAREST,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "float_div_oob_slice1",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::float_div(
-                    float.as_ptr(),
-                    float.len(),
-                    float.as_ptr().wrapping_add(1_000_000_000),
-                    float.len(),
-                    ptr,
-                    len,
-                    FLOAT_ROUNDING_MODES_TO_NEAREST,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "float_div_oob_slice2",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::float_root(
-                    float.as_ptr().wrapping_add(1_000_000_000),
-                    float.len(),
-                    3,
-                    ptr,
-                    len,
-                    FLOAT_ROUNDING_MODES_TO_NEAREST,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "float_root_oob_slice",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::float_pow(
-                    float.as_ptr().wrapping_add(1_000_000_000),
-                    float.len(),
-                    3,
-                    ptr,
-                    len,
-                    FLOAT_ROUNDING_MODES_TO_NEAREST,
-                )
-            },
-            error_codes::POINTER_OUT_OF_BOUNDS,
-            "float_pow_oob_slice",
-        )
-    });
-
-    // invalid UInt32
-
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::escrow_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "escrow_id_wrong_size_uint32",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::mpt_issuance_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "mpt_issuance_id_wrong_size_uint32",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::nft_offer_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "nft_offer_id_wrong_size_uint32",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::offer_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "offer_id_wrong_size_uint32",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::oracle_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "oracle_id_wrong_size_uint32",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::paychan_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "paychan_id_wrong_size_uint32",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::permissioned_domain_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "permissioned_domain_id_wrong_size_uint32",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::ticket_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "ticket_id_wrong_size_uint32",
-        )
-    });
-    with_buffer::<32, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::vault_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "vault_id_wrong_size_uint32",
-        )
-    });
-
-    // invalid UInt256
-
-    check_result(
-        unsafe { host::cache_le(locator.as_ptr(), locator.len(), 0) },
-        error_codes::INVALID_PARAMS,
-        "cache_le_wrong_size_uint256",
-    );
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::nft_uri(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    locator.as_ptr(),
-                    locator.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "nft_uri_wrong_size_uint256",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::nft_issuer(locator.as_ptr(), locator.len(), ptr, len) },
-            error_codes::INVALID_PARAMS,
-            "nft_issuer_wrong_size_uint256",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::nft_taxon(locator.as_ptr(), locator.len(), ptr, len) },
-            error_codes::INVALID_PARAMS,
-            "nft_taxon_wrong_size_uint256",
-        )
-    });
-    check_result(
-        unsafe { host::nft_flags(locator.as_ptr(), locator.len()) },
-        error_codes::INVALID_PARAMS,
-        "nft_flags_wrong_size_uint256",
-    );
-    check_result(
-        unsafe { host::nft_xfer_fee(locator.as_ptr(), locator.len()) },
-        error_codes::INVALID_PARAMS,
-        "nft_xfer_fee_wrong_size_uint256",
-    );
-    with_buffer::<4, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::nft_serial(locator.as_ptr(), locator.len(), ptr, len) },
-            error_codes::INVALID_PARAMS,
-            "nft_serial_wrong_size_uint256",
-        )
-    });
-
-    // invalid AccountID
-
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::accountroot_id(locator.as_ptr(), locator.len(), ptr, len) },
-            error_codes::INVALID_PARAMS,
-            "accountroot_id_wrong_size_account_id",
-        )
-    });
-    let seq: i32 = 1;
-    let seq_bytes = seq.to_be_bytes();
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::check_id(
-                    locator.as_ptr(),
-                    locator.len(),
-                    seq_bytes.as_ptr(),
-                    seq_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "check_id_wrong_size_account_id",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::credential_id(
-                    locator.as_ptr(), // invalid AccountID size
-                    locator.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    locator.as_ptr(), // valid slice size
-                    locator.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "credential_id_wrong_size_account_id1",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::credential_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    locator.as_ptr(), // invalid AccountID size
-                    locator.len(),
-                    locator.as_ptr(), // valid slice size
-                    locator.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "credential_id_wrong_size_account_id2",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::delegate_id(
-                    locator.as_ptr(), // invalid AccountID size
-                    locator.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "delegate_id_wrong_size_account_id1",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::delegate_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    locator.as_ptr(), // invalid AccountID size
-                    locator.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "delegate_id_wrong_size_account_id2",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::deposit_preauth_id(
-                    locator.as_ptr(), // invalid AccountID size
-                    locator.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "deposit_preauth_id_wrong_size_account_id1",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::deposit_preauth_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    locator.as_ptr(), // invalid AccountID size
-                    locator.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "deposit_preauth_id_wrong_size_account_id2",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::did_id(locator.as_ptr(), locator.len(), ptr, len) },
-            error_codes::INVALID_PARAMS,
-            "did_id_wrong_size_account_id",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::escrow_id(
-                    locator.as_ptr(),
-                    locator.len(),
-                    seq_bytes.as_ptr(),
-                    seq_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "escrow_id_wrong_size_account_id",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::trustline_id(
-                    locator.as_ptr(), // invalid AccountID size
-                    locator.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    currency.as_ptr(),
-                    currency.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "trustline_id_wrong_size_account_id1",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::trustline_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    locator.as_ptr(), // invalid AccountID size
-                    locator.len(),
-                    currency.as_ptr(),
-                    currency.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "trustline_id_wrong_size_account_id2",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::mpt_issuance_id(
-                    locator.as_ptr(),
-                    locator.len(),
-                    seq_bytes.as_ptr(),
-                    seq_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "mpt_issuance_id_wrong_size_account_id",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::mptoken_id(
-                    mptid.as_ptr(),
-                    mptid.len(),
-                    locator.as_ptr(),
-                    locator.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "mptoken_id_wrong_size_account_id",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::nft_offer_id(
-                    locator.as_ptr(),
-                    locator.len(),
-                    seq_bytes.as_ptr(),
-                    seq_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "nft_offer_id_wrong_size_account_id",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::offer_id(
-                    locator.as_ptr(),
-                    locator.len(),
-                    seq_bytes.as_ptr(),
-                    seq_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "offer_id_wrong_size_account_id",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::oracle_id(
-                    locator.as_ptr(),
-                    locator.len(),
-                    seq_bytes.as_ptr(),
-                    seq_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "oracle_id_wrong_size_account_id",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::paychan_id(
-                    locator.as_ptr(), // invalid AccountID size
-                    locator.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    seq_bytes.as_ptr(),
-                    seq_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "paychan_id_wrong_size_account_id1",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::paychan_id(
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    locator.as_ptr(), // invalid AccountID size
-                    locator.len(),
-                    seq_bytes.as_ptr(),
-                    seq_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "paychan_id_wrong_size_account_id2",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::permissioned_domain_id(
-                    locator.as_ptr(),
-                    locator.len(),
-                    seq_bytes.as_ptr(),
-                    seq_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "permissioned_domain_id_wrong_size_account_id",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe { host::signers_id(locator.as_ptr(), locator.len(), ptr, len) },
-            error_codes::INVALID_PARAMS,
-            "signers_id_wrong_size_account_id",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::ticket_id(
-                    locator.as_ptr(),
-                    locator.len(),
-                    seq_bytes.as_ptr(),
-                    seq_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "ticket_id_wrong_size_account_id",
-        )
-    });
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::vault_id(
-                    locator.as_ptr(),
-                    locator.len(),
-                    seq_bytes.as_ptr(),
-                    seq_bytes.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "vault_id_wrong_size_account_id",
-        )
-    });
-    let uint256: &[u8] = b"00000000000000000000000000000001";
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::nft_uri(
-                    locator.as_ptr(),
-                    locator.len(),
-                    uint256.as_ptr(),
-                    uint256.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "nft_uri_wrong_size_account_id",
-        )
-    });
-    unsafe {
-        host::trace(
-            message.as_ptr(),
-            message.len(),
-            TraceDataType::Account as i32,
-            locator.as_ptr(),
-            locator.len(),
-        )
-    };
-
-    // invalid Currency was already tested above
-    // invalid string
-
-    unsafe {
-        host::trace(
-            message.as_ptr().wrapping_add(1_000_000_000),
-            message.len(),
-            TraceDataType::AsText as i32,
-            uint256.as_ptr(),
-            uint256.len(),
-        )
-    };
-    unsafe {
-        host::trace(
-            message.as_ptr().wrapping_add(1_000_000_000),
-            message.len(),
-            TraceDataType::Xfloat as i32,
-            float.as_ptr(),
-            float.len(),
-        )
-    };
-    unsafe {
-        host::trace(
-            message.as_ptr().wrapping_add(1_000_000_000),
-            message.len(),
-            TraceDataType::Account as i32,
-            account.0.as_ptr(),
-            account.0.len(),
-        )
-    };
-    unsafe {
-        host::trace(
-            message.as_ptr().wrapping_add(1_000_000_000),
-            message.len(),
-            TraceDataType::Amount as i32,
-            amount.as_ptr(),
-            amount.len(),
-        )
-    };
-
-    // trace too large
-
-    unsafe {
-        host::trace(
-            locator.as_ptr(),
-            locator.len(),
-            TraceDataType::AsText as i32,
-            locator.as_ptr(),
-            long_len,
-        )
-    };
-    let too_long_num = 1i64.to_le_bytes();
-    unsafe {
-        host::trace(
-            locator.as_ptr(),
-            long_len,
-            TraceDataType::Int64 as i32,
-            too_long_num.as_ptr(),
-            too_long_num.len(),
-        )
-    };
-    unsafe {
-        host::trace(
-            message.as_ptr(),
-            long_len,
-            TraceDataType::Xfloat as i32,
-            float.as_ptr(),
-            float.len(),
-        )
-    };
-    unsafe {
-        host::trace(
-            message.as_ptr(),
-            long_len,
-            TraceDataType::Account as i32,
-            account.0.as_ptr(),
-            account.0.len(),
-        )
-    };
-    unsafe {
-        host::trace(
-            message.as_ptr(),
-            long_len,
-            TraceDataType::Amount as i32,
-            amount.as_ptr(),
-            amount.len(),
-        )
-    };
-
-    // trace amount errors
-
-    unsafe {
-        host::trace(
-            message.as_ptr(),
-            message.len(),
-            TraceDataType::Amount as i32,
-            locator.as_ptr(),
-            locator.len(),
-        )
-    };
-
-    // other misc errors
-
-    with_buffer::<2, _, _>(|ptr, len| {
-        check_result(
-            unsafe {
-                host::mptoken_id(
-                    locator.as_ptr(),
-                    locator.len(),
-                    account.0.as_ptr(),
-                    account.0.len(),
-                    ptr,
-                    len,
-                )
-            },
-            error_codes::INVALID_PARAMS,
-            "mptoken_id_mptid_wrong_length",
-        )
-    });
-    // Unknown data_type: the host logs "invalid arguments" and returns.
-    unsafe {
-        host::trace(
-            message.as_ptr(),
-            message.len(),
-            99,
-            locator.as_ptr(),
-            locator.len(),
-        )
-    };
-
-    // ensure that the Slice index desync issue is fixed
-    let empty: &[u8] = b"";
-    unsafe {
-        host::trace(
-            empty.as_ptr(),
-            empty.len(),
-            TraceDataType::Account as i32,
-            account.0.as_ptr(),
-            account.0.len(),
-        )
-    };
-
-    1 // <-- If we get here, finish the escrow.
-}
diff --git a/src/test/app/wasm_fixtures/copyFixtures.py b/src/test/app/wasm_fixtures/copyFixtures.py
deleted file mode 100644
index 23d116cbef..0000000000
--- a/src/test/app/wasm_fixtures/copyFixtures.py
+++ /dev/null
@@ -1,302 +0,0 @@
-# cspell: disable
-import os
-import re
-import shlex
-import subprocess
-import sys
-import tempfile
-import zipfile
-from difflib import get_close_matches
-
-OPT = "-Oz"
-BASE_PATH = os.path.abspath(os.path.dirname(__file__))
-
-
-def pascal_case(name):
-    return "".join(word[:1].upper() + word[1:] for word in re.split(r"[_\W]+", name))
-
-
-def normalize_name(name):
-    name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name)
-    return re.sub(r"[^a-z0-9]", "", name.lower())
-
-
-def fixture_key(name):
-    name = normalize_name(name).removeprefix("k")
-    return name.removesuffix("wasmhex").removesuffix("hex")
-
-
-def declared_fixtures():
-    h_path = os.path.join(BASE_PATH, "fixtures.h")
-    with open(h_path, "r", encoding="utf8") as f:
-        return re.findall(
-            r"extern std::string const ([A-Za-z_][A-Za-z0-9_]*);", f.read()
-        )
-
-
-def find_fixture_name(project_name, suffix):
-    default = re.sub(r"_([a-z])", lambda m: m.group(1).upper(), project_name) + suffix
-    k_default = f"k{pascal_case(project_name)}{suffix}"
-    declarations = declared_fixtures()
-    normalized = {normalize_name(name): name for name in declarations}
-    fixture_keys = {fixture_key(name): name for name in declarations}
-
-    for name in (default, k_default):
-        if normalize_name(name) in normalized:
-            return normalized[normalize_name(name)]
-
-    project_key = normalize_name(project_name)
-    matches = [
-        name
-        for key, name in fixture_keys.items()
-        if key.endswith(project_key)
-        or key.startswith(project_key)
-        or project_key.endswith(key)
-        or project_key.startswith(key)
-    ]
-    if len(matches) == 1:
-        return matches[0]
-
-    close = get_close_matches(project_key, fixture_keys.keys(), n=1, cutoff=0.82)
-    if close:
-        return fixture_keys[close[0]]
-
-    return k_default
-
-
-def fixture_cpp_path(fixture_name):
-    pattern = rf"extern std::string const {fixture_name} ="
-    for file_name in os.listdir(BASE_PATH):
-        if not file_name.endswith(".cpp"):
-            continue
-        cpp_path = os.path.join(BASE_PATH, file_name)
-        with open(cpp_path, "r", encoding="utf8") as f:
-            if re.search(pattern, f.read()):
-                return cpp_path
-    return os.path.join(BASE_PATH, "fixtures.cpp")
-
-
-def update_fixture(project_name, wasm, suffix="WasmHex"):
-    fixture_name = find_fixture_name(project_name, suffix)
-    print(f"Updating fixture: {fixture_name}")
-
-    cpp_path = fixture_cpp_path(fixture_name)
-    h_path = os.path.join(BASE_PATH, "fixtures.h")
-    with open(cpp_path, "r", encoding="utf8") as f:
-        cpp_content = f.read()
-
-    pattern = rf'extern std::string const {fixture_name} =[ \n]+"[^;]*;'
-    if re.search(pattern, cpp_content, flags=re.MULTILINE):
-        updated_cpp_content = re.sub(
-            pattern,
-            f'extern std::string const {fixture_name} = "{wasm}";',
-            cpp_content,
-            flags=re.MULTILINE,
-        )
-    else:
-        with open(h_path, "r", encoding="utf8") as f:
-            h_content = f.read()
-        updated_h_content = (
-            h_content.rstrip() + f"\n\nextern std::string const {fixture_name};\n"
-        )
-        with open(h_path, "w", encoding="utf8") as f:
-            f.write(updated_h_content)
-        updated_cpp_content = (
-            cpp_content.rstrip()
-            + f'\n\nextern std::string const {fixture_name} = "{wasm}";\n'
-        )
-
-    with open(cpp_path, "w", encoding="utf8") as f:
-        f.write(updated_cpp_content)
-
-
-def read_wasm_hex(path):
-    with open(path, "rb") as f:
-        return f.read().hex()
-
-
-def process_rust(project_name):
-    project_path = os.path.join(BASE_PATH, project_name)
-    wasm_location = os.path.join(
-        project_path, "target", "wasm32v1-none", "release", f"{project_name}.wasm"
-    )
-    try:
-        subprocess.run(
-            ["cargo", "build", "--target", "wasm32v1-none", "--release"],
-            cwd=project_path,
-            check=True,
-        )
-        subprocess.run(
-            ["wasm-opt", wasm_location, OPT, "-o", wasm_location], check=True
-        )
-        print(f"WASM file for {project_name} has been built and optimized.")
-    except FileNotFoundError as e:
-        print(f"exec error: {e.filename} is required to build Rust fixtures")
-        sys.exit(1)
-    except subprocess.CalledProcessError as e:
-        print(f"exec error: {e}")
-        sys.exit(1)
-
-    update_fixture(project_name, read_wasm_hex(wasm_location))
-
-
-def process_c(project_name):
-    project_path = os.path.join(BASE_PATH, f"{project_name}.c")
-    wasm_path = os.path.join(BASE_PATH, f"{project_name}.wasm")
-    cc = os.environ.get("CC")
-    sysroot = os.environ.get("SYSROOT")
-    if not cc or not sysroot:
-        print("exec error: CC and SYSROOT are required to build C fixtures")
-        sys.exit(1)
-
-    build_cmd = [
-        *shlex.split(cc),
-        f"--sysroot={sysroot}",
-        "-O3",
-        "-ffast-math",
-        "--target=wasm32",
-        "-fno-exceptions",
-        "-fno-threadsafe-statics",
-        "-fvisibility=default",
-        "-Wl,--export-all",
-        "-Wl,--no-entry",
-        "-Wl,--allow-undefined",
-        "-DNDEBUG",
-        "--no-standard-libraries",
-        "-fno-builtin-memset",
-        "-o",
-        wasm_path,
-        project_path,
-    ]
-    try:
-        subprocess.run(build_cmd, check=True)
-        subprocess.run(["wasm-opt", wasm_path, OPT, "-o", wasm_path], check=True)
-        print(
-            f"WASM file for {project_name} has been built with WASI support using clang."
-        )
-    except FileNotFoundError as e:
-        print(f"exec error: {e.filename} is required to build C fixtures")
-        sys.exit(1)
-    except subprocess.CalledProcessError as e:
-        print(f"exec error: {e}")
-        sys.exit(1)
-
-    update_fixture(project_name, read_wasm_hex(wasm_path))
-
-
-def wat_to_wasm(wat_path, wasm_path):
-    build_cmd = ["wat2wasm", "--enable-all", wat_path, "-o", wasm_path]
-    try:
-        subprocess.run(build_cmd, check=True)
-        print(f"WASM file for {os.path.basename(wat_path)} has been built.")
-        return
-    except FileNotFoundError:
-        print("exec error: wat2wasm is required to build WAT fixtures")
-        sys.exit(1)
-    except subprocess.CalledProcessError:
-        # wat2wasm (wabt) does not support some proposal text syntax such as
-        # the GC instructions, so fall back to wasm-tools which does.
-        pass
-
-    fallback_cmd = ["wasm-tools", "parse", wat_path, "-o", wasm_path]
-    try:
-        subprocess.run(fallback_cmd, check=True)
-        print(
-            f"WASM file for {os.path.basename(wat_path)} has been built with wasm-tools."
-        )
-    except FileNotFoundError:
-        print("exec error: wasm-tools is required to build this WAT fixture")
-        sys.exit(1)
-    except subprocess.CalledProcessError as e:
-        print(f"exec error: {e}")
-        sys.exit(1)
-
-
-def process_wat_file(wat_path):
-    project_name = os.path.splitext(os.path.basename(wat_path))[0]
-    with open(wat_path, "r", encoding="utf8") as f:
-        if "(module" not in f.read():
-            print(f"Skipping WAT fixture without a module: {project_name}")
-            return
-
-    with tempfile.TemporaryDirectory() as tmpdir:
-        wasm_path = os.path.join(tmpdir, f"{project_name}.wasm")
-        wat_to_wasm(wat_path, wasm_path)
-        update_fixture(project_name, read_wasm_hex(wasm_path), "Hex")
-
-
-def process_wat_zip(zip_path):
-    project_name = os.path.splitext(os.path.basename(zip_path))[0]
-    with tempfile.TemporaryDirectory() as tmpdir:
-        with zipfile.ZipFile(zip_path) as archive:
-            wat_names = [name for name in archive.namelist() if name.endswith(".wat")]
-            if len(wat_names) != 1:
-                print(f"exec error: expected one .wat file in {zip_path}")
-                sys.exit(1)
-            archive.extract(wat_names[0], tmpdir)
-
-        wasm_path = os.path.join(tmpdir, f"{project_name}.wasm")
-        wat_to_wasm(os.path.join(tmpdir, wat_names[0]), wasm_path)
-        update_fixture(project_name, read_wasm_hex(wasm_path), "Hex")
-
-
-def process_wat(project_name):
-    candidates = [
-        os.path.join(BASE_PATH, f"{project_name}.wat"),
-        os.path.join(BASE_PATH, "wat", f"{project_name}.wat"),
-        os.path.join(BASE_PATH, "wat", f"{project_name}.zip"),
-    ]
-    for path in candidates:
-        if os.path.isfile(path):
-            if path.endswith(".zip"):
-                process_wat_zip(path)
-            else:
-                process_wat_file(path)
-            return
-
-    print(f"exec error: fixture {project_name} not found")
-    sys.exit(1)
-
-
-if __name__ == "__main__":
-    if len(sys.argv) > 2:
-        print("Usage: python copyFixtures.py []")
-        sys.exit(1)
-
-    if len(sys.argv) == 2:
-        project_name = os.path.splitext(os.path.basename(sys.argv[1]))[0]
-        if os.path.isfile(os.path.join(BASE_PATH, project_name, "Cargo.toml")):
-            process_rust(project_name)
-        elif os.path.isfile(os.path.join(BASE_PATH, f"{project_name}.c")):
-            process_c(project_name)
-        else:
-            process_wat(project_name)
-        print("Fixture has been processed.")
-    else:
-        dirs = [
-            d
-            for d in os.listdir(BASE_PATH)
-            if os.path.isfile(os.path.join(BASE_PATH, d, "Cargo.toml"))
-        ]
-        c_files = [f for f in os.listdir(BASE_PATH) if f.endswith(".c")]
-        wat_files = [f for f in os.listdir(BASE_PATH) if f.endswith(".wat")]
-        wat_path = os.path.join(BASE_PATH, "wat")
-        wat_fixture_files = [
-            f
-            for f in (os.listdir(wat_path) if os.path.isdir(wat_path) else [])
-            if f.endswith((".wat", ".zip"))
-        ]
-
-        for d in sorted(dirs):
-            process_rust(d)
-        for c in sorted(c_files):
-            process_c(c[:-2])
-        for wat in sorted(wat_files):
-            process_wat_file(os.path.join(BASE_PATH, wat))
-        for wat_fixture in sorted(wat_fixture_files):
-            path = os.path.join(wat_path, wat_fixture)
-            if wat_fixture.endswith(".zip"):
-                process_wat_zip(path)
-            else:
-                process_wat_file(path)
-        print("All fixtures have been processed.")
diff --git a/src/test/app/wasm_fixtures/disableFloat.wat b/src/test/app/wasm_fixtures/disableFloat.wat
deleted file mode 100644
index 5e09371ee9..0000000000
--- a/src/test/app/wasm_fixtures/disableFloat.wat
+++ /dev/null
@@ -1,34 +0,0 @@
-(module
-  (type (;0;) (func))
-  (type (;1;) (func (result i32)))
-  (func (;0;) (type 0))
-  (func (;1;) (type 1) (result i32)
-    f32.const -2048
-    f32.const 2050
-    f32.sub
-    drop
-    i32.const 1)
-  (memory (;0;) 2)
-  (global (;0;) i32 (i32.const 1024))
-  (global (;1;) i32 (i32.const 1024))
-  (global (;2;) i32 (i32.const 2048))
-  (global (;3;) i32 (i32.const 2048))
-  (global (;4;) i32 (i32.const 67584))
-  (global (;5;) i32 (i32.const 1024))
-  (global (;6;) i32 (i32.const 67584))
-  (global (;7;) i32 (i32.const 131072))
-  (global (;8;) i32 (i32.const 0))
-  (global (;9;) i32 (i32.const 1))
-  (export "memory" (memory 0))
-  (export "__wasm_call_ctors" (func 0))
-  (export "escrow_finish" (func 1))
-  (export "buf" (global 0))
-  (export "__dso_handle" (global 1))
-  (export "__data_end" (global 2))
-  (export "__stack_low" (global 3))
-  (export "__stack_high" (global 4))
-  (export "__global_base" (global 5))
-  (export "__heap_base" (global 6))
-  (export "__heap_end" (global 7))
-  (export "__memory_base" (global 8))
-  (export "__table_base" (global 9)))
diff --git a/src/test/app/wasm_fixtures/fib.c b/src/test/app/wasm_fixtures/fib.c
deleted file mode 100644
index e45cc4fe6c..0000000000
--- a/src/test/app/wasm_fixtures/fib.c
+++ /dev/null
@@ -1,11 +0,0 @@
-// typedef long long mint;
-typedef int mint;
-
-mint fib(mint n)
-{
-  if (!n)
-    return 0;
-  if (n <= 2)
-    return 1;
-  return fib(n - 1) + fib(n - 2);
-}
diff --git a/src/test/app/wasm_fixtures/fixture_functions_5k.cpp b/src/test/app/wasm_fixtures/fixture_functions_5k.cpp
deleted file mode 100644
index d65f602eec..0000000000
--- a/src/test/app/wasm_fixtures/fixture_functions_5k.cpp
+++ /dev/null
@@ -1,2240 +0,0 @@
-// TODO: consider moving these to separate files (and figure out the build)
-
-#include 
-
-#include 
-
-extern std::string const kFunctions5kHex =
-    "0061736d0100000001070160027f7f017f038a27882700000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000000000000000000000000000007e2d303882708"
-    "7465737430303030000008746573743030303100010874657374303030320002087465737430303033000308746573"
-    "7430303034000408746573743030303500050874657374303030360006087465737430303037000708746573743030"
-    "303800080874657374303030390009087465737430303130000a087465737430303131000b08746573743030313200"
-    "0c087465737430303133000d087465737430303134000e087465737430303135000f08746573743030313600100874"
-    "6573743030313700110874657374303031380012087465737430303139001308746573743030323000140874657374"
-    "3030323100150874657374303032320016087465737430303233001708746573743030323400180874657374303032"
-    "350019087465737430303236001a087465737430303237001b087465737430303238001c087465737430303239001d"
-    "087465737430303330001e087465737430303331001f08746573743030333200200874657374303033330021087465"
-    "7374303033340022087465737430303335002308746573743030333600240874657374303033370025087465737430"
-    "3033380026087465737430303339002708746573743030343000280874657374303034310029087465737430303432"
-    "002a087465737430303433002b087465737430303434002c087465737430303435002d087465737430303436002e08"
-    "7465737430303437002f08746573743030343800300874657374303034390031087465737430303530003208746573"
-    "7430303531003308746573743030353200340874657374303035330035087465737430303534003608746573743030"
-    "3535003708746573743030353600380874657374303035370039087465737430303538003a08746573743030353900"
-    "3b087465737430303630003c087465737430303631003d087465737430303632003e087465737430303633003f0874"
-    "6573743030363400400874657374303036350041087465737430303636004208746573743030363700430874657374"
-    "3030363800440874657374303036390045087465737430303730004608746573743030373100470874657374303037"
-    "3200480874657374303037330049087465737430303734004a087465737430303735004b087465737430303736004c"
-    "087465737430303737004d087465737430303738004e087465737430303739004f0874657374303038300050087465"
-    "7374303038310051087465737430303832005208746573743030383300530874657374303038340054087465737430"
-    "3038350055087465737430303836005608746573743030383700570874657374303038380058087465737430303839"
-    "0059087465737430303930005a087465737430303931005b087465737430303932005c087465737430303933005d08"
-    "7465737430303934005e087465737430303935005f0874657374303039360060087465737430303937006108746573"
-    "7430303938006208746573743030393900630874657374303130300064087465737430313031006508746573743031"
-    "3032006608746573743031303300670874657374303130340068087465737430313035006908746573743031303600"
-    "6a087465737430313037006b087465737430313038006c087465737430313039006d087465737430313130006e0874"
-    "65737430313131006f0874657374303131320070087465737430313133007108746573743031313400720874657374"
-    "3031313500730874657374303131360074087465737430313137007508746573743031313800760874657374303131"
-    "39007708746573743031323000780874657374303132310079087465737430313232007a087465737430313233007b"
-    "087465737430313234007c087465737430313235007d087465737430313236007e087465737430313237007f087465"
-    "7374303132380080010874657374303132390081010874657374303133300082010874657374303133310083010874"
-    "6573743031333200840108746573743031333300850108746573743031333400860108746573743031333500870108"
-    "7465737430313336008801087465737430313337008901087465737430313338008a01087465737430313339008b01"
-    "087465737430313430008c01087465737430313431008d01087465737430313432008e01087465737430313433008f"
-    "0108746573743031343400900108746573743031343500910108746573743031343600920108746573743031343700"
-    "9301087465737430313438009401087465737430313439009501087465737430313530009601087465737430313531"
-    "009701087465737430313532009801087465737430313533009901087465737430313534009a010874657374303135"
-    "35009b01087465737430313536009c01087465737430313537009d01087465737430313538009e0108746573743031"
-    "3539009f0108746573743031363000a00108746573743031363100a10108746573743031363200a201087465737430"
-    "31363300a30108746573743031363400a40108746573743031363500a50108746573743031363600a6010874657374"
-    "3031363700a70108746573743031363800a80108746573743031363900a90108746573743031373000aa0108746573"
-    "743031373100ab0108746573743031373200ac0108746573743031373300ad0108746573743031373400ae01087465"
-    "73743031373500af0108746573743031373600b00108746573743031373700b10108746573743031373800b2010874"
-    "6573743031373900b30108746573743031383000b40108746573743031383100b50108746573743031383200b60108"
-    "746573743031383300b70108746573743031383400b80108746573743031383500b90108746573743031383600ba01"
-    "08746573743031383700bb0108746573743031383800bc0108746573743031383900bd0108746573743031393000be"
-    "0108746573743031393100bf0108746573743031393200c00108746573743031393300c10108746573743031393400"
-    "c20108746573743031393500c30108746573743031393600c40108746573743031393700c501087465737430313938"
-    "00c60108746573743031393900c70108746573743032303000c80108746573743032303100c9010874657374303230"
-    "3200ca0108746573743032303300cb0108746573743032303400cc0108746573743032303500cd0108746573743032"
-    "303600ce0108746573743032303700cf0108746573743032303800d00108746573743032303900d101087465737430"
-    "32313000d20108746573743032313100d30108746573743032313200d40108746573743032313300d5010874657374"
-    "3032313400d60108746573743032313500d70108746573743032313600d80108746573743032313700d90108746573"
-    "743032313800da0108746573743032313900db0108746573743032323000dc0108746573743032323100dd01087465"
-    "73743032323200de0108746573743032323300df0108746573743032323400e00108746573743032323500e1010874"
-    "6573743032323600e20108746573743032323700e30108746573743032323800e40108746573743032323900e50108"
-    "746573743032333000e60108746573743032333100e70108746573743032333200e80108746573743032333300e901"
-    "08746573743032333400ea0108746573743032333500eb0108746573743032333600ec0108746573743032333700ed"
-    "0108746573743032333800ee0108746573743032333900ef0108746573743032343000f00108746573743032343100"
-    "f10108746573743032343200f20108746573743032343300f30108746573743032343400f401087465737430323435"
-    "00f50108746573743032343600f60108746573743032343700f70108746573743032343800f8010874657374303234"
-    "3900f90108746573743032353000fa0108746573743032353100fb0108746573743032353200fc0108746573743032"
-    "353300fd0108746573743032353400fe0108746573743032353500ff01087465737430323536008002087465737430"
-    "3235370081020874657374303235380082020874657374303235390083020874657374303236300084020874657374"
-    "3032363100850208746573743032363200860208746573743032363300870208746573743032363400880208746573"
-    "7430323635008902087465737430323636008a02087465737430323637008b02087465737430323638008c02087465"
-    "737430323639008d02087465737430323730008e02087465737430323731008f020874657374303237320090020874"
-    "6573743032373300910208746573743032373400920208746573743032373500930208746573743032373600940208"
-    "7465737430323737009502087465737430323738009602087465737430323739009702087465737430323830009802"
-    "087465737430323831009902087465737430323832009a02087465737430323833009b02087465737430323834009c"
-    "02087465737430323835009d02087465737430323836009e02087465737430323837009f0208746573743032383800"
-    "a00208746573743032383900a10208746573743032393000a20208746573743032393100a302087465737430323932"
-    "00a40208746573743032393300a50208746573743032393400a60208746573743032393500a7020874657374303239"
-    "3600a80208746573743032393700a90208746573743032393800aa0208746573743032393900ab0208746573743033"
-    "303000ac0208746573743033303100ad0208746573743033303200ae0208746573743033303300af02087465737430"
-    "33303400b00208746573743033303500b10208746573743033303600b20208746573743033303700b3020874657374"
-    "3033303800b40208746573743033303900b50208746573743033313000b60208746573743033313100b70208746573"
-    "743033313200b80208746573743033313300b90208746573743033313400ba0208746573743033313500bb02087465"
-    "73743033313600bc0208746573743033313700bd0208746573743033313800be0208746573743033313900bf020874"
-    "6573743033323000c00208746573743033323100c10208746573743033323200c20208746573743033323300c30208"
-    "746573743033323400c40208746573743033323500c50208746573743033323600c60208746573743033323700c702"
-    "08746573743033323800c80208746573743033323900c90208746573743033333000ca0208746573743033333100cb"
-    "0208746573743033333200cc0208746573743033333300cd0208746573743033333400ce0208746573743033333500"
-    "cf0208746573743033333600d00208746573743033333700d10208746573743033333800d202087465737430333339"
-    "00d30208746573743033343000d40208746573743033343100d50208746573743033343200d6020874657374303334"
-    "3300d70208746573743033343400d80208746573743033343500d90208746573743033343600da0208746573743033"
-    "343700db0208746573743033343800dc0208746573743033343900dd0208746573743033353000de02087465737430"
-    "33353100df0208746573743033353200e00208746573743033353300e10208746573743033353400e2020874657374"
-    "3033353500e30208746573743033353600e40208746573743033353700e50208746573743033353800e60208746573"
-    "743033353900e70208746573743033363000e80208746573743033363100e90208746573743033363200ea02087465"
-    "73743033363300eb0208746573743033363400ec0208746573743033363500ed0208746573743033363600ee020874"
-    "6573743033363700ef0208746573743033363800f00208746573743033363900f10208746573743033373000f20208"
-    "746573743033373100f30208746573743033373200f40208746573743033373300f50208746573743033373400f602"
-    "08746573743033373500f70208746573743033373600f80208746573743033373700f90208746573743033373800fa"
-    "0208746573743033373900fb0208746573743033383000fc0208746573743033383100fd0208746573743033383200"
-    "fe0208746573743033383300ff02087465737430333834008003087465737430333835008103087465737430333836"
-    "0082030874657374303338370083030874657374303338380084030874657374303338390085030874657374303339"
-    "3000860308746573743033393100870308746573743033393200880308746573743033393300890308746573743033"
-    "3934008a03087465737430333935008b03087465737430333936008c03087465737430333937008d03087465737430"
-    "333938008e03087465737430333939008f030874657374303430300090030874657374303430310091030874657374"
-    "3034303200920308746573743034303300930308746573743034303400940308746573743034303500950308746573"
-    "7430343036009603087465737430343037009703087465737430343038009803087465737430343039009903087465"
-    "737430343130009a03087465737430343131009b03087465737430343132009c03087465737430343133009d030874"
-    "65737430343134009e03087465737430343135009f0308746573743034313600a00308746573743034313700a10308"
-    "746573743034313800a20308746573743034313900a30308746573743034323000a40308746573743034323100a503"
-    "08746573743034323200a60308746573743034323300a70308746573743034323400a80308746573743034323500a9"
-    "0308746573743034323600aa0308746573743034323700ab0308746573743034323800ac0308746573743034323900"
-    "ad0308746573743034333000ae0308746573743034333100af0308746573743034333200b003087465737430343333"
-    "00b10308746573743034333400b20308746573743034333500b30308746573743034333600b4030874657374303433"
-    "3700b50308746573743034333800b60308746573743034333900b70308746573743034343000b80308746573743034"
-    "343100b90308746573743034343200ba0308746573743034343300bb0308746573743034343400bc03087465737430"
-    "34343500bd0308746573743034343600be0308746573743034343700bf0308746573743034343800c0030874657374"
-    "3034343900c10308746573743034353000c20308746573743034353100c30308746573743034353200c40308746573"
-    "743034353300c50308746573743034353400c60308746573743034353500c70308746573743034353600c803087465"
-    "73743034353700c90308746573743034353800ca0308746573743034353900cb0308746573743034363000cc030874"
-    "6573743034363100cd0308746573743034363200ce0308746573743034363300cf0308746573743034363400d00308"
-    "746573743034363500d10308746573743034363600d20308746573743034363700d30308746573743034363800d403"
-    "08746573743034363900d50308746573743034373000d60308746573743034373100d70308746573743034373200d8"
-    "0308746573743034373300d90308746573743034373400da0308746573743034373500db0308746573743034373600"
-    "dc0308746573743034373700dd0308746573743034373800de0308746573743034373900df03087465737430343830"
-    "00e00308746573743034383100e10308746573743034383200e20308746573743034383300e3030874657374303438"
-    "3400e40308746573743034383500e50308746573743034383600e60308746573743034383700e70308746573743034"
-    "383800e80308746573743034383900e90308746573743034393000ea0308746573743034393100eb03087465737430"
-    "34393200ec0308746573743034393300ed0308746573743034393400ee0308746573743034393500ef030874657374"
-    "3034393600f00308746573743034393700f10308746573743034393800f20308746573743034393900f30308746573"
-    "743035303000f40308746573743035303100f50308746573743035303200f60308746573743035303300f703087465"
-    "73743035303400f80308746573743035303500f90308746573743035303600fa0308746573743035303700fb030874"
-    "6573743035303800fc0308746573743035303900fd0308746573743035313000fe0308746573743035313100ff0308"
-    "7465737430353132008004087465737430353133008104087465737430353134008204087465737430353135008304"
-    "0874657374303531360084040874657374303531370085040874657374303531380086040874657374303531390087"
-    "04087465737430353230008804087465737430353231008904087465737430353232008a0408746573743035323300"
-    "8b04087465737430353234008c04087465737430353235008d04087465737430353236008e04087465737430353237"
-    "008f040874657374303532380090040874657374303532390091040874657374303533300092040874657374303533"
-    "3100930408746573743035333200940408746573743035333300950408746573743035333400960408746573743035"
-    "3335009704087465737430353336009804087465737430353337009904087465737430353338009a04087465737430"
-    "353339009b04087465737430353430009c04087465737430353431009d04087465737430353432009e040874657374"
-    "30353433009f0408746573743035343400a00408746573743035343500a10408746573743035343600a20408746573"
-    "743035343700a30408746573743035343800a40408746573743035343900a50408746573743035353000a604087465"
-    "73743035353100a70408746573743035353200a80408746573743035353300a90408746573743035353400aa040874"
-    "6573743035353500ab0408746573743035353600ac0408746573743035353700ad0408746573743035353800ae0408"
-    "746573743035353900af0408746573743035363000b00408746573743035363100b10408746573743035363200b204"
-    "08746573743035363300b30408746573743035363400b40408746573743035363500b50408746573743035363600b6"
-    "0408746573743035363700b70408746573743035363800b80408746573743035363900b90408746573743035373000"
-    "ba0408746573743035373100bb0408746573743035373200bc0408746573743035373300bd04087465737430353734"
-    "00be0408746573743035373500bf0408746573743035373600c00408746573743035373700c1040874657374303537"
-    "3800c20408746573743035373900c30408746573743035383000c40408746573743035383100c50408746573743035"
-    "383200c60408746573743035383300c70408746573743035383400c80408746573743035383500c904087465737430"
-    "35383600ca0408746573743035383700cb0408746573743035383800cc0408746573743035383900cd040874657374"
-    "3035393000ce0408746573743035393100cf0408746573743035393200d00408746573743035393300d10408746573"
-    "743035393400d20408746573743035393500d30408746573743035393600d40408746573743035393700d504087465"
-    "73743035393800d60408746573743035393900d70408746573743036303000d80408746573743036303100d9040874"
-    "6573743036303200da0408746573743036303300db0408746573743036303400dc0408746573743036303500dd0408"
-    "746573743036303600de0408746573743036303700df0408746573743036303800e00408746573743036303900e104"
-    "08746573743036313000e20408746573743036313100e30408746573743036313200e40408746573743036313300e5"
-    "0408746573743036313400e60408746573743036313500e70408746573743036313600e80408746573743036313700"
-    "e90408746573743036313800ea0408746573743036313900eb0408746573743036323000ec04087465737430363231"
-    "00ed0408746573743036323200ee0408746573743036323300ef0408746573743036323400f0040874657374303632"
-    "3500f10408746573743036323600f20408746573743036323700f30408746573743036323800f40408746573743036"
-    "323900f50408746573743036333000f60408746573743036333100f70408746573743036333200f804087465737430"
-    "36333300f90408746573743036333400fa0408746573743036333500fb0408746573743036333600fc040874657374"
-    "3036333700fd0408746573743036333800fe0408746573743036333900ff0408746573743036343000800508746573"
-    "7430363431008105087465737430363432008205087465737430363433008305087465737430363434008405087465"
-    "7374303634350085050874657374303634360086050874657374303634370087050874657374303634380088050874"
-    "65737430363439008905087465737430363530008a05087465737430363531008b05087465737430363532008c0508"
-    "7465737430363533008d05087465737430363534008e05087465737430363535008f05087465737430363536009005"
-    "0874657374303635370091050874657374303635380092050874657374303635390093050874657374303636300094"
-    "0508746573743036363100950508746573743036363200960508746573743036363300970508746573743036363400"
-    "9805087465737430363635009905087465737430363636009a05087465737430363637009b05087465737430363638"
-    "009c05087465737430363639009d05087465737430363730009e05087465737430363731009f050874657374303637"
-    "3200a00508746573743036373300a10508746573743036373400a20508746573743036373500a30508746573743036"
-    "373600a40508746573743036373700a50508746573743036373800a60508746573743036373900a705087465737430"
-    "36383000a80508746573743036383100a90508746573743036383200aa0508746573743036383300ab050874657374"
-    "3036383400ac0508746573743036383500ad0508746573743036383600ae0508746573743036383700af0508746573"
-    "743036383800b00508746573743036383900b10508746573743036393000b20508746573743036393100b305087465"
-    "73743036393200b40508746573743036393300b50508746573743036393400b60508746573743036393500b7050874"
-    "6573743036393600b80508746573743036393700b90508746573743036393800ba0508746573743036393900bb0508"
-    "746573743037303000bc0508746573743037303100bd0508746573743037303200be0508746573743037303300bf05"
-    "08746573743037303400c00508746573743037303500c10508746573743037303600c20508746573743037303700c3"
-    "0508746573743037303800c40508746573743037303900c50508746573743037313000c60508746573743037313100"
-    "c70508746573743037313200c80508746573743037313300c90508746573743037313400ca05087465737430373135"
-    "00cb0508746573743037313600cc0508746573743037313700cd0508746573743037313800ce050874657374303731"
-    "3900cf0508746573743037323000d00508746573743037323100d10508746573743037323200d20508746573743037"
-    "323300d30508746573743037323400d40508746573743037323500d50508746573743037323600d605087465737430"
-    "37323700d70508746573743037323800d80508746573743037323900d90508746573743037333000da050874657374"
-    "3037333100db0508746573743037333200dc0508746573743037333300dd0508746573743037333400de0508746573"
-    "743037333500df0508746573743037333600e00508746573743037333700e10508746573743037333800e205087465"
-    "73743037333900e30508746573743037343000e40508746573743037343100e50508746573743037343200e6050874"
-    "6573743037343300e70508746573743037343400e80508746573743037343500e90508746573743037343600ea0508"
-    "746573743037343700eb0508746573743037343800ec0508746573743037343900ed0508746573743037353000ee05"
-    "08746573743037353100ef0508746573743037353200f00508746573743037353300f10508746573743037353400f2"
-    "0508746573743037353500f30508746573743037353600f40508746573743037353700f50508746573743037353800"
-    "f60508746573743037353900f70508746573743037363000f80508746573743037363100f905087465737430373632"
-    "00fa0508746573743037363300fb0508746573743037363400fc0508746573743037363500fd050874657374303736"
-    "3600fe0508746573743037363700ff0508746573743037363800800608746573743037363900810608746573743037"
-    "3730008206087465737430373731008306087465737430373732008406087465737430373733008506087465737430"
-    "3737340086060874657374303737350087060874657374303737360088060874657374303737370089060874657374"
-    "30373738008a06087465737430373739008b06087465737430373830008c06087465737430373831008d0608746573"
-    "7430373832008e06087465737430373833008f06087465737430373834009006087465737430373835009106087465"
-    "7374303738360092060874657374303738370093060874657374303738380094060874657374303738390095060874"
-    "6573743037393000960608746573743037393100970608746573743037393200980608746573743037393300990608"
-    "7465737430373934009a06087465737430373935009b06087465737430373936009c06087465737430373937009d06"
-    "087465737430373938009e06087465737430373939009f0608746573743038303000a00608746573743038303100a1"
-    "0608746573743038303200a20608746573743038303300a30608746573743038303400a40608746573743038303500"
-    "a50608746573743038303600a60608746573743038303700a70608746573743038303800a806087465737430383039"
-    "00a90608746573743038313000aa0608746573743038313100ab0608746573743038313200ac060874657374303831"
-    "3300ad0608746573743038313400ae0608746573743038313500af0608746573743038313600b00608746573743038"
-    "313700b10608746573743038313800b20608746573743038313900b30608746573743038323000b406087465737430"
-    "38323100b50608746573743038323200b60608746573743038323300b70608746573743038323400b8060874657374"
-    "3038323500b90608746573743038323600ba0608746573743038323700bb0608746573743038323800bc0608746573"
-    "743038323900bd0608746573743038333000be0608746573743038333100bf0608746573743038333200c006087465"
-    "73743038333300c10608746573743038333400c20608746573743038333500c30608746573743038333600c4060874"
-    "6573743038333700c50608746573743038333800c60608746573743038333900c70608746573743038343000c80608"
-    "746573743038343100c90608746573743038343200ca0608746573743038343300cb0608746573743038343400cc06"
-    "08746573743038343500cd0608746573743038343600ce0608746573743038343700cf0608746573743038343800d0"
-    "0608746573743038343900d10608746573743038353000d20608746573743038353100d30608746573743038353200"
-    "d40608746573743038353300d50608746573743038353400d60608746573743038353500d706087465737430383536"
-    "00d80608746573743038353700d90608746573743038353800da0608746573743038353900db060874657374303836"
-    "3000dc0608746573743038363100dd0608746573743038363200de0608746573743038363300df0608746573743038"
-    "363400e00608746573743038363500e10608746573743038363600e20608746573743038363700e306087465737430"
-    "38363800e40608746573743038363900e50608746573743038373000e60608746573743038373100e7060874657374"
-    "3038373200e80608746573743038373300e90608746573743038373400ea0608746573743038373500eb0608746573"
-    "743038373600ec0608746573743038373700ed0608746573743038373800ee0608746573743038373900ef06087465"
-    "73743038383000f00608746573743038383100f10608746573743038383200f20608746573743038383300f3060874"
-    "6573743038383400f40608746573743038383500f50608746573743038383600f60608746573743038383700f70608"
-    "746573743038383800f80608746573743038383900f90608746573743038393000fa0608746573743038393100fb06"
-    "08746573743038393200fc0608746573743038393300fd0608746573743038393400fe0608746573743038393500ff"
-    "0608746573743038393600800708746573743038393700810708746573743038393800820708746573743038393900"
-    "8307087465737430393030008407087465737430393031008507087465737430393032008607087465737430393033"
-    "008707087465737430393034008807087465737430393035008907087465737430393036008a070874657374303930"
-    "37008b07087465737430393038008c07087465737430393039008d07087465737430393130008e0708746573743039"
-    "3131008f07087465737430393132009007087465737430393133009107087465737430393134009207087465737430"
-    "3931350093070874657374303931360094070874657374303931370095070874657374303931380096070874657374"
-    "30393139009707087465737430393230009807087465737430393231009907087465737430393232009a0708746573"
-    "7430393233009b07087465737430393234009c07087465737430393235009d07087465737430393236009e07087465"
-    "737430393237009f0708746573743039323800a00708746573743039323900a10708746573743039333000a2070874"
-    "6573743039333100a30708746573743039333200a40708746573743039333300a50708746573743039333400a60708"
-    "746573743039333500a70708746573743039333600a80708746573743039333700a90708746573743039333800aa07"
-    "08746573743039333900ab0708746573743039343000ac0708746573743039343100ad0708746573743039343200ae"
-    "0708746573743039343300af0708746573743039343400b00708746573743039343500b10708746573743039343600"
-    "b20708746573743039343700b30708746573743039343800b40708746573743039343900b507087465737430393530"
-    "00b60708746573743039353100b70708746573743039353200b80708746573743039353300b9070874657374303935"
-    "3400ba0708746573743039353500bb0708746573743039353600bc0708746573743039353700bd0708746573743039"
-    "353800be0708746573743039353900bf0708746573743039363000c00708746573743039363100c107087465737430"
-    "39363200c20708746573743039363300c30708746573743039363400c40708746573743039363500c5070874657374"
-    "3039363600c60708746573743039363700c70708746573743039363800c80708746573743039363900c90708746573"
-    "743039373000ca0708746573743039373100cb0708746573743039373200cc0708746573743039373300cd07087465"
-    "73743039373400ce0708746573743039373500cf0708746573743039373600d00708746573743039373700d1070874"
-    "6573743039373800d20708746573743039373900d30708746573743039383000d40708746573743039383100d50708"
-    "746573743039383200d60708746573743039383300d70708746573743039383400d80708746573743039383500d907"
-    "08746573743039383600da0708746573743039383700db0708746573743039383800dc0708746573743039383900dd"
-    "0708746573743039393000de0708746573743039393100df0708746573743039393200e00708746573743039393300"
-    "e10708746573743039393400e20708746573743039393500e30708746573743039393600e407087465737430393937"
-    "00e50708746573743039393800e60708746573743039393900e70708746573743130303000e8070874657374313030"
-    "3100e90708746573743130303200ea0708746573743130303300eb0708746573743130303400ec0708746573743130"
-    "303500ed0708746573743130303600ee0708746573743130303700ef0708746573743130303800f007087465737431"
-    "30303900f10708746573743130313000f20708746573743130313100f30708746573743130313200f4070874657374"
-    "3130313300f50708746573743130313400f60708746573743130313500f70708746573743130313600f80708746573"
-    "743130313700f90708746573743130313800fa0708746573743130313900fb0708746573743130323000fc07087465"
-    "73743130323100fd0708746573743130323200fe0708746573743130323300ff070874657374313032340080080874"
-    "6573743130323500810808746573743130323600820808746573743130323700830808746573743130323800840808"
-    "7465737431303239008508087465737431303330008608087465737431303331008708087465737431303332008808"
-    "087465737431303333008908087465737431303334008a08087465737431303335008b08087465737431303336008c"
-    "08087465737431303337008d08087465737431303338008e08087465737431303339008f0808746573743130343000"
-    "9008087465737431303431009108087465737431303432009208087465737431303433009308087465737431303434"
-    "0094080874657374313034350095080874657374313034360096080874657374313034370097080874657374313034"
-    "38009808087465737431303439009908087465737431303530009a08087465737431303531009b0808746573743130"
-    "3532009c08087465737431303533009d08087465737431303534009e08087465737431303535009f08087465737431"
-    "30353600a00808746573743130353700a10808746573743130353800a20808746573743130353900a3080874657374"
-    "3130363000a40808746573743130363100a50808746573743130363200a60808746573743130363300a70808746573"
-    "743130363400a80808746573743130363500a90808746573743130363600aa0808746573743130363700ab08087465"
-    "73743130363800ac0808746573743130363900ad0808746573743130373000ae0808746573743130373100af080874"
-    "6573743130373200b00808746573743130373300b10808746573743130373400b20808746573743130373500b30808"
-    "746573743130373600b40808746573743130373700b50808746573743130373800b60808746573743130373900b708"
-    "08746573743130383000b80808746573743130383100b90808746573743130383200ba0808746573743130383300bb"
-    "0808746573743130383400bc0808746573743130383500bd0808746573743130383600be0808746573743130383700"
-    "bf0808746573743130383800c00808746573743130383900c10808746573743130393000c208087465737431303931"
-    "00c30808746573743130393200c40808746573743130393300c50808746573743130393400c6080874657374313039"
-    "3500c70808746573743130393600c80808746573743130393700c90808746573743130393800ca0808746573743130"
-    "393900cb0808746573743131303000cc0808746573743131303100cd0808746573743131303200ce08087465737431"
-    "31303300cf0808746573743131303400d00808746573743131303500d10808746573743131303600d2080874657374"
-    "3131303700d30808746573743131303800d40808746573743131303900d50808746573743131313000d60808746573"
-    "743131313100d70808746573743131313200d80808746573743131313300d90808746573743131313400da08087465"
-    "73743131313500db0808746573743131313600dc0808746573743131313700dd0808746573743131313800de080874"
-    "6573743131313900df0808746573743131323000e00808746573743131323100e10808746573743131323200e20808"
-    "746573743131323300e30808746573743131323400e40808746573743131323500e50808746573743131323600e608"
-    "08746573743131323700e70808746573743131323800e80808746573743131323900e90808746573743131333000ea"
-    "0808746573743131333100eb0808746573743131333200ec0808746573743131333300ed0808746573743131333400"
-    "ee0808746573743131333500ef0808746573743131333600f00808746573743131333700f108087465737431313338"
-    "00f20808746573743131333900f30808746573743131343000f40808746573743131343100f5080874657374313134"
-    "3200f60808746573743131343300f70808746573743131343400f80808746573743131343500f90808746573743131"
-    "343600fa0808746573743131343700fb0808746573743131343800fc0808746573743131343900fd08087465737431"
-    "31353000fe0808746573743131353100ff080874657374313135320080090874657374313135330081090874657374"
-    "3131353400820908746573743131353500830908746573743131353600840908746573743131353700850908746573"
-    "7431313538008609087465737431313539008709087465737431313630008809087465737431313631008909087465"
-    "737431313632008a09087465737431313633008b09087465737431313634008c09087465737431313635008d090874"
-    "65737431313636008e09087465737431313637008f0908746573743131363800900908746573743131363900910908"
-    "7465737431313730009209087465737431313731009309087465737431313732009409087465737431313733009509"
-    "0874657374313137340096090874657374313137350097090874657374313137360098090874657374313137370099"
-    "09087465737431313738009a09087465737431313739009b09087465737431313830009c0908746573743131383100"
-    "9d09087465737431313832009e09087465737431313833009f0908746573743131383400a009087465737431313835"
-    "00a10908746573743131383600a20908746573743131383700a30908746573743131383800a4090874657374313138"
-    "3900a50908746573743131393000a60908746573743131393100a70908746573743131393200a80908746573743131"
-    "393300a90908746573743131393400aa0908746573743131393500ab0908746573743131393600ac09087465737431"
-    "31393700ad0908746573743131393800ae0908746573743131393900af0908746573743132303000b0090874657374"
-    "3132303100b10908746573743132303200b20908746573743132303300b30908746573743132303400b40908746573"
-    "743132303500b50908746573743132303600b60908746573743132303700b70908746573743132303800b809087465"
-    "73743132303900b90908746573743132313000ba0908746573743132313100bb0908746573743132313200bc090874"
-    "6573743132313300bd0908746573743132313400be0908746573743132313500bf0908746573743132313600c00908"
-    "746573743132313700c10908746573743132313800c20908746573743132313900c30908746573743132323000c409"
-    "08746573743132323100c50908746573743132323200c60908746573743132323300c70908746573743132323400c8"
-    "0908746573743132323500c90908746573743132323600ca0908746573743132323700cb0908746573743132323800"
-    "cc0908746573743132323900cd0908746573743132333000ce0908746573743132333100cf09087465737431323332"
-    "00d00908746573743132333300d10908746573743132333400d20908746573743132333500d3090874657374313233"
-    "3600d40908746573743132333700d50908746573743132333800d60908746573743132333900d70908746573743132"
-    "343000d80908746573743132343100d90908746573743132343200da0908746573743132343300db09087465737431"
-    "32343400dc0908746573743132343500dd0908746573743132343600de0908746573743132343700df090874657374"
-    "3132343800e00908746573743132343900e10908746573743132353000e20908746573743132353100e30908746573"
-    "743132353200e40908746573743132353300e50908746573743132353400e60908746573743132353500e709087465"
-    "73743132353600e80908746573743132353700e90908746573743132353800ea0908746573743132353900eb090874"
-    "6573743132363000ec0908746573743132363100ed0908746573743132363200ee0908746573743132363300ef0908"
-    "746573743132363400f00908746573743132363500f10908746573743132363600f20908746573743132363700f309"
-    "08746573743132363800f40908746573743132363900f50908746573743132373000f60908746573743132373100f7"
-    "0908746573743132373200f80908746573743132373300f90908746573743132373400fa0908746573743132373500"
-    "fb0908746573743132373600fc0908746573743132373700fd0908746573743132373800fe09087465737431323739"
-    "00ff0908746573743132383000800a08746573743132383100810a08746573743132383200820a0874657374313238"
-    "3300830a08746573743132383400840a08746573743132383500850a08746573743132383600860a08746573743132"
-    "383700870a08746573743132383800880a08746573743132383900890a087465737431323930008a0a087465737431"
-    "323931008b0a087465737431323932008c0a087465737431323933008d0a087465737431323934008e0a0874657374"
-    "31323935008f0a08746573743132393600900a08746573743132393700910a08746573743132393800920a08746573"
-    "743132393900930a08746573743133303000940a08746573743133303100950a08746573743133303200960a087465"
-    "73743133303300970a08746573743133303400980a08746573743133303500990a087465737431333036009a0a0874"
-    "65737431333037009b0a087465737431333038009c0a087465737431333039009d0a087465737431333130009e0a08"
-    "7465737431333131009f0a08746573743133313200a00a08746573743133313300a10a08746573743133313400a20a"
-    "08746573743133313500a30a08746573743133313600a40a08746573743133313700a50a08746573743133313800a6"
-    "0a08746573743133313900a70a08746573743133323000a80a08746573743133323100a90a08746573743133323200"
-    "aa0a08746573743133323300ab0a08746573743133323400ac0a08746573743133323500ad0a087465737431333236"
-    "00ae0a08746573743133323700af0a08746573743133323800b00a08746573743133323900b10a0874657374313333"
-    "3000b20a08746573743133333100b30a08746573743133333200b40a08746573743133333300b50a08746573743133"
-    "333400b60a08746573743133333500b70a08746573743133333600b80a08746573743133333700b90a087465737431"
-    "33333800ba0a08746573743133333900bb0a08746573743133343000bc0a08746573743133343100bd0a0874657374"
-    "3133343200be0a08746573743133343300bf0a08746573743133343400c00a08746573743133343500c10a08746573"
-    "743133343600c20a08746573743133343700c30a08746573743133343800c40a08746573743133343900c50a087465"
-    "73743133353000c60a08746573743133353100c70a08746573743133353200c80a08746573743133353300c90a0874"
-    "6573743133353400ca0a08746573743133353500cb0a08746573743133353600cc0a08746573743133353700cd0a08"
-    "746573743133353800ce0a08746573743133353900cf0a08746573743133363000d00a08746573743133363100d10a"
-    "08746573743133363200d20a08746573743133363300d30a08746573743133363400d40a08746573743133363500d5"
-    "0a08746573743133363600d60a08746573743133363700d70a08746573743133363800d80a08746573743133363900"
-    "d90a08746573743133373000da0a08746573743133373100db0a08746573743133373200dc0a087465737431333733"
-    "00dd0a08746573743133373400de0a08746573743133373500df0a08746573743133373600e00a0874657374313337"
-    "3700e10a08746573743133373800e20a08746573743133373900e30a08746573743133383000e40a08746573743133"
-    "383100e50a08746573743133383200e60a08746573743133383300e70a08746573743133383400e80a087465737431"
-    "33383500e90a08746573743133383600ea0a08746573743133383700eb0a08746573743133383800ec0a0874657374"
-    "3133383900ed0a08746573743133393000ee0a08746573743133393100ef0a08746573743133393200f00a08746573"
-    "743133393300f10a08746573743133393400f20a08746573743133393500f30a08746573743133393600f40a087465"
-    "73743133393700f50a08746573743133393800f60a08746573743133393900f70a08746573743134303000f80a0874"
-    "6573743134303100f90a08746573743134303200fa0a08746573743134303300fb0a08746573743134303400fc0a08"
-    "746573743134303500fd0a08746573743134303600fe0a08746573743134303700ff0a08746573743134303800800b"
-    "08746573743134303900810b08746573743134313000820b08746573743134313100830b0874657374313431320084"
-    "0b08746573743134313300850b08746573743134313400860b08746573743134313500870b08746573743134313600"
-    "880b08746573743134313700890b087465737431343138008a0b087465737431343139008b0b087465737431343230"
-    "008c0b087465737431343231008d0b087465737431343232008e0b087465737431343233008f0b0874657374313432"
-    "3400900b08746573743134323500910b08746573743134323600920b08746573743134323700930b08746573743134"
-    "323800940b08746573743134323900950b08746573743134333000960b08746573743134333100970b087465737431"
-    "34333200980b08746573743134333300990b087465737431343334009a0b087465737431343335009b0b0874657374"
-    "31343336009c0b087465737431343337009d0b087465737431343338009e0b087465737431343339009f0b08746573"
-    "743134343000a00b08746573743134343100a10b08746573743134343200a20b08746573743134343300a30b087465"
-    "73743134343400a40b08746573743134343500a50b08746573743134343600a60b08746573743134343700a70b0874"
-    "6573743134343800a80b08746573743134343900a90b08746573743134353000aa0b08746573743134353100ab0b08"
-    "746573743134353200ac0b08746573743134353300ad0b08746573743134353400ae0b08746573743134353500af0b"
-    "08746573743134353600b00b08746573743134353700b10b08746573743134353800b20b08746573743134353900b3"
-    "0b08746573743134363000b40b08746573743134363100b50b08746573743134363200b60b08746573743134363300"
-    "b70b08746573743134363400b80b08746573743134363500b90b08746573743134363600ba0b087465737431343637"
-    "00bb0b08746573743134363800bc0b08746573743134363900bd0b08746573743134373000be0b0874657374313437"
-    "3100bf0b08746573743134373200c00b08746573743134373300c10b08746573743134373400c20b08746573743134"
-    "373500c30b08746573743134373600c40b08746573743134373700c50b08746573743134373800c60b087465737431"
-    "34373900c70b08746573743134383000c80b08746573743134383100c90b08746573743134383200ca0b0874657374"
-    "3134383300cb0b08746573743134383400cc0b08746573743134383500cd0b08746573743134383600ce0b08746573"
-    "743134383700cf0b08746573743134383800d00b08746573743134383900d10b08746573743134393000d20b087465"
-    "73743134393100d30b08746573743134393200d40b08746573743134393300d50b08746573743134393400d60b0874"
-    "6573743134393500d70b08746573743134393600d80b08746573743134393700d90b08746573743134393800da0b08"
-    "746573743134393900db0b08746573743135303000dc0b08746573743135303100dd0b08746573743135303200de0b"
-    "08746573743135303300df0b08746573743135303400e00b08746573743135303500e10b08746573743135303600e2"
-    "0b08746573743135303700e30b08746573743135303800e40b08746573743135303900e50b08746573743135313000"
-    "e60b08746573743135313100e70b08746573743135313200e80b08746573743135313300e90b087465737431353134"
-    "00ea0b08746573743135313500eb0b08746573743135313600ec0b08746573743135313700ed0b0874657374313531"
-    "3800ee0b08746573743135313900ef0b08746573743135323000f00b08746573743135323100f10b08746573743135"
-    "323200f20b08746573743135323300f30b08746573743135323400f40b08746573743135323500f50b087465737431"
-    "35323600f60b08746573743135323700f70b08746573743135323800f80b08746573743135323900f90b0874657374"
-    "3135333000fa0b08746573743135333100fb0b08746573743135333200fc0b08746573743135333300fd0b08746573"
-    "743135333400fe0b08746573743135333500ff0b08746573743135333600800c08746573743135333700810c087465"
-    "73743135333800820c08746573743135333900830c08746573743135343000840c08746573743135343100850c0874"
-    "6573743135343200860c08746573743135343300870c08746573743135343400880c08746573743135343500890c08"
-    "7465737431353436008a0c087465737431353437008b0c087465737431353438008c0c087465737431353439008d0c"
-    "087465737431353530008e0c087465737431353531008f0c08746573743135353200900c0874657374313535330091"
-    "0c08746573743135353400920c08746573743135353500930c08746573743135353600940c08746573743135353700"
-    "950c08746573743135353800960c08746573743135353900970c08746573743135363000980c087465737431353631"
-    "00990c087465737431353632009a0c087465737431353633009b0c087465737431353634009c0c0874657374313536"
-    "35009d0c087465737431353636009e0c087465737431353637009f0c08746573743135363800a00c08746573743135"
-    "363900a10c08746573743135373000a20c08746573743135373100a30c08746573743135373200a40c087465737431"
-    "35373300a50c08746573743135373400a60c08746573743135373500a70c08746573743135373600a80c0874657374"
-    "3135373700a90c08746573743135373800aa0c08746573743135373900ab0c08746573743135383000ac0c08746573"
-    "743135383100ad0c08746573743135383200ae0c08746573743135383300af0c08746573743135383400b00c087465"
-    "73743135383500b10c08746573743135383600b20c08746573743135383700b30c08746573743135383800b40c0874"
-    "6573743135383900b50c08746573743135393000b60c08746573743135393100b70c08746573743135393200b80c08"
-    "746573743135393300b90c08746573743135393400ba0c08746573743135393500bb0c08746573743135393600bc0c"
-    "08746573743135393700bd0c08746573743135393800be0c08746573743135393900bf0c08746573743136303000c0"
-    "0c08746573743136303100c10c08746573743136303200c20c08746573743136303300c30c08746573743136303400"
-    "c40c08746573743136303500c50c08746573743136303600c60c08746573743136303700c70c087465737431363038"
-    "00c80c08746573743136303900c90c08746573743136313000ca0c08746573743136313100cb0c0874657374313631"
-    "3200cc0c08746573743136313300cd0c08746573743136313400ce0c08746573743136313500cf0c08746573743136"
-    "313600d00c08746573743136313700d10c08746573743136313800d20c08746573743136313900d30c087465737431"
-    "36323000d40c08746573743136323100d50c08746573743136323200d60c08746573743136323300d70c0874657374"
-    "3136323400d80c08746573743136323500d90c08746573743136323600da0c08746573743136323700db0c08746573"
-    "743136323800dc0c08746573743136323900dd0c08746573743136333000de0c08746573743136333100df0c087465"
-    "73743136333200e00c08746573743136333300e10c08746573743136333400e20c08746573743136333500e30c0874"
-    "6573743136333600e40c08746573743136333700e50c08746573743136333800e60c08746573743136333900e70c08"
-    "746573743136343000e80c08746573743136343100e90c08746573743136343200ea0c08746573743136343300eb0c"
-    "08746573743136343400ec0c08746573743136343500ed0c08746573743136343600ee0c08746573743136343700ef"
-    "0c08746573743136343800f00c08746573743136343900f10c08746573743136353000f20c08746573743136353100"
-    "f30c08746573743136353200f40c08746573743136353300f50c08746573743136353400f60c087465737431363535"
-    "00f70c08746573743136353600f80c08746573743136353700f90c08746573743136353800fa0c0874657374313635"
-    "3900fb0c08746573743136363000fc0c08746573743136363100fd0c08746573743136363200fe0c08746573743136"
-    "363300ff0c08746573743136363400800d08746573743136363500810d08746573743136363600820d087465737431"
-    "36363700830d08746573743136363800840d08746573743136363900850d08746573743136373000860d0874657374"
-    "3136373100870d08746573743136373200880d08746573743136373300890d087465737431363734008a0d08746573"
-    "7431363735008b0d087465737431363736008c0d087465737431363737008d0d087465737431363738008e0d087465"
-    "737431363739008f0d08746573743136383000900d08746573743136383100910d08746573743136383200920d0874"
-    "6573743136383300930d08746573743136383400940d08746573743136383500950d08746573743136383600960d08"
-    "746573743136383700970d08746573743136383800980d08746573743136383900990d087465737431363930009a0d"
-    "087465737431363931009b0d087465737431363932009c0d087465737431363933009d0d087465737431363934009e"
-    "0d087465737431363935009f0d08746573743136393600a00d08746573743136393700a10d08746573743136393800"
-    "a20d08746573743136393900a30d08746573743137303000a40d08746573743137303100a50d087465737431373032"
-    "00a60d08746573743137303300a70d08746573743137303400a80d08746573743137303500a90d0874657374313730"
-    "3600aa0d08746573743137303700ab0d08746573743137303800ac0d08746573743137303900ad0d08746573743137"
-    "313000ae0d08746573743137313100af0d08746573743137313200b00d08746573743137313300b10d087465737431"
-    "37313400b20d08746573743137313500b30d08746573743137313600b40d08746573743137313700b50d0874657374"
-    "3137313800b60d08746573743137313900b70d08746573743137323000b80d08746573743137323100b90d08746573"
-    "743137323200ba0d08746573743137323300bb0d08746573743137323400bc0d08746573743137323500bd0d087465"
-    "73743137323600be0d08746573743137323700bf0d08746573743137323800c00d08746573743137323900c10d0874"
-    "6573743137333000c20d08746573743137333100c30d08746573743137333200c40d08746573743137333300c50d08"
-    "746573743137333400c60d08746573743137333500c70d08746573743137333600c80d08746573743137333700c90d"
-    "08746573743137333800ca0d08746573743137333900cb0d08746573743137343000cc0d08746573743137343100cd"
-    "0d08746573743137343200ce0d08746573743137343300cf0d08746573743137343400d00d08746573743137343500"
-    "d10d08746573743137343600d20d08746573743137343700d30d08746573743137343800d40d087465737431373439"
-    "00d50d08746573743137353000d60d08746573743137353100d70d08746573743137353200d80d0874657374313735"
-    "3300d90d08746573743137353400da0d08746573743137353500db0d08746573743137353600dc0d08746573743137"
-    "353700dd0d08746573743137353800de0d08746573743137353900df0d08746573743137363000e00d087465737431"
-    "37363100e10d08746573743137363200e20d08746573743137363300e30d08746573743137363400e40d0874657374"
-    "3137363500e50d08746573743137363600e60d08746573743137363700e70d08746573743137363800e80d08746573"
-    "743137363900e90d08746573743137373000ea0d08746573743137373100eb0d08746573743137373200ec0d087465"
-    "73743137373300ed0d08746573743137373400ee0d08746573743137373500ef0d08746573743137373600f00d0874"
-    "6573743137373700f10d08746573743137373800f20d08746573743137373900f30d08746573743137383000f40d08"
-    "746573743137383100f50d08746573743137383200f60d08746573743137383300f70d08746573743137383400f80d"
-    "08746573743137383500f90d08746573743137383600fa0d08746573743137383700fb0d08746573743137383800fc"
-    "0d08746573743137383900fd0d08746573743137393000fe0d08746573743137393100ff0d08746573743137393200"
-    "800e08746573743137393300810e08746573743137393400820e08746573743137393500830e087465737431373936"
-    "00840e08746573743137393700850e08746573743137393800860e08746573743137393900870e0874657374313830"
-    "3000880e08746573743138303100890e087465737431383032008a0e087465737431383033008b0e08746573743138"
-    "3034008c0e087465737431383035008d0e087465737431383036008e0e087465737431383037008f0e087465737431"
-    "38303800900e08746573743138303900910e08746573743138313000920e08746573743138313100930e0874657374"
-    "3138313200940e08746573743138313300950e08746573743138313400960e08746573743138313500970e08746573"
-    "743138313600980e08746573743138313700990e087465737431383138009a0e087465737431383139009b0e087465"
-    "737431383230009c0e087465737431383231009d0e087465737431383232009e0e087465737431383233009f0e0874"
-    "6573743138323400a00e08746573743138323500a10e08746573743138323600a20e08746573743138323700a30e08"
-    "746573743138323800a40e08746573743138323900a50e08746573743138333000a60e08746573743138333100a70e"
-    "08746573743138333200a80e08746573743138333300a90e08746573743138333400aa0e08746573743138333500ab"
-    "0e08746573743138333600ac0e08746573743138333700ad0e08746573743138333800ae0e08746573743138333900"
-    "af0e08746573743138343000b00e08746573743138343100b10e08746573743138343200b20e087465737431383433"
-    "00b30e08746573743138343400b40e08746573743138343500b50e08746573743138343600b60e0874657374313834"
-    "3700b70e08746573743138343800b80e08746573743138343900b90e08746573743138353000ba0e08746573743138"
-    "353100bb0e08746573743138353200bc0e08746573743138353300bd0e08746573743138353400be0e087465737431"
-    "38353500bf0e08746573743138353600c00e08746573743138353700c10e08746573743138353800c20e0874657374"
-    "3138353900c30e08746573743138363000c40e08746573743138363100c50e08746573743138363200c60e08746573"
-    "743138363300c70e08746573743138363400c80e08746573743138363500c90e08746573743138363600ca0e087465"
-    "73743138363700cb0e08746573743138363800cc0e08746573743138363900cd0e08746573743138373000ce0e0874"
-    "6573743138373100cf0e08746573743138373200d00e08746573743138373300d10e08746573743138373400d20e08"
-    "746573743138373500d30e08746573743138373600d40e08746573743138373700d50e08746573743138373800d60e"
-    "08746573743138373900d70e08746573743138383000d80e08746573743138383100d90e08746573743138383200da"
-    "0e08746573743138383300db0e08746573743138383400dc0e08746573743138383500dd0e08746573743138383600"
-    "de0e08746573743138383700df0e08746573743138383800e00e08746573743138383900e10e087465737431383930"
-    "00e20e08746573743138393100e30e08746573743138393200e40e08746573743138393300e50e0874657374313839"
-    "3400e60e08746573743138393500e70e08746573743138393600e80e08746573743138393700e90e08746573743138"
-    "393800ea0e08746573743138393900eb0e08746573743139303000ec0e08746573743139303100ed0e087465737431"
-    "39303200ee0e08746573743139303300ef0e08746573743139303400f00e08746573743139303500f10e0874657374"
-    "3139303600f20e08746573743139303700f30e08746573743139303800f40e08746573743139303900f50e08746573"
-    "743139313000f60e08746573743139313100f70e08746573743139313200f80e08746573743139313300f90e087465"
-    "73743139313400fa0e08746573743139313500fb0e08746573743139313600fc0e08746573743139313700fd0e0874"
-    "6573743139313800fe0e08746573743139313900ff0e08746573743139323000800f08746573743139323100810f08"
-    "746573743139323200820f08746573743139323300830f08746573743139323400840f08746573743139323500850f"
-    "08746573743139323600860f08746573743139323700870f08746573743139323800880f0874657374313932390089"
-    "0f087465737431393330008a0f087465737431393331008b0f087465737431393332008c0f08746573743139333300"
-    "8d0f087465737431393334008e0f087465737431393335008f0f08746573743139333600900f087465737431393337"
-    "00910f08746573743139333800920f08746573743139333900930f08746573743139343000940f0874657374313934"
-    "3100950f08746573743139343200960f08746573743139343300970f08746573743139343400980f08746573743139"
-    "343500990f087465737431393436009a0f087465737431393437009b0f087465737431393438009c0f087465737431"
-    "393439009d0f087465737431393530009e0f087465737431393531009f0f08746573743139353200a00f0874657374"
-    "3139353300a10f08746573743139353400a20f08746573743139353500a30f08746573743139353600a40f08746573"
-    "743139353700a50f08746573743139353800a60f08746573743139353900a70f08746573743139363000a80f087465"
-    "73743139363100a90f08746573743139363200aa0f08746573743139363300ab0f08746573743139363400ac0f0874"
-    "6573743139363500ad0f08746573743139363600ae0f08746573743139363700af0f08746573743139363800b00f08"
-    "746573743139363900b10f08746573743139373000b20f08746573743139373100b30f08746573743139373200b40f"
-    "08746573743139373300b50f08746573743139373400b60f08746573743139373500b70f08746573743139373600b8"
-    "0f08746573743139373700b90f08746573743139373800ba0f08746573743139373900bb0f08746573743139383000"
-    "bc0f08746573743139383100bd0f08746573743139383200be0f08746573743139383300bf0f087465737431393834"
-    "00c00f08746573743139383500c10f08746573743139383600c20f08746573743139383700c30f0874657374313938"
-    "3800c40f08746573743139383900c50f08746573743139393000c60f08746573743139393100c70f08746573743139"
-    "393200c80f08746573743139393300c90f08746573743139393400ca0f08746573743139393500cb0f087465737431"
-    "39393600cc0f08746573743139393700cd0f08746573743139393800ce0f08746573743139393900cf0f0874657374"
-    "3230303000d00f08746573743230303100d10f08746573743230303200d20f08746573743230303300d30f08746573"
-    "743230303400d40f08746573743230303500d50f08746573743230303600d60f08746573743230303700d70f087465"
-    "73743230303800d80f08746573743230303900d90f08746573743230313000da0f08746573743230313100db0f0874"
-    "6573743230313200dc0f08746573743230313300dd0f08746573743230313400de0f08746573743230313500df0f08"
-    "746573743230313600e00f08746573743230313700e10f08746573743230313800e20f08746573743230313900e30f"
-    "08746573743230323000e40f08746573743230323100e50f08746573743230323200e60f08746573743230323300e7"
-    "0f08746573743230323400e80f08746573743230323500e90f08746573743230323600ea0f08746573743230323700"
-    "eb0f08746573743230323800ec0f08746573743230323900ed0f08746573743230333000ee0f087465737432303331"
-    "00ef0f08746573743230333200f00f08746573743230333300f10f08746573743230333400f20f0874657374323033"
-    "3500f30f08746573743230333600f40f08746573743230333700f50f08746573743230333800f60f08746573743230"
-    "333900f70f08746573743230343000f80f08746573743230343100f90f08746573743230343200fa0f087465737432"
-    "30343300fb0f08746573743230343400fc0f08746573743230343500fd0f08746573743230343600fe0f0874657374"
-    "3230343700ff0f08746573743230343800801008746573743230343900811008746573743230353000821008746573"
-    "7432303531008310087465737432303532008410087465737432303533008510087465737432303534008610087465"
-    "737432303535008710087465737432303536008810087465737432303537008910087465737432303538008a100874"
-    "65737432303539008b10087465737432303630008c10087465737432303631008d10087465737432303632008e1008"
-    "7465737432303633008f10087465737432303634009010087465737432303635009110087465737432303636009210"
-    "0874657374323036370093100874657374323036380094100874657374323036390095100874657374323037300096"
-    "1008746573743230373100971008746573743230373200981008746573743230373300991008746573743230373400"
-    "9a10087465737432303735009b10087465737432303736009c10087465737432303737009d10087465737432303738"
-    "009e10087465737432303739009f1008746573743230383000a01008746573743230383100a1100874657374323038"
-    "3200a21008746573743230383300a31008746573743230383400a41008746573743230383500a51008746573743230"
-    "383600a61008746573743230383700a71008746573743230383800a81008746573743230383900a910087465737432"
-    "30393000aa1008746573743230393100ab1008746573743230393200ac1008746573743230393300ad100874657374"
-    "3230393400ae1008746573743230393500af1008746573743230393600b01008746573743230393700b11008746573"
-    "743230393800b21008746573743230393900b31008746573743231303000b41008746573743231303100b510087465"
-    "73743231303200b61008746573743231303300b71008746573743231303400b81008746573743231303500b9100874"
-    "6573743231303600ba1008746573743231303700bb1008746573743231303800bc1008746573743231303900bd1008"
-    "746573743231313000be1008746573743231313100bf1008746573743231313200c01008746573743231313300c110"
-    "08746573743231313400c21008746573743231313500c31008746573743231313600c41008746573743231313700c5"
-    "1008746573743231313800c61008746573743231313900c71008746573743231323000c81008746573743231323100"
-    "c91008746573743231323200ca1008746573743231323300cb1008746573743231323400cc10087465737432313235"
-    "00cd1008746573743231323600ce1008746573743231323700cf1008746573743231323800d0100874657374323132"
-    "3900d11008746573743231333000d21008746573743231333100d31008746573743231333200d41008746573743231"
-    "333300d51008746573743231333400d61008746573743231333500d71008746573743231333600d810087465737432"
-    "31333700d91008746573743231333800da1008746573743231333900db1008746573743231343000dc100874657374"
-    "3231343100dd1008746573743231343200de1008746573743231343300df1008746573743231343400e01008746573"
-    "743231343500e11008746573743231343600e21008746573743231343700e31008746573743231343800e410087465"
-    "73743231343900e51008746573743231353000e61008746573743231353100e71008746573743231353200e8100874"
-    "6573743231353300e91008746573743231353400ea1008746573743231353500eb1008746573743231353600ec1008"
-    "746573743231353700ed1008746573743231353800ee1008746573743231353900ef1008746573743231363000f010"
-    "08746573743231363100f11008746573743231363200f21008746573743231363300f31008746573743231363400f4"
-    "1008746573743231363500f51008746573743231363600f61008746573743231363700f71008746573743231363800"
-    "f81008746573743231363900f91008746573743231373000fa1008746573743231373100fb10087465737432313732"
-    "00fc1008746573743231373300fd1008746573743231373400fe1008746573743231373500ff100874657374323137"
-    "3600801108746573743231373700811108746573743231373800821108746573743231373900831108746573743231"
-    "3830008411087465737432313831008511087465737432313832008611087465737432313833008711087465737432"
-    "313834008811087465737432313835008911087465737432313836008a11087465737432313837008b110874657374"
-    "32313838008c11087465737432313839008d11087465737432313930008e11087465737432313931008f1108746573"
-    "7432313932009011087465737432313933009111087465737432313934009211087465737432313935009311087465"
-    "7374323139360094110874657374323139370095110874657374323139380096110874657374323139390097110874"
-    "65737432323030009811087465737432323031009911087465737432323032009a11087465737432323033009b1108"
-    "7465737432323034009c11087465737432323035009d11087465737432323036009e11087465737432323037009f11"
-    "08746573743232303800a01108746573743232303900a11108746573743232313000a21108746573743232313100a3"
-    "1108746573743232313200a41108746573743232313300a51108746573743232313400a61108746573743232313500"
-    "a71108746573743232313600a81108746573743232313700a91108746573743232313800aa11087465737432323139"
-    "00ab1108746573743232323000ac1108746573743232323100ad1108746573743232323200ae110874657374323232"
-    "3300af1108746573743232323400b01108746573743232323500b11108746573743232323600b21108746573743232"
-    "323700b31108746573743232323800b41108746573743232323900b51108746573743232333000b611087465737432"
-    "32333100b71108746573743232333200b81108746573743232333300b91108746573743232333400ba110874657374"
-    "3232333500bb1108746573743232333600bc1108746573743232333700bd1108746573743232333800be1108746573"
-    "743232333900bf1108746573743232343000c01108746573743232343100c11108746573743232343200c211087465"
-    "73743232343300c31108746573743232343400c41108746573743232343500c51108746573743232343600c6110874"
-    "6573743232343700c71108746573743232343800c81108746573743232343900c91108746573743232353000ca1108"
-    "746573743232353100cb1108746573743232353200cc1108746573743232353300cd1108746573743232353400ce11"
-    "08746573743232353500cf1108746573743232353600d01108746573743232353700d11108746573743232353800d2"
-    "1108746573743232353900d31108746573743232363000d41108746573743232363100d51108746573743232363200"
-    "d61108746573743232363300d71108746573743232363400d81108746573743232363500d911087465737432323636"
-    "00da1108746573743232363700db1108746573743232363800dc1108746573743232363900dd110874657374323237"
-    "3000de1108746573743232373100df1108746573743232373200e01108746573743232373300e11108746573743232"
-    "373400e21108746573743232373500e31108746573743232373600e41108746573743232373700e511087465737432"
-    "32373800e61108746573743232373900e71108746573743232383000e81108746573743232383100e9110874657374"
-    "3232383200ea1108746573743232383300eb1108746573743232383400ec1108746573743232383500ed1108746573"
-    "743232383600ee1108746573743232383700ef1108746573743232383800f01108746573743232383900f111087465"
-    "73743232393000f21108746573743232393100f31108746573743232393200f41108746573743232393300f5110874"
-    "6573743232393400f61108746573743232393500f71108746573743232393600f81108746573743232393700f91108"
-    "746573743232393800fa1108746573743232393900fb1108746573743233303000fc1108746573743233303100fd11"
-    "08746573743233303200fe1108746573743233303300ff110874657374323330340080120874657374323330350081"
-    "1208746573743233303600821208746573743233303700831208746573743233303800841208746573743233303900"
-    "8512087465737432333130008612087465737432333131008712087465737432333132008812087465737432333133"
-    "008912087465737432333134008a12087465737432333135008b12087465737432333136008c120874657374323331"
-    "37008d12087465737432333138008e12087465737432333139008f1208746573743233323000901208746573743233"
-    "3231009112087465737432333232009212087465737432333233009312087465737432333234009412087465737432"
-    "3332350095120874657374323332360096120874657374323332370097120874657374323332380098120874657374"
-    "32333239009912087465737432333330009a12087465737432333331009b12087465737432333332009c1208746573"
-    "7432333333009d12087465737432333334009e12087465737432333335009f1208746573743233333600a012087465"
-    "73743233333700a11208746573743233333800a21208746573743233333900a31208746573743233343000a4120874"
-    "6573743233343100a51208746573743233343200a61208746573743233343300a71208746573743233343400a81208"
-    "746573743233343500a91208746573743233343600aa1208746573743233343700ab1208746573743233343800ac12"
-    "08746573743233343900ad1208746573743233353000ae1208746573743233353100af1208746573743233353200b0"
-    "1208746573743233353300b11208746573743233353400b21208746573743233353500b31208746573743233353600"
-    "b41208746573743233353700b51208746573743233353800b61208746573743233353900b712087465737432333630"
-    "00b81208746573743233363100b91208746573743233363200ba1208746573743233363300bb120874657374323336"
-    "3400bc1208746573743233363500bd1208746573743233363600be1208746573743233363700bf1208746573743233"
-    "363800c01208746573743233363900c11208746573743233373000c21208746573743233373100c312087465737432"
-    "33373200c41208746573743233373300c51208746573743233373400c61208746573743233373500c7120874657374"
-    "3233373600c81208746573743233373700c91208746573743233373800ca1208746573743233373900cb1208746573"
-    "743233383000cc1208746573743233383100cd1208746573743233383200ce1208746573743233383300cf12087465"
-    "73743233383400d01208746573743233383500d11208746573743233383600d21208746573743233383700d3120874"
-    "6573743233383800d41208746573743233383900d51208746573743233393000d61208746573743233393100d71208"
-    "746573743233393200d81208746573743233393300d91208746573743233393400da1208746573743233393500db12"
-    "08746573743233393600dc1208746573743233393700dd1208746573743233393800de1208746573743233393900df"
-    "1208746573743234303000e01208746573743234303100e11208746573743234303200e21208746573743234303300"
-    "e31208746573743234303400e41208746573743234303500e51208746573743234303600e612087465737432343037"
-    "00e71208746573743234303800e81208746573743234303900e91208746573743234313000ea120874657374323431"
-    "3100eb1208746573743234313200ec1208746573743234313300ed1208746573743234313400ee1208746573743234"
-    "313500ef1208746573743234313600f01208746573743234313700f11208746573743234313800f212087465737432"
-    "34313900f31208746573743234323000f41208746573743234323100f51208746573743234323200f6120874657374"
-    "3234323300f71208746573743234323400f81208746573743234323500f91208746573743234323600fa1208746573"
-    "743234323700fb1208746573743234323800fc1208746573743234323900fd1208746573743234333000fe12087465"
-    "73743234333100ff120874657374323433320080130874657374323433330081130874657374323433340082130874"
-    "6573743234333500831308746573743234333600841308746573743234333700851308746573743234333800861308"
-    "7465737432343339008713087465737432343430008813087465737432343431008913087465737432343432008a13"
-    "087465737432343433008b13087465737432343434008c13087465737432343435008d13087465737432343436008e"
-    "13087465737432343437008f1308746573743234343800901308746573743234343900911308746573743234353000"
-    "9213087465737432343531009313087465737432343532009413087465737432343533009513087465737432343534"
-    "0096130874657374323435350097130874657374323435360098130874657374323435370099130874657374323435"
-    "38009a13087465737432343539009b13087465737432343630009c13087465737432343631009d1308746573743234"
-    "3632009e13087465737432343633009f1308746573743234363400a01308746573743234363500a113087465737432"
-    "34363600a21308746573743234363700a31308746573743234363800a41308746573743234363900a5130874657374"
-    "3234373000a61308746573743234373100a71308746573743234373200a81308746573743234373300a91308746573"
-    "743234373400aa1308746573743234373500ab1308746573743234373600ac1308746573743234373700ad13087465"
-    "73743234373800ae1308746573743234373900af1308746573743234383000b01308746573743234383100b1130874"
-    "6573743234383200b21308746573743234383300b31308746573743234383400b41308746573743234383500b51308"
-    "746573743234383600b61308746573743234383700b71308746573743234383800b81308746573743234383900b913"
-    "08746573743234393000ba1308746573743234393100bb1308746573743234393200bc1308746573743234393300bd"
-    "1308746573743234393400be1308746573743234393500bf1308746573743234393600c01308746573743234393700"
-    "c11308746573743234393800c21308746573743234393900c31308746573743235303000c413087465737432353031"
-    "00c51308746573743235303200c61308746573743235303300c71308746573743235303400c8130874657374323530"
-    "3500c91308746573743235303600ca1308746573743235303700cb1308746573743235303800cc1308746573743235"
-    "303900cd1308746573743235313000ce1308746573743235313100cf1308746573743235313200d013087465737432"
-    "35313300d11308746573743235313400d21308746573743235313500d31308746573743235313600d4130874657374"
-    "3235313700d51308746573743235313800d61308746573743235313900d71308746573743235323000d81308746573"
-    "743235323100d91308746573743235323200da1308746573743235323300db1308746573743235323400dc13087465"
-    "73743235323500dd1308746573743235323600de1308746573743235323700df1308746573743235323800e0130874"
-    "6573743235323900e11308746573743235333000e21308746573743235333100e31308746573743235333200e41308"
-    "746573743235333300e51308746573743235333400e61308746573743235333500e71308746573743235333600e813"
-    "08746573743235333700e91308746573743235333800ea1308746573743235333900eb1308746573743235343000ec"
-    "1308746573743235343100ed1308746573743235343200ee1308746573743235343300ef1308746573743235343400"
-    "f01308746573743235343500f11308746573743235343600f21308746573743235343700f313087465737432353438"
-    "00f41308746573743235343900f51308746573743235353000f61308746573743235353100f7130874657374323535"
-    "3200f81308746573743235353300f91308746573743235353400fa1308746573743235353500fb1308746573743235"
-    "353600fc1308746573743235353700fd1308746573743235353800fe1308746573743235353900ff13087465737432"
-    "3536300080140874657374323536310081140874657374323536320082140874657374323536330083140874657374"
-    "3235363400841408746573743235363500851408746573743235363600861408746573743235363700871408746573"
-    "7432353638008814087465737432353639008914087465737432353730008a14087465737432353731008b14087465"
-    "737432353732008c14087465737432353733008d14087465737432353734008e14087465737432353735008f140874"
-    "6573743235373600901408746573743235373700911408746573743235373800921408746573743235373900931408"
-    "7465737432353830009414087465737432353831009514087465737432353832009614087465737432353833009714"
-    "087465737432353834009814087465737432353835009914087465737432353836009a14087465737432353837009b"
-    "14087465737432353838009c14087465737432353839009d14087465737432353930009e1408746573743235393100"
-    "9f1408746573743235393200a01408746573743235393300a11408746573743235393400a214087465737432353935"
-    "00a31408746573743235393600a41408746573743235393700a51408746573743235393800a6140874657374323539"
-    "3900a71408746573743236303000a81408746573743236303100a91408746573743236303200aa1408746573743236"
-    "303300ab1408746573743236303400ac1408746573743236303500ad1408746573743236303600ae14087465737432"
-    "36303700af1408746573743236303800b01408746573743236303900b11408746573743236313000b2140874657374"
-    "3236313100b31408746573743236313200b41408746573743236313300b51408746573743236313400b61408746573"
-    "743236313500b71408746573743236313600b81408746573743236313700b91408746573743236313800ba14087465"
-    "73743236313900bb1408746573743236323000bc1408746573743236323100bd1408746573743236323200be140874"
-    "6573743236323300bf1408746573743236323400c01408746573743236323500c11408746573743236323600c21408"
-    "746573743236323700c31408746573743236323800c41408746573743236323900c51408746573743236333000c614"
-    "08746573743236333100c71408746573743236333200c81408746573743236333300c91408746573743236333400ca"
-    "1408746573743236333500cb1408746573743236333600cc1408746573743236333700cd1408746573743236333800"
-    "ce1408746573743236333900cf1408746573743236343000d01408746573743236343100d114087465737432363432"
-    "00d21408746573743236343300d31408746573743236343400d41408746573743236343500d5140874657374323634"
-    "3600d61408746573743236343700d71408746573743236343800d81408746573743236343900d91408746573743236"
-    "353000da1408746573743236353100db1408746573743236353200dc1408746573743236353300dd14087465737432"
-    "36353400de1408746573743236353500df1408746573743236353600e01408746573743236353700e1140874657374"
-    "3236353800e21408746573743236353900e31408746573743236363000e41408746573743236363100e51408746573"
-    "743236363200e61408746573743236363300e71408746573743236363400e81408746573743236363500e914087465"
-    "73743236363600ea1408746573743236363700eb1408746573743236363800ec1408746573743236363900ed140874"
-    "6573743236373000ee1408746573743236373100ef1408746573743236373200f01408746573743236373300f11408"
-    "746573743236373400f21408746573743236373500f31408746573743236373600f41408746573743236373700f514"
-    "08746573743236373800f61408746573743236373900f71408746573743236383000f81408746573743236383100f9"
-    "1408746573743236383200fa1408746573743236383300fb1408746573743236383400fc1408746573743236383500"
-    "fd1408746573743236383600fe1408746573743236383700ff14087465737432363838008015087465737432363839"
-    "0081150874657374323639300082150874657374323639310083150874657374323639320084150874657374323639"
-    "3300851508746573743236393400861508746573743236393500871508746573743236393600881508746573743236"
-    "3937008915087465737432363938008a15087465737432363939008b15087465737432373030008c15087465737432"
-    "373031008d15087465737432373032008e15087465737432373033008f150874657374323730340090150874657374"
-    "3237303500911508746573743237303600921508746573743237303700931508746573743237303800941508746573"
-    "7432373039009515087465737432373130009615087465737432373131009715087465737432373132009815087465"
-    "737432373133009915087465737432373134009a15087465737432373135009b15087465737432373136009c150874"
-    "65737432373137009d15087465737432373138009e15087465737432373139009f1508746573743237323000a01508"
-    "746573743237323100a11508746573743237323200a21508746573743237323300a31508746573743237323400a415"
-    "08746573743237323500a51508746573743237323600a61508746573743237323700a71508746573743237323800a8"
-    "1508746573743237323900a91508746573743237333000aa1508746573743237333100ab1508746573743237333200"
-    "ac1508746573743237333300ad1508746573743237333400ae1508746573743237333500af15087465737432373336"
-    "00b01508746573743237333700b11508746573743237333800b21508746573743237333900b3150874657374323734"
-    "3000b41508746573743237343100b51508746573743237343200b61508746573743237343300b71508746573743237"
-    "343400b81508746573743237343500b91508746573743237343600ba1508746573743237343700bb15087465737432"
-    "37343800bc1508746573743237343900bd1508746573743237353000be1508746573743237353100bf150874657374"
-    "3237353200c01508746573743237353300c11508746573743237353400c21508746573743237353500c31508746573"
-    "743237353600c41508746573743237353700c51508746573743237353800c61508746573743237353900c715087465"
-    "73743237363000c81508746573743237363100c91508746573743237363200ca1508746573743237363300cb150874"
-    "6573743237363400cc1508746573743237363500cd1508746573743237363600ce1508746573743237363700cf1508"
-    "746573743237363800d01508746573743237363900d11508746573743237373000d21508746573743237373100d315"
-    "08746573743237373200d41508746573743237373300d51508746573743237373400d61508746573743237373500d7"
-    "1508746573743237373600d81508746573743237373700d91508746573743237373800da1508746573743237373900"
-    "db1508746573743237383000dc1508746573743237383100dd1508746573743237383200de15087465737432373833"
-    "00df1508746573743237383400e01508746573743237383500e11508746573743237383600e2150874657374323738"
-    "3700e31508746573743237383800e41508746573743237383900e51508746573743237393000e61508746573743237"
-    "393100e71508746573743237393200e81508746573743237393300e91508746573743237393400ea15087465737432"
-    "37393500eb1508746573743237393600ec1508746573743237393700ed1508746573743237393800ee150874657374"
-    "3237393900ef1508746573743238303000f01508746573743238303100f11508746573743238303200f21508746573"
-    "743238303300f31508746573743238303400f41508746573743238303500f51508746573743238303600f615087465"
-    "73743238303700f71508746573743238303800f81508746573743238303900f91508746573743238313000fa150874"
-    "6573743238313100fb1508746573743238313200fc1508746573743238313300fd1508746573743238313400fe1508"
-    "746573743238313500ff15087465737432383136008016087465737432383137008116087465737432383138008216"
-    "0874657374323831390083160874657374323832300084160874657374323832310085160874657374323832320086"
-    "1608746573743238323300871608746573743238323400881608746573743238323500891608746573743238323600"
-    "8a16087465737432383237008b16087465737432383238008c16087465737432383239008d16087465737432383330"
-    "008e16087465737432383331008f160874657374323833320090160874657374323833330091160874657374323833"
-    "3400921608746573743238333500931608746573743238333600941608746573743238333700951608746573743238"
-    "3338009616087465737432383339009716087465737432383430009816087465737432383431009916087465737432"
-    "383432009a16087465737432383433009b16087465737432383434009c16087465737432383435009d160874657374"
-    "32383436009e16087465737432383437009f1608746573743238343800a01608746573743238343900a11608746573"
-    "743238353000a21608746573743238353100a31608746573743238353200a41608746573743238353300a516087465"
-    "73743238353400a61608746573743238353500a71608746573743238353600a81608746573743238353700a9160874"
-    "6573743238353800aa1608746573743238353900ab1608746573743238363000ac1608746573743238363100ad1608"
-    "746573743238363200ae1608746573743238363300af1608746573743238363400b01608746573743238363500b116"
-    "08746573743238363600b21608746573743238363700b31608746573743238363800b41608746573743238363900b5"
-    "1608746573743238373000b61608746573743238373100b71608746573743238373200b81608746573743238373300"
-    "b91608746573743238373400ba1608746573743238373500bb1608746573743238373600bc16087465737432383737"
-    "00bd1608746573743238373800be1608746573743238373900bf1608746573743238383000c0160874657374323838"
-    "3100c11608746573743238383200c21608746573743238383300c31608746573743238383400c41608746573743238"
-    "383500c51608746573743238383600c61608746573743238383700c71608746573743238383800c816087465737432"
-    "38383900c91608746573743238393000ca1608746573743238393100cb1608746573743238393200cc160874657374"
-    "3238393300cd1608746573743238393400ce1608746573743238393500cf1608746573743238393600d01608746573"
-    "743238393700d11608746573743238393800d21608746573743238393900d31608746573743239303000d416087465"
-    "73743239303100d51608746573743239303200d61608746573743239303300d71608746573743239303400d8160874"
-    "6573743239303500d91608746573743239303600da1608746573743239303700db1608746573743239303800dc1608"
-    "746573743239303900dd1608746573743239313000de1608746573743239313100df1608746573743239313200e016"
-    "08746573743239313300e11608746573743239313400e21608746573743239313500e31608746573743239313600e4"
-    "1608746573743239313700e51608746573743239313800e61608746573743239313900e71608746573743239323000"
-    "e81608746573743239323100e91608746573743239323200ea1608746573743239323300eb16087465737432393234"
-    "00ec1608746573743239323500ed1608746573743239323600ee1608746573743239323700ef160874657374323932"
-    "3800f01608746573743239323900f11608746573743239333000f21608746573743239333100f31608746573743239"
-    "333200f41608746573743239333300f51608746573743239333400f61608746573743239333500f716087465737432"
-    "39333600f81608746573743239333700f91608746573743239333800fa1608746573743239333900fb160874657374"
-    "3239343000fc1608746573743239343100fd1608746573743239343200fe1608746573743239343300ff1608746573"
-    "7432393434008017087465737432393435008117087465737432393436008217087465737432393437008317087465"
-    "7374323934380084170874657374323934390085170874657374323935300086170874657374323935310087170874"
-    "65737432393532008817087465737432393533008917087465737432393534008a17087465737432393535008b1708"
-    "7465737432393536008c17087465737432393537008d17087465737432393538008e17087465737432393539008f17"
-    "0874657374323936300090170874657374323936310091170874657374323936320092170874657374323936330093"
-    "1708746573743239363400941708746573743239363500951708746573743239363600961708746573743239363700"
-    "9717087465737432393638009817087465737432393639009917087465737432393730009a17087465737432393731"
-    "009b17087465737432393732009c17087465737432393733009d17087465737432393734009e170874657374323937"
-    "35009f1708746573743239373600a01708746573743239373700a11708746573743239373800a21708746573743239"
-    "373900a31708746573743239383000a41708746573743239383100a51708746573743239383200a617087465737432"
-    "39383300a71708746573743239383400a81708746573743239383500a91708746573743239383600aa170874657374"
-    "3239383700ab1708746573743239383800ac1708746573743239383900ad1708746573743239393000ae1708746573"
-    "743239393100af1708746573743239393200b01708746573743239393300b11708746573743239393400b217087465"
-    "73743239393500b31708746573743239393600b41708746573743239393700b51708746573743239393800b6170874"
-    "6573743239393900b71708746573743330303000b81708746573743330303100b91708746573743330303200ba1708"
-    "746573743330303300bb1708746573743330303400bc1708746573743330303500bd1708746573743330303600be17"
-    "08746573743330303700bf1708746573743330303800c01708746573743330303900c11708746573743330313000c2"
-    "1708746573743330313100c31708746573743330313200c41708746573743330313300c51708746573743330313400"
-    "c61708746573743330313500c71708746573743330313600c81708746573743330313700c917087465737433303138"
-    "00ca1708746573743330313900cb1708746573743330323000cc1708746573743330323100cd170874657374333032"
-    "3200ce1708746573743330323300cf1708746573743330323400d01708746573743330323500d11708746573743330"
-    "323600d21708746573743330323700d31708746573743330323800d41708746573743330323900d517087465737433"
-    "30333000d61708746573743330333100d71708746573743330333200d81708746573743330333300d9170874657374"
-    "3330333400da1708746573743330333500db1708746573743330333600dc1708746573743330333700dd1708746573"
-    "743330333800de1708746573743330333900df1708746573743330343000e01708746573743330343100e117087465"
-    "73743330343200e21708746573743330343300e31708746573743330343400e41708746573743330343500e5170874"
-    "6573743330343600e61708746573743330343700e71708746573743330343800e81708746573743330343900e91708"
-    "746573743330353000ea1708746573743330353100eb1708746573743330353200ec1708746573743330353300ed17"
-    "08746573743330353400ee1708746573743330353500ef1708746573743330353600f01708746573743330353700f1"
-    "1708746573743330353800f21708746573743330353900f31708746573743330363000f41708746573743330363100"
-    "f51708746573743330363200f61708746573743330363300f71708746573743330363400f817087465737433303635"
-    "00f91708746573743330363600fa1708746573743330363700fb1708746573743330363800fc170874657374333036"
-    "3900fd1708746573743330373000fe1708746573743330373100ff1708746573743330373200801808746573743330"
-    "3733008118087465737433303734008218087465737433303735008318087465737433303736008418087465737433"
-    "3037370085180874657374333037380086180874657374333037390087180874657374333038300088180874657374"
-    "33303831008918087465737433303832008a18087465737433303833008b18087465737433303834008c1808746573"
-    "7433303835008d18087465737433303836008e18087465737433303837008f18087465737433303838009018087465"
-    "7374333038390091180874657374333039300092180874657374333039310093180874657374333039320094180874"
-    "6573743330393300951808746573743330393400961808746573743330393500971808746573743330393600981808"
-    "7465737433303937009918087465737433303938009a18087465737433303939009b18087465737433313030009c18"
-    "087465737433313031009d18087465737433313032009e18087465737433313033009f1808746573743331303400a0"
-    "1808746573743331303500a11808746573743331303600a21808746573743331303700a31808746573743331303800"
-    "a41808746573743331303900a51808746573743331313000a61808746573743331313100a718087465737433313132"
-    "00a81808746573743331313300a91808746573743331313400aa1808746573743331313500ab180874657374333131"
-    "3600ac1808746573743331313700ad1808746573743331313800ae1808746573743331313900af1808746573743331"
-    "323000b01808746573743331323100b11808746573743331323200b21808746573743331323300b318087465737433"
-    "31323400b41808746573743331323500b51808746573743331323600b61808746573743331323700b7180874657374"
-    "3331323800b81808746573743331323900b91808746573743331333000ba1808746573743331333100bb1808746573"
-    "743331333200bc1808746573743331333300bd1808746573743331333400be1808746573743331333500bf18087465"
-    "73743331333600c01808746573743331333700c11808746573743331333800c21808746573743331333900c3180874"
-    "6573743331343000c41808746573743331343100c51808746573743331343200c61808746573743331343300c71808"
-    "746573743331343400c81808746573743331343500c91808746573743331343600ca1808746573743331343700cb18"
-    "08746573743331343800cc1808746573743331343900cd1808746573743331353000ce1808746573743331353100cf"
-    "1808746573743331353200d01808746573743331353300d11808746573743331353400d21808746573743331353500"
-    "d31808746573743331353600d41808746573743331353700d51808746573743331353800d618087465737433313539"
-    "00d71808746573743331363000d81808746573743331363100d91808746573743331363200da180874657374333136"
-    "3300db1808746573743331363400dc1808746573743331363500dd1808746573743331363600de1808746573743331"
-    "363700df1808746573743331363800e01808746573743331363900e11808746573743331373000e218087465737433"
-    "31373100e31808746573743331373200e41808746573743331373300e51808746573743331373400e6180874657374"
-    "3331373500e71808746573743331373600e81808746573743331373700e91808746573743331373800ea1808746573"
-    "743331373900eb1808746573743331383000ec1808746573743331383100ed1808746573743331383200ee18087465"
-    "73743331383300ef1808746573743331383400f01808746573743331383500f11808746573743331383600f2180874"
-    "6573743331383700f31808746573743331383800f41808746573743331383900f51808746573743331393000f61808"
-    "746573743331393100f71808746573743331393200f81808746573743331393300f91808746573743331393400fa18"
-    "08746573743331393500fb1808746573743331393600fc1808746573743331393700fd1808746573743331393800fe"
-    "1808746573743331393900ff1808746573743332303000801908746573743332303100811908746573743332303200"
-    "8219087465737433323033008319087465737433323034008419087465737433323035008519087465737433323036"
-    "0086190874657374333230370087190874657374333230380088190874657374333230390089190874657374333231"
-    "30008a19087465737433323131008b19087465737433323132008c19087465737433323133008d1908746573743332"
-    "3134008e19087465737433323135008f19087465737433323136009019087465737433323137009119087465737433"
-    "3231380092190874657374333231390093190874657374333232300094190874657374333232310095190874657374"
-    "3332323200961908746573743332323300971908746573743332323400981908746573743332323500991908746573"
-    "7433323236009a19087465737433323237009b19087465737433323238009c19087465737433323239009d19087465"
-    "737433323330009e19087465737433323331009f1908746573743332333200a01908746573743332333300a1190874"
-    "6573743332333400a21908746573743332333500a31908746573743332333600a41908746573743332333700a51908"
-    "746573743332333800a61908746573743332333900a71908746573743332343000a81908746573743332343100a919"
-    "08746573743332343200aa1908746573743332343300ab1908746573743332343400ac1908746573743332343500ad"
-    "1908746573743332343600ae1908746573743332343700af1908746573743332343800b01908746573743332343900"
-    "b11908746573743332353000b21908746573743332353100b31908746573743332353200b419087465737433323533"
-    "00b51908746573743332353400b61908746573743332353500b71908746573743332353600b8190874657374333235"
-    "3700b91908746573743332353800ba1908746573743332353900bb1908746573743332363000bc1908746573743332"
-    "363100bd1908746573743332363200be1908746573743332363300bf1908746573743332363400c019087465737433"
-    "32363500c11908746573743332363600c21908746573743332363700c31908746573743332363800c4190874657374"
-    "3332363900c51908746573743332373000c61908746573743332373100c71908746573743332373200c81908746573"
-    "743332373300c91908746573743332373400ca1908746573743332373500cb1908746573743332373600cc19087465"
-    "73743332373700cd1908746573743332373800ce1908746573743332373900cf1908746573743332383000d0190874"
-    "6573743332383100d11908746573743332383200d21908746573743332383300d31908746573743332383400d41908"
-    "746573743332383500d51908746573743332383600d61908746573743332383700d71908746573743332383800d819"
-    "08746573743332383900d91908746573743332393000da1908746573743332393100db1908746573743332393200dc"
-    "1908746573743332393300dd1908746573743332393400de1908746573743332393500df1908746573743332393600"
-    "e01908746573743332393700e11908746573743332393800e21908746573743332393900e319087465737433333030"
-    "00e41908746573743333303100e51908746573743333303200e61908746573743333303300e7190874657374333330"
-    "3400e81908746573743333303500e91908746573743333303600ea1908746573743333303700eb1908746573743333"
-    "303800ec1908746573743333303900ed1908746573743333313000ee1908746573743333313100ef19087465737433"
-    "33313200f01908746573743333313300f11908746573743333313400f21908746573743333313500f3190874657374"
-    "3333313600f41908746573743333313700f51908746573743333313800f61908746573743333313900f71908746573"
-    "743333323000f81908746573743333323100f91908746573743333323200fa1908746573743333323300fb19087465"
-    "73743333323400fc1908746573743333323500fd1908746573743333323600fe1908746573743333323700ff190874"
-    "6573743333323800801a08746573743333323900811a08746573743333333000821a08746573743333333100831a08"
-    "746573743333333200841a08746573743333333300851a08746573743333333400861a08746573743333333500871a"
-    "08746573743333333600881a08746573743333333700891a087465737433333338008a1a087465737433333339008b"
-    "1a087465737433333430008c1a087465737433333431008d1a087465737433333432008e1a08746573743333343300"
-    "8f1a08746573743333343400901a08746573743333343500911a08746573743333343600921a087465737433333437"
-    "00931a08746573743333343800941a08746573743333343900951a08746573743333353000961a0874657374333335"
-    "3100971a08746573743333353200981a08746573743333353300991a087465737433333534009a1a08746573743333"
-    "3535009b1a087465737433333536009c1a087465737433333537009d1a087465737433333538009e1a087465737433"
-    "333539009f1a08746573743333363000a01a08746573743333363100a11a08746573743333363200a21a0874657374"
-    "3333363300a31a08746573743333363400a41a08746573743333363500a51a08746573743333363600a61a08746573"
-    "743333363700a71a08746573743333363800a81a08746573743333363900a91a08746573743333373000aa1a087465"
-    "73743333373100ab1a08746573743333373200ac1a08746573743333373300ad1a08746573743333373400ae1a0874"
-    "6573743333373500af1a08746573743333373600b01a08746573743333373700b11a08746573743333373800b21a08"
-    "746573743333373900b31a08746573743333383000b41a08746573743333383100b51a08746573743333383200b61a"
-    "08746573743333383300b71a08746573743333383400b81a08746573743333383500b91a08746573743333383600ba"
-    "1a08746573743333383700bb1a08746573743333383800bc1a08746573743333383900bd1a08746573743333393000"
-    "be1a08746573743333393100bf1a08746573743333393200c01a08746573743333393300c11a087465737433333934"
-    "00c21a08746573743333393500c31a08746573743333393600c41a08746573743333393700c51a0874657374333339"
-    "3800c61a08746573743333393900c71a08746573743334303000c81a08746573743334303100c91a08746573743334"
-    "303200ca1a08746573743334303300cb1a08746573743334303400cc1a08746573743334303500cd1a087465737433"
-    "34303600ce1a08746573743334303700cf1a08746573743334303800d01a08746573743334303900d11a0874657374"
-    "3334313000d21a08746573743334313100d31a08746573743334313200d41a08746573743334313300d51a08746573"
-    "743334313400d61a08746573743334313500d71a08746573743334313600d81a08746573743334313700d91a087465"
-    "73743334313800da1a08746573743334313900db1a08746573743334323000dc1a08746573743334323100dd1a0874"
-    "6573743334323200de1a08746573743334323300df1a08746573743334323400e01a08746573743334323500e11a08"
-    "746573743334323600e21a08746573743334323700e31a08746573743334323800e41a08746573743334323900e51a"
-    "08746573743334333000e61a08746573743334333100e71a08746573743334333200e81a08746573743334333300e9"
-    "1a08746573743334333400ea1a08746573743334333500eb1a08746573743334333600ec1a08746573743334333700"
-    "ed1a08746573743334333800ee1a08746573743334333900ef1a08746573743334343000f01a087465737433343431"
-    "00f11a08746573743334343200f21a08746573743334343300f31a08746573743334343400f41a0874657374333434"
-    "3500f51a08746573743334343600f61a08746573743334343700f71a08746573743334343800f81a08746573743334"
-    "343900f91a08746573743334353000fa1a08746573743334353100fb1a08746573743334353200fc1a087465737433"
-    "34353300fd1a08746573743334353400fe1a08746573743334353500ff1a08746573743334353600801b0874657374"
-    "3334353700811b08746573743334353800821b08746573743334353900831b08746573743334363000841b08746573"
-    "743334363100851b08746573743334363200861b08746573743334363300871b08746573743334363400881b087465"
-    "73743334363500891b087465737433343636008a1b087465737433343637008b1b087465737433343638008c1b0874"
-    "65737433343639008d1b087465737433343730008e1b087465737433343731008f1b08746573743334373200901b08"
-    "746573743334373300911b08746573743334373400921b08746573743334373500931b08746573743334373600941b"
-    "08746573743334373700951b08746573743334373800961b08746573743334373900971b0874657374333438300098"
-    "1b08746573743334383100991b087465737433343832009a1b087465737433343833009b1b08746573743334383400"
-    "9c1b087465737433343835009d1b087465737433343836009e1b087465737433343837009f1b087465737433343838"
-    "00a01b08746573743334383900a11b08746573743334393000a21b08746573743334393100a31b0874657374333439"
-    "3200a41b08746573743334393300a51b08746573743334393400a61b08746573743334393500a71b08746573743334"
-    "393600a81b08746573743334393700a91b08746573743334393800aa1b08746573743334393900ab1b087465737433"
-    "35303000ac1b08746573743335303100ad1b08746573743335303200ae1b08746573743335303300af1b0874657374"
-    "3335303400b01b08746573743335303500b11b08746573743335303600b21b08746573743335303700b31b08746573"
-    "743335303800b41b08746573743335303900b51b08746573743335313000b61b08746573743335313100b71b087465"
-    "73743335313200b81b08746573743335313300b91b08746573743335313400ba1b08746573743335313500bb1b0874"
-    "6573743335313600bc1b08746573743335313700bd1b08746573743335313800be1b08746573743335313900bf1b08"
-    "746573743335323000c01b08746573743335323100c11b08746573743335323200c21b08746573743335323300c31b"
-    "08746573743335323400c41b08746573743335323500c51b08746573743335323600c61b08746573743335323700c7"
-    "1b08746573743335323800c81b08746573743335323900c91b08746573743335333000ca1b08746573743335333100"
-    "cb1b08746573743335333200cc1b08746573743335333300cd1b08746573743335333400ce1b087465737433353335"
-    "00cf1b08746573743335333600d01b08746573743335333700d11b08746573743335333800d21b0874657374333533"
-    "3900d31b08746573743335343000d41b08746573743335343100d51b08746573743335343200d61b08746573743335"
-    "343300d71b08746573743335343400d81b08746573743335343500d91b08746573743335343600da1b087465737433"
-    "35343700db1b08746573743335343800dc1b08746573743335343900dd1b08746573743335353000de1b0874657374"
-    "3335353100df1b08746573743335353200e01b08746573743335353300e11b08746573743335353400e21b08746573"
-    "743335353500e31b08746573743335353600e41b08746573743335353700e51b08746573743335353800e61b087465"
-    "73743335353900e71b08746573743335363000e81b08746573743335363100e91b08746573743335363200ea1b0874"
-    "6573743335363300eb1b08746573743335363400ec1b08746573743335363500ed1b08746573743335363600ee1b08"
-    "746573743335363700ef1b08746573743335363800f01b08746573743335363900f11b08746573743335373000f21b"
-    "08746573743335373100f31b08746573743335373200f41b08746573743335373300f51b08746573743335373400f6"
-    "1b08746573743335373500f71b08746573743335373600f81b08746573743335373700f91b08746573743335373800"
-    "fa1b08746573743335373900fb1b08746573743335383000fc1b08746573743335383100fd1b087465737433353832"
-    "00fe1b08746573743335383300ff1b08746573743335383400801c08746573743335383500811c0874657374333538"
-    "3600821c08746573743335383700831c08746573743335383800841c08746573743335383900851c08746573743335"
-    "393000861c08746573743335393100871c08746573743335393200881c08746573743335393300891c087465737433"
-    "353934008a1c087465737433353935008b1c087465737433353936008c1c087465737433353937008d1c0874657374"
-    "33353938008e1c087465737433353939008f1c08746573743336303000901c08746573743336303100911c08746573"
-    "743336303200921c08746573743336303300931c08746573743336303400941c08746573743336303500951c087465"
-    "73743336303600961c08746573743336303700971c08746573743336303800981c08746573743336303900991c0874"
-    "65737433363130009a1c087465737433363131009b1c087465737433363132009c1c087465737433363133009d1c08"
-    "7465737433363134009e1c087465737433363135009f1c08746573743336313600a01c08746573743336313700a11c"
-    "08746573743336313800a21c08746573743336313900a31c08746573743336323000a41c08746573743336323100a5"
-    "1c08746573743336323200a61c08746573743336323300a71c08746573743336323400a81c08746573743336323500"
-    "a91c08746573743336323600aa1c08746573743336323700ab1c08746573743336323800ac1c087465737433363239"
-    "00ad1c08746573743336333000ae1c08746573743336333100af1c08746573743336333200b01c0874657374333633"
-    "3300b11c08746573743336333400b21c08746573743336333500b31c08746573743336333600b41c08746573743336"
-    "333700b51c08746573743336333800b61c08746573743336333900b71c08746573743336343000b81c087465737433"
-    "36343100b91c08746573743336343200ba1c08746573743336343300bb1c08746573743336343400bc1c0874657374"
-    "3336343500bd1c08746573743336343600be1c08746573743336343700bf1c08746573743336343800c01c08746573"
-    "743336343900c11c08746573743336353000c21c08746573743336353100c31c08746573743336353200c41c087465"
-    "73743336353300c51c08746573743336353400c61c08746573743336353500c71c08746573743336353600c81c0874"
-    "6573743336353700c91c08746573743336353800ca1c08746573743336353900cb1c08746573743336363000cc1c08"
-    "746573743336363100cd1c08746573743336363200ce1c08746573743336363300cf1c08746573743336363400d01c"
-    "08746573743336363500d11c08746573743336363600d21c08746573743336363700d31c08746573743336363800d4"
-    "1c08746573743336363900d51c08746573743336373000d61c08746573743336373100d71c08746573743336373200"
-    "d81c08746573743336373300d91c08746573743336373400da1c08746573743336373500db1c087465737433363736"
-    "00dc1c08746573743336373700dd1c08746573743336373800de1c08746573743336373900df1c0874657374333638"
-    "3000e01c08746573743336383100e11c08746573743336383200e21c08746573743336383300e31c08746573743336"
-    "383400e41c08746573743336383500e51c08746573743336383600e61c08746573743336383700e71c087465737433"
-    "36383800e81c08746573743336383900e91c08746573743336393000ea1c08746573743336393100eb1c0874657374"
-    "3336393200ec1c08746573743336393300ed1c08746573743336393400ee1c08746573743336393500ef1c08746573"
-    "743336393600f01c08746573743336393700f11c08746573743336393800f21c08746573743336393900f31c087465"
-    "73743337303000f41c08746573743337303100f51c08746573743337303200f61c08746573743337303300f71c0874"
-    "6573743337303400f81c08746573743337303500f91c08746573743337303600fa1c08746573743337303700fb1c08"
-    "746573743337303800fc1c08746573743337303900fd1c08746573743337313000fe1c08746573743337313100ff1c"
-    "08746573743337313200801d08746573743337313300811d08746573743337313400821d0874657374333731350083"
-    "1d08746573743337313600841d08746573743337313700851d08746573743337313800861d08746573743337313900"
-    "871d08746573743337323000881d08746573743337323100891d087465737433373232008a1d087465737433373233"
-    "008b1d087465737433373234008c1d087465737433373235008d1d087465737433373236008e1d0874657374333732"
-    "37008f1d08746573743337323800901d08746573743337323900911d08746573743337333000921d08746573743337"
-    "333100931d08746573743337333200941d08746573743337333300951d08746573743337333400961d087465737433"
-    "37333500971d08746573743337333600981d08746573743337333700991d087465737433373338009a1d0874657374"
-    "33373339009b1d087465737433373430009c1d087465737433373431009d1d087465737433373432009e1d08746573"
-    "7433373433009f1d08746573743337343400a01d08746573743337343500a11d08746573743337343600a21d087465"
-    "73743337343700a31d08746573743337343800a41d08746573743337343900a51d08746573743337353000a61d0874"
-    "6573743337353100a71d08746573743337353200a81d08746573743337353300a91d08746573743337353400aa1d08"
-    "746573743337353500ab1d08746573743337353600ac1d08746573743337353700ad1d08746573743337353800ae1d"
-    "08746573743337353900af1d08746573743337363000b01d08746573743337363100b11d08746573743337363200b2"
-    "1d08746573743337363300b31d08746573743337363400b41d08746573743337363500b51d08746573743337363600"
-    "b61d08746573743337363700b71d08746573743337363800b81d08746573743337363900b91d087465737433373730"
-    "00ba1d08746573743337373100bb1d08746573743337373200bc1d08746573743337373300bd1d0874657374333737"
-    "3400be1d08746573743337373500bf1d08746573743337373600c01d08746573743337373700c11d08746573743337"
-    "373800c21d08746573743337373900c31d08746573743337383000c41d08746573743337383100c51d087465737433"
-    "37383200c61d08746573743337383300c71d08746573743337383400c81d08746573743337383500c91d0874657374"
-    "3337383600ca1d08746573743337383700cb1d08746573743337383800cc1d08746573743337383900cd1d08746573"
-    "743337393000ce1d08746573743337393100cf1d08746573743337393200d01d08746573743337393300d11d087465"
-    "73743337393400d21d08746573743337393500d31d08746573743337393600d41d08746573743337393700d51d0874"
-    "6573743337393800d61d08746573743337393900d71d08746573743338303000d81d08746573743338303100d91d08"
-    "746573743338303200da1d08746573743338303300db1d08746573743338303400dc1d08746573743338303500dd1d"
-    "08746573743338303600de1d08746573743338303700df1d08746573743338303800e01d08746573743338303900e1"
-    "1d08746573743338313000e21d08746573743338313100e31d08746573743338313200e41d08746573743338313300"
-    "e51d08746573743338313400e61d08746573743338313500e71d08746573743338313600e81d087465737433383137"
-    "00e91d08746573743338313800ea1d08746573743338313900eb1d08746573743338323000ec1d0874657374333832"
-    "3100ed1d08746573743338323200ee1d08746573743338323300ef1d08746573743338323400f01d08746573743338"
-    "323500f11d08746573743338323600f21d08746573743338323700f31d08746573743338323800f41d087465737433"
-    "38323900f51d08746573743338333000f61d08746573743338333100f71d08746573743338333200f81d0874657374"
-    "3338333300f91d08746573743338333400fa1d08746573743338333500fb1d08746573743338333600fc1d08746573"
-    "743338333700fd1d08746573743338333800fe1d08746573743338333900ff1d08746573743338343000801e087465"
-    "73743338343100811e08746573743338343200821e08746573743338343300831e08746573743338343400841e0874"
-    "6573743338343500851e08746573743338343600861e08746573743338343700871e08746573743338343800881e08"
-    "746573743338343900891e087465737433383530008a1e087465737433383531008b1e087465737433383532008c1e"
-    "087465737433383533008d1e087465737433383534008e1e087465737433383535008f1e0874657374333835360090"
-    "1e08746573743338353700911e08746573743338353800921e08746573743338353900931e08746573743338363000"
-    "941e08746573743338363100951e08746573743338363200961e08746573743338363300971e087465737433383634"
-    "00981e08746573743338363500991e087465737433383636009a1e087465737433383637009b1e0874657374333836"
-    "38009c1e087465737433383639009d1e087465737433383730009e1e087465737433383731009f1e08746573743338"
-    "373200a01e08746573743338373300a11e08746573743338373400a21e08746573743338373500a31e087465737433"
-    "38373600a41e08746573743338373700a51e08746573743338373800a61e08746573743338373900a71e0874657374"
-    "3338383000a81e08746573743338383100a91e08746573743338383200aa1e08746573743338383300ab1e08746573"
-    "743338383400ac1e08746573743338383500ad1e08746573743338383600ae1e08746573743338383700af1e087465"
-    "73743338383800b01e08746573743338383900b11e08746573743338393000b21e08746573743338393100b31e0874"
-    "6573743338393200b41e08746573743338393300b51e08746573743338393400b61e08746573743338393500b71e08"
-    "746573743338393600b81e08746573743338393700b91e08746573743338393800ba1e08746573743338393900bb1e"
-    "08746573743339303000bc1e08746573743339303100bd1e08746573743339303200be1e08746573743339303300bf"
-    "1e08746573743339303400c01e08746573743339303500c11e08746573743339303600c21e08746573743339303700"
-    "c31e08746573743339303800c41e08746573743339303900c51e08746573743339313000c61e087465737433393131"
-    "00c71e08746573743339313200c81e08746573743339313300c91e08746573743339313400ca1e0874657374333931"
-    "3500cb1e08746573743339313600cc1e08746573743339313700cd1e08746573743339313800ce1e08746573743339"
-    "313900cf1e08746573743339323000d01e08746573743339323100d11e08746573743339323200d21e087465737433"
-    "39323300d31e08746573743339323400d41e08746573743339323500d51e08746573743339323600d61e0874657374"
-    "3339323700d71e08746573743339323800d81e08746573743339323900d91e08746573743339333000da1e08746573"
-    "743339333100db1e08746573743339333200dc1e08746573743339333300dd1e08746573743339333400de1e087465"
-    "73743339333500df1e08746573743339333600e01e08746573743339333700e11e08746573743339333800e21e0874"
-    "6573743339333900e31e08746573743339343000e41e08746573743339343100e51e08746573743339343200e61e08"
-    "746573743339343300e71e08746573743339343400e81e08746573743339343500e91e08746573743339343600ea1e"
-    "08746573743339343700eb1e08746573743339343800ec1e08746573743339343900ed1e08746573743339353000ee"
-    "1e08746573743339353100ef1e08746573743339353200f01e08746573743339353300f11e08746573743339353400"
-    "f21e08746573743339353500f31e08746573743339353600f41e08746573743339353700f51e087465737433393538"
-    "00f61e08746573743339353900f71e08746573743339363000f81e08746573743339363100f91e0874657374333936"
-    "3200fa1e08746573743339363300fb1e08746573743339363400fc1e08746573743339363500fd1e08746573743339"
-    "363600fe1e08746573743339363700ff1e08746573743339363800801f08746573743339363900811f087465737433"
-    "39373000821f08746573743339373100831f08746573743339373200841f08746573743339373300851f0874657374"
-    "3339373400861f08746573743339373500871f08746573743339373600881f08746573743339373700891f08746573"
-    "7433393738008a1f087465737433393739008b1f087465737433393830008c1f087465737433393831008d1f087465"
-    "737433393832008e1f087465737433393833008f1f08746573743339383400901f08746573743339383500911f0874"
-    "6573743339383600921f08746573743339383700931f08746573743339383800941f08746573743339383900951f08"
-    "746573743339393000961f08746573743339393100971f08746573743339393200981f08746573743339393300991f"
-    "087465737433393934009a1f087465737433393935009b1f087465737433393936009c1f087465737433393937009d"
-    "1f087465737433393938009e1f087465737433393939009f1f08746573743430303000a01f08746573743430303100"
-    "a11f08746573743430303200a21f08746573743430303300a31f08746573743430303400a41f087465737434303035"
-    "00a51f08746573743430303600a61f08746573743430303700a71f08746573743430303800a81f0874657374343030"
-    "3900a91f08746573743430313000aa1f08746573743430313100ab1f08746573743430313200ac1f08746573743430"
-    "313300ad1f08746573743430313400ae1f08746573743430313500af1f08746573743430313600b01f087465737434"
-    "30313700b11f08746573743430313800b21f08746573743430313900b31f08746573743430323000b41f0874657374"
-    "3430323100b51f08746573743430323200b61f08746573743430323300b71f08746573743430323400b81f08746573"
-    "743430323500b91f08746573743430323600ba1f08746573743430323700bb1f08746573743430323800bc1f087465"
-    "73743430323900bd1f08746573743430333000be1f08746573743430333100bf1f08746573743430333200c01f0874"
-    "6573743430333300c11f08746573743430333400c21f08746573743430333500c31f08746573743430333600c41f08"
-    "746573743430333700c51f08746573743430333800c61f08746573743430333900c71f08746573743430343000c81f"
-    "08746573743430343100c91f08746573743430343200ca1f08746573743430343300cb1f08746573743430343400cc"
-    "1f08746573743430343500cd1f08746573743430343600ce1f08746573743430343700cf1f08746573743430343800"
-    "d01f08746573743430343900d11f08746573743430353000d21f08746573743430353100d31f087465737434303532"
-    "00d41f08746573743430353300d51f08746573743430353400d61f08746573743430353500d71f0874657374343035"
-    "3600d81f08746573743430353700d91f08746573743430353800da1f08746573743430353900db1f08746573743430"
-    "363000dc1f08746573743430363100dd1f08746573743430363200de1f08746573743430363300df1f087465737434"
-    "30363400e01f08746573743430363500e11f08746573743430363600e21f08746573743430363700e31f0874657374"
-    "3430363800e41f08746573743430363900e51f08746573743430373000e61f08746573743430373100e71f08746573"
-    "743430373200e81f08746573743430373300e91f08746573743430373400ea1f08746573743430373500eb1f087465"
-    "73743430373600ec1f08746573743430373700ed1f08746573743430373800ee1f08746573743430373900ef1f0874"
-    "6573743430383000f01f08746573743430383100f11f08746573743430383200f21f08746573743430383300f31f08"
-    "746573743430383400f41f08746573743430383500f51f08746573743430383600f61f08746573743430383700f71f"
-    "08746573743430383800f81f08746573743430383900f91f08746573743430393000fa1f08746573743430393100fb"
-    "1f08746573743430393200fc1f08746573743430393300fd1f08746573743430393400fe1f08746573743430393500"
-    "ff1f087465737434303936008020087465737434303937008120087465737434303938008220087465737434303939"
-    "0083200874657374343130300084200874657374343130310085200874657374343130320086200874657374343130"
-    "33008720087465737434313034008820087465737434313035008920087465737434313036008a2008746573743431"
-    "3037008b20087465737434313038008c20087465737434313039008d20087465737434313130008e20087465737434"
-    "313131008f200874657374343131320090200874657374343131330091200874657374343131340092200874657374"
-    "3431313500932008746573743431313600942008746573743431313700952008746573743431313800962008746573"
-    "7434313139009720087465737434313230009820087465737434313231009920087465737434313232009a20087465"
-    "737434313233009b20087465737434313234009c20087465737434313235009d20087465737434313236009e200874"
-    "65737434313237009f2008746573743431323800a02008746573743431323900a12008746573743431333000a22008"
-    "746573743431333100a32008746573743431333200a42008746573743431333300a52008746573743431333400a620"
-    "08746573743431333500a72008746573743431333600a82008746573743431333700a92008746573743431333800aa"
-    "2008746573743431333900ab2008746573743431343000ac2008746573743431343100ad2008746573743431343200"
-    "ae2008746573743431343300af2008746573743431343400b02008746573743431343500b120087465737434313436"
-    "00b22008746573743431343700b32008746573743431343800b42008746573743431343900b5200874657374343135"
-    "3000b62008746573743431353100b72008746573743431353200b82008746573743431353300b92008746573743431"
-    "353400ba2008746573743431353500bb2008746573743431353600bc2008746573743431353700bd20087465737434"
-    "31353800be2008746573743431353900bf2008746573743431363000c02008746573743431363100c1200874657374"
-    "3431363200c22008746573743431363300c32008746573743431363400c42008746573743431363500c52008746573"
-    "743431363600c62008746573743431363700c72008746573743431363800c82008746573743431363900c920087465"
-    "73743431373000ca2008746573743431373100cb2008746573743431373200cc2008746573743431373300cd200874"
-    "6573743431373400ce2008746573743431373500cf2008746573743431373600d02008746573743431373700d12008"
-    "746573743431373800d22008746573743431373900d32008746573743431383000d42008746573743431383100d520"
-    "08746573743431383200d62008746573743431383300d72008746573743431383400d82008746573743431383500d9"
-    "2008746573743431383600da2008746573743431383700db2008746573743431383800dc2008746573743431383900"
-    "dd2008746573743431393000de2008746573743431393100df2008746573743431393200e020087465737434313933"
-    "00e12008746573743431393400e22008746573743431393500e32008746573743431393600e4200874657374343139"
-    "3700e52008746573743431393800e62008746573743431393900e72008746573743432303000e82008746573743432"
-    "303100e92008746573743432303200ea2008746573743432303300eb2008746573743432303400ec20087465737434"
-    "32303500ed2008746573743432303600ee2008746573743432303700ef2008746573743432303800f0200874657374"
-    "3432303900f12008746573743432313000f22008746573743432313100f32008746573743432313200f42008746573"
-    "743432313300f52008746573743432313400f62008746573743432313500f72008746573743432313600f820087465"
-    "73743432313700f92008746573743432313800fa2008746573743432313900fb2008746573743432323000fc200874"
-    "6573743432323100fd2008746573743432323200fe2008746573743432323300ff2008746573743432323400802108"
-    "7465737434323235008121087465737434323236008221087465737434323237008321087465737434323238008421"
-    "0874657374343232390085210874657374343233300086210874657374343233310087210874657374343233320088"
-    "21087465737434323333008921087465737434323334008a21087465737434323335008b2108746573743432333600"
-    "8c21087465737434323337008d21087465737434323338008e21087465737434323339008f21087465737434323430"
-    "0090210874657374343234310091210874657374343234320092210874657374343234330093210874657374343234"
-    "3400942108746573743432343500952108746573743432343600962108746573743432343700972108746573743432"
-    "3438009821087465737434323439009921087465737434323530009a21087465737434323531009b21087465737434"
-    "323532009c21087465737434323533009d21087465737434323534009e21087465737434323535009f210874657374"
-    "3432353600a02108746573743432353700a12108746573743432353800a22108746573743432353900a32108746573"
-    "743432363000a42108746573743432363100a52108746573743432363200a62108746573743432363300a721087465"
-    "73743432363400a82108746573743432363500a92108746573743432363600aa2108746573743432363700ab210874"
-    "6573743432363800ac2108746573743432363900ad2108746573743432373000ae2108746573743432373100af2108"
-    "746573743432373200b02108746573743432373300b12108746573743432373400b22108746573743432373500b321"
-    "08746573743432373600b42108746573743432373700b52108746573743432373800b62108746573743432373900b7"
-    "2108746573743432383000b82108746573743432383100b92108746573743432383200ba2108746573743432383300"
-    "bb2108746573743432383400bc2108746573743432383500bd2108746573743432383600be21087465737434323837"
-    "00bf2108746573743432383800c02108746573743432383900c12108746573743432393000c2210874657374343239"
-    "3100c32108746573743432393200c42108746573743432393300c52108746573743432393400c62108746573743432"
-    "393500c72108746573743432393600c82108746573743432393700c92108746573743432393800ca21087465737434"
-    "32393900cb2108746573743433303000cc2108746573743433303100cd2108746573743433303200ce210874657374"
-    "3433303300cf2108746573743433303400d02108746573743433303500d12108746573743433303600d22108746573"
-    "743433303700d32108746573743433303800d42108746573743433303900d52108746573743433313000d621087465"
-    "73743433313100d72108746573743433313200d82108746573743433313300d92108746573743433313400da210874"
-    "6573743433313500db2108746573743433313600dc2108746573743433313700dd2108746573743433313800de2108"
-    "746573743433313900df2108746573743433323000e02108746573743433323100e12108746573743433323200e221"
-    "08746573743433323300e32108746573743433323400e42108746573743433323500e52108746573743433323600e6"
-    "2108746573743433323700e72108746573743433323800e82108746573743433323900e92108746573743433333000"
-    "ea2108746573743433333100eb2108746573743433333200ec2108746573743433333300ed21087465737434333334"
-    "00ee2108746573743433333500ef2108746573743433333600f02108746573743433333700f1210874657374343333"
-    "3800f22108746573743433333900f32108746573743433343000f42108746573743433343100f52108746573743433"
-    "343200f62108746573743433343300f72108746573743433343400f82108746573743433343500f921087465737434"
-    "33343600fa2108746573743433343700fb2108746573743433343800fc2108746573743433343900fd210874657374"
-    "3433353000fe2108746573743433353100ff2108746573743433353200802208746573743433353300812208746573"
-    "7434333534008222087465737434333535008322087465737434333536008422087465737434333537008522087465"
-    "7374343335380086220874657374343335390087220874657374343336300088220874657374343336310089220874"
-    "65737434333632008a22087465737434333633008b22087465737434333634008c22087465737434333635008d2208"
-    "7465737434333636008e22087465737434333637008f22087465737434333638009022087465737434333639009122"
-    "0874657374343337300092220874657374343337310093220874657374343337320094220874657374343337330095"
-    "2208746573743433373400962208746573743433373500972208746573743433373600982208746573743433373700"
-    "9922087465737434333738009a22087465737434333739009b22087465737434333830009c22087465737434333831"
-    "009d22087465737434333832009e22087465737434333833009f2208746573743433383400a0220874657374343338"
-    "3500a12208746573743433383600a22208746573743433383700a32208746573743433383800a42208746573743433"
-    "383900a52208746573743433393000a62208746573743433393100a72208746573743433393200a822087465737434"
-    "33393300a92208746573743433393400aa2208746573743433393500ab2208746573743433393600ac220874657374"
-    "3433393700ad2208746573743433393800ae2208746573743433393900af2208746573743434303000b02208746573"
-    "743434303100b12208746573743434303200b22208746573743434303300b32208746573743434303400b422087465"
-    "73743434303500b52208746573743434303600b62208746573743434303700b72208746573743434303800b8220874"
-    "6573743434303900b92208746573743434313000ba2208746573743434313100bb2208746573743434313200bc2208"
-    "746573743434313300bd2208746573743434313400be2208746573743434313500bf2208746573743434313600c022"
-    "08746573743434313700c12208746573743434313800c22208746573743434313900c32208746573743434323000c4"
-    "2208746573743434323100c52208746573743434323200c62208746573743434323300c72208746573743434323400"
-    "c82208746573743434323500c92208746573743434323600ca2208746573743434323700cb22087465737434343238"
-    "00cc2208746573743434323900cd2208746573743434333000ce2208746573743434333100cf220874657374343433"
-    "3200d02208746573743434333300d12208746573743434333400d22208746573743434333500d32208746573743434"
-    "333600d42208746573743434333700d52208746573743434333800d62208746573743434333900d722087465737434"
-    "34343000d82208746573743434343100d92208746573743434343200da2208746573743434343300db220874657374"
-    "3434343400dc2208746573743434343500dd2208746573743434343600de2208746573743434343700df2208746573"
-    "743434343800e02208746573743434343900e12208746573743434353000e22208746573743434353100e322087465"
-    "73743434353200e42208746573743434353300e52208746573743434353400e62208746573743434353500e7220874"
-    "6573743434353600e82208746573743434353700e92208746573743434353800ea2208746573743434353900eb2208"
-    "746573743434363000ec2208746573743434363100ed2208746573743434363200ee2208746573743434363300ef22"
-    "08746573743434363400f02208746573743434363500f12208746573743434363600f22208746573743434363700f3"
-    "2208746573743434363800f42208746573743434363900f52208746573743434373000f62208746573743434373100"
-    "f72208746573743434373200f82208746573743434373300f92208746573743434373400fa22087465737434343735"
-    "00fb2208746573743434373600fc2208746573743434373700fd2208746573743434373800fe220874657374343437"
-    "3900ff2208746573743434383000802308746573743434383100812308746573743434383200822308746573743434"
-    "3833008323087465737434343834008423087465737434343835008523087465737434343836008623087465737434"
-    "343837008723087465737434343838008823087465737434343839008923087465737434343930008a230874657374"
-    "34343931008b23087465737434343932008c23087465737434343933008d23087465737434343934008e2308746573"
-    "7434343935008f23087465737434343936009023087465737434343937009123087465737434343938009223087465"
-    "7374343439390093230874657374343530300094230874657374343530310095230874657374343530320096230874"
-    "65737434353033009723087465737434353034009823087465737434353035009923087465737434353036009a2308"
-    "7465737434353037009b23087465737434353038009c23087465737434353039009d23087465737434353130009e23"
-    "087465737434353131009f2308746573743435313200a02308746573743435313300a12308746573743435313400a2"
-    "2308746573743435313500a32308746573743435313600a42308746573743435313700a52308746573743435313800"
-    "a62308746573743435313900a72308746573743435323000a82308746573743435323100a923087465737434353232"
-    "00aa2308746573743435323300ab2308746573743435323400ac2308746573743435323500ad230874657374343532"
-    "3600ae2308746573743435323700af2308746573743435323800b02308746573743435323900b12308746573743435"
-    "333000b22308746573743435333100b32308746573743435333200b42308746573743435333300b523087465737434"
-    "35333400b62308746573743435333500b72308746573743435333600b82308746573743435333700b9230874657374"
-    "3435333800ba2308746573743435333900bb2308746573743435343000bc2308746573743435343100bd2308746573"
-    "743435343200be2308746573743435343300bf2308746573743435343400c02308746573743435343500c123087465"
-    "73743435343600c22308746573743435343700c32308746573743435343800c42308746573743435343900c5230874"
-    "6573743435353000c62308746573743435353100c72308746573743435353200c82308746573743435353300c92308"
-    "746573743435353400ca2308746573743435353500cb2308746573743435353600cc2308746573743435353700cd23"
-    "08746573743435353800ce2308746573743435353900cf2308746573743435363000d02308746573743435363100d1"
-    "2308746573743435363200d22308746573743435363300d32308746573743435363400d42308746573743435363500"
-    "d52308746573743435363600d62308746573743435363700d72308746573743435363800d823087465737434353639"
-    "00d92308746573743435373000da2308746573743435373100db2308746573743435373200dc230874657374343537"
-    "3300dd2308746573743435373400de2308746573743435373500df2308746573743435373600e02308746573743435"
-    "373700e12308746573743435373800e22308746573743435373900e32308746573743435383000e423087465737434"
-    "35383100e52308746573743435383200e62308746573743435383300e72308746573743435383400e8230874657374"
-    "3435383500e92308746573743435383600ea2308746573743435383700eb2308746573743435383800ec2308746573"
-    "743435383900ed2308746573743435393000ee2308746573743435393100ef2308746573743435393200f023087465"
-    "73743435393300f12308746573743435393400f22308746573743435393500f32308746573743435393600f4230874"
-    "6573743435393700f52308746573743435393800f62308746573743435393900f72308746573743436303000f82308"
-    "746573743436303100f92308746573743436303200fa2308746573743436303300fb2308746573743436303400fc23"
-    "08746573743436303500fd2308746573743436303600fe2308746573743436303700ff230874657374343630380080"
-    "2408746573743436303900812408746573743436313000822408746573743436313100832408746573743436313200"
-    "8424087465737434363133008524087465737434363134008624087465737434363135008724087465737434363136"
-    "008824087465737434363137008924087465737434363138008a24087465737434363139008b240874657374343632"
-    "30008c24087465737434363231008d24087465737434363232008e24087465737434363233008f2408746573743436"
-    "3234009024087465737434363235009124087465737434363236009224087465737434363237009324087465737434"
-    "3632380094240874657374343632390095240874657374343633300096240874657374343633310097240874657374"
-    "34363332009824087465737434363333009924087465737434363334009a24087465737434363335009b2408746573"
-    "7434363336009c24087465737434363337009d24087465737434363338009e24087465737434363339009f24087465"
-    "73743436343000a02408746573743436343100a12408746573743436343200a22408746573743436343300a3240874"
-    "6573743436343400a42408746573743436343500a52408746573743436343600a62408746573743436343700a72408"
-    "746573743436343800a82408746573743436343900a92408746573743436353000aa2408746573743436353100ab24"
-    "08746573743436353200ac2408746573743436353300ad2408746573743436353400ae2408746573743436353500af"
-    "2408746573743436353600b02408746573743436353700b12408746573743436353800b22408746573743436353900"
-    "b32408746573743436363000b42408746573743436363100b52408746573743436363200b624087465737434363633"
-    "00b72408746573743436363400b82408746573743436363500b92408746573743436363600ba240874657374343636"
-    "3700bb2408746573743436363800bc2408746573743436363900bd2408746573743436373000be2408746573743436"
-    "373100bf2408746573743436373200c02408746573743436373300c12408746573743436373400c224087465737434"
-    "36373500c32408746573743436373600c42408746573743436373700c52408746573743436373800c6240874657374"
-    "3436373900c72408746573743436383000c82408746573743436383100c92408746573743436383200ca2408746573"
-    "743436383300cb2408746573743436383400cc2408746573743436383500cd2408746573743436383600ce24087465"
-    "73743436383700cf2408746573743436383800d02408746573743436383900d12408746573743436393000d2240874"
-    "6573743436393100d32408746573743436393200d42408746573743436393300d52408746573743436393400d62408"
-    "746573743436393500d72408746573743436393600d82408746573743436393700d92408746573743436393800da24"
-    "08746573743436393900db2408746573743437303000dc2408746573743437303100dd2408746573743437303200de"
-    "2408746573743437303300df2408746573743437303400e02408746573743437303500e12408746573743437303600"
-    "e22408746573743437303700e32408746573743437303800e42408746573743437303900e524087465737434373130"
-    "00e62408746573743437313100e72408746573743437313200e82408746573743437313300e9240874657374343731"
-    "3400ea2408746573743437313500eb2408746573743437313600ec2408746573743437313700ed2408746573743437"
-    "313800ee2408746573743437313900ef2408746573743437323000f02408746573743437323100f124087465737434"
-    "37323200f22408746573743437323300f32408746573743437323400f42408746573743437323500f5240874657374"
-    "3437323600f62408746573743437323700f72408746573743437323800f82408746573743437323900f92408746573"
-    "743437333000fa2408746573743437333100fb2408746573743437333200fc2408746573743437333300fd24087465"
-    "73743437333400fe2408746573743437333500ff240874657374343733360080250874657374343733370081250874"
-    "6573743437333800822508746573743437333900832508746573743437343000842508746573743437343100852508"
-    "7465737434373432008625087465737434373433008725087465737434373434008825087465737434373435008925"
-    "087465737434373436008a25087465737434373437008b25087465737434373438008c25087465737434373439008d"
-    "25087465737434373530008e25087465737434373531008f2508746573743437353200902508746573743437353300"
-    "9125087465737434373534009225087465737434373535009325087465737434373536009425087465737434373537"
-    "0095250874657374343735380096250874657374343735390097250874657374343736300098250874657374343736"
-    "31009925087465737434373632009a25087465737434373633009b25087465737434373634009c2508746573743437"
-    "3635009d25087465737434373636009e25087465737434373637009f2508746573743437363800a025087465737434"
-    "37363900a12508746573743437373000a22508746573743437373100a32508746573743437373200a4250874657374"
-    "3437373300a52508746573743437373400a62508746573743437373500a72508746573743437373600a82508746573"
-    "743437373700a92508746573743437373800aa2508746573743437373900ab2508746573743437383000ac25087465"
-    "73743437383100ad2508746573743437383200ae2508746573743437383300af2508746573743437383400b0250874"
-    "6573743437383500b12508746573743437383600b22508746573743437383700b32508746573743437383800b42508"
-    "746573743437383900b52508746573743437393000b62508746573743437393100b72508746573743437393200b825"
-    "08746573743437393300b92508746573743437393400ba2508746573743437393500bb2508746573743437393600bc"
-    "2508746573743437393700bd2508746573743437393800be2508746573743437393900bf2508746573743438303000"
-    "c02508746573743438303100c12508746573743438303200c22508746573743438303300c325087465737434383034"
-    "00c42508746573743438303500c52508746573743438303600c62508746573743438303700c7250874657374343830"
-    "3800c82508746573743438303900c92508746573743438313000ca2508746573743438313100cb2508746573743438"
-    "313200cc2508746573743438313300cd2508746573743438313400ce2508746573743438313500cf25087465737434"
-    "38313600d02508746573743438313700d12508746573743438313800d22508746573743438313900d3250874657374"
-    "3438323000d42508746573743438323100d52508746573743438323200d62508746573743438323300d72508746573"
-    "743438323400d82508746573743438323500d92508746573743438323600da2508746573743438323700db25087465"
-    "73743438323800dc2508746573743438323900dd2508746573743438333000de2508746573743438333100df250874"
-    "6573743438333200e02508746573743438333300e12508746573743438333400e22508746573743438333500e32508"
-    "746573743438333600e42508746573743438333700e52508746573743438333800e62508746573743438333900e725"
-    "08746573743438343000e82508746573743438343100e92508746573743438343200ea2508746573743438343300eb"
-    "2508746573743438343400ec2508746573743438343500ed2508746573743438343600ee2508746573743438343700"
-    "ef2508746573743438343800f02508746573743438343900f12508746573743438353000f225087465737434383531"
-    "00f32508746573743438353200f42508746573743438353300f52508746573743438353400f6250874657374343835"
-    "3500f72508746573743438353600f82508746573743438353700f92508746573743438353800fa2508746573743438"
-    "353900fb2508746573743438363000fc2508746573743438363100fd2508746573743438363200fe25087465737434"
-    "38363300ff250874657374343836340080260874657374343836350081260874657374343836360082260874657374"
-    "3438363700832608746573743438363800842608746573743438363900852608746573743438373000862608746573"
-    "7434383731008726087465737434383732008826087465737434383733008926087465737434383734008a26087465"
-    "737434383735008b26087465737434383736008c26087465737434383737008d26087465737434383738008e260874"
-    "65737434383739008f2608746573743438383000902608746573743438383100912608746573743438383200922608"
-    "7465737434383833009326087465737434383834009426087465737434383835009526087465737434383836009626"
-    "087465737434383837009726087465737434383838009826087465737434383839009926087465737434383930009a"
-    "26087465737434383931009b26087465737434383932009c26087465737434383933009d2608746573743438393400"
-    "9e26087465737434383935009f2608746573743438393600a02608746573743438393700a126087465737434383938"
-    "00a22608746573743438393900a32608746573743439303000a42608746573743439303100a5260874657374343930"
-    "3200a62608746573743439303300a72608746573743439303400a82608746573743439303500a92608746573743439"
-    "303600aa2608746573743439303700ab2608746573743439303800ac2608746573743439303900ad26087465737434"
-    "39313000ae2608746573743439313100af2608746573743439313200b02608746573743439313300b1260874657374"
-    "3439313400b22608746573743439313500b32608746573743439313600b42608746573743439313700b52608746573"
-    "743439313800b62608746573743439313900b72608746573743439323000b82608746573743439323100b926087465"
-    "73743439323200ba2608746573743439323300bb2608746573743439323400bc2608746573743439323500bd260874"
-    "6573743439323600be2608746573743439323700bf2608746573743439323800c02608746573743439323900c12608"
-    "746573743439333000c22608746573743439333100c32608746573743439333200c42608746573743439333300c526"
-    "08746573743439333400c62608746573743439333500c72608746573743439333600c82608746573743439333700c9"
-    "2608746573743439333800ca2608746573743439333900cb2608746573743439343000cc2608746573743439343100"
-    "cd2608746573743439343200ce2608746573743439343300cf2608746573743439343400d026087465737434393435"
-    "00d12608746573743439343600d22608746573743439343700d32608746573743439343800d4260874657374343934"
-    "3900d52608746573743439353000d62608746573743439353100d72608746573743439353200d82608746573743439"
-    "353300d92608746573743439353400da2608746573743439353500db2608746573743439353600dc26087465737434"
-    "39353700dd2608746573743439353800de2608746573743439353900df2608746573743439363000e0260874657374"
-    "3439363100e12608746573743439363200e22608746573743439363300e32608746573743439363400e42608746573"
-    "743439363500e52608746573743439363600e62608746573743439363700e72608746573743439363800e826087465"
-    "73743439363900e92608746573743439373000ea2608746573743439373100eb2608746573743439373200ec260874"
-    "6573743439373300ed2608746573743439373400ee2608746573743439373500ef2608746573743439373600f02608"
-    "746573743439373700f12608746573743439373800f22608746573743439373900f32608746573743439383000f426"
-    "08746573743439383100f52608746573743439383200f62608746573743439383300f72608746573743439383400f8"
-    "2608746573743439383500f92608746573743439383600fa2608746573743439383700fb2608746573743439383800"
-    "fc2608746573743439383900fd2608746573743439393000fe2608746573743439393100ff26087465737434393932"
-    "0080270874657374343939330081270874657374343939340082270874657374343939350083270874657374343939"
-    "360084270874657374343939370085270874657374343939380086270874657374343939390087270ac2b802882707"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020"
-    "016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07002000"
-    "20016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020"
-    "0020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700"
-    "200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b07"
-    "00200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b"
-    "0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a"
-    "0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b0700200020016a0b070020002001"
-    "6a0b";
diff --git a/src/test/app/wasm_fixtures/fixture_locals_10k.cpp b/src/test/app/wasm_fixtures/fixture_locals_10k.cpp
deleted file mode 100644
index 75a799243d..0000000000
--- a/src/test/app/wasm_fixtures/fixture_locals_10k.cpp
+++ /dev/null
@@ -1,2128 +0,0 @@
-// TODO: consider moving these to separate files (and figure out the build)
-
-#include 
-
-#include 
-
-extern std::string const kLocals10kHex =
-    "0061736d0100000001070160027f7f017f03020100070801047465737400000a9b8a0601978a06018e4e7f20002001"
-    "6a2102200120026a2103200220036a2104200320046a2105200420056a2106200520066a2107200620076a21082007"
-    "20086a2109200820096a210a2009200a6a210b200a200b6a210c200b200c6a210d200c200d6a210e200d200e6a210f"
-    "200e200f6a2110200f20106a2111201020116a2112201120126a2113201220136a2114201320146a2115201420156a"
-    "2116201520166a2117201620176a2118201720186a2119201820196a211a2019201a6a211b201a201b6a211c201b20"
-    "1c6a211d201c201d6a211e201d201e6a211f201e201f6a2120201f20206a2121202020216a2122202120226a212320"
-    "2220236a2124202320246a2125202420256a2126202520266a2127202620276a2128202720286a2129202820296a21"
-    "2a2029202a6a212b202a202b6a212c202b202c6a212d202c202d6a212e202d202e6a212f202e202f6a2130202f2030"
-    "6a2131203020316a2132203120326a2133203220336a2134203320346a2135203420356a2136203520366a21372036"
-    "20376a2138203720386a2139203820396a213a2039203a6a213b203a203b6a213c203b203c6a213d203c203d6a213e"
-    "203d203e6a213f203e203f6a2140203f20406a2141204020416a2142204120426a2143204220436a2144204320446a"
-    "2145204420456a2146204520466a2147204620476a2148204720486a2149204820496a214a2049204a6a214b204a20"
-    "4b6a214c204b204c6a214d204c204d6a214e204d204e6a214f204e204f6a2150204f20506a2151205020516a215220"
-    "5120526a2153205220536a2154205320546a2155205420556a2156205520566a2157205620576a2158205720586a21"
-    "59205820596a215a2059205a6a215b205a205b6a215c205b205c6a215d205c205d6a215e205d205e6a215f205e205f"
-    "6a2160205f20606a2161206020616a2162206120626a2163206220636a2164206320646a2165206420656a21662065"
-    "20666a2167206620676a2168206720686a2169206820696a216a2069206a6a216b206a206b6a216c206b206c6a216d"
-    "206c206d6a216e206d206e6a216f206e206f6a2170206f20706a2171207020716a2172207120726a2173207220736a"
-    "2174207320746a2175207420756a2176207520766a2177207620776a2178207720786a2179207820796a217a207920"
-    "7a6a217b207a207b6a217c207b207c6a217d207c207d6a217e207d207e6a217f207e207f6a218001207f2080016a21"
-    "81012080012081016a2182012081012082016a2183012082012083016a2184012083012084016a2185012084012085"
-    "016a2186012085012086016a2187012086012087016a2188012087012088016a2189012088012089016a218a012089"
-    "01208a016a218b01208a01208b016a218c01208b01208c016a218d01208c01208d016a218e01208d01208e016a218f"
-    "01208e01208f016a219001208f012090016a2191012090012091016a2192012091012092016a219301209201209301"
-    "6a2194012093012094016a2195012094012095016a2196012095012096016a2197012096012097016a219801209701"
-    "2098016a2199012098012099016a219a01209901209a016a219b01209a01209b016a219c01209b01209c016a219d01"
-    "209c01209d016a219e01209d01209e016a219f01209e01209f016a21a001209f0120a0016a21a10120a00120a1016a"
-    "21a20120a10120a2016a21a30120a20120a3016a21a40120a30120a4016a21a50120a40120a5016a21a60120a50120"
-    "a6016a21a70120a60120a7016a21a80120a70120a8016a21a90120a80120a9016a21aa0120a90120aa016a21ab0120"
-    "aa0120ab016a21ac0120ab0120ac016a21ad0120ac0120ad016a21ae0120ad0120ae016a21af0120ae0120af016a21"
-    "b00120af0120b0016a21b10120b00120b1016a21b20120b10120b2016a21b30120b20120b3016a21b40120b30120b4"
-    "016a21b50120b40120b5016a21b60120b50120b6016a21b70120b60120b7016a21b80120b70120b8016a21b90120b8"
-    "0120b9016a21ba0120b90120ba016a21bb0120ba0120bb016a21bc0120bb0120bc016a21bd0120bc0120bd016a21be"
-    "0120bd0120be016a21bf0120be0120bf016a21c00120bf0120c0016a21c10120c00120c1016a21c20120c10120c201"
-    "6a21c30120c20120c3016a21c40120c30120c4016a21c50120c40120c5016a21c60120c50120c6016a21c70120c601"
-    "20c7016a21c80120c70120c8016a21c90120c80120c9016a21ca0120c90120ca016a21cb0120ca0120cb016a21cc01"
-    "20cb0120cc016a21cd0120cc0120cd016a21ce0120cd0120ce016a21cf0120ce0120cf016a21d00120cf0120d0016a"
-    "21d10120d00120d1016a21d20120d10120d2016a21d30120d20120d3016a21d40120d30120d4016a21d50120d40120"
-    "d5016a21d60120d50120d6016a21d70120d60120d7016a21d80120d70120d8016a21d90120d80120d9016a21da0120"
-    "d90120da016a21db0120da0120db016a21dc0120db0120dc016a21dd0120dc0120dd016a21de0120dd0120de016a21"
-    "df0120de0120df016a21e00120df0120e0016a21e10120e00120e1016a21e20120e10120e2016a21e30120e20120e3"
-    "016a21e40120e30120e4016a21e50120e40120e5016a21e60120e50120e6016a21e70120e60120e7016a21e80120e7"
-    "0120e8016a21e90120e80120e9016a21ea0120e90120ea016a21eb0120ea0120eb016a21ec0120eb0120ec016a21ed"
-    "0120ec0120ed016a21ee0120ed0120ee016a21ef0120ee0120ef016a21f00120ef0120f0016a21f10120f00120f101"
-    "6a21f20120f10120f2016a21f30120f20120f3016a21f40120f30120f4016a21f50120f40120f5016a21f60120f501"
-    "20f6016a21f70120f60120f7016a21f80120f70120f8016a21f90120f80120f9016a21fa0120f90120fa016a21fb01"
-    "20fa0120fb016a21fc0120fb0120fc016a21fd0120fc0120fd016a21fe0120fd0120fe016a21ff0120fe0120ff016a"
-    "21800220ff012080026a2181022080022081026a2182022081022082026a2183022082022083026a21840220830220"
-    "84026a2185022084022085026a2186022085022086026a2187022086022087026a2188022087022088026a21890220"
-    "88022089026a218a02208902208a026a218b02208a02208b026a218c02208b02208c026a218d02208c02208d026a21"
-    "8e02208d02208e026a218f02208e02208f026a219002208f022090026a2191022090022091026a2192022091022092"
-    "026a2193022092022093026a2194022093022094026a2195022094022095026a2196022095022096026a2197022096"
-    "022097026a2198022097022098026a2199022098022099026a219a02209902209a026a219b02209a02209b026a219c"
-    "02209b02209c026a219d02209c02209d026a219e02209d02209e026a219f02209e02209f026a21a002209f0220a002"
-    "6a21a10220a00220a1026a21a20220a10220a2026a21a30220a20220a3026a21a40220a30220a4026a21a50220a402"
-    "20a5026a21a60220a50220a6026a21a70220a60220a7026a21a80220a70220a8026a21a90220a80220a9026a21aa02"
-    "20a90220aa026a21ab0220aa0220ab026a21ac0220ab0220ac026a21ad0220ac0220ad026a21ae0220ad0220ae026a"
-    "21af0220ae0220af026a21b00220af0220b0026a21b10220b00220b1026a21b20220b10220b2026a21b30220b20220"
-    "b3026a21b40220b30220b4026a21b50220b40220b5026a21b60220b50220b6026a21b70220b60220b7026a21b80220"
-    "b70220b8026a21b90220b80220b9026a21ba0220b90220ba026a21bb0220ba0220bb026a21bc0220bb0220bc026a21"
-    "bd0220bc0220bd026a21be0220bd0220be026a21bf0220be0220bf026a21c00220bf0220c0026a21c10220c00220c1"
-    "026a21c20220c10220c2026a21c30220c20220c3026a21c40220c30220c4026a21c50220c40220c5026a21c60220c5"
-    "0220c6026a21c70220c60220c7026a21c80220c70220c8026a21c90220c80220c9026a21ca0220c90220ca026a21cb"
-    "0220ca0220cb026a21cc0220cb0220cc026a21cd0220cc0220cd026a21ce0220cd0220ce026a21cf0220ce0220cf02"
-    "6a21d00220cf0220d0026a21d10220d00220d1026a21d20220d10220d2026a21d30220d20220d3026a21d40220d302"
-    "20d4026a21d50220d40220d5026a21d60220d50220d6026a21d70220d60220d7026a21d80220d70220d8026a21d902"
-    "20d80220d9026a21da0220d90220da026a21db0220da0220db026a21dc0220db0220dc026a21dd0220dc0220dd026a"
-    "21de0220dd0220de026a21df0220de0220df026a21e00220df0220e0026a21e10220e00220e1026a21e20220e10220"
-    "e2026a21e30220e20220e3026a21e40220e30220e4026a21e50220e40220e5026a21e60220e50220e6026a21e70220"
-    "e60220e7026a21e80220e70220e8026a21e90220e80220e9026a21ea0220e90220ea026a21eb0220ea0220eb026a21"
-    "ec0220eb0220ec026a21ed0220ec0220ed026a21ee0220ed0220ee026a21ef0220ee0220ef026a21f00220ef0220f0"
-    "026a21f10220f00220f1026a21f20220f10220f2026a21f30220f20220f3026a21f40220f30220f4026a21f50220f4"
-    "0220f5026a21f60220f50220f6026a21f70220f60220f7026a21f80220f70220f8026a21f90220f80220f9026a21fa"
-    "0220f90220fa026a21fb0220fa0220fb026a21fc0220fb0220fc026a21fd0220fc0220fd026a21fe0220fd0220fe02"
-    "6a21ff0220fe0220ff026a21800320ff022080036a2181032080032081036a2182032081032082036a218303208203"
-    "2083036a2184032083032084036a2185032084032085036a2186032085032086036a2187032086032087036a218803"
-    "2087032088036a2189032088032089036a218a03208903208a036a218b03208a03208b036a218c03208b03208c036a"
-    "218d03208c03208d036a218e03208d03208e036a218f03208e03208f036a219003208f032090036a21910320900320"
-    "91036a2192032091032092036a2193032092032093036a2194032093032094036a2195032094032095036a21960320"
-    "95032096036a2197032096032097036a2198032097032098036a2199032098032099036a219a03209903209a036a21"
-    "9b03209a03209b036a219c03209b03209c036a219d03209c03209d036a219e03209d03209e036a219f03209e03209f"
-    "036a21a003209f0320a0036a21a10320a00320a1036a21a20320a10320a2036a21a30320a20320a3036a21a40320a3"
-    "0320a4036a21a50320a40320a5036a21a60320a50320a6036a21a70320a60320a7036a21a80320a70320a8036a21a9"
-    "0320a80320a9036a21aa0320a90320aa036a21ab0320aa0320ab036a21ac0320ab0320ac036a21ad0320ac0320ad03"
-    "6a21ae0320ad0320ae036a21af0320ae0320af036a21b00320af0320b0036a21b10320b00320b1036a21b20320b103"
-    "20b2036a21b30320b20320b3036a21b40320b30320b4036a21b50320b40320b5036a21b60320b50320b6036a21b703"
-    "20b60320b7036a21b80320b70320b8036a21b90320b80320b9036a21ba0320b90320ba036a21bb0320ba0320bb036a"
-    "21bc0320bb0320bc036a21bd0320bc0320bd036a21be0320bd0320be036a21bf0320be0320bf036a21c00320bf0320"
-    "c0036a21c10320c00320c1036a21c20320c10320c2036a21c30320c20320c3036a21c40320c30320c4036a21c50320"
-    "c40320c5036a21c60320c50320c6036a21c70320c60320c7036a21c80320c70320c8036a21c90320c80320c9036a21"
-    "ca0320c90320ca036a21cb0320ca0320cb036a21cc0320cb0320cc036a21cd0320cc0320cd036a21ce0320cd0320ce"
-    "036a21cf0320ce0320cf036a21d00320cf0320d0036a21d10320d00320d1036a21d20320d10320d2036a21d30320d2"
-    "0320d3036a21d40320d30320d4036a21d50320d40320d5036a21d60320d50320d6036a21d70320d60320d7036a21d8"
-    "0320d70320d8036a21d90320d80320d9036a21da0320d90320da036a21db0320da0320db036a21dc0320db0320dc03"
-    "6a21dd0320dc0320dd036a21de0320dd0320de036a21df0320de0320df036a21e00320df0320e0036a21e10320e003"
-    "20e1036a21e20320e10320e2036a21e30320e20320e3036a21e40320e30320e4036a21e50320e40320e5036a21e603"
-    "20e50320e6036a21e70320e60320e7036a21e80320e70320e8036a21e90320e80320e9036a21ea0320e90320ea036a"
-    "21eb0320ea0320eb036a21ec0320eb0320ec036a21ed0320ec0320ed036a21ee0320ed0320ee036a21ef0320ee0320"
-    "ef036a21f00320ef0320f0036a21f10320f00320f1036a21f20320f10320f2036a21f30320f20320f3036a21f40320"
-    "f30320f4036a21f50320f40320f5036a21f60320f50320f6036a21f70320f60320f7036a21f80320f70320f8036a21"
-    "f90320f80320f9036a21fa0320f90320fa036a21fb0320fa0320fb036a21fc0320fb0320fc036a21fd0320fc0320fd"
-    "036a21fe0320fd0320fe036a21ff0320fe0320ff036a21800420ff032080046a2181042080042081046a2182042081"
-    "042082046a2183042082042083046a2184042083042084046a2185042084042085046a2186042085042086046a2187"
-    "042086042087046a2188042087042088046a2189042088042089046a218a04208904208a046a218b04208a04208b04"
-    "6a218c04208b04208c046a218d04208c04208d046a218e04208d04208e046a218f04208e04208f046a219004208f04"
-    "2090046a2191042090042091046a2192042091042092046a2193042092042093046a2194042093042094046a219504"
-    "2094042095046a2196042095042096046a2197042096042097046a2198042097042098046a2199042098042099046a"
-    "219a04209904209a046a219b04209a04209b046a219c04209b04209c046a219d04209c04209d046a219e04209d0420"
-    "9e046a219f04209e04209f046a21a004209f0420a0046a21a10420a00420a1046a21a20420a10420a2046a21a30420"
-    "a20420a3046a21a40420a30420a4046a21a50420a40420a5046a21a60420a50420a6046a21a70420a60420a7046a21"
-    "a80420a70420a8046a21a90420a80420a9046a21aa0420a90420aa046a21ab0420aa0420ab046a21ac0420ab0420ac"
-    "046a21ad0420ac0420ad046a21ae0420ad0420ae046a21af0420ae0420af046a21b00420af0420b0046a21b10420b0"
-    "0420b1046a21b20420b10420b2046a21b30420b20420b3046a21b40420b30420b4046a21b50420b40420b5046a21b6"
-    "0420b50420b6046a21b70420b60420b7046a21b80420b70420b8046a21b90420b80420b9046a21ba0420b90420ba04"
-    "6a21bb0420ba0420bb046a21bc0420bb0420bc046a21bd0420bc0420bd046a21be0420bd0420be046a21bf0420be04"
-    "20bf046a21c00420bf0420c0046a21c10420c00420c1046a21c20420c10420c2046a21c30420c20420c3046a21c404"
-    "20c30420c4046a21c50420c40420c5046a21c60420c50420c6046a21c70420c60420c7046a21c80420c70420c8046a"
-    "21c90420c80420c9046a21ca0420c90420ca046a21cb0420ca0420cb046a21cc0420cb0420cc046a21cd0420cc0420"
-    "cd046a21ce0420cd0420ce046a21cf0420ce0420cf046a21d00420cf0420d0046a21d10420d00420d1046a21d20420"
-    "d10420d2046a21d30420d20420d3046a21d40420d30420d4046a21d50420d40420d5046a21d60420d50420d6046a21"
-    "d70420d60420d7046a21d80420d70420d8046a21d90420d80420d9046a21da0420d90420da046a21db0420da0420db"
-    "046a21dc0420db0420dc046a21dd0420dc0420dd046a21de0420dd0420de046a21df0420de0420df046a21e00420df"
-    "0420e0046a21e10420e00420e1046a21e20420e10420e2046a21e30420e20420e3046a21e40420e30420e4046a21e5"
-    "0420e40420e5046a21e60420e50420e6046a21e70420e60420e7046a21e80420e70420e8046a21e90420e80420e904"
-    "6a21ea0420e90420ea046a21eb0420ea0420eb046a21ec0420eb0420ec046a21ed0420ec0420ed046a21ee0420ed04"
-    "20ee046a21ef0420ee0420ef046a21f00420ef0420f0046a21f10420f00420f1046a21f20420f10420f2046a21f304"
-    "20f20420f3046a21f40420f30420f4046a21f50420f40420f5046a21f60420f50420f6046a21f70420f60420f7046a"
-    "21f80420f70420f8046a21f90420f80420f9046a21fa0420f90420fa046a21fb0420fa0420fb046a21fc0420fb0420"
-    "fc046a21fd0420fc0420fd046a21fe0420fd0420fe046a21ff0420fe0420ff046a21800520ff042080056a21810520"
-    "80052081056a2182052081052082056a2183052082052083056a2184052083052084056a2185052084052085056a21"
-    "86052085052086056a2187052086052087056a2188052087052088056a2189052088052089056a218a05208905208a"
-    "056a218b05208a05208b056a218c05208b05208c056a218d05208c05208d056a218e05208d05208e056a218f05208e"
-    "05208f056a219005208f052090056a2191052090052091056a2192052091052092056a2193052092052093056a2194"
-    "052093052094056a2195052094052095056a2196052095052096056a2197052096052097056a219805209705209805"
-    "6a2199052098052099056a219a05209905209a056a219b05209a05209b056a219c05209b05209c056a219d05209c05"
-    "209d056a219e05209d05209e056a219f05209e05209f056a21a005209f0520a0056a21a10520a00520a1056a21a205"
-    "20a10520a2056a21a30520a20520a3056a21a40520a30520a4056a21a50520a40520a5056a21a60520a50520a6056a"
-    "21a70520a60520a7056a21a80520a70520a8056a21a90520a80520a9056a21aa0520a90520aa056a21ab0520aa0520"
-    "ab056a21ac0520ab0520ac056a21ad0520ac0520ad056a21ae0520ad0520ae056a21af0520ae0520af056a21b00520"
-    "af0520b0056a21b10520b00520b1056a21b20520b10520b2056a21b30520b20520b3056a21b40520b30520b4056a21"
-    "b50520b40520b5056a21b60520b50520b6056a21b70520b60520b7056a21b80520b70520b8056a21b90520b80520b9"
-    "056a21ba0520b90520ba056a21bb0520ba0520bb056a21bc0520bb0520bc056a21bd0520bc0520bd056a21be0520bd"
-    "0520be056a21bf0520be0520bf056a21c00520bf0520c0056a21c10520c00520c1056a21c20520c10520c2056a21c3"
-    "0520c20520c3056a21c40520c30520c4056a21c50520c40520c5056a21c60520c50520c6056a21c70520c60520c705"
-    "6a21c80520c70520c8056a21c90520c80520c9056a21ca0520c90520ca056a21cb0520ca0520cb056a21cc0520cb05"
-    "20cc056a21cd0520cc0520cd056a21ce0520cd0520ce056a21cf0520ce0520cf056a21d00520cf0520d0056a21d105"
-    "20d00520d1056a21d20520d10520d2056a21d30520d20520d3056a21d40520d30520d4056a21d50520d40520d5056a"
-    "21d60520d50520d6056a21d70520d60520d7056a21d80520d70520d8056a21d90520d80520d9056a21da0520d90520"
-    "da056a21db0520da0520db056a21dc0520db0520dc056a21dd0520dc0520dd056a21de0520dd0520de056a21df0520"
-    "de0520df056a21e00520df0520e0056a21e10520e00520e1056a21e20520e10520e2056a21e30520e20520e3056a21"
-    "e40520e30520e4056a21e50520e40520e5056a21e60520e50520e6056a21e70520e60520e7056a21e80520e70520e8"
-    "056a21e90520e80520e9056a21ea0520e90520ea056a21eb0520ea0520eb056a21ec0520eb0520ec056a21ed0520ec"
-    "0520ed056a21ee0520ed0520ee056a21ef0520ee0520ef056a21f00520ef0520f0056a21f10520f00520f1056a21f2"
-    "0520f10520f2056a21f30520f20520f3056a21f40520f30520f4056a21f50520f40520f5056a21f60520f50520f605"
-    "6a21f70520f60520f7056a21f80520f70520f8056a21f90520f80520f9056a21fa0520f90520fa056a21fb0520fa05"
-    "20fb056a21fc0520fb0520fc056a21fd0520fc0520fd056a21fe0520fd0520fe056a21ff0520fe0520ff056a218006"
-    "20ff052080066a2181062080062081066a2182062081062082066a2183062082062083066a2184062083062084066a"
-    "2185062084062085066a2186062085062086066a2187062086062087066a2188062087062088066a21890620880620"
-    "89066a218a06208906208a066a218b06208a06208b066a218c06208b06208c066a218d06208c06208d066a218e0620"
-    "8d06208e066a218f06208e06208f066a219006208f062090066a2191062090062091066a2192062091062092066a21"
-    "93062092062093066a2194062093062094066a2195062094062095066a2196062095062096066a2197062096062097"
-    "066a2198062097062098066a2199062098062099066a219a06209906209a066a219b06209a06209b066a219c06209b"
-    "06209c066a219d06209c06209d066a219e06209d06209e066a219f06209e06209f066a21a006209f0620a0066a21a1"
-    "0620a00620a1066a21a20620a10620a2066a21a30620a20620a3066a21a40620a30620a4066a21a50620a40620a506"
-    "6a21a60620a50620a6066a21a70620a60620a7066a21a80620a70620a8066a21a90620a80620a9066a21aa0620a906"
-    "20aa066a21ab0620aa0620ab066a21ac0620ab0620ac066a21ad0620ac0620ad066a21ae0620ad0620ae066a21af06"
-    "20ae0620af066a21b00620af0620b0066a21b10620b00620b1066a21b20620b10620b2066a21b30620b20620b3066a"
-    "21b40620b30620b4066a21b50620b40620b5066a21b60620b50620b6066a21b70620b60620b7066a21b80620b70620"
-    "b8066a21b90620b80620b9066a21ba0620b90620ba066a21bb0620ba0620bb066a21bc0620bb0620bc066a21bd0620"
-    "bc0620bd066a21be0620bd0620be066a21bf0620be0620bf066a21c00620bf0620c0066a21c10620c00620c1066a21"
-    "c20620c10620c2066a21c30620c20620c3066a21c40620c30620c4066a21c50620c40620c5066a21c60620c50620c6"
-    "066a21c70620c60620c7066a21c80620c70620c8066a21c90620c80620c9066a21ca0620c90620ca066a21cb0620ca"
-    "0620cb066a21cc0620cb0620cc066a21cd0620cc0620cd066a21ce0620cd0620ce066a21cf0620ce0620cf066a21d0"
-    "0620cf0620d0066a21d10620d00620d1066a21d20620d10620d2066a21d30620d20620d3066a21d40620d30620d406"
-    "6a21d50620d40620d5066a21d60620d50620d6066a21d70620d60620d7066a21d80620d70620d8066a21d90620d806"
-    "20d9066a21da0620d90620da066a21db0620da0620db066a21dc0620db0620dc066a21dd0620dc0620dd066a21de06"
-    "20dd0620de066a21df0620de0620df066a21e00620df0620e0066a21e10620e00620e1066a21e20620e10620e2066a"
-    "21e30620e20620e3066a21e40620e30620e4066a21e50620e40620e5066a21e60620e50620e6066a21e70620e60620"
-    "e7066a21e80620e70620e8066a21e90620e80620e9066a21ea0620e90620ea066a21eb0620ea0620eb066a21ec0620"
-    "eb0620ec066a21ed0620ec0620ed066a21ee0620ed0620ee066a21ef0620ee0620ef066a21f00620ef0620f0066a21"
-    "f10620f00620f1066a21f20620f10620f2066a21f30620f20620f3066a21f40620f30620f4066a21f50620f40620f5"
-    "066a21f60620f50620f6066a21f70620f60620f7066a21f80620f70620f8066a21f90620f80620f9066a21fa0620f9"
-    "0620fa066a21fb0620fa0620fb066a21fc0620fb0620fc066a21fd0620fc0620fd066a21fe0620fd0620fe066a21ff"
-    "0620fe0620ff066a21800720ff062080076a2181072080072081076a2182072081072082076a218307208207208307"
-    "6a2184072083072084076a2185072084072085076a2186072085072086076a2187072086072087076a218807208707"
-    "2088076a2189072088072089076a218a07208907208a076a218b07208a07208b076a218c07208b07208c076a218d07"
-    "208c07208d076a218e07208d07208e076a218f07208e07208f076a219007208f072090076a2191072090072091076a"
-    "2192072091072092076a2193072092072093076a2194072093072094076a2195072094072095076a21960720950720"
-    "96076a2197072096072097076a2198072097072098076a2199072098072099076a219a07209907209a076a219b0720"
-    "9a07209b076a219c07209b07209c076a219d07209c07209d076a219e07209d07209e076a219f07209e07209f076a21"
-    "a007209f0720a0076a21a10720a00720a1076a21a20720a10720a2076a21a30720a20720a3076a21a40720a30720a4"
-    "076a21a50720a40720a5076a21a60720a50720a6076a21a70720a60720a7076a21a80720a70720a8076a21a90720a8"
-    "0720a9076a21aa0720a90720aa076a21ab0720aa0720ab076a21ac0720ab0720ac076a21ad0720ac0720ad076a21ae"
-    "0720ad0720ae076a21af0720ae0720af076a21b00720af0720b0076a21b10720b00720b1076a21b20720b10720b207"
-    "6a21b30720b20720b3076a21b40720b30720b4076a21b50720b40720b5076a21b60720b50720b6076a21b70720b607"
-    "20b7076a21b80720b70720b8076a21b90720b80720b9076a21ba0720b90720ba076a21bb0720ba0720bb076a21bc07"
-    "20bb0720bc076a21bd0720bc0720bd076a21be0720bd0720be076a21bf0720be0720bf076a21c00720bf0720c0076a"
-    "21c10720c00720c1076a21c20720c10720c2076a21c30720c20720c3076a21c40720c30720c4076a21c50720c40720"
-    "c5076a21c60720c50720c6076a21c70720c60720c7076a21c80720c70720c8076a21c90720c80720c9076a21ca0720"
-    "c90720ca076a21cb0720ca0720cb076a21cc0720cb0720cc076a21cd0720cc0720cd076a21ce0720cd0720ce076a21"
-    "cf0720ce0720cf076a21d00720cf0720d0076a21d10720d00720d1076a21d20720d10720d2076a21d30720d20720d3"
-    "076a21d40720d30720d4076a21d50720d40720d5076a21d60720d50720d6076a21d70720d60720d7076a21d80720d7"
-    "0720d8076a21d90720d80720d9076a21da0720d90720da076a21db0720da0720db076a21dc0720db0720dc076a21dd"
-    "0720dc0720dd076a21de0720dd0720de076a21df0720de0720df076a21e00720df0720e0076a21e10720e00720e107"
-    "6a21e20720e10720e2076a21e30720e20720e3076a21e40720e30720e4076a21e50720e40720e5076a21e60720e507"
-    "20e6076a21e70720e60720e7076a21e80720e70720e8076a21e90720e80720e9076a21ea0720e90720ea076a21eb07"
-    "20ea0720eb076a21ec0720eb0720ec076a21ed0720ec0720ed076a21ee0720ed0720ee076a21ef0720ee0720ef076a"
-    "21f00720ef0720f0076a21f10720f00720f1076a21f20720f10720f2076a21f30720f20720f3076a21f40720f30720"
-    "f4076a21f50720f40720f5076a21f60720f50720f6076a21f70720f60720f7076a21f80720f70720f8076a21f90720"
-    "f80720f9076a21fa0720f90720fa076a21fb0720fa0720fb076a21fc0720fb0720fc076a21fd0720fc0720fd076a21"
-    "fe0720fd0720fe076a21ff0720fe0720ff076a21800820ff072080086a2181082080082081086a2182082081082082"
-    "086a2183082082082083086a2184082083082084086a2185082084082085086a2186082085082086086a2187082086"
-    "082087086a2188082087082088086a2189082088082089086a218a08208908208a086a218b08208a08208b086a218c"
-    "08208b08208c086a218d08208c08208d086a218e08208d08208e086a218f08208e08208f086a219008208f08209008"
-    "6a2191082090082091086a2192082091082092086a2193082092082093086a2194082093082094086a219508209408"
-    "2095086a2196082095082096086a2197082096082097086a2198082097082098086a2199082098082099086a219a08"
-    "209908209a086a219b08209a08209b086a219c08209b08209c086a219d08209c08209d086a219e08209d08209e086a"
-    "219f08209e08209f086a21a008209f0820a0086a21a10820a00820a1086a21a20820a10820a2086a21a30820a20820"
-    "a3086a21a40820a30820a4086a21a50820a40820a5086a21a60820a50820a6086a21a70820a60820a7086a21a80820"
-    "a70820a8086a21a90820a80820a9086a21aa0820a90820aa086a21ab0820aa0820ab086a21ac0820ab0820ac086a21"
-    "ad0820ac0820ad086a21ae0820ad0820ae086a21af0820ae0820af086a21b00820af0820b0086a21b10820b00820b1"
-    "086a21b20820b10820b2086a21b30820b20820b3086a21b40820b30820b4086a21b50820b40820b5086a21b60820b5"
-    "0820b6086a21b70820b60820b7086a21b80820b70820b8086a21b90820b80820b9086a21ba0820b90820ba086a21bb"
-    "0820ba0820bb086a21bc0820bb0820bc086a21bd0820bc0820bd086a21be0820bd0820be086a21bf0820be0820bf08"
-    "6a21c00820bf0820c0086a21c10820c00820c1086a21c20820c10820c2086a21c30820c20820c3086a21c40820c308"
-    "20c4086a21c50820c40820c5086a21c60820c50820c6086a21c70820c60820c7086a21c80820c70820c8086a21c908"
-    "20c80820c9086a21ca0820c90820ca086a21cb0820ca0820cb086a21cc0820cb0820cc086a21cd0820cc0820cd086a"
-    "21ce0820cd0820ce086a21cf0820ce0820cf086a21d00820cf0820d0086a21d10820d00820d1086a21d20820d10820"
-    "d2086a21d30820d20820d3086a21d40820d30820d4086a21d50820d40820d5086a21d60820d50820d6086a21d70820"
-    "d60820d7086a21d80820d70820d8086a21d90820d80820d9086a21da0820d90820da086a21db0820da0820db086a21"
-    "dc0820db0820dc086a21dd0820dc0820dd086a21de0820dd0820de086a21df0820de0820df086a21e00820df0820e0"
-    "086a21e10820e00820e1086a21e20820e10820e2086a21e30820e20820e3086a21e40820e30820e4086a21e50820e4"
-    "0820e5086a21e60820e50820e6086a21e70820e60820e7086a21e80820e70820e8086a21e90820e80820e9086a21ea"
-    "0820e90820ea086a21eb0820ea0820eb086a21ec0820eb0820ec086a21ed0820ec0820ed086a21ee0820ed0820ee08"
-    "6a21ef0820ee0820ef086a21f00820ef0820f0086a21f10820f00820f1086a21f20820f10820f2086a21f30820f208"
-    "20f3086a21f40820f30820f4086a21f50820f40820f5086a21f60820f50820f6086a21f70820f60820f7086a21f808"
-    "20f70820f8086a21f90820f80820f9086a21fa0820f90820fa086a21fb0820fa0820fb086a21fc0820fb0820fc086a"
-    "21fd0820fc0820fd086a21fe0820fd0820fe086a21ff0820fe0820ff086a21800920ff082080096a21810920800920"
-    "81096a2182092081092082096a2183092082092083096a2184092083092084096a2185092084092085096a21860920"
-    "85092086096a2187092086092087096a2188092087092088096a2189092088092089096a218a09208909208a096a21"
-    "8b09208a09208b096a218c09208b09208c096a218d09208c09208d096a218e09208d09208e096a218f09208e09208f"
-    "096a219009208f092090096a2191092090092091096a2192092091092092096a2193092092092093096a2194092093"
-    "092094096a2195092094092095096a2196092095092096096a2197092096092097096a2198092097092098096a2199"
-    "092098092099096a219a09209909209a096a219b09209a09209b096a219c09209b09209c096a219d09209c09209d09"
-    "6a219e09209d09209e096a219f09209e09209f096a21a009209f0920a0096a21a10920a00920a1096a21a20920a109"
-    "20a2096a21a30920a20920a3096a21a40920a30920a4096a21a50920a40920a5096a21a60920a50920a6096a21a709"
-    "20a60920a7096a21a80920a70920a8096a21a90920a80920a9096a21aa0920a90920aa096a21ab0920aa0920ab096a"
-    "21ac0920ab0920ac096a21ad0920ac0920ad096a21ae0920ad0920ae096a21af0920ae0920af096a21b00920af0920"
-    "b0096a21b10920b00920b1096a21b20920b10920b2096a21b30920b20920b3096a21b40920b30920b4096a21b50920"
-    "b40920b5096a21b60920b50920b6096a21b70920b60920b7096a21b80920b70920b8096a21b90920b80920b9096a21"
-    "ba0920b90920ba096a21bb0920ba0920bb096a21bc0920bb0920bc096a21bd0920bc0920bd096a21be0920bd0920be"
-    "096a21bf0920be0920bf096a21c00920bf0920c0096a21c10920c00920c1096a21c20920c10920c2096a21c30920c2"
-    "0920c3096a21c40920c30920c4096a21c50920c40920c5096a21c60920c50920c6096a21c70920c60920c7096a21c8"
-    "0920c70920c8096a21c90920c80920c9096a21ca0920c90920ca096a21cb0920ca0920cb096a21cc0920cb0920cc09"
-    "6a21cd0920cc0920cd096a21ce0920cd0920ce096a21cf0920ce0920cf096a21d00920cf0920d0096a21d10920d009"
-    "20d1096a21d20920d10920d2096a21d30920d20920d3096a21d40920d30920d4096a21d50920d40920d5096a21d609"
-    "20d50920d6096a21d70920d60920d7096a21d80920d70920d8096a21d90920d80920d9096a21da0920d90920da096a"
-    "21db0920da0920db096a21dc0920db0920dc096a21dd0920dc0920dd096a21de0920dd0920de096a21df0920de0920"
-    "df096a21e00920df0920e0096a21e10920e00920e1096a21e20920e10920e2096a21e30920e20920e3096a21e40920"
-    "e30920e4096a21e50920e40920e5096a21e60920e50920e6096a21e70920e60920e7096a21e80920e70920e8096a21"
-    "e90920e80920e9096a21ea0920e90920ea096a21eb0920ea0920eb096a21ec0920eb0920ec096a21ed0920ec0920ed"
-    "096a21ee0920ed0920ee096a21ef0920ee0920ef096a21f00920ef0920f0096a21f10920f00920f1096a21f20920f1"
-    "0920f2096a21f30920f20920f3096a21f40920f30920f4096a21f50920f40920f5096a21f60920f50920f6096a21f7"
-    "0920f60920f7096a21f80920f70920f8096a21f90920f80920f9096a21fa0920f90920fa096a21fb0920fa0920fb09"
-    "6a21fc0920fb0920fc096a21fd0920fc0920fd096a21fe0920fd0920fe096a21ff0920fe0920ff096a21800a20ff09"
-    "20800a6a21810a20800a20810a6a21820a20810a20820a6a21830a20820a20830a6a21840a20830a20840a6a21850a"
-    "20840a20850a6a21860a20850a20860a6a21870a20860a20870a6a21880a20870a20880a6a21890a20880a20890a6a"
-    "218a0a20890a208a0a6a218b0a208a0a208b0a6a218c0a208b0a208c0a6a218d0a208c0a208d0a6a218e0a208d0a20"
-    "8e0a6a218f0a208e0a208f0a6a21900a208f0a20900a6a21910a20900a20910a6a21920a20910a20920a6a21930a20"
-    "920a20930a6a21940a20930a20940a6a21950a20940a20950a6a21960a20950a20960a6a21970a20960a20970a6a21"
-    "980a20970a20980a6a21990a20980a20990a6a219a0a20990a209a0a6a219b0a209a0a209b0a6a219c0a209b0a209c"
-    "0a6a219d0a209c0a209d0a6a219e0a209d0a209e0a6a219f0a209e0a209f0a6a21a00a209f0a20a00a6a21a10a20a0"
-    "0a20a10a6a21a20a20a10a20a20a6a21a30a20a20a20a30a6a21a40a20a30a20a40a6a21a50a20a40a20a50a6a21a6"
-    "0a20a50a20a60a6a21a70a20a60a20a70a6a21a80a20a70a20a80a6a21a90a20a80a20a90a6a21aa0a20a90a20aa0a"
-    "6a21ab0a20aa0a20ab0a6a21ac0a20ab0a20ac0a6a21ad0a20ac0a20ad0a6a21ae0a20ad0a20ae0a6a21af0a20ae0a"
-    "20af0a6a21b00a20af0a20b00a6a21b10a20b00a20b10a6a21b20a20b10a20b20a6a21b30a20b20a20b30a6a21b40a"
-    "20b30a20b40a6a21b50a20b40a20b50a6a21b60a20b50a20b60a6a21b70a20b60a20b70a6a21b80a20b70a20b80a6a"
-    "21b90a20b80a20b90a6a21ba0a20b90a20ba0a6a21bb0a20ba0a20bb0a6a21bc0a20bb0a20bc0a6a21bd0a20bc0a20"
-    "bd0a6a21be0a20bd0a20be0a6a21bf0a20be0a20bf0a6a21c00a20bf0a20c00a6a21c10a20c00a20c10a6a21c20a20"
-    "c10a20c20a6a21c30a20c20a20c30a6a21c40a20c30a20c40a6a21c50a20c40a20c50a6a21c60a20c50a20c60a6a21"
-    "c70a20c60a20c70a6a21c80a20c70a20c80a6a21c90a20c80a20c90a6a21ca0a20c90a20ca0a6a21cb0a20ca0a20cb"
-    "0a6a21cc0a20cb0a20cc0a6a21cd0a20cc0a20cd0a6a21ce0a20cd0a20ce0a6a21cf0a20ce0a20cf0a6a21d00a20cf"
-    "0a20d00a6a21d10a20d00a20d10a6a21d20a20d10a20d20a6a21d30a20d20a20d30a6a21d40a20d30a20d40a6a21d5"
-    "0a20d40a20d50a6a21d60a20d50a20d60a6a21d70a20d60a20d70a6a21d80a20d70a20d80a6a21d90a20d80a20d90a"
-    "6a21da0a20d90a20da0a6a21db0a20da0a20db0a6a21dc0a20db0a20dc0a6a21dd0a20dc0a20dd0a6a21de0a20dd0a"
-    "20de0a6a21df0a20de0a20df0a6a21e00a20df0a20e00a6a21e10a20e00a20e10a6a21e20a20e10a20e20a6a21e30a"
-    "20e20a20e30a6a21e40a20e30a20e40a6a21e50a20e40a20e50a6a21e60a20e50a20e60a6a21e70a20e60a20e70a6a"
-    "21e80a20e70a20e80a6a21e90a20e80a20e90a6a21ea0a20e90a20ea0a6a21eb0a20ea0a20eb0a6a21ec0a20eb0a20"
-    "ec0a6a21ed0a20ec0a20ed0a6a21ee0a20ed0a20ee0a6a21ef0a20ee0a20ef0a6a21f00a20ef0a20f00a6a21f10a20"
-    "f00a20f10a6a21f20a20f10a20f20a6a21f30a20f20a20f30a6a21f40a20f30a20f40a6a21f50a20f40a20f50a6a21"
-    "f60a20f50a20f60a6a21f70a20f60a20f70a6a21f80a20f70a20f80a6a21f90a20f80a20f90a6a21fa0a20f90a20fa"
-    "0a6a21fb0a20fa0a20fb0a6a21fc0a20fb0a20fc0a6a21fd0a20fc0a20fd0a6a21fe0a20fd0a20fe0a6a21ff0a20fe"
-    "0a20ff0a6a21800b20ff0a20800b6a21810b20800b20810b6a21820b20810b20820b6a21830b20820b20830b6a2184"
-    "0b20830b20840b6a21850b20840b20850b6a21860b20850b20860b6a21870b20860b20870b6a21880b20870b20880b"
-    "6a21890b20880b20890b6a218a0b20890b208a0b6a218b0b208a0b208b0b6a218c0b208b0b208c0b6a218d0b208c0b"
-    "208d0b6a218e0b208d0b208e0b6a218f0b208e0b208f0b6a21900b208f0b20900b6a21910b20900b20910b6a21920b"
-    "20910b20920b6a21930b20920b20930b6a21940b20930b20940b6a21950b20940b20950b6a21960b20950b20960b6a"
-    "21970b20960b20970b6a21980b20970b20980b6a21990b20980b20990b6a219a0b20990b209a0b6a219b0b209a0b20"
-    "9b0b6a219c0b209b0b209c0b6a219d0b209c0b209d0b6a219e0b209d0b209e0b6a219f0b209e0b209f0b6a21a00b20"
-    "9f0b20a00b6a21a10b20a00b20a10b6a21a20b20a10b20a20b6a21a30b20a20b20a30b6a21a40b20a30b20a40b6a21"
-    "a50b20a40b20a50b6a21a60b20a50b20a60b6a21a70b20a60b20a70b6a21a80b20a70b20a80b6a21a90b20a80b20a9"
-    "0b6a21aa0b20a90b20aa0b6a21ab0b20aa0b20ab0b6a21ac0b20ab0b20ac0b6a21ad0b20ac0b20ad0b6a21ae0b20ad"
-    "0b20ae0b6a21af0b20ae0b20af0b6a21b00b20af0b20b00b6a21b10b20b00b20b10b6a21b20b20b10b20b20b6a21b3"
-    "0b20b20b20b30b6a21b40b20b30b20b40b6a21b50b20b40b20b50b6a21b60b20b50b20b60b6a21b70b20b60b20b70b"
-    "6a21b80b20b70b20b80b6a21b90b20b80b20b90b6a21ba0b20b90b20ba0b6a21bb0b20ba0b20bb0b6a21bc0b20bb0b"
-    "20bc0b6a21bd0b20bc0b20bd0b6a21be0b20bd0b20be0b6a21bf0b20be0b20bf0b6a21c00b20bf0b20c00b6a21c10b"
-    "20c00b20c10b6a21c20b20c10b20c20b6a21c30b20c20b20c30b6a21c40b20c30b20c40b6a21c50b20c40b20c50b6a"
-    "21c60b20c50b20c60b6a21c70b20c60b20c70b6a21c80b20c70b20c80b6a21c90b20c80b20c90b6a21ca0b20c90b20"
-    "ca0b6a21cb0b20ca0b20cb0b6a21cc0b20cb0b20cc0b6a21cd0b20cc0b20cd0b6a21ce0b20cd0b20ce0b6a21cf0b20"
-    "ce0b20cf0b6a21d00b20cf0b20d00b6a21d10b20d00b20d10b6a21d20b20d10b20d20b6a21d30b20d20b20d30b6a21"
-    "d40b20d30b20d40b6a21d50b20d40b20d50b6a21d60b20d50b20d60b6a21d70b20d60b20d70b6a21d80b20d70b20d8"
-    "0b6a21d90b20d80b20d90b6a21da0b20d90b20da0b6a21db0b20da0b20db0b6a21dc0b20db0b20dc0b6a21dd0b20dc"
-    "0b20dd0b6a21de0b20dd0b20de0b6a21df0b20de0b20df0b6a21e00b20df0b20e00b6a21e10b20e00b20e10b6a21e2"
-    "0b20e10b20e20b6a21e30b20e20b20e30b6a21e40b20e30b20e40b6a21e50b20e40b20e50b6a21e60b20e50b20e60b"
-    "6a21e70b20e60b20e70b6a21e80b20e70b20e80b6a21e90b20e80b20e90b6a21ea0b20e90b20ea0b6a21eb0b20ea0b"
-    "20eb0b6a21ec0b20eb0b20ec0b6a21ed0b20ec0b20ed0b6a21ee0b20ed0b20ee0b6a21ef0b20ee0b20ef0b6a21f00b"
-    "20ef0b20f00b6a21f10b20f00b20f10b6a21f20b20f10b20f20b6a21f30b20f20b20f30b6a21f40b20f30b20f40b6a"
-    "21f50b20f40b20f50b6a21f60b20f50b20f60b6a21f70b20f60b20f70b6a21f80b20f70b20f80b6a21f90b20f80b20"
-    "f90b6a21fa0b20f90b20fa0b6a21fb0b20fa0b20fb0b6a21fc0b20fb0b20fc0b6a21fd0b20fc0b20fd0b6a21fe0b20"
-    "fd0b20fe0b6a21ff0b20fe0b20ff0b6a21800c20ff0b20800c6a21810c20800c20810c6a21820c20810c20820c6a21"
-    "830c20820c20830c6a21840c20830c20840c6a21850c20840c20850c6a21860c20850c20860c6a21870c20860c2087"
-    "0c6a21880c20870c20880c6a21890c20880c20890c6a218a0c20890c208a0c6a218b0c208a0c208b0c6a218c0c208b"
-    "0c208c0c6a218d0c208c0c208d0c6a218e0c208d0c208e0c6a218f0c208e0c208f0c6a21900c208f0c20900c6a2191"
-    "0c20900c20910c6a21920c20910c20920c6a21930c20920c20930c6a21940c20930c20940c6a21950c20940c20950c"
-    "6a21960c20950c20960c6a21970c20960c20970c6a21980c20970c20980c6a21990c20980c20990c6a219a0c20990c"
-    "209a0c6a219b0c209a0c209b0c6a219c0c209b0c209c0c6a219d0c209c0c209d0c6a219e0c209d0c209e0c6a219f0c"
-    "209e0c209f0c6a21a00c209f0c20a00c6a21a10c20a00c20a10c6a21a20c20a10c20a20c6a21a30c20a20c20a30c6a"
-    "21a40c20a30c20a40c6a21a50c20a40c20a50c6a21a60c20a50c20a60c6a21a70c20a60c20a70c6a21a80c20a70c20"
-    "a80c6a21a90c20a80c20a90c6a21aa0c20a90c20aa0c6a21ab0c20aa0c20ab0c6a21ac0c20ab0c20ac0c6a21ad0c20"
-    "ac0c20ad0c6a21ae0c20ad0c20ae0c6a21af0c20ae0c20af0c6a21b00c20af0c20b00c6a21b10c20b00c20b10c6a21"
-    "b20c20b10c20b20c6a21b30c20b20c20b30c6a21b40c20b30c20b40c6a21b50c20b40c20b50c6a21b60c20b50c20b6"
-    "0c6a21b70c20b60c20b70c6a21b80c20b70c20b80c6a21b90c20b80c20b90c6a21ba0c20b90c20ba0c6a21bb0c20ba"
-    "0c20bb0c6a21bc0c20bb0c20bc0c6a21bd0c20bc0c20bd0c6a21be0c20bd0c20be0c6a21bf0c20be0c20bf0c6a21c0"
-    "0c20bf0c20c00c6a21c10c20c00c20c10c6a21c20c20c10c20c20c6a21c30c20c20c20c30c6a21c40c20c30c20c40c"
-    "6a21c50c20c40c20c50c6a21c60c20c50c20c60c6a21c70c20c60c20c70c6a21c80c20c70c20c80c6a21c90c20c80c"
-    "20c90c6a21ca0c20c90c20ca0c6a21cb0c20ca0c20cb0c6a21cc0c20cb0c20cc0c6a21cd0c20cc0c20cd0c6a21ce0c"
-    "20cd0c20ce0c6a21cf0c20ce0c20cf0c6a21d00c20cf0c20d00c6a21d10c20d00c20d10c6a21d20c20d10c20d20c6a"
-    "21d30c20d20c20d30c6a21d40c20d30c20d40c6a21d50c20d40c20d50c6a21d60c20d50c20d60c6a21d70c20d60c20"
-    "d70c6a21d80c20d70c20d80c6a21d90c20d80c20d90c6a21da0c20d90c20da0c6a21db0c20da0c20db0c6a21dc0c20"
-    "db0c20dc0c6a21dd0c20dc0c20dd0c6a21de0c20dd0c20de0c6a21df0c20de0c20df0c6a21e00c20df0c20e00c6a21"
-    "e10c20e00c20e10c6a21e20c20e10c20e20c6a21e30c20e20c20e30c6a21e40c20e30c20e40c6a21e50c20e40c20e5"
-    "0c6a21e60c20e50c20e60c6a21e70c20e60c20e70c6a21e80c20e70c20e80c6a21e90c20e80c20e90c6a21ea0c20e9"
-    "0c20ea0c6a21eb0c20ea0c20eb0c6a21ec0c20eb0c20ec0c6a21ed0c20ec0c20ed0c6a21ee0c20ed0c20ee0c6a21ef"
-    "0c20ee0c20ef0c6a21f00c20ef0c20f00c6a21f10c20f00c20f10c6a21f20c20f10c20f20c6a21f30c20f20c20f30c"
-    "6a21f40c20f30c20f40c6a21f50c20f40c20f50c6a21f60c20f50c20f60c6a21f70c20f60c20f70c6a21f80c20f70c"
-    "20f80c6a21f90c20f80c20f90c6a21fa0c20f90c20fa0c6a21fb0c20fa0c20fb0c6a21fc0c20fb0c20fc0c6a21fd0c"
-    "20fc0c20fd0c6a21fe0c20fd0c20fe0c6a21ff0c20fe0c20ff0c6a21800d20ff0c20800d6a21810d20800d20810d6a"
-    "21820d20810d20820d6a21830d20820d20830d6a21840d20830d20840d6a21850d20840d20850d6a21860d20850d20"
-    "860d6a21870d20860d20870d6a21880d20870d20880d6a21890d20880d20890d6a218a0d20890d208a0d6a218b0d20"
-    "8a0d208b0d6a218c0d208b0d208c0d6a218d0d208c0d208d0d6a218e0d208d0d208e0d6a218f0d208e0d208f0d6a21"
-    "900d208f0d20900d6a21910d20900d20910d6a21920d20910d20920d6a21930d20920d20930d6a21940d20930d2094"
-    "0d6a21950d20940d20950d6a21960d20950d20960d6a21970d20960d20970d6a21980d20970d20980d6a21990d2098"
-    "0d20990d6a219a0d20990d209a0d6a219b0d209a0d209b0d6a219c0d209b0d209c0d6a219d0d209c0d209d0d6a219e"
-    "0d209d0d209e0d6a219f0d209e0d209f0d6a21a00d209f0d20a00d6a21a10d20a00d20a10d6a21a20d20a10d20a20d"
-    "6a21a30d20a20d20a30d6a21a40d20a30d20a40d6a21a50d20a40d20a50d6a21a60d20a50d20a60d6a21a70d20a60d"
-    "20a70d6a21a80d20a70d20a80d6a21a90d20a80d20a90d6a21aa0d20a90d20aa0d6a21ab0d20aa0d20ab0d6a21ac0d"
-    "20ab0d20ac0d6a21ad0d20ac0d20ad0d6a21ae0d20ad0d20ae0d6a21af0d20ae0d20af0d6a21b00d20af0d20b00d6a"
-    "21b10d20b00d20b10d6a21b20d20b10d20b20d6a21b30d20b20d20b30d6a21b40d20b30d20b40d6a21b50d20b40d20"
-    "b50d6a21b60d20b50d20b60d6a21b70d20b60d20b70d6a21b80d20b70d20b80d6a21b90d20b80d20b90d6a21ba0d20"
-    "b90d20ba0d6a21bb0d20ba0d20bb0d6a21bc0d20bb0d20bc0d6a21bd0d20bc0d20bd0d6a21be0d20bd0d20be0d6a21"
-    "bf0d20be0d20bf0d6a21c00d20bf0d20c00d6a21c10d20c00d20c10d6a21c20d20c10d20c20d6a21c30d20c20d20c3"
-    "0d6a21c40d20c30d20c40d6a21c50d20c40d20c50d6a21c60d20c50d20c60d6a21c70d20c60d20c70d6a21c80d20c7"
-    "0d20c80d6a21c90d20c80d20c90d6a21ca0d20c90d20ca0d6a21cb0d20ca0d20cb0d6a21cc0d20cb0d20cc0d6a21cd"
-    "0d20cc0d20cd0d6a21ce0d20cd0d20ce0d6a21cf0d20ce0d20cf0d6a21d00d20cf0d20d00d6a21d10d20d00d20d10d"
-    "6a21d20d20d10d20d20d6a21d30d20d20d20d30d6a21d40d20d30d20d40d6a21d50d20d40d20d50d6a21d60d20d50d"
-    "20d60d6a21d70d20d60d20d70d6a21d80d20d70d20d80d6a21d90d20d80d20d90d6a21da0d20d90d20da0d6a21db0d"
-    "20da0d20db0d6a21dc0d20db0d20dc0d6a21dd0d20dc0d20dd0d6a21de0d20dd0d20de0d6a21df0d20de0d20df0d6a"
-    "21e00d20df0d20e00d6a21e10d20e00d20e10d6a21e20d20e10d20e20d6a21e30d20e20d20e30d6a21e40d20e30d20"
-    "e40d6a21e50d20e40d20e50d6a21e60d20e50d20e60d6a21e70d20e60d20e70d6a21e80d20e70d20e80d6a21e90d20"
-    "e80d20e90d6a21ea0d20e90d20ea0d6a21eb0d20ea0d20eb0d6a21ec0d20eb0d20ec0d6a21ed0d20ec0d20ed0d6a21"
-    "ee0d20ed0d20ee0d6a21ef0d20ee0d20ef0d6a21f00d20ef0d20f00d6a21f10d20f00d20f10d6a21f20d20f10d20f2"
-    "0d6a21f30d20f20d20f30d6a21f40d20f30d20f40d6a21f50d20f40d20f50d6a21f60d20f50d20f60d6a21f70d20f6"
-    "0d20f70d6a21f80d20f70d20f80d6a21f90d20f80d20f90d6a21fa0d20f90d20fa0d6a21fb0d20fa0d20fb0d6a21fc"
-    "0d20fb0d20fc0d6a21fd0d20fc0d20fd0d6a21fe0d20fd0d20fe0d6a21ff0d20fe0d20ff0d6a21800e20ff0d20800e"
-    "6a21810e20800e20810e6a21820e20810e20820e6a21830e20820e20830e6a21840e20830e20840e6a21850e20840e"
-    "20850e6a21860e20850e20860e6a21870e20860e20870e6a21880e20870e20880e6a21890e20880e20890e6a218a0e"
-    "20890e208a0e6a218b0e208a0e208b0e6a218c0e208b0e208c0e6a218d0e208c0e208d0e6a218e0e208d0e208e0e6a"
-    "218f0e208e0e208f0e6a21900e208f0e20900e6a21910e20900e20910e6a21920e20910e20920e6a21930e20920e20"
-    "930e6a21940e20930e20940e6a21950e20940e20950e6a21960e20950e20960e6a21970e20960e20970e6a21980e20"
-    "970e20980e6a21990e20980e20990e6a219a0e20990e209a0e6a219b0e209a0e209b0e6a219c0e209b0e209c0e6a21"
-    "9d0e209c0e209d0e6a219e0e209d0e209e0e6a219f0e209e0e209f0e6a21a00e209f0e20a00e6a21a10e20a00e20a1"
-    "0e6a21a20e20a10e20a20e6a21a30e20a20e20a30e6a21a40e20a30e20a40e6a21a50e20a40e20a50e6a21a60e20a5"
-    "0e20a60e6a21a70e20a60e20a70e6a21a80e20a70e20a80e6a21a90e20a80e20a90e6a21aa0e20a90e20aa0e6a21ab"
-    "0e20aa0e20ab0e6a21ac0e20ab0e20ac0e6a21ad0e20ac0e20ad0e6a21ae0e20ad0e20ae0e6a21af0e20ae0e20af0e"
-    "6a21b00e20af0e20b00e6a21b10e20b00e20b10e6a21b20e20b10e20b20e6a21b30e20b20e20b30e6a21b40e20b30e"
-    "20b40e6a21b50e20b40e20b50e6a21b60e20b50e20b60e6a21b70e20b60e20b70e6a21b80e20b70e20b80e6a21b90e"
-    "20b80e20b90e6a21ba0e20b90e20ba0e6a21bb0e20ba0e20bb0e6a21bc0e20bb0e20bc0e6a21bd0e20bc0e20bd0e6a"
-    "21be0e20bd0e20be0e6a21bf0e20be0e20bf0e6a21c00e20bf0e20c00e6a21c10e20c00e20c10e6a21c20e20c10e20"
-    "c20e6a21c30e20c20e20c30e6a21c40e20c30e20c40e6a21c50e20c40e20c50e6a21c60e20c50e20c60e6a21c70e20"
-    "c60e20c70e6a21c80e20c70e20c80e6a21c90e20c80e20c90e6a21ca0e20c90e20ca0e6a21cb0e20ca0e20cb0e6a21"
-    "cc0e20cb0e20cc0e6a21cd0e20cc0e20cd0e6a21ce0e20cd0e20ce0e6a21cf0e20ce0e20cf0e6a21d00e20cf0e20d0"
-    "0e6a21d10e20d00e20d10e6a21d20e20d10e20d20e6a21d30e20d20e20d30e6a21d40e20d30e20d40e6a21d50e20d4"
-    "0e20d50e6a21d60e20d50e20d60e6a21d70e20d60e20d70e6a21d80e20d70e20d80e6a21d90e20d80e20d90e6a21da"
-    "0e20d90e20da0e6a21db0e20da0e20db0e6a21dc0e20db0e20dc0e6a21dd0e20dc0e20dd0e6a21de0e20dd0e20de0e"
-    "6a21df0e20de0e20df0e6a21e00e20df0e20e00e6a21e10e20e00e20e10e6a21e20e20e10e20e20e6a21e30e20e20e"
-    "20e30e6a21e40e20e30e20e40e6a21e50e20e40e20e50e6a21e60e20e50e20e60e6a21e70e20e60e20e70e6a21e80e"
-    "20e70e20e80e6a21e90e20e80e20e90e6a21ea0e20e90e20ea0e6a21eb0e20ea0e20eb0e6a21ec0e20eb0e20ec0e6a"
-    "21ed0e20ec0e20ed0e6a21ee0e20ed0e20ee0e6a21ef0e20ee0e20ef0e6a21f00e20ef0e20f00e6a21f10e20f00e20"
-    "f10e6a21f20e20f10e20f20e6a21f30e20f20e20f30e6a21f40e20f30e20f40e6a21f50e20f40e20f50e6a21f60e20"
-    "f50e20f60e6a21f70e20f60e20f70e6a21f80e20f70e20f80e6a21f90e20f80e20f90e6a21fa0e20f90e20fa0e6a21"
-    "fb0e20fa0e20fb0e6a21fc0e20fb0e20fc0e6a21fd0e20fc0e20fd0e6a21fe0e20fd0e20fe0e6a21ff0e20fe0e20ff"
-    "0e6a21800f20ff0e20800f6a21810f20800f20810f6a21820f20810f20820f6a21830f20820f20830f6a21840f2083"
-    "0f20840f6a21850f20840f20850f6a21860f20850f20860f6a21870f20860f20870f6a21880f20870f20880f6a2189"
-    "0f20880f20890f6a218a0f20890f208a0f6a218b0f208a0f208b0f6a218c0f208b0f208c0f6a218d0f208c0f208d0f"
-    "6a218e0f208d0f208e0f6a218f0f208e0f208f0f6a21900f208f0f20900f6a21910f20900f20910f6a21920f20910f"
-    "20920f6a21930f20920f20930f6a21940f20930f20940f6a21950f20940f20950f6a21960f20950f20960f6a21970f"
-    "20960f20970f6a21980f20970f20980f6a21990f20980f20990f6a219a0f20990f209a0f6a219b0f209a0f209b0f6a"
-    "219c0f209b0f209c0f6a219d0f209c0f209d0f6a219e0f209d0f209e0f6a219f0f209e0f209f0f6a21a00f209f0f20"
-    "a00f6a21a10f20a00f20a10f6a21a20f20a10f20a20f6a21a30f20a20f20a30f6a21a40f20a30f20a40f6a21a50f20"
-    "a40f20a50f6a21a60f20a50f20a60f6a21a70f20a60f20a70f6a21a80f20a70f20a80f6a21a90f20a80f20a90f6a21"
-    "aa0f20a90f20aa0f6a21ab0f20aa0f20ab0f6a21ac0f20ab0f20ac0f6a21ad0f20ac0f20ad0f6a21ae0f20ad0f20ae"
-    "0f6a21af0f20ae0f20af0f6a21b00f20af0f20b00f6a21b10f20b00f20b10f6a21b20f20b10f20b20f6a21b30f20b2"
-    "0f20b30f6a21b40f20b30f20b40f6a21b50f20b40f20b50f6a21b60f20b50f20b60f6a21b70f20b60f20b70f6a21b8"
-    "0f20b70f20b80f6a21b90f20b80f20b90f6a21ba0f20b90f20ba0f6a21bb0f20ba0f20bb0f6a21bc0f20bb0f20bc0f"
-    "6a21bd0f20bc0f20bd0f6a21be0f20bd0f20be0f6a21bf0f20be0f20bf0f6a21c00f20bf0f20c00f6a21c10f20c00f"
-    "20c10f6a21c20f20c10f20c20f6a21c30f20c20f20c30f6a21c40f20c30f20c40f6a21c50f20c40f20c50f6a21c60f"
-    "20c50f20c60f6a21c70f20c60f20c70f6a21c80f20c70f20c80f6a21c90f20c80f20c90f6a21ca0f20c90f20ca0f6a"
-    "21cb0f20ca0f20cb0f6a21cc0f20cb0f20cc0f6a21cd0f20cc0f20cd0f6a21ce0f20cd0f20ce0f6a21cf0f20ce0f20"
-    "cf0f6a21d00f20cf0f20d00f6a21d10f20d00f20d10f6a21d20f20d10f20d20f6a21d30f20d20f20d30f6a21d40f20"
-    "d30f20d40f6a21d50f20d40f20d50f6a21d60f20d50f20d60f6a21d70f20d60f20d70f6a21d80f20d70f20d80f6a21"
-    "d90f20d80f20d90f6a21da0f20d90f20da0f6a21db0f20da0f20db0f6a21dc0f20db0f20dc0f6a21dd0f20dc0f20dd"
-    "0f6a21de0f20dd0f20de0f6a21df0f20de0f20df0f6a21e00f20df0f20e00f6a21e10f20e00f20e10f6a21e20f20e1"
-    "0f20e20f6a21e30f20e20f20e30f6a21e40f20e30f20e40f6a21e50f20e40f20e50f6a21e60f20e50f20e60f6a21e7"
-    "0f20e60f20e70f6a21e80f20e70f20e80f6a21e90f20e80f20e90f6a21ea0f20e90f20ea0f6a21eb0f20ea0f20eb0f"
-    "6a21ec0f20eb0f20ec0f6a21ed0f20ec0f20ed0f6a21ee0f20ed0f20ee0f6a21ef0f20ee0f20ef0f6a21f00f20ef0f"
-    "20f00f6a21f10f20f00f20f10f6a21f20f20f10f20f20f6a21f30f20f20f20f30f6a21f40f20f30f20f40f6a21f50f"
-    "20f40f20f50f6a21f60f20f50f20f60f6a21f70f20f60f20f70f6a21f80f20f70f20f80f6a21f90f20f80f20f90f6a"
-    "21fa0f20f90f20fa0f6a21fb0f20fa0f20fb0f6a21fc0f20fb0f20fc0f6a21fd0f20fc0f20fd0f6a21fe0f20fd0f20"
-    "fe0f6a21ff0f20fe0f20ff0f6a21801020ff0f2080106a2181102080102081106a2182102081102082106a21831020"
-    "82102083106a2184102083102084106a2185102084102085106a2186102085102086106a2187102086102087106a21"
-    "88102087102088106a2189102088102089106a218a10208910208a106a218b10208a10208b106a218c10208b10208c"
-    "106a218d10208c10208d106a218e10208d10208e106a218f10208e10208f106a219010208f102090106a2191102090"
-    "102091106a2192102091102092106a2193102092102093106a2194102093102094106a2195102094102095106a2196"
-    "102095102096106a2197102096102097106a2198102097102098106a2199102098102099106a219a10209910209a10"
-    "6a219b10209a10209b106a219c10209b10209c106a219d10209c10209d106a219e10209d10209e106a219f10209e10"
-    "209f106a21a010209f1020a0106a21a11020a01020a1106a21a21020a11020a2106a21a31020a21020a3106a21a410"
-    "20a31020a4106a21a51020a41020a5106a21a61020a51020a6106a21a71020a61020a7106a21a81020a71020a8106a"
-    "21a91020a81020a9106a21aa1020a91020aa106a21ab1020aa1020ab106a21ac1020ab1020ac106a21ad1020ac1020"
-    "ad106a21ae1020ad1020ae106a21af1020ae1020af106a21b01020af1020b0106a21b11020b01020b1106a21b21020"
-    "b11020b2106a21b31020b21020b3106a21b41020b31020b4106a21b51020b41020b5106a21b61020b51020b6106a21"
-    "b71020b61020b7106a21b81020b71020b8106a21b91020b81020b9106a21ba1020b91020ba106a21bb1020ba1020bb"
-    "106a21bc1020bb1020bc106a21bd1020bc1020bd106a21be1020bd1020be106a21bf1020be1020bf106a21c01020bf"
-    "1020c0106a21c11020c01020c1106a21c21020c11020c2106a21c31020c21020c3106a21c41020c31020c4106a21c5"
-    "1020c41020c5106a21c61020c51020c6106a21c71020c61020c7106a21c81020c71020c8106a21c91020c81020c910"
-    "6a21ca1020c91020ca106a21cb1020ca1020cb106a21cc1020cb1020cc106a21cd1020cc1020cd106a21ce1020cd10"
-    "20ce106a21cf1020ce1020cf106a21d01020cf1020d0106a21d11020d01020d1106a21d21020d11020d2106a21d310"
-    "20d21020d3106a21d41020d31020d4106a21d51020d41020d5106a21d61020d51020d6106a21d71020d61020d7106a"
-    "21d81020d71020d8106a21d91020d81020d9106a21da1020d91020da106a21db1020da1020db106a21dc1020db1020"
-    "dc106a21dd1020dc1020dd106a21de1020dd1020de106a21df1020de1020df106a21e01020df1020e0106a21e11020"
-    "e01020e1106a21e21020e11020e2106a21e31020e21020e3106a21e41020e31020e4106a21e51020e41020e5106a21"
-    "e61020e51020e6106a21e71020e61020e7106a21e81020e71020e8106a21e91020e81020e9106a21ea1020e91020ea"
-    "106a21eb1020ea1020eb106a21ec1020eb1020ec106a21ed1020ec1020ed106a21ee1020ed1020ee106a21ef1020ee"
-    "1020ef106a21f01020ef1020f0106a21f11020f01020f1106a21f21020f11020f2106a21f31020f21020f3106a21f4"
-    "1020f31020f4106a21f51020f41020f5106a21f61020f51020f6106a21f71020f61020f7106a21f81020f71020f810"
-    "6a21f91020f81020f9106a21fa1020f91020fa106a21fb1020fa1020fb106a21fc1020fb1020fc106a21fd1020fc10"
-    "20fd106a21fe1020fd1020fe106a21ff1020fe1020ff106a21801120ff102080116a2181112080112081116a218211"
-    "2081112082116a2183112082112083116a2184112083112084116a2185112084112085116a2186112085112086116a"
-    "2187112086112087116a2188112087112088116a2189112088112089116a218a11208911208a116a218b11208a1120"
-    "8b116a218c11208b11208c116a218d11208c11208d116a218e11208d11208e116a218f11208e11208f116a21901120"
-    "8f112090116a2191112090112091116a2192112091112092116a2193112092112093116a2194112093112094116a21"
-    "95112094112095116a2196112095112096116a2197112096112097116a2198112097112098116a2199112098112099"
-    "116a219a11209911209a116a219b11209a11209b116a219c11209b11209c116a219d11209c11209d116a219e11209d"
-    "11209e116a219f11209e11209f116a21a011209f1120a0116a21a11120a01120a1116a21a21120a11120a2116a21a3"
-    "1120a21120a3116a21a41120a31120a4116a21a51120a41120a5116a21a61120a51120a6116a21a71120a61120a711"
-    "6a21a81120a71120a8116a21a91120a81120a9116a21aa1120a91120aa116a21ab1120aa1120ab116a21ac1120ab11"
-    "20ac116a21ad1120ac1120ad116a21ae1120ad1120ae116a21af1120ae1120af116a21b01120af1120b0116a21b111"
-    "20b01120b1116a21b21120b11120b2116a21b31120b21120b3116a21b41120b31120b4116a21b51120b41120b5116a"
-    "21b61120b51120b6116a21b71120b61120b7116a21b81120b71120b8116a21b91120b81120b9116a21ba1120b91120"
-    "ba116a21bb1120ba1120bb116a21bc1120bb1120bc116a21bd1120bc1120bd116a21be1120bd1120be116a21bf1120"
-    "be1120bf116a21c01120bf1120c0116a21c11120c01120c1116a21c21120c11120c2116a21c31120c21120c3116a21"
-    "c41120c31120c4116a21c51120c41120c5116a21c61120c51120c6116a21c71120c61120c7116a21c81120c71120c8"
-    "116a21c91120c81120c9116a21ca1120c91120ca116a21cb1120ca1120cb116a21cc1120cb1120cc116a21cd1120cc"
-    "1120cd116a21ce1120cd1120ce116a21cf1120ce1120cf116a21d01120cf1120d0116a21d11120d01120d1116a21d2"
-    "1120d11120d2116a21d31120d21120d3116a21d41120d31120d4116a21d51120d41120d5116a21d61120d51120d611"
-    "6a21d71120d61120d7116a21d81120d71120d8116a21d91120d81120d9116a21da1120d91120da116a21db1120da11"
-    "20db116a21dc1120db1120dc116a21dd1120dc1120dd116a21de1120dd1120de116a21df1120de1120df116a21e011"
-    "20df1120e0116a21e11120e01120e1116a21e21120e11120e2116a21e31120e21120e3116a21e41120e31120e4116a"
-    "21e51120e41120e5116a21e61120e51120e6116a21e71120e61120e7116a21e81120e71120e8116a21e91120e81120"
-    "e9116a21ea1120e91120ea116a21eb1120ea1120eb116a21ec1120eb1120ec116a21ed1120ec1120ed116a21ee1120"
-    "ed1120ee116a21ef1120ee1120ef116a21f01120ef1120f0116a21f11120f01120f1116a21f21120f11120f2116a21"
-    "f31120f21120f3116a21f41120f31120f4116a21f51120f41120f5116a21f61120f51120f6116a21f71120f61120f7"
-    "116a21f81120f71120f8116a21f91120f81120f9116a21fa1120f91120fa116a21fb1120fa1120fb116a21fc1120fb"
-    "1120fc116a21fd1120fc1120fd116a21fe1120fd1120fe116a21ff1120fe1120ff116a21801220ff112080126a2181"
-    "122080122081126a2182122081122082126a2183122082122083126a2184122083122084126a218512208412208512"
-    "6a2186122085122086126a2187122086122087126a2188122087122088126a2189122088122089126a218a12208912"
-    "208a126a218b12208a12208b126a218c12208b12208c126a218d12208c12208d126a218e12208d12208e126a218f12"
-    "208e12208f126a219012208f122090126a2191122090122091126a2192122091122092126a2193122092122093126a"
-    "2194122093122094126a2195122094122095126a2196122095122096126a2197122096122097126a21981220971220"
-    "98126a2199122098122099126a219a12209912209a126a219b12209a12209b126a219c12209b12209c126a219d1220"
-    "9c12209d126a219e12209d12209e126a219f12209e12209f126a21a012209f1220a0126a21a11220a01220a1126a21"
-    "a21220a11220a2126a21a31220a21220a3126a21a41220a31220a4126a21a51220a41220a5126a21a61220a51220a6"
-    "126a21a71220a61220a7126a21a81220a71220a8126a21a91220a81220a9126a21aa1220a91220aa126a21ab1220aa"
-    "1220ab126a21ac1220ab1220ac126a21ad1220ac1220ad126a21ae1220ad1220ae126a21af1220ae1220af126a21b0"
-    "1220af1220b0126a21b11220b01220b1126a21b21220b11220b2126a21b31220b21220b3126a21b41220b31220b412"
-    "6a21b51220b41220b5126a21b61220b51220b6126a21b71220b61220b7126a21b81220b71220b8126a21b91220b812"
-    "20b9126a21ba1220b91220ba126a21bb1220ba1220bb126a21bc1220bb1220bc126a21bd1220bc1220bd126a21be12"
-    "20bd1220be126a21bf1220be1220bf126a21c01220bf1220c0126a21c11220c01220c1126a21c21220c11220c2126a"
-    "21c31220c21220c3126a21c41220c31220c4126a21c51220c41220c5126a21c61220c51220c6126a21c71220c61220"
-    "c7126a21c81220c71220c8126a21c91220c81220c9126a21ca1220c91220ca126a21cb1220ca1220cb126a21cc1220"
-    "cb1220cc126a21cd1220cc1220cd126a21ce1220cd1220ce126a21cf1220ce1220cf126a21d01220cf1220d0126a21"
-    "d11220d01220d1126a21d21220d11220d2126a21d31220d21220d3126a21d41220d31220d4126a21d51220d41220d5"
-    "126a21d61220d51220d6126a21d71220d61220d7126a21d81220d71220d8126a21d91220d81220d9126a21da1220d9"
-    "1220da126a21db1220da1220db126a21dc1220db1220dc126a21dd1220dc1220dd126a21de1220dd1220de126a21df"
-    "1220de1220df126a21e01220df1220e0126a21e11220e01220e1126a21e21220e11220e2126a21e31220e21220e312"
-    "6a21e41220e31220e4126a21e51220e41220e5126a21e61220e51220e6126a21e71220e61220e7126a21e81220e712"
-    "20e8126a21e91220e81220e9126a21ea1220e91220ea126a21eb1220ea1220eb126a21ec1220eb1220ec126a21ed12"
-    "20ec1220ed126a21ee1220ed1220ee126a21ef1220ee1220ef126a21f01220ef1220f0126a21f11220f01220f1126a"
-    "21f21220f11220f2126a21f31220f21220f3126a21f41220f31220f4126a21f51220f41220f5126a21f61220f51220"
-    "f6126a21f71220f61220f7126a21f81220f71220f8126a21f91220f81220f9126a21fa1220f91220fa126a21fb1220"
-    "fa1220fb126a21fc1220fb1220fc126a21fd1220fc1220fd126a21fe1220fd1220fe126a21ff1220fe1220ff126a21"
-    "801320ff122080136a2181132080132081136a2182132081132082136a2183132082132083136a2184132083132084"
-    "136a2185132084132085136a2186132085132086136a2187132086132087136a2188132087132088136a2189132088"
-    "132089136a218a13208913208a136a218b13208a13208b136a218c13208b13208c136a218d13208c13208d136a218e"
-    "13208d13208e136a218f13208e13208f136a219013208f132090136a2191132090132091136a219213209113209213"
-    "6a2193132092132093136a2194132093132094136a2195132094132095136a2196132095132096136a219713209613"
-    "2097136a2198132097132098136a2199132098132099136a219a13209913209a136a219b13209a13209b136a219c13"
-    "209b13209c136a219d13209c13209d136a219e13209d13209e136a219f13209e13209f136a21a013209f1320a0136a"
-    "21a11320a01320a1136a21a21320a11320a2136a21a31320a21320a3136a21a41320a31320a4136a21a51320a41320"
-    "a5136a21a61320a51320a6136a21a71320a61320a7136a21a81320a71320a8136a21a91320a81320a9136a21aa1320"
-    "a91320aa136a21ab1320aa1320ab136a21ac1320ab1320ac136a21ad1320ac1320ad136a21ae1320ad1320ae136a21"
-    "af1320ae1320af136a21b01320af1320b0136a21b11320b01320b1136a21b21320b11320b2136a21b31320b21320b3"
-    "136a21b41320b31320b4136a21b51320b41320b5136a21b61320b51320b6136a21b71320b61320b7136a21b81320b7"
-    "1320b8136a21b91320b81320b9136a21ba1320b91320ba136a21bb1320ba1320bb136a21bc1320bb1320bc136a21bd"
-    "1320bc1320bd136a21be1320bd1320be136a21bf1320be1320bf136a21c01320bf1320c0136a21c11320c01320c113"
-    "6a21c21320c11320c2136a21c31320c21320c3136a21c41320c31320c4136a21c51320c41320c5136a21c61320c513"
-    "20c6136a21c71320c61320c7136a21c81320c71320c8136a21c91320c81320c9136a21ca1320c91320ca136a21cb13"
-    "20ca1320cb136a21cc1320cb1320cc136a21cd1320cc1320cd136a21ce1320cd1320ce136a21cf1320ce1320cf136a"
-    "21d01320cf1320d0136a21d11320d01320d1136a21d21320d11320d2136a21d31320d21320d3136a21d41320d31320"
-    "d4136a21d51320d41320d5136a21d61320d51320d6136a21d71320d61320d7136a21d81320d71320d8136a21d91320"
-    "d81320d9136a21da1320d91320da136a21db1320da1320db136a21dc1320db1320dc136a21dd1320dc1320dd136a21"
-    "de1320dd1320de136a21df1320de1320df136a21e01320df1320e0136a21e11320e01320e1136a21e21320e11320e2"
-    "136a21e31320e21320e3136a21e41320e31320e4136a21e51320e41320e5136a21e61320e51320e6136a21e71320e6"
-    "1320e7136a21e81320e71320e8136a21e91320e81320e9136a21ea1320e91320ea136a21eb1320ea1320eb136a21ec"
-    "1320eb1320ec136a21ed1320ec1320ed136a21ee1320ed1320ee136a21ef1320ee1320ef136a21f01320ef1320f013"
-    "6a21f11320f01320f1136a21f21320f11320f2136a21f31320f21320f3136a21f41320f31320f4136a21f51320f413"
-    "20f5136a21f61320f51320f6136a21f71320f61320f7136a21f81320f71320f8136a21f91320f81320f9136a21fa13"
-    "20f91320fa136a21fb1320fa1320fb136a21fc1320fb1320fc136a21fd1320fc1320fd136a21fe1320fd1320fe136a"
-    "21ff1320fe1320ff136a21801420ff132080146a2181142080142081146a2182142081142082146a21831420821420"
-    "83146a2184142083142084146a2185142084142085146a2186142085142086146a2187142086142087146a21881420"
-    "87142088146a2189142088142089146a218a14208914208a146a218b14208a14208b146a218c14208b14208c146a21"
-    "8d14208c14208d146a218e14208d14208e146a218f14208e14208f146a219014208f142090146a2191142090142091"
-    "146a2192142091142092146a2193142092142093146a2194142093142094146a2195142094142095146a2196142095"
-    "142096146a2197142096142097146a2198142097142098146a2199142098142099146a219a14209914209a146a219b"
-    "14209a14209b146a219c14209b14209c146a219d14209c14209d146a219e14209d14209e146a219f14209e14209f14"
-    "6a21a014209f1420a0146a21a11420a01420a1146a21a21420a11420a2146a21a31420a21420a3146a21a41420a314"
-    "20a4146a21a51420a41420a5146a21a61420a51420a6146a21a71420a61420a7146a21a81420a71420a8146a21a914"
-    "20a81420a9146a21aa1420a91420aa146a21ab1420aa1420ab146a21ac1420ab1420ac146a21ad1420ac1420ad146a"
-    "21ae1420ad1420ae146a21af1420ae1420af146a21b01420af1420b0146a21b11420b01420b1146a21b21420b11420"
-    "b2146a21b31420b21420b3146a21b41420b31420b4146a21b51420b41420b5146a21b61420b51420b6146a21b71420"
-    "b61420b7146a21b81420b71420b8146a21b91420b81420b9146a21ba1420b91420ba146a21bb1420ba1420bb146a21"
-    "bc1420bb1420bc146a21bd1420bc1420bd146a21be1420bd1420be146a21bf1420be1420bf146a21c01420bf1420c0"
-    "146a21c11420c01420c1146a21c21420c11420c2146a21c31420c21420c3146a21c41420c31420c4146a21c51420c4"
-    "1420c5146a21c61420c51420c6146a21c71420c61420c7146a21c81420c71420c8146a21c91420c81420c9146a21ca"
-    "1420c91420ca146a21cb1420ca1420cb146a21cc1420cb1420cc146a21cd1420cc1420cd146a21ce1420cd1420ce14"
-    "6a21cf1420ce1420cf146a21d01420cf1420d0146a21d11420d01420d1146a21d21420d11420d2146a21d31420d214"
-    "20d3146a21d41420d31420d4146a21d51420d41420d5146a21d61420d51420d6146a21d71420d61420d7146a21d814"
-    "20d71420d8146a21d91420d81420d9146a21da1420d91420da146a21db1420da1420db146a21dc1420db1420dc146a"
-    "21dd1420dc1420dd146a21de1420dd1420de146a21df1420de1420df146a21e01420df1420e0146a21e11420e01420"
-    "e1146a21e21420e11420e2146a21e31420e21420e3146a21e41420e31420e4146a21e51420e41420e5146a21e61420"
-    "e51420e6146a21e71420e61420e7146a21e81420e71420e8146a21e91420e81420e9146a21ea1420e91420ea146a21"
-    "eb1420ea1420eb146a21ec1420eb1420ec146a21ed1420ec1420ed146a21ee1420ed1420ee146a21ef1420ee1420ef"
-    "146a21f01420ef1420f0146a21f11420f01420f1146a21f21420f11420f2146a21f31420f21420f3146a21f41420f3"
-    "1420f4146a21f51420f41420f5146a21f61420f51420f6146a21f71420f61420f7146a21f81420f71420f8146a21f9"
-    "1420f81420f9146a21fa1420f91420fa146a21fb1420fa1420fb146a21fc1420fb1420fc146a21fd1420fc1420fd14"
-    "6a21fe1420fd1420fe146a21ff1420fe1420ff146a21801520ff142080156a2181152080152081156a218215208115"
-    "2082156a2183152082152083156a2184152083152084156a2185152084152085156a2186152085152086156a218715"
-    "2086152087156a2188152087152088156a2189152088152089156a218a15208915208a156a218b15208a15208b156a"
-    "218c15208b15208c156a218d15208c15208d156a218e15208d15208e156a218f15208e15208f156a219015208f1520"
-    "90156a2191152090152091156a2192152091152092156a2193152092152093156a2194152093152094156a21951520"
-    "94152095156a2196152095152096156a2197152096152097156a2198152097152098156a2199152098152099156a21"
-    "9a15209915209a156a219b15209a15209b156a219c15209b15209c156a219d15209c15209d156a219e15209d15209e"
-    "156a219f15209e15209f156a21a015209f1520a0156a21a11520a01520a1156a21a21520a11520a2156a21a31520a2"
-    "1520a3156a21a41520a31520a4156a21a51520a41520a5156a21a61520a51520a6156a21a71520a61520a7156a21a8"
-    "1520a71520a8156a21a91520a81520a9156a21aa1520a91520aa156a21ab1520aa1520ab156a21ac1520ab1520ac15"
-    "6a21ad1520ac1520ad156a21ae1520ad1520ae156a21af1520ae1520af156a21b01520af1520b0156a21b11520b015"
-    "20b1156a21b21520b11520b2156a21b31520b21520b3156a21b41520b31520b4156a21b51520b41520b5156a21b615"
-    "20b51520b6156a21b71520b61520b7156a21b81520b71520b8156a21b91520b81520b9156a21ba1520b91520ba156a"
-    "21bb1520ba1520bb156a21bc1520bb1520bc156a21bd1520bc1520bd156a21be1520bd1520be156a21bf1520be1520"
-    "bf156a21c01520bf1520c0156a21c11520c01520c1156a21c21520c11520c2156a21c31520c21520c3156a21c41520"
-    "c31520c4156a21c51520c41520c5156a21c61520c51520c6156a21c71520c61520c7156a21c81520c71520c8156a21"
-    "c91520c81520c9156a21ca1520c91520ca156a21cb1520ca1520cb156a21cc1520cb1520cc156a21cd1520cc1520cd"
-    "156a21ce1520cd1520ce156a21cf1520ce1520cf156a21d01520cf1520d0156a21d11520d01520d1156a21d21520d1"
-    "1520d2156a21d31520d21520d3156a21d41520d31520d4156a21d51520d41520d5156a21d61520d51520d6156a21d7"
-    "1520d61520d7156a21d81520d71520d8156a21d91520d81520d9156a21da1520d91520da156a21db1520da1520db15"
-    "6a21dc1520db1520dc156a21dd1520dc1520dd156a21de1520dd1520de156a21df1520de1520df156a21e01520df15"
-    "20e0156a21e11520e01520e1156a21e21520e11520e2156a21e31520e21520e3156a21e41520e31520e4156a21e515"
-    "20e41520e5156a21e61520e51520e6156a21e71520e61520e7156a21e81520e71520e8156a21e91520e81520e9156a"
-    "21ea1520e91520ea156a21eb1520ea1520eb156a21ec1520eb1520ec156a21ed1520ec1520ed156a21ee1520ed1520"
-    "ee156a21ef1520ee1520ef156a21f01520ef1520f0156a21f11520f01520f1156a21f21520f11520f2156a21f31520"
-    "f21520f3156a21f41520f31520f4156a21f51520f41520f5156a21f61520f51520f6156a21f71520f61520f7156a21"
-    "f81520f71520f8156a21f91520f81520f9156a21fa1520f91520fa156a21fb1520fa1520fb156a21fc1520fb1520fc"
-    "156a21fd1520fc1520fd156a21fe1520fd1520fe156a21ff1520fe1520ff156a21801620ff152080166a2181162080"
-    "162081166a2182162081162082166a2183162082162083166a2184162083162084166a2185162084162085166a2186"
-    "162085162086166a2187162086162087166a2188162087162088166a2189162088162089166a218a16208916208a16"
-    "6a218b16208a16208b166a218c16208b16208c166a218d16208c16208d166a218e16208d16208e166a218f16208e16"
-    "208f166a219016208f162090166a2191162090162091166a2192162091162092166a2193162092162093166a219416"
-    "2093162094166a2195162094162095166a2196162095162096166a2197162096162097166a2198162097162098166a"
-    "2199162098162099166a219a16209916209a166a219b16209a16209b166a219c16209b16209c166a219d16209c1620"
-    "9d166a219e16209d16209e166a219f16209e16209f166a21a016209f1620a0166a21a11620a01620a1166a21a21620"
-    "a11620a2166a21a31620a21620a3166a21a41620a31620a4166a21a51620a41620a5166a21a61620a51620a6166a21"
-    "a71620a61620a7166a21a81620a71620a8166a21a91620a81620a9166a21aa1620a91620aa166a21ab1620aa1620ab"
-    "166a21ac1620ab1620ac166a21ad1620ac1620ad166a21ae1620ad1620ae166a21af1620ae1620af166a21b01620af"
-    "1620b0166a21b11620b01620b1166a21b21620b11620b2166a21b31620b21620b3166a21b41620b31620b4166a21b5"
-    "1620b41620b5166a21b61620b51620b6166a21b71620b61620b7166a21b81620b71620b8166a21b91620b81620b916"
-    "6a21ba1620b91620ba166a21bb1620ba1620bb166a21bc1620bb1620bc166a21bd1620bc1620bd166a21be1620bd16"
-    "20be166a21bf1620be1620bf166a21c01620bf1620c0166a21c11620c01620c1166a21c21620c11620c2166a21c316"
-    "20c21620c3166a21c41620c31620c4166a21c51620c41620c5166a21c61620c51620c6166a21c71620c61620c7166a"
-    "21c81620c71620c8166a21c91620c81620c9166a21ca1620c91620ca166a21cb1620ca1620cb166a21cc1620cb1620"
-    "cc166a21cd1620cc1620cd166a21ce1620cd1620ce166a21cf1620ce1620cf166a21d01620cf1620d0166a21d11620"
-    "d01620d1166a21d21620d11620d2166a21d31620d21620d3166a21d41620d31620d4166a21d51620d41620d5166a21"
-    "d61620d51620d6166a21d71620d61620d7166a21d81620d71620d8166a21d91620d81620d9166a21da1620d91620da"
-    "166a21db1620da1620db166a21dc1620db1620dc166a21dd1620dc1620dd166a21de1620dd1620de166a21df1620de"
-    "1620df166a21e01620df1620e0166a21e11620e01620e1166a21e21620e11620e2166a21e31620e21620e3166a21e4"
-    "1620e31620e4166a21e51620e41620e5166a21e61620e51620e6166a21e71620e61620e7166a21e81620e71620e816"
-    "6a21e91620e81620e9166a21ea1620e91620ea166a21eb1620ea1620eb166a21ec1620eb1620ec166a21ed1620ec16"
-    "20ed166a21ee1620ed1620ee166a21ef1620ee1620ef166a21f01620ef1620f0166a21f11620f01620f1166a21f216"
-    "20f11620f2166a21f31620f21620f3166a21f41620f31620f4166a21f51620f41620f5166a21f61620f51620f6166a"
-    "21f71620f61620f7166a21f81620f71620f8166a21f91620f81620f9166a21fa1620f91620fa166a21fb1620fa1620"
-    "fb166a21fc1620fb1620fc166a21fd1620fc1620fd166a21fe1620fd1620fe166a21ff1620fe1620ff166a21801720"
-    "ff162080176a2181172080172081176a2182172081172082176a2183172082172083176a2184172083172084176a21"
-    "85172084172085176a2186172085172086176a2187172086172087176a2188172087172088176a2189172088172089"
-    "176a218a17208917208a176a218b17208a17208b176a218c17208b17208c176a218d17208c17208d176a218e17208d"
-    "17208e176a218f17208e17208f176a219017208f172090176a2191172090172091176a2192172091172092176a2193"
-    "172092172093176a2194172093172094176a2195172094172095176a2196172095172096176a219717209617209717"
-    "6a2198172097172098176a2199172098172099176a219a17209917209a176a219b17209a17209b176a219c17209b17"
-    "209c176a219d17209c17209d176a219e17209d17209e176a219f17209e17209f176a21a017209f1720a0176a21a117"
-    "20a01720a1176a21a21720a11720a2176a21a31720a21720a3176a21a41720a31720a4176a21a51720a41720a5176a"
-    "21a61720a51720a6176a21a71720a61720a7176a21a81720a71720a8176a21a91720a81720a9176a21aa1720a91720"
-    "aa176a21ab1720aa1720ab176a21ac1720ab1720ac176a21ad1720ac1720ad176a21ae1720ad1720ae176a21af1720"
-    "ae1720af176a21b01720af1720b0176a21b11720b01720b1176a21b21720b11720b2176a21b31720b21720b3176a21"
-    "b41720b31720b4176a21b51720b41720b5176a21b61720b51720b6176a21b71720b61720b7176a21b81720b71720b8"
-    "176a21b91720b81720b9176a21ba1720b91720ba176a21bb1720ba1720bb176a21bc1720bb1720bc176a21bd1720bc"
-    "1720bd176a21be1720bd1720be176a21bf1720be1720bf176a21c01720bf1720c0176a21c11720c01720c1176a21c2"
-    "1720c11720c2176a21c31720c21720c3176a21c41720c31720c4176a21c51720c41720c5176a21c61720c51720c617"
-    "6a21c71720c61720c7176a21c81720c71720c8176a21c91720c81720c9176a21ca1720c91720ca176a21cb1720ca17"
-    "20cb176a21cc1720cb1720cc176a21cd1720cc1720cd176a21ce1720cd1720ce176a21cf1720ce1720cf176a21d017"
-    "20cf1720d0176a21d11720d01720d1176a21d21720d11720d2176a21d31720d21720d3176a21d41720d31720d4176a"
-    "21d51720d41720d5176a21d61720d51720d6176a21d71720d61720d7176a21d81720d71720d8176a21d91720d81720"
-    "d9176a21da1720d91720da176a21db1720da1720db176a21dc1720db1720dc176a21dd1720dc1720dd176a21de1720"
-    "dd1720de176a21df1720de1720df176a21e01720df1720e0176a21e11720e01720e1176a21e21720e11720e2176a21"
-    "e31720e21720e3176a21e41720e31720e4176a21e51720e41720e5176a21e61720e51720e6176a21e71720e61720e7"
-    "176a21e81720e71720e8176a21e91720e81720e9176a21ea1720e91720ea176a21eb1720ea1720eb176a21ec1720eb"
-    "1720ec176a21ed1720ec1720ed176a21ee1720ed1720ee176a21ef1720ee1720ef176a21f01720ef1720f0176a21f1"
-    "1720f01720f1176a21f21720f11720f2176a21f31720f21720f3176a21f41720f31720f4176a21f51720f41720f517"
-    "6a21f61720f51720f6176a21f71720f61720f7176a21f81720f71720f8176a21f91720f81720f9176a21fa1720f917"
-    "20fa176a21fb1720fa1720fb176a21fc1720fb1720fc176a21fd1720fc1720fd176a21fe1720fd1720fe176a21ff17"
-    "20fe1720ff176a21801820ff172080186a2181182080182081186a2182182081182082186a2183182082182083186a"
-    "2184182083182084186a2185182084182085186a2186182085182086186a2187182086182087186a21881820871820"
-    "88186a2189182088182089186a218a18208918208a186a218b18208a18208b186a218c18208b18208c186a218d1820"
-    "8c18208d186a218e18208d18208e186a218f18208e18208f186a219018208f182090186a2191182090182091186a21"
-    "92182091182092186a2193182092182093186a2194182093182094186a2195182094182095186a2196182095182096"
-    "186a2197182096182097186a2198182097182098186a2199182098182099186a219a18209918209a186a219b18209a"
-    "18209b186a219c18209b18209c186a219d18209c18209d186a219e18209d18209e186a219f18209e18209f186a21a0"
-    "18209f1820a0186a21a11820a01820a1186a21a21820a11820a2186a21a31820a21820a3186a21a41820a31820a418"
-    "6a21a51820a41820a5186a21a61820a51820a6186a21a71820a61820a7186a21a81820a71820a8186a21a91820a818"
-    "20a9186a21aa1820a91820aa186a21ab1820aa1820ab186a21ac1820ab1820ac186a21ad1820ac1820ad186a21ae18"
-    "20ad1820ae186a21af1820ae1820af186a21b01820af1820b0186a21b11820b01820b1186a21b21820b11820b2186a"
-    "21b31820b21820b3186a21b41820b31820b4186a21b51820b41820b5186a21b61820b51820b6186a21b71820b61820"
-    "b7186a21b81820b71820b8186a21b91820b81820b9186a21ba1820b91820ba186a21bb1820ba1820bb186a21bc1820"
-    "bb1820bc186a21bd1820bc1820bd186a21be1820bd1820be186a21bf1820be1820bf186a21c01820bf1820c0186a21"
-    "c11820c01820c1186a21c21820c11820c2186a21c31820c21820c3186a21c41820c31820c4186a21c51820c41820c5"
-    "186a21c61820c51820c6186a21c71820c61820c7186a21c81820c71820c8186a21c91820c81820c9186a21ca1820c9"
-    "1820ca186a21cb1820ca1820cb186a21cc1820cb1820cc186a21cd1820cc1820cd186a21ce1820cd1820ce186a21cf"
-    "1820ce1820cf186a21d01820cf1820d0186a21d11820d01820d1186a21d21820d11820d2186a21d31820d21820d318"
-    "6a21d41820d31820d4186a21d51820d41820d5186a21d61820d51820d6186a21d71820d61820d7186a21d81820d718"
-    "20d8186a21d91820d81820d9186a21da1820d91820da186a21db1820da1820db186a21dc1820db1820dc186a21dd18"
-    "20dc1820dd186a21de1820dd1820de186a21df1820de1820df186a21e01820df1820e0186a21e11820e01820e1186a"
-    "21e21820e11820e2186a21e31820e21820e3186a21e41820e31820e4186a21e51820e41820e5186a21e61820e51820"
-    "e6186a21e71820e61820e7186a21e81820e71820e8186a21e91820e81820e9186a21ea1820e91820ea186a21eb1820"
-    "ea1820eb186a21ec1820eb1820ec186a21ed1820ec1820ed186a21ee1820ed1820ee186a21ef1820ee1820ef186a21"
-    "f01820ef1820f0186a21f11820f01820f1186a21f21820f11820f2186a21f31820f21820f3186a21f41820f31820f4"
-    "186a21f51820f41820f5186a21f61820f51820f6186a21f71820f61820f7186a21f81820f71820f8186a21f91820f8"
-    "1820f9186a21fa1820f91820fa186a21fb1820fa1820fb186a21fc1820fb1820fc186a21fd1820fc1820fd186a21fe"
-    "1820fd1820fe186a21ff1820fe1820ff186a21801920ff182080196a2181192080192081196a218219208119208219"
-    "6a2183192082192083196a2184192083192084196a2185192084192085196a2186192085192086196a218719208619"
-    "2087196a2188192087192088196a2189192088192089196a218a19208919208a196a218b19208a19208b196a218c19"
-    "208b19208c196a218d19208c19208d196a218e19208d19208e196a218f19208e19208f196a219019208f192090196a"
-    "2191192090192091196a2192192091192092196a2193192092192093196a2194192093192094196a21951920941920"
-    "95196a2196192095192096196a2197192096192097196a2198192097192098196a2199192098192099196a219a1920"
-    "9919209a196a219b19209a19209b196a219c19209b19209c196a219d19209c19209d196a219e19209d19209e196a21"
-    "9f19209e19209f196a21a019209f1920a0196a21a11920a01920a1196a21a21920a11920a2196a21a31920a21920a3"
-    "196a21a41920a31920a4196a21a51920a41920a5196a21a61920a51920a6196a21a71920a61920a7196a21a81920a7"
-    "1920a8196a21a91920a81920a9196a21aa1920a91920aa196a21ab1920aa1920ab196a21ac1920ab1920ac196a21ad"
-    "1920ac1920ad196a21ae1920ad1920ae196a21af1920ae1920af196a21b01920af1920b0196a21b11920b01920b119"
-    "6a21b21920b11920b2196a21b31920b21920b3196a21b41920b31920b4196a21b51920b41920b5196a21b61920b519"
-    "20b6196a21b71920b61920b7196a21b81920b71920b8196a21b91920b81920b9196a21ba1920b91920ba196a21bb19"
-    "20ba1920bb196a21bc1920bb1920bc196a21bd1920bc1920bd196a21be1920bd1920be196a21bf1920be1920bf196a"
-    "21c01920bf1920c0196a21c11920c01920c1196a21c21920c11920c2196a21c31920c21920c3196a21c41920c31920"
-    "c4196a21c51920c41920c5196a21c61920c51920c6196a21c71920c61920c7196a21c81920c71920c8196a21c91920"
-    "c81920c9196a21ca1920c91920ca196a21cb1920ca1920cb196a21cc1920cb1920cc196a21cd1920cc1920cd196a21"
-    "ce1920cd1920ce196a21cf1920ce1920cf196a21d01920cf1920d0196a21d11920d01920d1196a21d21920d11920d2"
-    "196a21d31920d21920d3196a21d41920d31920d4196a21d51920d41920d5196a21d61920d51920d6196a21d71920d6"
-    "1920d7196a21d81920d71920d8196a21d91920d81920d9196a21da1920d91920da196a21db1920da1920db196a21dc"
-    "1920db1920dc196a21dd1920dc1920dd196a21de1920dd1920de196a21df1920de1920df196a21e01920df1920e019"
-    "6a21e11920e01920e1196a21e21920e11920e2196a21e31920e21920e3196a21e41920e31920e4196a21e51920e419"
-    "20e5196a21e61920e51920e6196a21e71920e61920e7196a21e81920e71920e8196a21e91920e81920e9196a21ea19"
-    "20e91920ea196a21eb1920ea1920eb196a21ec1920eb1920ec196a21ed1920ec1920ed196a21ee1920ed1920ee196a"
-    "21ef1920ee1920ef196a21f01920ef1920f0196a21f11920f01920f1196a21f21920f11920f2196a21f31920f21920"
-    "f3196a21f41920f31920f4196a21f51920f41920f5196a21f61920f51920f6196a21f71920f61920f7196a21f81920"
-    "f71920f8196a21f91920f81920f9196a21fa1920f91920fa196a21fb1920fa1920fb196a21fc1920fb1920fc196a21"
-    "fd1920fc1920fd196a21fe1920fd1920fe196a21ff1920fe1920ff196a21801a20ff1920801a6a21811a20801a2081"
-    "1a6a21821a20811a20821a6a21831a20821a20831a6a21841a20831a20841a6a21851a20841a20851a6a21861a2085"
-    "1a20861a6a21871a20861a20871a6a21881a20871a20881a6a21891a20881a20891a6a218a1a20891a208a1a6a218b"
-    "1a208a1a208b1a6a218c1a208b1a208c1a6a218d1a208c1a208d1a6a218e1a208d1a208e1a6a218f1a208e1a208f1a"
-    "6a21901a208f1a20901a6a21911a20901a20911a6a21921a20911a20921a6a21931a20921a20931a6a21941a20931a"
-    "20941a6a21951a20941a20951a6a21961a20951a20961a6a21971a20961a20971a6a21981a20971a20981a6a21991a"
-    "20981a20991a6a219a1a20991a209a1a6a219b1a209a1a209b1a6a219c1a209b1a209c1a6a219d1a209c1a209d1a6a"
-    "219e1a209d1a209e1a6a219f1a209e1a209f1a6a21a01a209f1a20a01a6a21a11a20a01a20a11a6a21a21a20a11a20"
-    "a21a6a21a31a20a21a20a31a6a21a41a20a31a20a41a6a21a51a20a41a20a51a6a21a61a20a51a20a61a6a21a71a20"
-    "a61a20a71a6a21a81a20a71a20a81a6a21a91a20a81a20a91a6a21aa1a20a91a20aa1a6a21ab1a20aa1a20ab1a6a21"
-    "ac1a20ab1a20ac1a6a21ad1a20ac1a20ad1a6a21ae1a20ad1a20ae1a6a21af1a20ae1a20af1a6a21b01a20af1a20b0"
-    "1a6a21b11a20b01a20b11a6a21b21a20b11a20b21a6a21b31a20b21a20b31a6a21b41a20b31a20b41a6a21b51a20b4"
-    "1a20b51a6a21b61a20b51a20b61a6a21b71a20b61a20b71a6a21b81a20b71a20b81a6a21b91a20b81a20b91a6a21ba"
-    "1a20b91a20ba1a6a21bb1a20ba1a20bb1a6a21bc1a20bb1a20bc1a6a21bd1a20bc1a20bd1a6a21be1a20bd1a20be1a"
-    "6a21bf1a20be1a20bf1a6a21c01a20bf1a20c01a6a21c11a20c01a20c11a6a21c21a20c11a20c21a6a21c31a20c21a"
-    "20c31a6a21c41a20c31a20c41a6a21c51a20c41a20c51a6a21c61a20c51a20c61a6a21c71a20c61a20c71a6a21c81a"
-    "20c71a20c81a6a21c91a20c81a20c91a6a21ca1a20c91a20ca1a6a21cb1a20ca1a20cb1a6a21cc1a20cb1a20cc1a6a"
-    "21cd1a20cc1a20cd1a6a21ce1a20cd1a20ce1a6a21cf1a20ce1a20cf1a6a21d01a20cf1a20d01a6a21d11a20d01a20"
-    "d11a6a21d21a20d11a20d21a6a21d31a20d21a20d31a6a21d41a20d31a20d41a6a21d51a20d41a20d51a6a21d61a20"
-    "d51a20d61a6a21d71a20d61a20d71a6a21d81a20d71a20d81a6a21d91a20d81a20d91a6a21da1a20d91a20da1a6a21"
-    "db1a20da1a20db1a6a21dc1a20db1a20dc1a6a21dd1a20dc1a20dd1a6a21de1a20dd1a20de1a6a21df1a20de1a20df"
-    "1a6a21e01a20df1a20e01a6a21e11a20e01a20e11a6a21e21a20e11a20e21a6a21e31a20e21a20e31a6a21e41a20e3"
-    "1a20e41a6a21e51a20e41a20e51a6a21e61a20e51a20e61a6a21e71a20e61a20e71a6a21e81a20e71a20e81a6a21e9"
-    "1a20e81a20e91a6a21ea1a20e91a20ea1a6a21eb1a20ea1a20eb1a6a21ec1a20eb1a20ec1a6a21ed1a20ec1a20ed1a"
-    "6a21ee1a20ed1a20ee1a6a21ef1a20ee1a20ef1a6a21f01a20ef1a20f01a6a21f11a20f01a20f11a6a21f21a20f11a"
-    "20f21a6a21f31a20f21a20f31a6a21f41a20f31a20f41a6a21f51a20f41a20f51a6a21f61a20f51a20f61a6a21f71a"
-    "20f61a20f71a6a21f81a20f71a20f81a6a21f91a20f81a20f91a6a21fa1a20f91a20fa1a6a21fb1a20fa1a20fb1a6a"
-    "21fc1a20fb1a20fc1a6a21fd1a20fc1a20fd1a6a21fe1a20fd1a20fe1a6a21ff1a20fe1a20ff1a6a21801b20ff1a20"
-    "801b6a21811b20801b20811b6a21821b20811b20821b6a21831b20821b20831b6a21841b20831b20841b6a21851b20"
-    "841b20851b6a21861b20851b20861b6a21871b20861b20871b6a21881b20871b20881b6a21891b20881b20891b6a21"
-    "8a1b20891b208a1b6a218b1b208a1b208b1b6a218c1b208b1b208c1b6a218d1b208c1b208d1b6a218e1b208d1b208e"
-    "1b6a218f1b208e1b208f1b6a21901b208f1b20901b6a21911b20901b20911b6a21921b20911b20921b6a21931b2092"
-    "1b20931b6a21941b20931b20941b6a21951b20941b20951b6a21961b20951b20961b6a21971b20961b20971b6a2198"
-    "1b20971b20981b6a21991b20981b20991b6a219a1b20991b209a1b6a219b1b209a1b209b1b6a219c1b209b1b209c1b"
-    "6a219d1b209c1b209d1b6a219e1b209d1b209e1b6a219f1b209e1b209f1b6a21a01b209f1b20a01b6a21a11b20a01b"
-    "20a11b6a21a21b20a11b20a21b6a21a31b20a21b20a31b6a21a41b20a31b20a41b6a21a51b20a41b20a51b6a21a61b"
-    "20a51b20a61b6a21a71b20a61b20a71b6a21a81b20a71b20a81b6a21a91b20a81b20a91b6a21aa1b20a91b20aa1b6a"
-    "21ab1b20aa1b20ab1b6a21ac1b20ab1b20ac1b6a21ad1b20ac1b20ad1b6a21ae1b20ad1b20ae1b6a21af1b20ae1b20"
-    "af1b6a21b01b20af1b20b01b6a21b11b20b01b20b11b6a21b21b20b11b20b21b6a21b31b20b21b20b31b6a21b41b20"
-    "b31b20b41b6a21b51b20b41b20b51b6a21b61b20b51b20b61b6a21b71b20b61b20b71b6a21b81b20b71b20b81b6a21"
-    "b91b20b81b20b91b6a21ba1b20b91b20ba1b6a21bb1b20ba1b20bb1b6a21bc1b20bb1b20bc1b6a21bd1b20bc1b20bd"
-    "1b6a21be1b20bd1b20be1b6a21bf1b20be1b20bf1b6a21c01b20bf1b20c01b6a21c11b20c01b20c11b6a21c21b20c1"
-    "1b20c21b6a21c31b20c21b20c31b6a21c41b20c31b20c41b6a21c51b20c41b20c51b6a21c61b20c51b20c61b6a21c7"
-    "1b20c61b20c71b6a21c81b20c71b20c81b6a21c91b20c81b20c91b6a21ca1b20c91b20ca1b6a21cb1b20ca1b20cb1b"
-    "6a21cc1b20cb1b20cc1b6a21cd1b20cc1b20cd1b6a21ce1b20cd1b20ce1b6a21cf1b20ce1b20cf1b6a21d01b20cf1b"
-    "20d01b6a21d11b20d01b20d11b6a21d21b20d11b20d21b6a21d31b20d21b20d31b6a21d41b20d31b20d41b6a21d51b"
-    "20d41b20d51b6a21d61b20d51b20d61b6a21d71b20d61b20d71b6a21d81b20d71b20d81b6a21d91b20d81b20d91b6a"
-    "21da1b20d91b20da1b6a21db1b20da1b20db1b6a21dc1b20db1b20dc1b6a21dd1b20dc1b20dd1b6a21de1b20dd1b20"
-    "de1b6a21df1b20de1b20df1b6a21e01b20df1b20e01b6a21e11b20e01b20e11b6a21e21b20e11b20e21b6a21e31b20"
-    "e21b20e31b6a21e41b20e31b20e41b6a21e51b20e41b20e51b6a21e61b20e51b20e61b6a21e71b20e61b20e71b6a21"
-    "e81b20e71b20e81b6a21e91b20e81b20e91b6a21ea1b20e91b20ea1b6a21eb1b20ea1b20eb1b6a21ec1b20eb1b20ec"
-    "1b6a21ed1b20ec1b20ed1b6a21ee1b20ed1b20ee1b6a21ef1b20ee1b20ef1b6a21f01b20ef1b20f01b6a21f11b20f0"
-    "1b20f11b6a21f21b20f11b20f21b6a21f31b20f21b20f31b6a21f41b20f31b20f41b6a21f51b20f41b20f51b6a21f6"
-    "1b20f51b20f61b6a21f71b20f61b20f71b6a21f81b20f71b20f81b6a21f91b20f81b20f91b6a21fa1b20f91b20fa1b"
-    "6a21fb1b20fa1b20fb1b6a21fc1b20fb1b20fc1b6a21fd1b20fc1b20fd1b6a21fe1b20fd1b20fe1b6a21ff1b20fe1b"
-    "20ff1b6a21801c20ff1b20801c6a21811c20801c20811c6a21821c20811c20821c6a21831c20821c20831c6a21841c"
-    "20831c20841c6a21851c20841c20851c6a21861c20851c20861c6a21871c20861c20871c6a21881c20871c20881c6a"
-    "21891c20881c20891c6a218a1c20891c208a1c6a218b1c208a1c208b1c6a218c1c208b1c208c1c6a218d1c208c1c20"
-    "8d1c6a218e1c208d1c208e1c6a218f1c208e1c208f1c6a21901c208f1c20901c6a21911c20901c20911c6a21921c20"
-    "911c20921c6a21931c20921c20931c6a21941c20931c20941c6a21951c20941c20951c6a21961c20951c20961c6a21"
-    "971c20961c20971c6a21981c20971c20981c6a21991c20981c20991c6a219a1c20991c209a1c6a219b1c209a1c209b"
-    "1c6a219c1c209b1c209c1c6a219d1c209c1c209d1c6a219e1c209d1c209e1c6a219f1c209e1c209f1c6a21a01c209f"
-    "1c20a01c6a21a11c20a01c20a11c6a21a21c20a11c20a21c6a21a31c20a21c20a31c6a21a41c20a31c20a41c6a21a5"
-    "1c20a41c20a51c6a21a61c20a51c20a61c6a21a71c20a61c20a71c6a21a81c20a71c20a81c6a21a91c20a81c20a91c"
-    "6a21aa1c20a91c20aa1c6a21ab1c20aa1c20ab1c6a21ac1c20ab1c20ac1c6a21ad1c20ac1c20ad1c6a21ae1c20ad1c"
-    "20ae1c6a21af1c20ae1c20af1c6a21b01c20af1c20b01c6a21b11c20b01c20b11c6a21b21c20b11c20b21c6a21b31c"
-    "20b21c20b31c6a21b41c20b31c20b41c6a21b51c20b41c20b51c6a21b61c20b51c20b61c6a21b71c20b61c20b71c6a"
-    "21b81c20b71c20b81c6a21b91c20b81c20b91c6a21ba1c20b91c20ba1c6a21bb1c20ba1c20bb1c6a21bc1c20bb1c20"
-    "bc1c6a21bd1c20bc1c20bd1c6a21be1c20bd1c20be1c6a21bf1c20be1c20bf1c6a21c01c20bf1c20c01c6a21c11c20"
-    "c01c20c11c6a21c21c20c11c20c21c6a21c31c20c21c20c31c6a21c41c20c31c20c41c6a21c51c20c41c20c51c6a21"
-    "c61c20c51c20c61c6a21c71c20c61c20c71c6a21c81c20c71c20c81c6a21c91c20c81c20c91c6a21ca1c20c91c20ca"
-    "1c6a21cb1c20ca1c20cb1c6a21cc1c20cb1c20cc1c6a21cd1c20cc1c20cd1c6a21ce1c20cd1c20ce1c6a21cf1c20ce"
-    "1c20cf1c6a21d01c20cf1c20d01c6a21d11c20d01c20d11c6a21d21c20d11c20d21c6a21d31c20d21c20d31c6a21d4"
-    "1c20d31c20d41c6a21d51c20d41c20d51c6a21d61c20d51c20d61c6a21d71c20d61c20d71c6a21d81c20d71c20d81c"
-    "6a21d91c20d81c20d91c6a21da1c20d91c20da1c6a21db1c20da1c20db1c6a21dc1c20db1c20dc1c6a21dd1c20dc1c"
-    "20dd1c6a21de1c20dd1c20de1c6a21df1c20de1c20df1c6a21e01c20df1c20e01c6a21e11c20e01c20e11c6a21e21c"
-    "20e11c20e21c6a21e31c20e21c20e31c6a21e41c20e31c20e41c6a21e51c20e41c20e51c6a21e61c20e51c20e61c6a"
-    "21e71c20e61c20e71c6a21e81c20e71c20e81c6a21e91c20e81c20e91c6a21ea1c20e91c20ea1c6a21eb1c20ea1c20"
-    "eb1c6a21ec1c20eb1c20ec1c6a21ed1c20ec1c20ed1c6a21ee1c20ed1c20ee1c6a21ef1c20ee1c20ef1c6a21f01c20"
-    "ef1c20f01c6a21f11c20f01c20f11c6a21f21c20f11c20f21c6a21f31c20f21c20f31c6a21f41c20f31c20f41c6a21"
-    "f51c20f41c20f51c6a21f61c20f51c20f61c6a21f71c20f61c20f71c6a21f81c20f71c20f81c6a21f91c20f81c20f9"
-    "1c6a21fa1c20f91c20fa1c6a21fb1c20fa1c20fb1c6a21fc1c20fb1c20fc1c6a21fd1c20fc1c20fd1c6a21fe1c20fd"
-    "1c20fe1c6a21ff1c20fe1c20ff1c6a21801d20ff1c20801d6a21811d20801d20811d6a21821d20811d20821d6a2183"
-    "1d20821d20831d6a21841d20831d20841d6a21851d20841d20851d6a21861d20851d20861d6a21871d20861d20871d"
-    "6a21881d20871d20881d6a21891d20881d20891d6a218a1d20891d208a1d6a218b1d208a1d208b1d6a218c1d208b1d"
-    "208c1d6a218d1d208c1d208d1d6a218e1d208d1d208e1d6a218f1d208e1d208f1d6a21901d208f1d20901d6a21911d"
-    "20901d20911d6a21921d20911d20921d6a21931d20921d20931d6a21941d20931d20941d6a21951d20941d20951d6a"
-    "21961d20951d20961d6a21971d20961d20971d6a21981d20971d20981d6a21991d20981d20991d6a219a1d20991d20"
-    "9a1d6a219b1d209a1d209b1d6a219c1d209b1d209c1d6a219d1d209c1d209d1d6a219e1d209d1d209e1d6a219f1d20"
-    "9e1d209f1d6a21a01d209f1d20a01d6a21a11d20a01d20a11d6a21a21d20a11d20a21d6a21a31d20a21d20a31d6a21"
-    "a41d20a31d20a41d6a21a51d20a41d20a51d6a21a61d20a51d20a61d6a21a71d20a61d20a71d6a21a81d20a71d20a8"
-    "1d6a21a91d20a81d20a91d6a21aa1d20a91d20aa1d6a21ab1d20aa1d20ab1d6a21ac1d20ab1d20ac1d6a21ad1d20ac"
-    "1d20ad1d6a21ae1d20ad1d20ae1d6a21af1d20ae1d20af1d6a21b01d20af1d20b01d6a21b11d20b01d20b11d6a21b2"
-    "1d20b11d20b21d6a21b31d20b21d20b31d6a21b41d20b31d20b41d6a21b51d20b41d20b51d6a21b61d20b51d20b61d"
-    "6a21b71d20b61d20b71d6a21b81d20b71d20b81d6a21b91d20b81d20b91d6a21ba1d20b91d20ba1d6a21bb1d20ba1d"
-    "20bb1d6a21bc1d20bb1d20bc1d6a21bd1d20bc1d20bd1d6a21be1d20bd1d20be1d6a21bf1d20be1d20bf1d6a21c01d"
-    "20bf1d20c01d6a21c11d20c01d20c11d6a21c21d20c11d20c21d6a21c31d20c21d20c31d6a21c41d20c31d20c41d6a"
-    "21c51d20c41d20c51d6a21c61d20c51d20c61d6a21c71d20c61d20c71d6a21c81d20c71d20c81d6a21c91d20c81d20"
-    "c91d6a21ca1d20c91d20ca1d6a21cb1d20ca1d20cb1d6a21cc1d20cb1d20cc1d6a21cd1d20cc1d20cd1d6a21ce1d20"
-    "cd1d20ce1d6a21cf1d20ce1d20cf1d6a21d01d20cf1d20d01d6a21d11d20d01d20d11d6a21d21d20d11d20d21d6a21"
-    "d31d20d21d20d31d6a21d41d20d31d20d41d6a21d51d20d41d20d51d6a21d61d20d51d20d61d6a21d71d20d61d20d7"
-    "1d6a21d81d20d71d20d81d6a21d91d20d81d20d91d6a21da1d20d91d20da1d6a21db1d20da1d20db1d6a21dc1d20db"
-    "1d20dc1d6a21dd1d20dc1d20dd1d6a21de1d20dd1d20de1d6a21df1d20de1d20df1d6a21e01d20df1d20e01d6a21e1"
-    "1d20e01d20e11d6a21e21d20e11d20e21d6a21e31d20e21d20e31d6a21e41d20e31d20e41d6a21e51d20e41d20e51d"
-    "6a21e61d20e51d20e61d6a21e71d20e61d20e71d6a21e81d20e71d20e81d6a21e91d20e81d20e91d6a21ea1d20e91d"
-    "20ea1d6a21eb1d20ea1d20eb1d6a21ec1d20eb1d20ec1d6a21ed1d20ec1d20ed1d6a21ee1d20ed1d20ee1d6a21ef1d"
-    "20ee1d20ef1d6a21f01d20ef1d20f01d6a21f11d20f01d20f11d6a21f21d20f11d20f21d6a21f31d20f21d20f31d6a"
-    "21f41d20f31d20f41d6a21f51d20f41d20f51d6a21f61d20f51d20f61d6a21f71d20f61d20f71d6a21f81d20f71d20"
-    "f81d6a21f91d20f81d20f91d6a21fa1d20f91d20fa1d6a21fb1d20fa1d20fb1d6a21fc1d20fb1d20fc1d6a21fd1d20"
-    "fc1d20fd1d6a21fe1d20fd1d20fe1d6a21ff1d20fe1d20ff1d6a21801e20ff1d20801e6a21811e20801e20811e6a21"
-    "821e20811e20821e6a21831e20821e20831e6a21841e20831e20841e6a21851e20841e20851e6a21861e20851e2086"
-    "1e6a21871e20861e20871e6a21881e20871e20881e6a21891e20881e20891e6a218a1e20891e208a1e6a218b1e208a"
-    "1e208b1e6a218c1e208b1e208c1e6a218d1e208c1e208d1e6a218e1e208d1e208e1e6a218f1e208e1e208f1e6a2190"
-    "1e208f1e20901e6a21911e20901e20911e6a21921e20911e20921e6a21931e20921e20931e6a21941e20931e20941e"
-    "6a21951e20941e20951e6a21961e20951e20961e6a21971e20961e20971e6a21981e20971e20981e6a21991e20981e"
-    "20991e6a219a1e20991e209a1e6a219b1e209a1e209b1e6a219c1e209b1e209c1e6a219d1e209c1e209d1e6a219e1e"
-    "209d1e209e1e6a219f1e209e1e209f1e6a21a01e209f1e20a01e6a21a11e20a01e20a11e6a21a21e20a11e20a21e6a"
-    "21a31e20a21e20a31e6a21a41e20a31e20a41e6a21a51e20a41e20a51e6a21a61e20a51e20a61e6a21a71e20a61e20"
-    "a71e6a21a81e20a71e20a81e6a21a91e20a81e20a91e6a21aa1e20a91e20aa1e6a21ab1e20aa1e20ab1e6a21ac1e20"
-    "ab1e20ac1e6a21ad1e20ac1e20ad1e6a21ae1e20ad1e20ae1e6a21af1e20ae1e20af1e6a21b01e20af1e20b01e6a21"
-    "b11e20b01e20b11e6a21b21e20b11e20b21e6a21b31e20b21e20b31e6a21b41e20b31e20b41e6a21b51e20b41e20b5"
-    "1e6a21b61e20b51e20b61e6a21b71e20b61e20b71e6a21b81e20b71e20b81e6a21b91e20b81e20b91e6a21ba1e20b9"
-    "1e20ba1e6a21bb1e20ba1e20bb1e6a21bc1e20bb1e20bc1e6a21bd1e20bc1e20bd1e6a21be1e20bd1e20be1e6a21bf"
-    "1e20be1e20bf1e6a21c01e20bf1e20c01e6a21c11e20c01e20c11e6a21c21e20c11e20c21e6a21c31e20c21e20c31e"
-    "6a21c41e20c31e20c41e6a21c51e20c41e20c51e6a21c61e20c51e20c61e6a21c71e20c61e20c71e6a21c81e20c71e"
-    "20c81e6a21c91e20c81e20c91e6a21ca1e20c91e20ca1e6a21cb1e20ca1e20cb1e6a21cc1e20cb1e20cc1e6a21cd1e"
-    "20cc1e20cd1e6a21ce1e20cd1e20ce1e6a21cf1e20ce1e20cf1e6a21d01e20cf1e20d01e6a21d11e20d01e20d11e6a"
-    "21d21e20d11e20d21e6a21d31e20d21e20d31e6a21d41e20d31e20d41e6a21d51e20d41e20d51e6a21d61e20d51e20"
-    "d61e6a21d71e20d61e20d71e6a21d81e20d71e20d81e6a21d91e20d81e20d91e6a21da1e20d91e20da1e6a21db1e20"
-    "da1e20db1e6a21dc1e20db1e20dc1e6a21dd1e20dc1e20dd1e6a21de1e20dd1e20de1e6a21df1e20de1e20df1e6a21"
-    "e01e20df1e20e01e6a21e11e20e01e20e11e6a21e21e20e11e20e21e6a21e31e20e21e20e31e6a21e41e20e31e20e4"
-    "1e6a21e51e20e41e20e51e6a21e61e20e51e20e61e6a21e71e20e61e20e71e6a21e81e20e71e20e81e6a21e91e20e8"
-    "1e20e91e6a21ea1e20e91e20ea1e6a21eb1e20ea1e20eb1e6a21ec1e20eb1e20ec1e6a21ed1e20ec1e20ed1e6a21ee"
-    "1e20ed1e20ee1e6a21ef1e20ee1e20ef1e6a21f01e20ef1e20f01e6a21f11e20f01e20f11e6a21f21e20f11e20f21e"
-    "6a21f31e20f21e20f31e6a21f41e20f31e20f41e6a21f51e20f41e20f51e6a21f61e20f51e20f61e6a21f71e20f61e"
-    "20f71e6a21f81e20f71e20f81e6a21f91e20f81e20f91e6a21fa1e20f91e20fa1e6a21fb1e20fa1e20fb1e6a21fc1e"
-    "20fb1e20fc1e6a21fd1e20fc1e20fd1e6a21fe1e20fd1e20fe1e6a21ff1e20fe1e20ff1e6a21801f20ff1e20801f6a"
-    "21811f20801f20811f6a21821f20811f20821f6a21831f20821f20831f6a21841f20831f20841f6a21851f20841f20"
-    "851f6a21861f20851f20861f6a21871f20861f20871f6a21881f20871f20881f6a21891f20881f20891f6a218a1f20"
-    "891f208a1f6a218b1f208a1f208b1f6a218c1f208b1f208c1f6a218d1f208c1f208d1f6a218e1f208d1f208e1f6a21"
-    "8f1f208e1f208f1f6a21901f208f1f20901f6a21911f20901f20911f6a21921f20911f20921f6a21931f20921f2093"
-    "1f6a21941f20931f20941f6a21951f20941f20951f6a21961f20951f20961f6a21971f20961f20971f6a21981f2097"
-    "1f20981f6a21991f20981f20991f6a219a1f20991f209a1f6a219b1f209a1f209b1f6a219c1f209b1f209c1f6a219d"
-    "1f209c1f209d1f6a219e1f209d1f209e1f6a219f1f209e1f209f1f6a21a01f209f1f20a01f6a21a11f20a01f20a11f"
-    "6a21a21f20a11f20a21f6a21a31f20a21f20a31f6a21a41f20a31f20a41f6a21a51f20a41f20a51f6a21a61f20a51f"
-    "20a61f6a21a71f20a61f20a71f6a21a81f20a71f20a81f6a21a91f20a81f20a91f6a21aa1f20a91f20aa1f6a21ab1f"
-    "20aa1f20ab1f6a21ac1f20ab1f20ac1f6a21ad1f20ac1f20ad1f6a21ae1f20ad1f20ae1f6a21af1f20ae1f20af1f6a"
-    "21b01f20af1f20b01f6a21b11f20b01f20b11f6a21b21f20b11f20b21f6a21b31f20b21f20b31f6a21b41f20b31f20"
-    "b41f6a21b51f20b41f20b51f6a21b61f20b51f20b61f6a21b71f20b61f20b71f6a21b81f20b71f20b81f6a21b91f20"
-    "b81f20b91f6a21ba1f20b91f20ba1f6a21bb1f20ba1f20bb1f6a21bc1f20bb1f20bc1f6a21bd1f20bc1f20bd1f6a21"
-    "be1f20bd1f20be1f6a21bf1f20be1f20bf1f6a21c01f20bf1f20c01f6a21c11f20c01f20c11f6a21c21f20c11f20c2"
-    "1f6a21c31f20c21f20c31f6a21c41f20c31f20c41f6a21c51f20c41f20c51f6a21c61f20c51f20c61f6a21c71f20c6"
-    "1f20c71f6a21c81f20c71f20c81f6a21c91f20c81f20c91f6a21ca1f20c91f20ca1f6a21cb1f20ca1f20cb1f6a21cc"
-    "1f20cb1f20cc1f6a21cd1f20cc1f20cd1f6a21ce1f20cd1f20ce1f6a21cf1f20ce1f20cf1f6a21d01f20cf1f20d01f"
-    "6a21d11f20d01f20d11f6a21d21f20d11f20d21f6a21d31f20d21f20d31f6a21d41f20d31f20d41f6a21d51f20d41f"
-    "20d51f6a21d61f20d51f20d61f6a21d71f20d61f20d71f6a21d81f20d71f20d81f6a21d91f20d81f20d91f6a21da1f"
-    "20d91f20da1f6a21db1f20da1f20db1f6a21dc1f20db1f20dc1f6a21dd1f20dc1f20dd1f6a21de1f20dd1f20de1f6a"
-    "21df1f20de1f20df1f6a21e01f20df1f20e01f6a21e11f20e01f20e11f6a21e21f20e11f20e21f6a21e31f20e21f20"
-    "e31f6a21e41f20e31f20e41f6a21e51f20e41f20e51f6a21e61f20e51f20e61f6a21e71f20e61f20e71f6a21e81f20"
-    "e71f20e81f6a21e91f20e81f20e91f6a21ea1f20e91f20ea1f6a21eb1f20ea1f20eb1f6a21ec1f20eb1f20ec1f6a21"
-    "ed1f20ec1f20ed1f6a21ee1f20ed1f20ee1f6a21ef1f20ee1f20ef1f6a21f01f20ef1f20f01f6a21f11f20f01f20f1"
-    "1f6a21f21f20f11f20f21f6a21f31f20f21f20f31f6a21f41f20f31f20f41f6a21f51f20f41f20f51f6a21f61f20f5"
-    "1f20f61f6a21f71f20f61f20f71f6a21f81f20f71f20f81f6a21f91f20f81f20f91f6a21fa1f20f91f20fa1f6a21fb"
-    "1f20fa1f20fb1f6a21fc1f20fb1f20fc1f6a21fd1f20fc1f20fd1f6a21fe1f20fd1f20fe1f6a21ff1f20fe1f20ff1f"
-    "6a21802020ff1f2080206a2181202080202081206a2182202081202082206a2183202082202083206a218420208320"
-    "2084206a2185202084202085206a2186202085202086206a2187202086202087206a2188202087202088206a218920"
-    "2088202089206a218a20208920208a206a218b20208a20208b206a218c20208b20208c206a218d20208c20208d206a"
-    "218e20208d20208e206a218f20208e20208f206a219020208f202090206a2191202090202091206a21922020912020"
-    "92206a2193202092202093206a2194202093202094206a2195202094202095206a2196202095202096206a21972020"
-    "96202097206a2198202097202098206a2199202098202099206a219a20209920209a206a219b20209a20209b206a21"
-    "9c20209b20209c206a219d20209c20209d206a219e20209d20209e206a219f20209e20209f206a21a020209f2020a0"
-    "206a21a12020a02020a1206a21a22020a12020a2206a21a32020a22020a3206a21a42020a32020a4206a21a52020a4"
-    "2020a5206a21a62020a52020a6206a21a72020a62020a7206a21a82020a72020a8206a21a92020a82020a9206a21aa"
-    "2020a92020aa206a21ab2020aa2020ab206a21ac2020ab2020ac206a21ad2020ac2020ad206a21ae2020ad2020ae20"
-    "6a21af2020ae2020af206a21b02020af2020b0206a21b12020b02020b1206a21b22020b12020b2206a21b32020b220"
-    "20b3206a21b42020b32020b4206a21b52020b42020b5206a21b62020b52020b6206a21b72020b62020b7206a21b820"
-    "20b72020b8206a21b92020b82020b9206a21ba2020b92020ba206a21bb2020ba2020bb206a21bc2020bb2020bc206a"
-    "21bd2020bc2020bd206a21be2020bd2020be206a21bf2020be2020bf206a21c02020bf2020c0206a21c12020c02020"
-    "c1206a21c22020c12020c2206a21c32020c22020c3206a21c42020c32020c4206a21c52020c42020c5206a21c62020"
-    "c52020c6206a21c72020c62020c7206a21c82020c72020c8206a21c92020c82020c9206a21ca2020c92020ca206a21"
-    "cb2020ca2020cb206a21cc2020cb2020cc206a21cd2020cc2020cd206a21ce2020cd2020ce206a21cf2020ce2020cf"
-    "206a21d02020cf2020d0206a21d12020d02020d1206a21d22020d12020d2206a21d32020d22020d3206a21d42020d3"
-    "2020d4206a21d52020d42020d5206a21d62020d52020d6206a21d72020d62020d7206a21d82020d72020d8206a21d9"
-    "2020d82020d9206a21da2020d92020da206a21db2020da2020db206a21dc2020db2020dc206a21dd2020dc2020dd20"
-    "6a21de2020dd2020de206a21df2020de2020df206a21e02020df2020e0206a21e12020e02020e1206a21e22020e120"
-    "20e2206a21e32020e22020e3206a21e42020e32020e4206a21e52020e42020e5206a21e62020e52020e6206a21e720"
-    "20e62020e7206a21e82020e72020e8206a21e92020e82020e9206a21ea2020e92020ea206a21eb2020ea2020eb206a"
-    "21ec2020eb2020ec206a21ed2020ec2020ed206a21ee2020ed2020ee206a21ef2020ee2020ef206a21f02020ef2020"
-    "f0206a21f12020f02020f1206a21f22020f12020f2206a21f32020f22020f3206a21f42020f32020f4206a21f52020"
-    "f42020f5206a21f62020f52020f6206a21f72020f62020f7206a21f82020f72020f8206a21f92020f82020f9206a21"
-    "fa2020f92020fa206a21fb2020fa2020fb206a21fc2020fb2020fc206a21fd2020fc2020fd206a21fe2020fd2020fe"
-    "206a21ff2020fe2020ff206a21802120ff202080216a2181212080212081216a2182212081212082216a2183212082"
-    "212083216a2184212083212084216a2185212084212085216a2186212085212086216a2187212086212087216a2188"
-    "212087212088216a2189212088212089216a218a21208921208a216a218b21208a21208b216a218c21208b21208c21"
-    "6a218d21208c21208d216a218e21208d21208e216a218f21208e21208f216a219021208f212090216a219121209021"
-    "2091216a2192212091212092216a2193212092212093216a2194212093212094216a2195212094212095216a219621"
-    "2095212096216a2197212096212097216a2198212097212098216a2199212098212099216a219a21209921209a216a"
-    "219b21209a21209b216a219c21209b21209c216a219d21209c21209d216a219e21209d21209e216a219f21209e2120"
-    "9f216a21a021209f2120a0216a21a12120a02120a1216a21a22120a12120a2216a21a32120a22120a3216a21a42120"
-    "a32120a4216a21a52120a42120a5216a21a62120a52120a6216a21a72120a62120a7216a21a82120a72120a8216a21"
-    "a92120a82120a9216a21aa2120a92120aa216a21ab2120aa2120ab216a21ac2120ab2120ac216a21ad2120ac2120ad"
-    "216a21ae2120ad2120ae216a21af2120ae2120af216a21b02120af2120b0216a21b12120b02120b1216a21b22120b1"
-    "2120b2216a21b32120b22120b3216a21b42120b32120b4216a21b52120b42120b5216a21b62120b52120b6216a21b7"
-    "2120b62120b7216a21b82120b72120b8216a21b92120b82120b9216a21ba2120b92120ba216a21bb2120ba2120bb21"
-    "6a21bc2120bb2120bc216a21bd2120bc2120bd216a21be2120bd2120be216a21bf2120be2120bf216a21c02120bf21"
-    "20c0216a21c12120c02120c1216a21c22120c12120c2216a21c32120c22120c3216a21c42120c32120c4216a21c521"
-    "20c42120c5216a21c62120c52120c6216a21c72120c62120c7216a21c82120c72120c8216a21c92120c82120c9216a"
-    "21ca2120c92120ca216a21cb2120ca2120cb216a21cc2120cb2120cc216a21cd2120cc2120cd216a21ce2120cd2120"
-    "ce216a21cf2120ce2120cf216a21d02120cf2120d0216a21d12120d02120d1216a21d22120d12120d2216a21d32120"
-    "d22120d3216a21d42120d32120d4216a21d52120d42120d5216a21d62120d52120d6216a21d72120d62120d7216a21"
-    "d82120d72120d8216a21d92120d82120d9216a21da2120d92120da216a21db2120da2120db216a21dc2120db2120dc"
-    "216a21dd2120dc2120dd216a21de2120dd2120de216a21df2120de2120df216a21e02120df2120e0216a21e12120e0"
-    "2120e1216a21e22120e12120e2216a21e32120e22120e3216a21e42120e32120e4216a21e52120e42120e5216a21e6"
-    "2120e52120e6216a21e72120e62120e7216a21e82120e72120e8216a21e92120e82120e9216a21ea2120e92120ea21"
-    "6a21eb2120ea2120eb216a21ec2120eb2120ec216a21ed2120ec2120ed216a21ee2120ed2120ee216a21ef2120ee21"
-    "20ef216a21f02120ef2120f0216a21f12120f02120f1216a21f22120f12120f2216a21f32120f22120f3216a21f421"
-    "20f32120f4216a21f52120f42120f5216a21f62120f52120f6216a21f72120f62120f7216a21f82120f72120f8216a"
-    "21f92120f82120f9216a21fa2120f92120fa216a21fb2120fa2120fb216a21fc2120fb2120fc216a21fd2120fc2120"
-    "fd216a21fe2120fd2120fe216a21ff2120fe2120ff216a21802220ff212080226a2181222080222081226a21822220"
-    "81222082226a2183222082222083226a2184222083222084226a2185222084222085226a2186222085222086226a21"
-    "87222086222087226a2188222087222088226a2189222088222089226a218a22208922208a226a218b22208a22208b"
-    "226a218c22208b22208c226a218d22208c22208d226a218e22208d22208e226a218f22208e22208f226a219022208f"
-    "222090226a2191222090222091226a2192222091222092226a2193222092222093226a2194222093222094226a2195"
-    "222094222095226a2196222095222096226a2197222096222097226a2198222097222098226a219922209822209922"
-    "6a219a22209922209a226a219b22209a22209b226a219c22209b22209c226a219d22209c22209d226a219e22209d22"
-    "209e226a219f22209e22209f226a21a022209f2220a0226a21a12220a02220a1226a21a22220a12220a2226a21a322"
-    "20a22220a3226a21a42220a32220a4226a21a52220a42220a5226a21a62220a52220a6226a21a72220a62220a7226a"
-    "21a82220a72220a8226a21a92220a82220a9226a21aa2220a92220aa226a21ab2220aa2220ab226a21ac2220ab2220"
-    "ac226a21ad2220ac2220ad226a21ae2220ad2220ae226a21af2220ae2220af226a21b02220af2220b0226a21b12220"
-    "b02220b1226a21b22220b12220b2226a21b32220b22220b3226a21b42220b32220b4226a21b52220b42220b5226a21"
-    "b62220b52220b6226a21b72220b62220b7226a21b82220b72220b8226a21b92220b82220b9226a21ba2220b92220ba"
-    "226a21bb2220ba2220bb226a21bc2220bb2220bc226a21bd2220bc2220bd226a21be2220bd2220be226a21bf2220be"
-    "2220bf226a21c02220bf2220c0226a21c12220c02220c1226a21c22220c12220c2226a21c32220c22220c3226a21c4"
-    "2220c32220c4226a21c52220c42220c5226a21c62220c52220c6226a21c72220c62220c7226a21c82220c72220c822"
-    "6a21c92220c82220c9226a21ca2220c92220ca226a21cb2220ca2220cb226a21cc2220cb2220cc226a21cd2220cc22"
-    "20cd226a21ce2220cd2220ce226a21cf2220ce2220cf226a21d02220cf2220d0226a21d12220d02220d1226a21d222"
-    "20d12220d2226a21d32220d22220d3226a21d42220d32220d4226a21d52220d42220d5226a21d62220d52220d6226a"
-    "21d72220d62220d7226a21d82220d72220d8226a21d92220d82220d9226a21da2220d92220da226a21db2220da2220"
-    "db226a21dc2220db2220dc226a21dd2220dc2220dd226a21de2220dd2220de226a21df2220de2220df226a21e02220"
-    "df2220e0226a21e12220e02220e1226a21e22220e12220e2226a21e32220e22220e3226a21e42220e32220e4226a21"
-    "e52220e42220e5226a21e62220e52220e6226a21e72220e62220e7226a21e82220e72220e8226a21e92220e82220e9"
-    "226a21ea2220e92220ea226a21eb2220ea2220eb226a21ec2220eb2220ec226a21ed2220ec2220ed226a21ee2220ed"
-    "2220ee226a21ef2220ee2220ef226a21f02220ef2220f0226a21f12220f02220f1226a21f22220f12220f2226a21f3"
-    "2220f22220f3226a21f42220f32220f4226a21f52220f42220f5226a21f62220f52220f6226a21f72220f62220f722"
-    "6a21f82220f72220f8226a21f92220f82220f9226a21fa2220f92220fa226a21fb2220fa2220fb226a21fc2220fb22"
-    "20fc226a21fd2220fc2220fd226a21fe2220fd2220fe226a21ff2220fe2220ff226a21802320ff222080236a218123"
-    "2080232081236a2182232081232082236a2183232082232083236a2184232083232084236a2185232084232085236a"
-    "2186232085232086236a2187232086232087236a2188232087232088236a2189232088232089236a218a2320892320"
-    "8a236a218b23208a23208b236a218c23208b23208c236a218d23208c23208d236a218e23208d23208e236a218f2320"
-    "8e23208f236a219023208f232090236a2191232090232091236a2192232091232092236a2193232092232093236a21"
-    "94232093232094236a2195232094232095236a2196232095232096236a2197232096232097236a2198232097232098"
-    "236a2199232098232099236a219a23209923209a236a219b23209a23209b236a219c23209b23209c236a219d23209c"
-    "23209d236a219e23209d23209e236a219f23209e23209f236a21a023209f2320a0236a21a12320a02320a1236a21a2"
-    "2320a12320a2236a21a32320a22320a3236a21a42320a32320a4236a21a52320a42320a5236a21a62320a52320a623"
-    "6a21a72320a62320a7236a21a82320a72320a8236a21a92320a82320a9236a21aa2320a92320aa236a21ab2320aa23"
-    "20ab236a21ac2320ab2320ac236a21ad2320ac2320ad236a21ae2320ad2320ae236a21af2320ae2320af236a21b023"
-    "20af2320b0236a21b12320b02320b1236a21b22320b12320b2236a21b32320b22320b3236a21b42320b32320b4236a"
-    "21b52320b42320b5236a21b62320b52320b6236a21b72320b62320b7236a21b82320b72320b8236a21b92320b82320"
-    "b9236a21ba2320b92320ba236a21bb2320ba2320bb236a21bc2320bb2320bc236a21bd2320bc2320bd236a21be2320"
-    "bd2320be236a21bf2320be2320bf236a21c02320bf2320c0236a21c12320c02320c1236a21c22320c12320c2236a21"
-    "c32320c22320c3236a21c42320c32320c4236a21c52320c42320c5236a21c62320c52320c6236a21c72320c62320c7"
-    "236a21c82320c72320c8236a21c92320c82320c9236a21ca2320c92320ca236a21cb2320ca2320cb236a21cc2320cb"
-    "2320cc236a21cd2320cc2320cd236a21ce2320cd2320ce236a21cf2320ce2320cf236a21d02320cf2320d0236a21d1"
-    "2320d02320d1236a21d22320d12320d2236a21d32320d22320d3236a21d42320d32320d4236a21d52320d42320d523"
-    "6a21d62320d52320d6236a21d72320d62320d7236a21d82320d72320d8236a21d92320d82320d9236a21da2320d923"
-    "20da236a21db2320da2320db236a21dc2320db2320dc236a21dd2320dc2320dd236a21de2320dd2320de236a21df23"
-    "20de2320df236a21e02320df2320e0236a21e12320e02320e1236a21e22320e12320e2236a21e32320e22320e3236a"
-    "21e42320e32320e4236a21e52320e42320e5236a21e62320e52320e6236a21e72320e62320e7236a21e82320e72320"
-    "e8236a21e92320e82320e9236a21ea2320e92320ea236a21eb2320ea2320eb236a21ec2320eb2320ec236a21ed2320"
-    "ec2320ed236a21ee2320ed2320ee236a21ef2320ee2320ef236a21f02320ef2320f0236a21f12320f02320f1236a21"
-    "f22320f12320f2236a21f32320f22320f3236a21f42320f32320f4236a21f52320f42320f5236a21f62320f52320f6"
-    "236a21f72320f62320f7236a21f82320f72320f8236a21f92320f82320f9236a21fa2320f92320fa236a21fb2320fa"
-    "2320fb236a21fc2320fb2320fc236a21fd2320fc2320fd236a21fe2320fd2320fe236a21ff2320fe2320ff236a2180"
-    "2420ff232080246a2181242080242081246a2182242081242082246a2183242082242083246a218424208324208424"
-    "6a2185242084242085246a2186242085242086246a2187242086242087246a2188242087242088246a218924208824"
-    "2089246a218a24208924208a246a218b24208a24208b246a218c24208b24208c246a218d24208c24208d246a218e24"
-    "208d24208e246a218f24208e24208f246a219024208f242090246a2191242090242091246a2192242091242092246a"
-    "2193242092242093246a2194242093242094246a2195242094242095246a2196242095242096246a21972420962420"
-    "97246a2198242097242098246a2199242098242099246a219a24209924209a246a219b24209a24209b246a219c2420"
-    "9b24209c246a219d24209c24209d246a219e24209d24209e246a219f24209e24209f246a21a024209f2420a0246a21"
-    "a12420a02420a1246a21a22420a12420a2246a21a32420a22420a3246a21a42420a32420a4246a21a52420a42420a5"
-    "246a21a62420a52420a6246a21a72420a62420a7246a21a82420a72420a8246a21a92420a82420a9246a21aa2420a9"
-    "2420aa246a21ab2420aa2420ab246a21ac2420ab2420ac246a21ad2420ac2420ad246a21ae2420ad2420ae246a21af"
-    "2420ae2420af246a21b02420af2420b0246a21b12420b02420b1246a21b22420b12420b2246a21b32420b22420b324"
-    "6a21b42420b32420b4246a21b52420b42420b5246a21b62420b52420b6246a21b72420b62420b7246a21b82420b724"
-    "20b8246a21b92420b82420b9246a21ba2420b92420ba246a21bb2420ba2420bb246a21bc2420bb2420bc246a21bd24"
-    "20bc2420bd246a21be2420bd2420be246a21bf2420be2420bf246a21c02420bf2420c0246a21c12420c02420c1246a"
-    "21c22420c12420c2246a21c32420c22420c3246a21c42420c32420c4246a21c52420c42420c5246a21c62420c52420"
-    "c6246a21c72420c62420c7246a21c82420c72420c8246a21c92420c82420c9246a21ca2420c92420ca246a21cb2420"
-    "ca2420cb246a21cc2420cb2420cc246a21cd2420cc2420cd246a21ce2420cd2420ce246a21cf2420ce2420cf246a21"
-    "d02420cf2420d0246a21d12420d02420d1246a21d22420d12420d2246a21d32420d22420d3246a21d42420d32420d4"
-    "246a21d52420d42420d5246a21d62420d52420d6246a21d72420d62420d7246a21d82420d72420d8246a21d92420d8"
-    "2420d9246a21da2420d92420da246a21db2420da2420db246a21dc2420db2420dc246a21dd2420dc2420dd246a21de"
-    "2420dd2420de246a21df2420de2420df246a21e02420df2420e0246a21e12420e02420e1246a21e22420e12420e224"
-    "6a21e32420e22420e3246a21e42420e32420e4246a21e52420e42420e5246a21e62420e52420e6246a21e72420e624"
-    "20e7246a21e82420e72420e8246a21e92420e82420e9246a21ea2420e92420ea246a21eb2420ea2420eb246a21ec24"
-    "20eb2420ec246a21ed2420ec2420ed246a21ee2420ed2420ee246a21ef2420ee2420ef246a21f02420ef2420f0246a"
-    "21f12420f02420f1246a21f22420f12420f2246a21f32420f22420f3246a21f42420f32420f4246a21f52420f42420"
-    "f5246a21f62420f52420f6246a21f72420f62420f7246a21f82420f72420f8246a21f92420f82420f9246a21fa2420"
-    "f92420fa246a21fb2420fa2420fb246a21fc2420fb2420fc246a21fd2420fc2420fd246a21fe2420fd2420fe246a21"
-    "ff2420fe2420ff246a21802520ff242080256a2181252080252081256a2182252081252082256a2183252082252083"
-    "256a2184252083252084256a2185252084252085256a2186252085252086256a2187252086252087256a2188252087"
-    "252088256a2189252088252089256a218a25208925208a256a218b25208a25208b256a218c25208b25208c256a218d"
-    "25208c25208d256a218e25208d25208e256a218f25208e25208f256a219025208f252090256a219125209025209125"
-    "6a2192252091252092256a2193252092252093256a2194252093252094256a2195252094252095256a219625209525"
-    "2096256a2197252096252097256a2198252097252098256a2199252098252099256a219a25209925209a256a219b25"
-    "209a25209b256a219c25209b25209c256a219d25209c25209d256a219e25209d25209e256a219f25209e25209f256a"
-    "21a025209f2520a0256a21a12520a02520a1256a21a22520a12520a2256a21a32520a22520a3256a21a42520a32520"
-    "a4256a21a52520a42520a5256a21a62520a52520a6256a21a72520a62520a7256a21a82520a72520a8256a21a92520"
-    "a82520a9256a21aa2520a92520aa256a21ab2520aa2520ab256a21ac2520ab2520ac256a21ad2520ac2520ad256a21"
-    "ae2520ad2520ae256a21af2520ae2520af256a21b02520af2520b0256a21b12520b02520b1256a21b22520b12520b2"
-    "256a21b32520b22520b3256a21b42520b32520b4256a21b52520b42520b5256a21b62520b52520b6256a21b72520b6"
-    "2520b7256a21b82520b72520b8256a21b92520b82520b9256a21ba2520b92520ba256a21bb2520ba2520bb256a21bc"
-    "2520bb2520bc256a21bd2520bc2520bd256a21be2520bd2520be256a21bf2520be2520bf256a21c02520bf2520c025"
-    "6a21c12520c02520c1256a21c22520c12520c2256a21c32520c22520c3256a21c42520c32520c4256a21c52520c425"
-    "20c5256a21c62520c52520c6256a21c72520c62520c7256a21c82520c72520c8256a21c92520c82520c9256a21ca25"
-    "20c92520ca256a21cb2520ca2520cb256a21cc2520cb2520cc256a21cd2520cc2520cd256a21ce2520cd2520ce256a"
-    "21cf2520ce2520cf256a21d02520cf2520d0256a21d12520d02520d1256a21d22520d12520d2256a21d32520d22520"
-    "d3256a21d42520d32520d4256a21d52520d42520d5256a21d62520d52520d6256a21d72520d62520d7256a21d82520"
-    "d72520d8256a21d92520d82520d9256a21da2520d92520da256a21db2520da2520db256a21dc2520db2520dc256a21"
-    "dd2520dc2520dd256a21de2520dd2520de256a21df2520de2520df256a21e02520df2520e0256a21e12520e02520e1"
-    "256a21e22520e12520e2256a21e32520e22520e3256a21e42520e32520e4256a21e52520e42520e5256a21e62520e5"
-    "2520e6256a21e72520e62520e7256a21e82520e72520e8256a21e92520e82520e9256a21ea2520e92520ea256a21eb"
-    "2520ea2520eb256a21ec2520eb2520ec256a21ed2520ec2520ed256a21ee2520ed2520ee256a21ef2520ee2520ef25"
-    "6a21f02520ef2520f0256a21f12520f02520f1256a21f22520f12520f2256a21f32520f22520f3256a21f42520f325"
-    "20f4256a21f52520f42520f5256a21f62520f52520f6256a21f72520f62520f7256a21f82520f72520f8256a21f925"
-    "20f82520f9256a21fa2520f92520fa256a21fb2520fa2520fb256a21fc2520fb2520fc256a21fd2520fc2520fd256a"
-    "21fe2520fd2520fe256a21ff2520fe2520ff256a21802620ff252080266a2181262080262081266a21822620812620"
-    "82266a2183262082262083266a2184262083262084266a2185262084262085266a2186262085262086266a21872620"
-    "86262087266a2188262087262088266a2189262088262089266a218a26208926208a266a218b26208a26208b266a21"
-    "8c26208b26208c266a218d26208c26208d266a218e26208d26208e266a218f26208e26208f266a219026208f262090"
-    "266a2191262090262091266a2192262091262092266a2193262092262093266a2194262093262094266a2195262094"
-    "262095266a2196262095262096266a2197262096262097266a2198262097262098266a2199262098262099266a219a"
-    "26209926209a266a219b26209a26209b266a219c26209b26209c266a219d26209c26209d266a219e26209d26209e26"
-    "6a219f26209e26209f266a21a026209f2620a0266a21a12620a02620a1266a21a22620a12620a2266a21a32620a226"
-    "20a3266a21a42620a32620a4266a21a52620a42620a5266a21a62620a52620a6266a21a72620a62620a7266a21a826"
-    "20a72620a8266a21a92620a82620a9266a21aa2620a92620aa266a21ab2620aa2620ab266a21ac2620ab2620ac266a"
-    "21ad2620ac2620ad266a21ae2620ad2620ae266a21af2620ae2620af266a21b02620af2620b0266a21b12620b02620"
-    "b1266a21b22620b12620b2266a21b32620b22620b3266a21b42620b32620b4266a21b52620b42620b5266a21b62620"
-    "b52620b6266a21b72620b62620b7266a21b82620b72620b8266a21b92620b82620b9266a21ba2620b92620ba266a21"
-    "bb2620ba2620bb266a21bc2620bb2620bc266a21bd2620bc2620bd266a21be2620bd2620be266a21bf2620be2620bf"
-    "266a21c02620bf2620c0266a21c12620c02620c1266a21c22620c12620c2266a21c32620c22620c3266a21c42620c3"
-    "2620c4266a21c52620c42620c5266a21c62620c52620c6266a21c72620c62620c7266a21c82620c72620c8266a21c9"
-    "2620c82620c9266a21ca2620c92620ca266a21cb2620ca2620cb266a21cc2620cb2620cc266a21cd2620cc2620cd26"
-    "6a21ce2620cd2620ce266a21cf2620ce2620cf266a21d02620cf2620d0266a21d12620d02620d1266a21d22620d126"
-    "20d2266a21d32620d22620d3266a21d42620d32620d4266a21d52620d42620d5266a21d62620d52620d6266a21d726"
-    "20d62620d7266a21d82620d72620d8266a21d92620d82620d9266a21da2620d92620da266a21db2620da2620db266a"
-    "21dc2620db2620dc266a21dd2620dc2620dd266a21de2620dd2620de266a21df2620de2620df266a21e02620df2620"
-    "e0266a21e12620e02620e1266a21e22620e12620e2266a21e32620e22620e3266a21e42620e32620e4266a21e52620"
-    "e42620e5266a21e62620e52620e6266a21e72620e62620e7266a21e82620e72620e8266a21e92620e82620e9266a21"
-    "ea2620e92620ea266a21eb2620ea2620eb266a21ec2620eb2620ec266a21ed2620ec2620ed266a21ee2620ed2620ee"
-    "266a21ef2620ee2620ef266a21f02620ef2620f0266a21f12620f02620f1266a21f22620f12620f2266a21f32620f2"
-    "2620f3266a21f42620f32620f4266a21f52620f42620f5266a21f62620f52620f6266a21f72620f62620f7266a21f8"
-    "2620f72620f8266a21f92620f82620f9266a21fa2620f92620fa266a21fb2620fa2620fb266a21fc2620fb2620fc26"
-    "6a21fd2620fc2620fd266a21fe2620fd2620fe266a21ff2620fe2620ff266a21802720ff262080276a218127208027"
-    "2081276a2182272081272082276a2183272082272083276a2184272083272084276a2185272084272085276a218627"
-    "2085272086276a2187272086272087276a2188272087272088276a2189272088272089276a218a27208927208a276a"
-    "218b27208a27208b276a218c27208b27208c276a218d27208c27208d276a218e27208d27208e276a218f27208e2720"
-    "8f276a219027208f272090276a2191272090272091276a2192272091272092276a2193272092272093276a21942720"
-    "93272094276a2195272094272095276a2196272095272096276a2197272096272097276a2198272097272098276a21"
-    "99272098272099276a219a27209927209a276a219b27209a27209b276a219c27209b27209c276a219d27209c27209d"
-    "276a219e27209d27209e276a219f27209e27209f276a21a027209f2720a0276a21a12720a02720a1276a21a22720a1"
-    "2720a2276a21a32720a22720a3276a21a42720a32720a4276a21a52720a42720a5276a21a62720a52720a6276a21a7"
-    "2720a62720a7276a21a82720a72720a8276a21a92720a82720a9276a21aa2720a92720aa276a21ab2720aa2720ab27"
-    "6a21ac2720ab2720ac276a21ad2720ac2720ad276a21ae2720ad2720ae276a21af2720ae2720af276a21b02720af27"
-    "20b0276a21b12720b02720b1276a21b22720b12720b2276a21b32720b22720b3276a21b42720b32720b4276a21b527"
-    "20b42720b5276a21b62720b52720b6276a21b72720b62720b7276a21b82720b72720b8276a21b92720b82720b9276a"
-    "21ba2720b92720ba276a21bb2720ba2720bb276a21bc2720bb2720bc276a21bd2720bc2720bd276a21be2720bd2720"
-    "be276a21bf2720be2720bf276a21c02720bf2720c0276a21c12720c02720c1276a21c22720c12720c2276a21c32720"
-    "c22720c3276a21c42720c32720c4276a21c52720c42720c5276a21c62720c52720c6276a21c72720c62720c7276a21"
-    "c82720c72720c8276a21c92720c82720c9276a21ca2720c92720ca276a21cb2720ca2720cb276a21cc2720cb2720cc"
-    "276a21cd2720cc2720cd276a21ce2720cd2720ce276a21cf2720ce2720cf276a21d02720cf2720d0276a21d12720d0"
-    "2720d1276a21d22720d12720d2276a21d32720d22720d3276a21d42720d32720d4276a21d52720d42720d5276a21d6"
-    "2720d52720d6276a21d72720d62720d7276a21d82720d72720d8276a21d92720d82720d9276a21da2720d92720da27"
-    "6a21db2720da2720db276a21dc2720db2720dc276a21dd2720dc2720dd276a21de2720dd2720de276a21df2720de27"
-    "20df276a21e02720df2720e0276a21e12720e02720e1276a21e22720e12720e2276a21e32720e22720e3276a21e427"
-    "20e32720e4276a21e52720e42720e5276a21e62720e52720e6276a21e72720e62720e7276a21e82720e72720e8276a"
-    "21e92720e82720e9276a21ea2720e92720ea276a21eb2720ea2720eb276a21ec2720eb2720ec276a21ed2720ec2720"
-    "ed276a21ee2720ed2720ee276a21ef2720ee2720ef276a21f02720ef2720f0276a21f12720f02720f1276a21f22720"
-    "f12720f2276a21f32720f22720f3276a21f42720f32720f4276a21f52720f42720f5276a21f62720f52720f6276a21"
-    "f72720f62720f7276a21f82720f72720f8276a21f92720f82720f9276a21fa2720f92720fa276a21fb2720fa2720fb"
-    "276a21fc2720fb2720fc276a21fd2720fc2720fd276a21fe2720fd2720fe276a21ff2720fe2720ff276a21802820ff"
-    "272080286a2181282080282081286a2182282081282082286a2183282082282083286a2184282083282084286a2185"
-    "282084282085286a2186282085282086286a2187282086282087286a2188282087282088286a218928208828208928"
-    "6a218a28208928208a286a218b28208a28208b286a218c28208b28208c286a218d28208c28208d286a218e28208d28"
-    "208e286a218f28208e28208f286a219028208f282090286a2191282090282091286a2192282091282092286a219328"
-    "2092282093286a2194282093282094286a2195282094282095286a2196282095282096286a2197282096282097286a"
-    "2198282097282098286a2199282098282099286a219a28209928209a286a219b28209a28209b286a219c28209b2820"
-    "9c286a219d28209c28209d286a219e28209d28209e286a219f28209e28209f286a21a028209f2820a0286a21a12820"
-    "a02820a1286a21a22820a12820a2286a21a32820a22820a3286a21a42820a32820a4286a21a52820a42820a5286a21"
-    "a62820a52820a6286a21a72820a62820a7286a21a82820a72820a8286a21a92820a82820a9286a21aa2820a92820aa"
-    "286a21ab2820aa2820ab286a21ac2820ab2820ac286a21ad2820ac2820ad286a21ae2820ad2820ae286a21af2820ae"
-    "2820af286a21b02820af2820b0286a21b12820b02820b1286a21b22820b12820b2286a21b32820b22820b3286a21b4"
-    "2820b32820b4286a21b52820b42820b5286a21b62820b52820b6286a21b72820b62820b7286a21b82820b72820b828"
-    "6a21b92820b82820b9286a21ba2820b92820ba286a21bb2820ba2820bb286a21bc2820bb2820bc286a21bd2820bc28"
-    "20bd286a21be2820bd2820be286a21bf2820be2820bf286a21c02820bf2820c0286a21c12820c02820c1286a21c228"
-    "20c12820c2286a21c32820c22820c3286a21c42820c32820c4286a21c52820c42820c5286a21c62820c52820c6286a"
-    "21c72820c62820c7286a21c82820c72820c8286a21c92820c82820c9286a21ca2820c92820ca286a21cb2820ca2820"
-    "cb286a21cc2820cb2820cc286a21cd2820cc2820cd286a21ce2820cd2820ce286a21cf2820ce2820cf286a21d02820"
-    "cf2820d0286a21d12820d02820d1286a21d22820d12820d2286a21d32820d22820d3286a21d42820d32820d4286a21"
-    "d52820d42820d5286a21d62820d52820d6286a21d72820d62820d7286a21d82820d72820d8286a21d92820d82820d9"
-    "286a21da2820d92820da286a21db2820da2820db286a21dc2820db2820dc286a21dd2820dc2820dd286a21de2820dd"
-    "2820de286a21df2820de2820df286a21e02820df2820e0286a21e12820e02820e1286a21e22820e12820e2286a21e3"
-    "2820e22820e3286a21e42820e32820e4286a21e52820e42820e5286a21e62820e52820e6286a21e72820e62820e728"
-    "6a21e82820e72820e8286a21e92820e82820e9286a21ea2820e92820ea286a21eb2820ea2820eb286a21ec2820eb28"
-    "20ec286a21ed2820ec2820ed286a21ee2820ed2820ee286a21ef2820ee2820ef286a21f02820ef2820f0286a21f128"
-    "20f02820f1286a21f22820f12820f2286a21f32820f22820f3286a21f42820f32820f4286a21f52820f42820f5286a"
-    "21f62820f52820f6286a21f72820f62820f7286a21f82820f72820f8286a21f92820f82820f9286a21fa2820f92820"
-    "fa286a21fb2820fa2820fb286a21fc2820fb2820fc286a21fd2820fc2820fd286a21fe2820fd2820fe286a21ff2820"
-    "fe2820ff286a21802920ff282080296a2181292080292081296a2182292081292082296a2183292082292083296a21"
-    "84292083292084296a2185292084292085296a2186292085292086296a2187292086292087296a2188292087292088"
-    "296a2189292088292089296a218a29208929208a296a218b29208a29208b296a218c29208b29208c296a218d29208c"
-    "29208d296a218e29208d29208e296a218f29208e29208f296a219029208f292090296a2191292090292091296a2192"
-    "292091292092296a2193292092292093296a2194292093292094296a2195292094292095296a219629209529209629"
-    "6a2197292096292097296a2198292097292098296a2199292098292099296a219a29209929209a296a219b29209a29"
-    "209b296a219c29209b29209c296a219d29209c29209d296a219e29209d29209e296a219f29209e29209f296a21a029"
-    "209f2920a0296a21a12920a02920a1296a21a22920a12920a2296a21a32920a22920a3296a21a42920a32920a4296a"
-    "21a52920a42920a5296a21a62920a52920a6296a21a72920a62920a7296a21a82920a72920a8296a21a92920a82920"
-    "a9296a21aa2920a92920aa296a21ab2920aa2920ab296a21ac2920ab2920ac296a21ad2920ac2920ad296a21ae2920"
-    "ad2920ae296a21af2920ae2920af296a21b02920af2920b0296a21b12920b02920b1296a21b22920b12920b2296a21"
-    "b32920b22920b3296a21b42920b32920b4296a21b52920b42920b5296a21b62920b52920b6296a21b72920b62920b7"
-    "296a21b82920b72920b8296a21b92920b82920b9296a21ba2920b92920ba296a21bb2920ba2920bb296a21bc2920bb"
-    "2920bc296a21bd2920bc2920bd296a21be2920bd2920be296a21bf2920be2920bf296a21c02920bf2920c0296a21c1"
-    "2920c02920c1296a21c22920c12920c2296a21c32920c22920c3296a21c42920c32920c4296a21c52920c42920c529"
-    "6a21c62920c52920c6296a21c72920c62920c7296a21c82920c72920c8296a21c92920c82920c9296a21ca2920c929"
-    "20ca296a21cb2920ca2920cb296a21cc2920cb2920cc296a21cd2920cc2920cd296a21ce2920cd2920ce296a21cf29"
-    "20ce2920cf296a21d02920cf2920d0296a21d12920d02920d1296a21d22920d12920d2296a21d32920d22920d3296a"
-    "21d42920d32920d4296a21d52920d42920d5296a21d62920d52920d6296a21d72920d62920d7296a21d82920d72920"
-    "d8296a21d92920d82920d9296a21da2920d92920da296a21db2920da2920db296a21dc2920db2920dc296a21dd2920"
-    "dc2920dd296a21de2920dd2920de296a21df2920de2920df296a21e02920df2920e0296a21e12920e02920e1296a21"
-    "e22920e12920e2296a21e32920e22920e3296a21e42920e32920e4296a21e52920e42920e5296a21e62920e52920e6"
-    "296a21e72920e62920e7296a21e82920e72920e8296a21e92920e82920e9296a21ea2920e92920ea296a21eb2920ea"
-    "2920eb296a21ec2920eb2920ec296a21ed2920ec2920ed296a21ee2920ed2920ee296a21ef2920ee2920ef296a21f0"
-    "2920ef2920f0296a21f12920f02920f1296a21f22920f12920f2296a21f32920f22920f3296a21f42920f32920f429"
-    "6a21f52920f42920f5296a21f62920f52920f6296a21f72920f62920f7296a21f82920f72920f8296a21f92920f829"
-    "20f9296a21fa2920f92920fa296a21fb2920fa2920fb296a21fc2920fb2920fc296a21fd2920fc2920fd296a21fe29"
-    "20fd2920fe296a21ff2920fe2920ff296a21802a20ff2920802a6a21812a20802a20812a6a21822a20812a20822a6a"
-    "21832a20822a20832a6a21842a20832a20842a6a21852a20842a20852a6a21862a20852a20862a6a21872a20862a20"
-    "872a6a21882a20872a20882a6a21892a20882a20892a6a218a2a20892a208a2a6a218b2a208a2a208b2a6a218c2a20"
-    "8b2a208c2a6a218d2a208c2a208d2a6a218e2a208d2a208e2a6a218f2a208e2a208f2a6a21902a208f2a20902a6a21"
-    "912a20902a20912a6a21922a20912a20922a6a21932a20922a20932a6a21942a20932a20942a6a21952a20942a2095"
-    "2a6a21962a20952a20962a6a21972a20962a20972a6a21982a20972a20982a6a21992a20982a20992a6a219a2a2099"
-    "2a209a2a6a219b2a209a2a209b2a6a219c2a209b2a209c2a6a219d2a209c2a209d2a6a219e2a209d2a209e2a6a219f"
-    "2a209e2a209f2a6a21a02a209f2a20a02a6a21a12a20a02a20a12a6a21a22a20a12a20a22a6a21a32a20a22a20a32a"
-    "6a21a42a20a32a20a42a6a21a52a20a42a20a52a6a21a62a20a52a20a62a6a21a72a20a62a20a72a6a21a82a20a72a"
-    "20a82a6a21a92a20a82a20a92a6a21aa2a20a92a20aa2a6a21ab2a20aa2a20ab2a6a21ac2a20ab2a20ac2a6a21ad2a"
-    "20ac2a20ad2a6a21ae2a20ad2a20ae2a6a21af2a20ae2a20af2a6a21b02a20af2a20b02a6a21b12a20b02a20b12a6a"
-    "21b22a20b12a20b22a6a21b32a20b22a20b32a6a21b42a20b32a20b42a6a21b52a20b42a20b52a6a21b62a20b52a20"
-    "b62a6a21b72a20b62a20b72a6a21b82a20b72a20b82a6a21b92a20b82a20b92a6a21ba2a20b92a20ba2a6a21bb2a20"
-    "ba2a20bb2a6a21bc2a20bb2a20bc2a6a21bd2a20bc2a20bd2a6a21be2a20bd2a20be2a6a21bf2a20be2a20bf2a6a21"
-    "c02a20bf2a20c02a6a21c12a20c02a20c12a6a21c22a20c12a20c22a6a21c32a20c22a20c32a6a21c42a20c32a20c4"
-    "2a6a21c52a20c42a20c52a6a21c62a20c52a20c62a6a21c72a20c62a20c72a6a21c82a20c72a20c82a6a21c92a20c8"
-    "2a20c92a6a21ca2a20c92a20ca2a6a21cb2a20ca2a20cb2a6a21cc2a20cb2a20cc2a6a21cd2a20cc2a20cd2a6a21ce"
-    "2a20cd2a20ce2a6a21cf2a20ce2a20cf2a6a21d02a20cf2a20d02a6a21d12a20d02a20d12a6a21d22a20d12a20d22a"
-    "6a21d32a20d22a20d32a6a21d42a20d32a20d42a6a21d52a20d42a20d52a6a21d62a20d52a20d62a6a21d72a20d62a"
-    "20d72a6a21d82a20d72a20d82a6a21d92a20d82a20d92a6a21da2a20d92a20da2a6a21db2a20da2a20db2a6a21dc2a"
-    "20db2a20dc2a6a21dd2a20dc2a20dd2a6a21de2a20dd2a20de2a6a21df2a20de2a20df2a6a21e02a20df2a20e02a6a"
-    "21e12a20e02a20e12a6a21e22a20e12a20e22a6a21e32a20e22a20e32a6a21e42a20e32a20e42a6a21e52a20e42a20"
-    "e52a6a21e62a20e52a20e62a6a21e72a20e62a20e72a6a21e82a20e72a20e82a6a21e92a20e82a20e92a6a21ea2a20"
-    "e92a20ea2a6a21eb2a20ea2a20eb2a6a21ec2a20eb2a20ec2a6a21ed2a20ec2a20ed2a6a21ee2a20ed2a20ee2a6a21"
-    "ef2a20ee2a20ef2a6a21f02a20ef2a20f02a6a21f12a20f02a20f12a6a21f22a20f12a20f22a6a21f32a20f22a20f3"
-    "2a6a21f42a20f32a20f42a6a21f52a20f42a20f52a6a21f62a20f52a20f62a6a21f72a20f62a20f72a6a21f82a20f7"
-    "2a20f82a6a21f92a20f82a20f92a6a21fa2a20f92a20fa2a6a21fb2a20fa2a20fb2a6a21fc2a20fb2a20fc2a6a21fd"
-    "2a20fc2a20fd2a6a21fe2a20fd2a20fe2a6a21ff2a20fe2a20ff2a6a21802b20ff2a20802b6a21812b20802b20812b"
-    "6a21822b20812b20822b6a21832b20822b20832b6a21842b20832b20842b6a21852b20842b20852b6a21862b20852b"
-    "20862b6a21872b20862b20872b6a21882b20872b20882b6a21892b20882b20892b6a218a2b20892b208a2b6a218b2b"
-    "208a2b208b2b6a218c2b208b2b208c2b6a218d2b208c2b208d2b6a218e2b208d2b208e2b6a218f2b208e2b208f2b6a"
-    "21902b208f2b20902b6a21912b20902b20912b6a21922b20912b20922b6a21932b20922b20932b6a21942b20932b20"
-    "942b6a21952b20942b20952b6a21962b20952b20962b6a21972b20962b20972b6a21982b20972b20982b6a21992b20"
-    "982b20992b6a219a2b20992b209a2b6a219b2b209a2b209b2b6a219c2b209b2b209c2b6a219d2b209c2b209d2b6a21"
-    "9e2b209d2b209e2b6a219f2b209e2b209f2b6a21a02b209f2b20a02b6a21a12b20a02b20a12b6a21a22b20a12b20a2"
-    "2b6a21a32b20a22b20a32b6a21a42b20a32b20a42b6a21a52b20a42b20a52b6a21a62b20a52b20a62b6a21a72b20a6"
-    "2b20a72b6a21a82b20a72b20a82b6a21a92b20a82b20a92b6a21aa2b20a92b20aa2b6a21ab2b20aa2b20ab2b6a21ac"
-    "2b20ab2b20ac2b6a21ad2b20ac2b20ad2b6a21ae2b20ad2b20ae2b6a21af2b20ae2b20af2b6a21b02b20af2b20b02b"
-    "6a21b12b20b02b20b12b6a21b22b20b12b20b22b6a21b32b20b22b20b32b6a21b42b20b32b20b42b6a21b52b20b42b"
-    "20b52b6a21b62b20b52b20b62b6a21b72b20b62b20b72b6a21b82b20b72b20b82b6a21b92b20b82b20b92b6a21ba2b"
-    "20b92b20ba2b6a21bb2b20ba2b20bb2b6a21bc2b20bb2b20bc2b6a21bd2b20bc2b20bd2b6a21be2b20bd2b20be2b6a"
-    "21bf2b20be2b20bf2b6a21c02b20bf2b20c02b6a21c12b20c02b20c12b6a21c22b20c12b20c22b6a21c32b20c22b20"
-    "c32b6a21c42b20c32b20c42b6a21c52b20c42b20c52b6a21c62b20c52b20c62b6a21c72b20c62b20c72b6a21c82b20"
-    "c72b20c82b6a21c92b20c82b20c92b6a21ca2b20c92b20ca2b6a21cb2b20ca2b20cb2b6a21cc2b20cb2b20cc2b6a21"
-    "cd2b20cc2b20cd2b6a21ce2b20cd2b20ce2b6a21cf2b20ce2b20cf2b6a21d02b20cf2b20d02b6a21d12b20d02b20d1"
-    "2b6a21d22b20d12b20d22b6a21d32b20d22b20d32b6a21d42b20d32b20d42b6a21d52b20d42b20d52b6a21d62b20d5"
-    "2b20d62b6a21d72b20d62b20d72b6a21d82b20d72b20d82b6a21d92b20d82b20d92b6a21da2b20d92b20da2b6a21db"
-    "2b20da2b20db2b6a21dc2b20db2b20dc2b6a21dd2b20dc2b20dd2b6a21de2b20dd2b20de2b6a21df2b20de2b20df2b"
-    "6a21e02b20df2b20e02b6a21e12b20e02b20e12b6a21e22b20e12b20e22b6a21e32b20e22b20e32b6a21e42b20e32b"
-    "20e42b6a21e52b20e42b20e52b6a21e62b20e52b20e62b6a21e72b20e62b20e72b6a21e82b20e72b20e82b6a21e92b"
-    "20e82b20e92b6a21ea2b20e92b20ea2b6a21eb2b20ea2b20eb2b6a21ec2b20eb2b20ec2b6a21ed2b20ec2b20ed2b6a"
-    "21ee2b20ed2b20ee2b6a21ef2b20ee2b20ef2b6a21f02b20ef2b20f02b6a21f12b20f02b20f12b6a21f22b20f12b20"
-    "f22b6a21f32b20f22b20f32b6a21f42b20f32b20f42b6a21f52b20f42b20f52b6a21f62b20f52b20f62b6a21f72b20"
-    "f62b20f72b6a21f82b20f72b20f82b6a21f92b20f82b20f92b6a21fa2b20f92b20fa2b6a21fb2b20fa2b20fb2b6a21"
-    "fc2b20fb2b20fc2b6a21fd2b20fc2b20fd2b6a21fe2b20fd2b20fe2b6a21ff2b20fe2b20ff2b6a21802c20ff2b2080"
-    "2c6a21812c20802c20812c6a21822c20812c20822c6a21832c20822c20832c6a21842c20832c20842c6a21852c2084"
-    "2c20852c6a21862c20852c20862c6a21872c20862c20872c6a21882c20872c20882c6a21892c20882c20892c6a218a"
-    "2c20892c208a2c6a218b2c208a2c208b2c6a218c2c208b2c208c2c6a218d2c208c2c208d2c6a218e2c208d2c208e2c"
-    "6a218f2c208e2c208f2c6a21902c208f2c20902c6a21912c20902c20912c6a21922c20912c20922c6a21932c20922c"
-    "20932c6a21942c20932c20942c6a21952c20942c20952c6a21962c20952c20962c6a21972c20962c20972c6a21982c"
-    "20972c20982c6a21992c20982c20992c6a219a2c20992c209a2c6a219b2c209a2c209b2c6a219c2c209b2c209c2c6a"
-    "219d2c209c2c209d2c6a219e2c209d2c209e2c6a219f2c209e2c209f2c6a21a02c209f2c20a02c6a21a12c20a02c20"
-    "a12c6a21a22c20a12c20a22c6a21a32c20a22c20a32c6a21a42c20a32c20a42c6a21a52c20a42c20a52c6a21a62c20"
-    "a52c20a62c6a21a72c20a62c20a72c6a21a82c20a72c20a82c6a21a92c20a82c20a92c6a21aa2c20a92c20aa2c6a21"
-    "ab2c20aa2c20ab2c6a21ac2c20ab2c20ac2c6a21ad2c20ac2c20ad2c6a21ae2c20ad2c20ae2c6a21af2c20ae2c20af"
-    "2c6a21b02c20af2c20b02c6a21b12c20b02c20b12c6a21b22c20b12c20b22c6a21b32c20b22c20b32c6a21b42c20b3"
-    "2c20b42c6a21b52c20b42c20b52c6a21b62c20b52c20b62c6a21b72c20b62c20b72c6a21b82c20b72c20b82c6a21b9"
-    "2c20b82c20b92c6a21ba2c20b92c20ba2c6a21bb2c20ba2c20bb2c6a21bc2c20bb2c20bc2c6a21bd2c20bc2c20bd2c"
-    "6a21be2c20bd2c20be2c6a21bf2c20be2c20bf2c6a21c02c20bf2c20c02c6a21c12c20c02c20c12c6a21c22c20c12c"
-    "20c22c6a21c32c20c22c20c32c6a21c42c20c32c20c42c6a21c52c20c42c20c52c6a21c62c20c52c20c62c6a21c72c"
-    "20c62c20c72c6a21c82c20c72c20c82c6a21c92c20c82c20c92c6a21ca2c20c92c20ca2c6a21cb2c20ca2c20cb2c6a"
-    "21cc2c20cb2c20cc2c6a21cd2c20cc2c20cd2c6a21ce2c20cd2c20ce2c6a21cf2c20ce2c20cf2c6a21d02c20cf2c20"
-    "d02c6a21d12c20d02c20d12c6a21d22c20d12c20d22c6a21d32c20d22c20d32c6a21d42c20d32c20d42c6a21d52c20"
-    "d42c20d52c6a21d62c20d52c20d62c6a21d72c20d62c20d72c6a21d82c20d72c20d82c6a21d92c20d82c20d92c6a21"
-    "da2c20d92c20da2c6a21db2c20da2c20db2c6a21dc2c20db2c20dc2c6a21dd2c20dc2c20dd2c6a21de2c20dd2c20de"
-    "2c6a21df2c20de2c20df2c6a21e02c20df2c20e02c6a21e12c20e02c20e12c6a21e22c20e12c20e22c6a21e32c20e2"
-    "2c20e32c6a21e42c20e32c20e42c6a21e52c20e42c20e52c6a21e62c20e52c20e62c6a21e72c20e62c20e72c6a21e8"
-    "2c20e72c20e82c6a21e92c20e82c20e92c6a21ea2c20e92c20ea2c6a21eb2c20ea2c20eb2c6a21ec2c20eb2c20ec2c"
-    "6a21ed2c20ec2c20ed2c6a21ee2c20ed2c20ee2c6a21ef2c20ee2c20ef2c6a21f02c20ef2c20f02c6a21f12c20f02c"
-    "20f12c6a21f22c20f12c20f22c6a21f32c20f22c20f32c6a21f42c20f32c20f42c6a21f52c20f42c20f52c6a21f62c"
-    "20f52c20f62c6a21f72c20f62c20f72c6a21f82c20f72c20f82c6a21f92c20f82c20f92c6a21fa2c20f92c20fa2c6a"
-    "21fb2c20fa2c20fb2c6a21fc2c20fb2c20fc2c6a21fd2c20fc2c20fd2c6a21fe2c20fd2c20fe2c6a21ff2c20fe2c20"
-    "ff2c6a21802d20ff2c20802d6a21812d20802d20812d6a21822d20812d20822d6a21832d20822d20832d6a21842d20"
-    "832d20842d6a21852d20842d20852d6a21862d20852d20862d6a21872d20862d20872d6a21882d20872d20882d6a21"
-    "892d20882d20892d6a218a2d20892d208a2d6a218b2d208a2d208b2d6a218c2d208b2d208c2d6a218d2d208c2d208d"
-    "2d6a218e2d208d2d208e2d6a218f2d208e2d208f2d6a21902d208f2d20902d6a21912d20902d20912d6a21922d2091"
-    "2d20922d6a21932d20922d20932d6a21942d20932d20942d6a21952d20942d20952d6a21962d20952d20962d6a2197"
-    "2d20962d20972d6a21982d20972d20982d6a21992d20982d20992d6a219a2d20992d209a2d6a219b2d209a2d209b2d"
-    "6a219c2d209b2d209c2d6a219d2d209c2d209d2d6a219e2d209d2d209e2d6a219f2d209e2d209f2d6a21a02d209f2d"
-    "20a02d6a21a12d20a02d20a12d6a21a22d20a12d20a22d6a21a32d20a22d20a32d6a21a42d20a32d20a42d6a21a52d"
-    "20a42d20a52d6a21a62d20a52d20a62d6a21a72d20a62d20a72d6a21a82d20a72d20a82d6a21a92d20a82d20a92d6a"
-    "21aa2d20a92d20aa2d6a21ab2d20aa2d20ab2d6a21ac2d20ab2d20ac2d6a21ad2d20ac2d20ad2d6a21ae2d20ad2d20"
-    "ae2d6a21af2d20ae2d20af2d6a21b02d20af2d20b02d6a21b12d20b02d20b12d6a21b22d20b12d20b22d6a21b32d20"
-    "b22d20b32d6a21b42d20b32d20b42d6a21b52d20b42d20b52d6a21b62d20b52d20b62d6a21b72d20b62d20b72d6a21"
-    "b82d20b72d20b82d6a21b92d20b82d20b92d6a21ba2d20b92d20ba2d6a21bb2d20ba2d20bb2d6a21bc2d20bb2d20bc"
-    "2d6a21bd2d20bc2d20bd2d6a21be2d20bd2d20be2d6a21bf2d20be2d20bf2d6a21c02d20bf2d20c02d6a21c12d20c0"
-    "2d20c12d6a21c22d20c12d20c22d6a21c32d20c22d20c32d6a21c42d20c32d20c42d6a21c52d20c42d20c52d6a21c6"
-    "2d20c52d20c62d6a21c72d20c62d20c72d6a21c82d20c72d20c82d6a21c92d20c82d20c92d6a21ca2d20c92d20ca2d"
-    "6a21cb2d20ca2d20cb2d6a21cc2d20cb2d20cc2d6a21cd2d20cc2d20cd2d6a21ce2d20cd2d20ce2d6a21cf2d20ce2d"
-    "20cf2d6a21d02d20cf2d20d02d6a21d12d20d02d20d12d6a21d22d20d12d20d22d6a21d32d20d22d20d32d6a21d42d"
-    "20d32d20d42d6a21d52d20d42d20d52d6a21d62d20d52d20d62d6a21d72d20d62d20d72d6a21d82d20d72d20d82d6a"
-    "21d92d20d82d20d92d6a21da2d20d92d20da2d6a21db2d20da2d20db2d6a21dc2d20db2d20dc2d6a21dd2d20dc2d20"
-    "dd2d6a21de2d20dd2d20de2d6a21df2d20de2d20df2d6a21e02d20df2d20e02d6a21e12d20e02d20e12d6a21e22d20"
-    "e12d20e22d6a21e32d20e22d20e32d6a21e42d20e32d20e42d6a21e52d20e42d20e52d6a21e62d20e52d20e62d6a21"
-    "e72d20e62d20e72d6a21e82d20e72d20e82d6a21e92d20e82d20e92d6a21ea2d20e92d20ea2d6a21eb2d20ea2d20eb"
-    "2d6a21ec2d20eb2d20ec2d6a21ed2d20ec2d20ed2d6a21ee2d20ed2d20ee2d6a21ef2d20ee2d20ef2d6a21f02d20ef"
-    "2d20f02d6a21f12d20f02d20f12d6a21f22d20f12d20f22d6a21f32d20f22d20f32d6a21f42d20f32d20f42d6a21f5"
-    "2d20f42d20f52d6a21f62d20f52d20f62d6a21f72d20f62d20f72d6a21f82d20f72d20f82d6a21f92d20f82d20f92d"
-    "6a21fa2d20f92d20fa2d6a21fb2d20fa2d20fb2d6a21fc2d20fb2d20fc2d6a21fd2d20fc2d20fd2d6a21fe2d20fd2d"
-    "20fe2d6a21ff2d20fe2d20ff2d6a21802e20ff2d20802e6a21812e20802e20812e6a21822e20812e20822e6a21832e"
-    "20822e20832e6a21842e20832e20842e6a21852e20842e20852e6a21862e20852e20862e6a21872e20862e20872e6a"
-    "21882e20872e20882e6a21892e20882e20892e6a218a2e20892e208a2e6a218b2e208a2e208b2e6a218c2e208b2e20"
-    "8c2e6a218d2e208c2e208d2e6a218e2e208d2e208e2e6a218f2e208e2e208f2e6a21902e208f2e20902e6a21912e20"
-    "902e20912e6a21922e20912e20922e6a21932e20922e20932e6a21942e20932e20942e6a21952e20942e20952e6a21"
-    "962e20952e20962e6a21972e20962e20972e6a21982e20972e20982e6a21992e20982e20992e6a219a2e20992e209a"
-    "2e6a219b2e209a2e209b2e6a219c2e209b2e209c2e6a219d2e209c2e209d2e6a219e2e209d2e209e2e6a219f2e209e"
-    "2e209f2e6a21a02e209f2e20a02e6a21a12e20a02e20a12e6a21a22e20a12e20a22e6a21a32e20a22e20a32e6a21a4"
-    "2e20a32e20a42e6a21a52e20a42e20a52e6a21a62e20a52e20a62e6a21a72e20a62e20a72e6a21a82e20a72e20a82e"
-    "6a21a92e20a82e20a92e6a21aa2e20a92e20aa2e6a21ab2e20aa2e20ab2e6a21ac2e20ab2e20ac2e6a21ad2e20ac2e"
-    "20ad2e6a21ae2e20ad2e20ae2e6a21af2e20ae2e20af2e6a21b02e20af2e20b02e6a21b12e20b02e20b12e6a21b22e"
-    "20b12e20b22e6a21b32e20b22e20b32e6a21b42e20b32e20b42e6a21b52e20b42e20b52e6a21b62e20b52e20b62e6a"
-    "21b72e20b62e20b72e6a21b82e20b72e20b82e6a21b92e20b82e20b92e6a21ba2e20b92e20ba2e6a21bb2e20ba2e20"
-    "bb2e6a21bc2e20bb2e20bc2e6a21bd2e20bc2e20bd2e6a21be2e20bd2e20be2e6a21bf2e20be2e20bf2e6a21c02e20"
-    "bf2e20c02e6a21c12e20c02e20c12e6a21c22e20c12e20c22e6a21c32e20c22e20c32e6a21c42e20c32e20c42e6a21"
-    "c52e20c42e20c52e6a21c62e20c52e20c62e6a21c72e20c62e20c72e6a21c82e20c72e20c82e6a21c92e20c82e20c9"
-    "2e6a21ca2e20c92e20ca2e6a21cb2e20ca2e20cb2e6a21cc2e20cb2e20cc2e6a21cd2e20cc2e20cd2e6a21ce2e20cd"
-    "2e20ce2e6a21cf2e20ce2e20cf2e6a21d02e20cf2e20d02e6a21d12e20d02e20d12e6a21d22e20d12e20d22e6a21d3"
-    "2e20d22e20d32e6a21d42e20d32e20d42e6a21d52e20d42e20d52e6a21d62e20d52e20d62e6a21d72e20d62e20d72e"
-    "6a21d82e20d72e20d82e6a21d92e20d82e20d92e6a21da2e20d92e20da2e6a21db2e20da2e20db2e6a21dc2e20db2e"
-    "20dc2e6a21dd2e20dc2e20dd2e6a21de2e20dd2e20de2e6a21df2e20de2e20df2e6a21e02e20df2e20e02e6a21e12e"
-    "20e02e20e12e6a21e22e20e12e20e22e6a21e32e20e22e20e32e6a21e42e20e32e20e42e6a21e52e20e42e20e52e6a"
-    "21e62e20e52e20e62e6a21e72e20e62e20e72e6a21e82e20e72e20e82e6a21e92e20e82e20e92e6a21ea2e20e92e20"
-    "ea2e6a21eb2e20ea2e20eb2e6a21ec2e20eb2e20ec2e6a21ed2e20ec2e20ed2e6a21ee2e20ed2e20ee2e6a21ef2e20"
-    "ee2e20ef2e6a21f02e20ef2e20f02e6a21f12e20f02e20f12e6a21f22e20f12e20f22e6a21f32e20f22e20f32e6a21"
-    "f42e20f32e20f42e6a21f52e20f42e20f52e6a21f62e20f52e20f62e6a21f72e20f62e20f72e6a21f82e20f72e20f8"
-    "2e6a21f92e20f82e20f92e6a21fa2e20f92e20fa2e6a21fb2e20fa2e20fb2e6a21fc2e20fb2e20fc2e6a21fd2e20fc"
-    "2e20fd2e6a21fe2e20fd2e20fe2e6a21ff2e20fe2e20ff2e6a21802f20ff2e20802f6a21812f20802f20812f6a2182"
-    "2f20812f20822f6a21832f20822f20832f6a21842f20832f20842f6a21852f20842f20852f6a21862f20852f20862f"
-    "6a21872f20862f20872f6a21882f20872f20882f6a21892f20882f20892f6a218a2f20892f208a2f6a218b2f208a2f"
-    "208b2f6a218c2f208b2f208c2f6a218d2f208c2f208d2f6a218e2f208d2f208e2f6a218f2f208e2f208f2f6a21902f"
-    "208f2f20902f6a21912f20902f20912f6a21922f20912f20922f6a21932f20922f20932f6a21942f20932f20942f6a"
-    "21952f20942f20952f6a21962f20952f20962f6a21972f20962f20972f6a21982f20972f20982f6a21992f20982f20"
-    "992f6a219a2f20992f209a2f6a219b2f209a2f209b2f6a219c2f209b2f209c2f6a219d2f209c2f209d2f6a219e2f20"
-    "9d2f209e2f6a219f2f209e2f209f2f6a21a02f209f2f20a02f6a21a12f20a02f20a12f6a21a22f20a12f20a22f6a21"
-    "a32f20a22f20a32f6a21a42f20a32f20a42f6a21a52f20a42f20a52f6a21a62f20a52f20a62f6a21a72f20a62f20a7"
-    "2f6a21a82f20a72f20a82f6a21a92f20a82f20a92f6a21aa2f20a92f20aa2f6a21ab2f20aa2f20ab2f6a21ac2f20ab"
-    "2f20ac2f6a21ad2f20ac2f20ad2f6a21ae2f20ad2f20ae2f6a21af2f20ae2f20af2f6a21b02f20af2f20b02f6a21b1"
-    "2f20b02f20b12f6a21b22f20b12f20b22f6a21b32f20b22f20b32f6a21b42f20b32f20b42f6a21b52f20b42f20b52f"
-    "6a21b62f20b52f20b62f6a21b72f20b62f20b72f6a21b82f20b72f20b82f6a21b92f20b82f20b92f6a21ba2f20b92f"
-    "20ba2f6a21bb2f20ba2f20bb2f6a21bc2f20bb2f20bc2f6a21bd2f20bc2f20bd2f6a21be2f20bd2f20be2f6a21bf2f"
-    "20be2f20bf2f6a21c02f20bf2f20c02f6a21c12f20c02f20c12f6a21c22f20c12f20c22f6a21c32f20c22f20c32f6a"
-    "21c42f20c32f20c42f6a21c52f20c42f20c52f6a21c62f20c52f20c62f6a21c72f20c62f20c72f6a21c82f20c72f20"
-    "c82f6a21c92f20c82f20c92f6a21ca2f20c92f20ca2f6a21cb2f20ca2f20cb2f6a21cc2f20cb2f20cc2f6a21cd2f20"
-    "cc2f20cd2f6a21ce2f20cd2f20ce2f6a21cf2f20ce2f20cf2f6a21d02f20cf2f20d02f6a21d12f20d02f20d12f6a21"
-    "d22f20d12f20d22f6a21d32f20d22f20d32f6a21d42f20d32f20d42f6a21d52f20d42f20d52f6a21d62f20d52f20d6"
-    "2f6a21d72f20d62f20d72f6a21d82f20d72f20d82f6a21d92f20d82f20d92f6a21da2f20d92f20da2f6a21db2f20da"
-    "2f20db2f6a21dc2f20db2f20dc2f6a21dd2f20dc2f20dd2f6a21de2f20dd2f20de2f6a21df2f20de2f20df2f6a21e0"
-    "2f20df2f20e02f6a21e12f20e02f20e12f6a21e22f20e12f20e22f6a21e32f20e22f20e32f6a21e42f20e32f20e42f"
-    "6a21e52f20e42f20e52f6a21e62f20e52f20e62f6a21e72f20e62f20e72f6a21e82f20e72f20e82f6a21e92f20e82f"
-    "20e92f6a21ea2f20e92f20ea2f6a21eb2f20ea2f20eb2f6a21ec2f20eb2f20ec2f6a21ed2f20ec2f20ed2f6a21ee2f"
-    "20ed2f20ee2f6a21ef2f20ee2f20ef2f6a21f02f20ef2f20f02f6a21f12f20f02f20f12f6a21f22f20f12f20f22f6a"
-    "21f32f20f22f20f32f6a21f42f20f32f20f42f6a21f52f20f42f20f52f6a21f62f20f52f20f62f6a21f72f20f62f20"
-    "f72f6a21f82f20f72f20f82f6a21f92f20f82f20f92f6a21fa2f20f92f20fa2f6a21fb2f20fa2f20fb2f6a21fc2f20"
-    "fb2f20fc2f6a21fd2f20fc2f20fd2f6a21fe2f20fd2f20fe2f6a21ff2f20fe2f20ff2f6a21803020ff2f2080306a21"
-    "81302080302081306a2182302081302082306a2183302082302083306a2184302083302084306a2185302084302085"
-    "306a2186302085302086306a2187302086302087306a2188302087302088306a2189302088302089306a218a302089"
-    "30208a306a218b30208a30208b306a218c30208b30208c306a218d30208c30208d306a218e30208d30208e306a218f"
-    "30208e30208f306a219030208f302090306a2191302090302091306a2192302091302092306a219330209230209330"
-    "6a2194302093302094306a2195302094302095306a2196302095302096306a2197302096302097306a219830209730"
-    "2098306a2199302098302099306a219a30209930209a306a219b30209a30209b306a219c30209b30209c306a219d30"
-    "209c30209d306a219e30209d30209e306a219f30209e30209f306a21a030209f3020a0306a21a13020a03020a1306a"
-    "21a23020a13020a2306a21a33020a23020a3306a21a43020a33020a4306a21a53020a43020a5306a21a63020a53020"
-    "a6306a21a73020a63020a7306a21a83020a73020a8306a21a93020a83020a9306a21aa3020a93020aa306a21ab3020"
-    "aa3020ab306a21ac3020ab3020ac306a21ad3020ac3020ad306a21ae3020ad3020ae306a21af3020ae3020af306a21"
-    "b03020af3020b0306a21b13020b03020b1306a21b23020b13020b2306a21b33020b23020b3306a21b43020b33020b4"
-    "306a21b53020b43020b5306a21b63020b53020b6306a21b73020b63020b7306a21b83020b73020b8306a21b93020b8"
-    "3020b9306a21ba3020b93020ba306a21bb3020ba3020bb306a21bc3020bb3020bc306a21bd3020bc3020bd306a21be"
-    "3020bd3020be306a21bf3020be3020bf306a21c03020bf3020c0306a21c13020c03020c1306a21c23020c13020c230"
-    "6a21c33020c23020c3306a21c43020c33020c4306a21c53020c43020c5306a21c63020c53020c6306a21c73020c630"
-    "20c7306a21c83020c73020c8306a21c93020c83020c9306a21ca3020c93020ca306a21cb3020ca3020cb306a21cc30"
-    "20cb3020cc306a21cd3020cc3020cd306a21ce3020cd3020ce306a21cf3020ce3020cf306a21d03020cf3020d0306a"
-    "21d13020d03020d1306a21d23020d13020d2306a21d33020d23020d3306a21d43020d33020d4306a21d53020d43020"
-    "d5306a21d63020d53020d6306a21d73020d63020d7306a21d83020d73020d8306a21d93020d83020d9306a21da3020"
-    "d93020da306a21db3020da3020db306a21dc3020db3020dc306a21dd3020dc3020dd306a21de3020dd3020de306a21"
-    "df3020de3020df306a21e03020df3020e0306a21e13020e03020e1306a21e23020e13020e2306a21e33020e23020e3"
-    "306a21e43020e33020e4306a21e53020e43020e5306a21e63020e53020e6306a21e73020e63020e7306a21e83020e7"
-    "3020e8306a21e93020e83020e9306a21ea3020e93020ea306a21eb3020ea3020eb306a21ec3020eb3020ec306a21ed"
-    "3020ec3020ed306a21ee3020ed3020ee306a21ef3020ee3020ef306a21f03020ef3020f0306a21f13020f03020f130"
-    "6a21f23020f13020f2306a21f33020f23020f3306a21f43020f33020f4306a21f53020f43020f5306a21f63020f530"
-    "20f6306a21f73020f63020f7306a21f83020f73020f8306a21f93020f83020f9306a21fa3020f93020fa306a21fb30"
-    "20fa3020fb306a21fc3020fb3020fc306a21fd3020fc3020fd306a21fe3020fd3020fe306a21ff3020fe3020ff306a"
-    "21803120ff302080316a2181312080312081316a2182312081312082316a2183312082312083316a21843120833120"
-    "84316a2185312084312085316a2186312085312086316a2187312086312087316a2188312087312088316a21893120"
-    "88312089316a218a31208931208a316a218b31208a31208b316a218c31208b31208c316a218d31208c31208d316a21"
-    "8e31208d31208e316a218f31208e31208f316a219031208f312090316a2191312090312091316a2192312091312092"
-    "316a2193312092312093316a2194312093312094316a2195312094312095316a2196312095312096316a2197312096"
-    "312097316a2198312097312098316a2199312098312099316a219a31209931209a316a219b31209a31209b316a219c"
-    "31209b31209c316a219d31209c31209d316a219e31209d31209e316a219f31209e31209f316a21a031209f3120a031"
-    "6a21a13120a03120a1316a21a23120a13120a2316a21a33120a23120a3316a21a43120a33120a4316a21a53120a431"
-    "20a5316a21a63120a53120a6316a21a73120a63120a7316a21a83120a73120a8316a21a93120a83120a9316a21aa31"
-    "20a93120aa316a21ab3120aa3120ab316a21ac3120ab3120ac316a21ad3120ac3120ad316a21ae3120ad3120ae316a"
-    "21af3120ae3120af316a21b03120af3120b0316a21b13120b03120b1316a21b23120b13120b2316a21b33120b23120"
-    "b3316a21b43120b33120b4316a21b53120b43120b5316a21b63120b53120b6316a21b73120b63120b7316a21b83120"
-    "b73120b8316a21b93120b83120b9316a21ba3120b93120ba316a21bb3120ba3120bb316a21bc3120bb3120bc316a21"
-    "bd3120bc3120bd316a21be3120bd3120be316a21bf3120be3120bf316a21c03120bf3120c0316a21c13120c03120c1"
-    "316a21c23120c13120c2316a21c33120c23120c3316a21c43120c33120c4316a21c53120c43120c5316a21c63120c5"
-    "3120c6316a21c73120c63120c7316a21c83120c73120c8316a21c93120c83120c9316a21ca3120c93120ca316a21cb"
-    "3120ca3120cb316a21cc3120cb3120cc316a21cd3120cc3120cd316a21ce3120cd3120ce316a21cf3120ce3120cf31"
-    "6a21d03120cf3120d0316a21d13120d03120d1316a21d23120d13120d2316a21d33120d23120d3316a21d43120d331"
-    "20d4316a21d53120d43120d5316a21d63120d53120d6316a21d73120d63120d7316a21d83120d73120d8316a21d931"
-    "20d83120d9316a21da3120d93120da316a21db3120da3120db316a21dc3120db3120dc316a21dd3120dc3120dd316a"
-    "21de3120dd3120de316a21df3120de3120df316a21e03120df3120e0316a21e13120e03120e1316a21e23120e13120"
-    "e2316a21e33120e23120e3316a21e43120e33120e4316a21e53120e43120e5316a21e63120e53120e6316a21e73120"
-    "e63120e7316a21e83120e73120e8316a21e93120e83120e9316a21ea3120e93120ea316a21eb3120ea3120eb316a21"
-    "ec3120eb3120ec316a21ed3120ec3120ed316a21ee3120ed3120ee316a21ef3120ee3120ef316a21f03120ef3120f0"
-    "316a21f13120f03120f1316a21f23120f13120f2316a21f33120f23120f3316a21f43120f33120f4316a21f53120f4"
-    "3120f5316a21f63120f53120f6316a21f73120f63120f7316a21f83120f73120f8316a21f93120f83120f9316a21fa"
-    "3120f93120fa316a21fb3120fa3120fb316a21fc3120fb3120fc316a21fd3120fc3120fd316a21fe3120fd3120fe31"
-    "6a21ff3120fe3120ff316a21803220ff312080326a2181322080322081326a2182322081322082326a218332208232"
-    "2083326a2184322083322084326a2185322084322085326a2186322085322086326a2187322086322087326a218832"
-    "2087322088326a2189322088322089326a218a32208932208a326a218b32208a32208b326a218c32208b32208c326a"
-    "218d32208c32208d326a218e32208d32208e326a218f32208e32208f326a219032208f322090326a21913220903220"
-    "91326a2192322091322092326a2193322092322093326a2194322093322094326a2195322094322095326a21963220"
-    "95322096326a2197322096322097326a2198322097322098326a2199322098322099326a219a32209932209a326a21"
-    "9b32209a32209b326a219c32209b32209c326a219d32209c32209d326a219e32209d32209e326a219f32209e32209f"
-    "326a21a032209f3220a0326a21a13220a03220a1326a21a23220a13220a2326a21a33220a23220a3326a21a43220a3"
-    "3220a4326a21a53220a43220a5326a21a63220a53220a6326a21a73220a63220a7326a21a83220a73220a8326a21a9"
-    "3220a83220a9326a21aa3220a93220aa326a21ab3220aa3220ab326a21ac3220ab3220ac326a21ad3220ac3220ad32"
-    "6a21ae3220ad3220ae326a21af3220ae3220af326a21b03220af3220b0326a21b13220b03220b1326a21b23220b132"
-    "20b2326a21b33220b23220b3326a21b43220b33220b4326a21b53220b43220b5326a21b63220b53220b6326a21b732"
-    "20b63220b7326a21b83220b73220b8326a21b93220b83220b9326a21ba3220b93220ba326a21bb3220ba3220bb326a"
-    "21bc3220bb3220bc326a21bd3220bc3220bd326a21be3220bd3220be326a21bf3220be3220bf326a21c03220bf3220"
-    "c0326a21c13220c03220c1326a21c23220c13220c2326a21c33220c23220c3326a21c43220c33220c4326a21c53220"
-    "c43220c5326a21c63220c53220c6326a21c73220c63220c7326a21c83220c73220c8326a21c93220c83220c9326a21"
-    "ca3220c93220ca326a21cb3220ca3220cb326a21cc3220cb3220cc326a21cd3220cc3220cd326a21ce3220cd3220ce"
-    "326a21cf3220ce3220cf326a21d03220cf3220d0326a21d13220d03220d1326a21d23220d13220d2326a21d33220d2"
-    "3220d3326a21d43220d33220d4326a21d53220d43220d5326a21d63220d53220d6326a21d73220d63220d7326a21d8"
-    "3220d73220d8326a21d93220d83220d9326a21da3220d93220da326a21db3220da3220db326a21dc3220db3220dc32"
-    "6a21dd3220dc3220dd326a21de3220dd3220de326a21df3220de3220df326a21e03220df3220e0326a21e13220e032"
-    "20e1326a21e23220e13220e2326a21e33220e23220e3326a21e43220e33220e4326a21e53220e43220e5326a21e632"
-    "20e53220e6326a21e73220e63220e7326a21e83220e73220e8326a21e93220e83220e9326a21ea3220e93220ea326a"
-    "21eb3220ea3220eb326a21ec3220eb3220ec326a21ed3220ec3220ed326a21ee3220ed3220ee326a21ef3220ee3220"
-    "ef326a21f03220ef3220f0326a21f13220f03220f1326a21f23220f13220f2326a21f33220f23220f3326a21f43220"
-    "f33220f4326a21f53220f43220f5326a21f63220f53220f6326a21f73220f63220f7326a21f83220f73220f8326a21"
-    "f93220f83220f9326a21fa3220f93220fa326a21fb3220fa3220fb326a21fc3220fb3220fc326a21fd3220fc3220fd"
-    "326a21fe3220fd3220fe326a21ff3220fe3220ff326a21803320ff322080336a2181332080332081336a2182332081"
-    "332082336a2183332082332083336a2184332083332084336a2185332084332085336a2186332085332086336a2187"
-    "332086332087336a2188332087332088336a2189332088332089336a218a33208933208a336a218b33208a33208b33"
-    "6a218c33208b33208c336a218d33208c33208d336a218e33208d33208e336a218f33208e33208f336a219033208f33"
-    "2090336a2191332090332091336a2192332091332092336a2193332092332093336a2194332093332094336a219533"
-    "2094332095336a2196332095332096336a2197332096332097336a2198332097332098336a2199332098332099336a"
-    "219a33209933209a336a219b33209a33209b336a219c33209b33209c336a219d33209c33209d336a219e33209d3320"
-    "9e336a219f33209e33209f336a21a033209f3320a0336a21a13320a03320a1336a21a23320a13320a2336a21a33320"
-    "a23320a3336a21a43320a33320a4336a21a53320a43320a5336a21a63320a53320a6336a21a73320a63320a7336a21"
-    "a83320a73320a8336a21a93320a83320a9336a21aa3320a93320aa336a21ab3320aa3320ab336a21ac3320ab3320ac"
-    "336a21ad3320ac3320ad336a21ae3320ad3320ae336a21af3320ae3320af336a21b03320af3320b0336a21b13320b0"
-    "3320b1336a21b23320b13320b2336a21b33320b23320b3336a21b43320b33320b4336a21b53320b43320b5336a21b6"
-    "3320b53320b6336a21b73320b63320b7336a21b83320b73320b8336a21b93320b83320b9336a21ba3320b93320ba33"
-    "6a21bb3320ba3320bb336a21bc3320bb3320bc336a21bd3320bc3320bd336a21be3320bd3320be336a21bf3320be33"
-    "20bf336a21c03320bf3320c0336a21c13320c03320c1336a21c23320c13320c2336a21c33320c23320c3336a21c433"
-    "20c33320c4336a21c53320c43320c5336a21c63320c53320c6336a21c73320c63320c7336a21c83320c73320c8336a"
-    "21c93320c83320c9336a21ca3320c93320ca336a21cb3320ca3320cb336a21cc3320cb3320cc336a21cd3320cc3320"
-    "cd336a21ce3320cd3320ce336a21cf3320ce3320cf336a21d03320cf3320d0336a21d13320d03320d1336a21d23320"
-    "d13320d2336a21d33320d23320d3336a21d43320d33320d4336a21d53320d43320d5336a21d63320d53320d6336a21"
-    "d73320d63320d7336a21d83320d73320d8336a21d93320d83320d9336a21da3320d93320da336a21db3320da3320db"
-    "336a21dc3320db3320dc336a21dd3320dc3320dd336a21de3320dd3320de336a21df3320de3320df336a21e03320df"
-    "3320e0336a21e13320e03320e1336a21e23320e13320e2336a21e33320e23320e3336a21e43320e33320e4336a21e5"
-    "3320e43320e5336a21e63320e53320e6336a21e73320e63320e7336a21e83320e73320e8336a21e93320e83320e933"
-    "6a21ea3320e93320ea336a21eb3320ea3320eb336a21ec3320eb3320ec336a21ed3320ec3320ed336a21ee3320ed33"
-    "20ee336a21ef3320ee3320ef336a21f03320ef3320f0336a21f13320f03320f1336a21f23320f13320f2336a21f333"
-    "20f23320f3336a21f43320f33320f4336a21f53320f43320f5336a21f63320f53320f6336a21f73320f63320f7336a"
-    "21f83320f73320f8336a21f93320f83320f9336a21fa3320f93320fa336a21fb3320fa3320fb336a21fc3320fb3320"
-    "fc336a21fd3320fc3320fd336a21fe3320fd3320fe336a21ff3320fe3320ff336a21803420ff332080346a21813420"
-    "80342081346a2182342081342082346a2183342082342083346a2184342083342084346a2185342084342085346a21"
-    "86342085342086346a2187342086342087346a2188342087342088346a2189342088342089346a218a34208934208a"
-    "346a218b34208a34208b346a218c34208b34208c346a218d34208c34208d346a218e34208d34208e346a218f34208e"
-    "34208f346a219034208f342090346a2191342090342091346a2192342091342092346a2193342092342093346a2194"
-    "342093342094346a2195342094342095346a2196342095342096346a2197342096342097346a219834209734209834"
-    "6a2199342098342099346a219a34209934209a346a219b34209a34209b346a219c34209b34209c346a219d34209c34"
-    "209d346a219e34209d34209e346a219f34209e34209f346a21a034209f3420a0346a21a13420a03420a1346a21a234"
-    "20a13420a2346a21a33420a23420a3346a21a43420a33420a4346a21a53420a43420a5346a21a63420a53420a6346a"
-    "21a73420a63420a7346a21a83420a73420a8346a21a93420a83420a9346a21aa3420a93420aa346a21ab3420aa3420"
-    "ab346a21ac3420ab3420ac346a21ad3420ac3420ad346a21ae3420ad3420ae346a21af3420ae3420af346a21b03420"
-    "af3420b0346a21b13420b03420b1346a21b23420b13420b2346a21b33420b23420b3346a21b43420b33420b4346a21"
-    "b53420b43420b5346a21b63420b53420b6346a21b73420b63420b7346a21b83420b73420b8346a21b93420b83420b9"
-    "346a21ba3420b93420ba346a21bb3420ba3420bb346a21bc3420bb3420bc346a21bd3420bc3420bd346a21be3420bd"
-    "3420be346a21bf3420be3420bf346a21c03420bf3420c0346a21c13420c03420c1346a21c23420c13420c2346a21c3"
-    "3420c23420c3346a21c43420c33420c4346a21c53420c43420c5346a21c63420c53420c6346a21c73420c63420c734"
-    "6a21c83420c73420c8346a21c93420c83420c9346a21ca3420c93420ca346a21cb3420ca3420cb346a21cc3420cb34"
-    "20cc346a21cd3420cc3420cd346a21ce3420cd3420ce346a21cf3420ce3420cf346a21d03420cf3420d0346a21d134"
-    "20d03420d1346a21d23420d13420d2346a21d33420d23420d3346a21d43420d33420d4346a21d53420d43420d5346a"
-    "21d63420d53420d6346a21d73420d63420d7346a21d83420d73420d8346a21d93420d83420d9346a21da3420d93420"
-    "da346a21db3420da3420db346a21dc3420db3420dc346a21dd3420dc3420dd346a21de3420dd3420de346a21df3420"
-    "de3420df346a21e03420df3420e0346a21e13420e03420e1346a21e23420e13420e2346a21e33420e23420e3346a21"
-    "e43420e33420e4346a21e53420e43420e5346a21e63420e53420e6346a21e73420e63420e7346a21e83420e73420e8"
-    "346a21e93420e83420e9346a21ea3420e93420ea346a21eb3420ea3420eb346a21ec3420eb3420ec346a21ed3420ec"
-    "3420ed346a21ee3420ed3420ee346a21ef3420ee3420ef346a21f03420ef3420f0346a21f13420f03420f1346a21f2"
-    "3420f13420f2346a21f33420f23420f3346a21f43420f33420f4346a21f53420f43420f5346a21f63420f53420f634"
-    "6a21f73420f63420f7346a21f83420f73420f8346a21f93420f83420f9346a21fa3420f93420fa346a21fb3420fa34"
-    "20fb346a21fc3420fb3420fc346a21fd3420fc3420fd346a21fe3420fd3420fe346a21ff3420fe3420ff346a218035"
-    "20ff342080356a2181352080352081356a2182352081352082356a2183352082352083356a2184352083352084356a"
-    "2185352084352085356a2186352085352086356a2187352086352087356a2188352087352088356a21893520883520"
-    "89356a218a35208935208a356a218b35208a35208b356a218c35208b35208c356a218d35208c35208d356a218e3520"
-    "8d35208e356a218f35208e35208f356a219035208f352090356a2191352090352091356a2192352091352092356a21"
-    "93352092352093356a2194352093352094356a2195352094352095356a2196352095352096356a2197352096352097"
-    "356a2198352097352098356a2199352098352099356a219a35209935209a356a219b35209a35209b356a219c35209b"
-    "35209c356a219d35209c35209d356a219e35209d35209e356a219f35209e35209f356a21a035209f3520a0356a21a1"
-    "3520a03520a1356a21a23520a13520a2356a21a33520a23520a3356a21a43520a33520a4356a21a53520a43520a535"
-    "6a21a63520a53520a6356a21a73520a63520a7356a21a83520a73520a8356a21a93520a83520a9356a21aa3520a935"
-    "20aa356a21ab3520aa3520ab356a21ac3520ab3520ac356a21ad3520ac3520ad356a21ae3520ad3520ae356a21af35"
-    "20ae3520af356a21b03520af3520b0356a21b13520b03520b1356a21b23520b13520b2356a21b33520b23520b3356a"
-    "21b43520b33520b4356a21b53520b43520b5356a21b63520b53520b6356a21b73520b63520b7356a21b83520b73520"
-    "b8356a21b93520b83520b9356a21ba3520b93520ba356a21bb3520ba3520bb356a21bc3520bb3520bc356a21bd3520"
-    "bc3520bd356a21be3520bd3520be356a21bf3520be3520bf356a21c03520bf3520c0356a21c13520c03520c1356a21"
-    "c23520c13520c2356a21c33520c23520c3356a21c43520c33520c4356a21c53520c43520c5356a21c63520c53520c6"
-    "356a21c73520c63520c7356a21c83520c73520c8356a21c93520c83520c9356a21ca3520c93520ca356a21cb3520ca"
-    "3520cb356a21cc3520cb3520cc356a21cd3520cc3520cd356a21ce3520cd3520ce356a21cf3520ce3520cf356a21d0"
-    "3520cf3520d0356a21d13520d03520d1356a21d23520d13520d2356a21d33520d23520d3356a21d43520d33520d435"
-    "6a21d53520d43520d5356a21d63520d53520d6356a21d73520d63520d7356a21d83520d73520d8356a21d93520d835"
-    "20d9356a21da3520d93520da356a21db3520da3520db356a21dc3520db3520dc356a21dd3520dc3520dd356a21de35"
-    "20dd3520de356a21df3520de3520df356a21e03520df3520e0356a21e13520e03520e1356a21e23520e13520e2356a"
-    "21e33520e23520e3356a21e43520e33520e4356a21e53520e43520e5356a21e63520e53520e6356a21e73520e63520"
-    "e7356a21e83520e73520e8356a21e93520e83520e9356a21ea3520e93520ea356a21eb3520ea3520eb356a21ec3520"
-    "eb3520ec356a21ed3520ec3520ed356a21ee3520ed3520ee356a21ef3520ee3520ef356a21f03520ef3520f0356a21"
-    "f13520f03520f1356a21f23520f13520f2356a21f33520f23520f3356a21f43520f33520f4356a21f53520f43520f5"
-    "356a21f63520f53520f6356a21f73520f63520f7356a21f83520f73520f8356a21f93520f83520f9356a21fa3520f9"
-    "3520fa356a21fb3520fa3520fb356a21fc3520fb3520fc356a21fd3520fc3520fd356a21fe3520fd3520fe356a21ff"
-    "3520fe3520ff356a21803620ff352080366a2181362080362081366a2182362081362082366a218336208236208336"
-    "6a2184362083362084366a2185362084362085366a2186362085362086366a2187362086362087366a218836208736"
-    "2088366a2189362088362089366a218a36208936208a366a218b36208a36208b366a218c36208b36208c366a218d36"
-    "208c36208d366a218e36208d36208e366a218f36208e36208f366a219036208f362090366a2191362090362091366a"
-    "2192362091362092366a2193362092362093366a2194362093362094366a2195362094362095366a21963620953620"
-    "96366a2197362096362097366a2198362097362098366a2199362098362099366a219a36209936209a366a219b3620"
-    "9a36209b366a219c36209b36209c366a219d36209c36209d366a219e36209d36209e366a219f36209e36209f366a21"
-    "a036209f3620a0366a21a13620a03620a1366a21a23620a13620a2366a21a33620a23620a3366a21a43620a33620a4"
-    "366a21a53620a43620a5366a21a63620a53620a6366a21a73620a63620a7366a21a83620a73620a8366a21a93620a8"
-    "3620a9366a21aa3620a93620aa366a21ab3620aa3620ab366a21ac3620ab3620ac366a21ad3620ac3620ad366a21ae"
-    "3620ad3620ae366a21af3620ae3620af366a21b03620af3620b0366a21b13620b03620b1366a21b23620b13620b236"
-    "6a21b33620b23620b3366a21b43620b33620b4366a21b53620b43620b5366a21b63620b53620b6366a21b73620b636"
-    "20b7366a21b83620b73620b8366a21b93620b83620b9366a21ba3620b93620ba366a21bb3620ba3620bb366a21bc36"
-    "20bb3620bc366a21bd3620bc3620bd366a21be3620bd3620be366a21bf3620be3620bf366a21c03620bf3620c0366a"
-    "21c13620c03620c1366a21c23620c13620c2366a21c33620c23620c3366a21c43620c33620c4366a21c53620c43620"
-    "c5366a21c63620c53620c6366a21c73620c63620c7366a21c83620c73620c8366a21c93620c83620c9366a21ca3620"
-    "c93620ca366a21cb3620ca3620cb366a21cc3620cb3620cc366a21cd3620cc3620cd366a21ce3620cd3620ce366a21"
-    "cf3620ce3620cf366a21d03620cf3620d0366a21d13620d03620d1366a21d23620d13620d2366a21d33620d23620d3"
-    "366a21d43620d33620d4366a21d53620d43620d5366a21d63620d53620d6366a21d73620d63620d7366a21d83620d7"
-    "3620d8366a21d93620d83620d9366a21da3620d93620da366a21db3620da3620db366a21dc3620db3620dc366a21dd"
-    "3620dc3620dd366a21de3620dd3620de366a21df3620de3620df366a21e03620df3620e0366a21e13620e03620e136"
-    "6a21e23620e13620e2366a21e33620e23620e3366a21e43620e33620e4366a21e53620e43620e5366a21e63620e536"
-    "20e6366a21e73620e63620e7366a21e83620e73620e8366a21e93620e83620e9366a21ea3620e93620ea366a21eb36"
-    "20ea3620eb366a21ec3620eb3620ec366a21ed3620ec3620ed366a21ee3620ed3620ee366a21ef3620ee3620ef366a"
-    "21f03620ef3620f0366a21f13620f03620f1366a21f23620f13620f2366a21f33620f23620f3366a21f43620f33620"
-    "f4366a21f53620f43620f5366a21f63620f53620f6366a21f73620f63620f7366a21f83620f73620f8366a21f93620"
-    "f83620f9366a21fa3620f93620fa366a21fb3620fa3620fb366a21fc3620fb3620fc366a21fd3620fc3620fd366a21"
-    "fe3620fd3620fe366a21ff3620fe3620ff366a21803720ff362080376a2181372080372081376a2182372081372082"
-    "376a2183372082372083376a2184372083372084376a2185372084372085376a2186372085372086376a2187372086"
-    "372087376a2188372087372088376a2189372088372089376a218a37208937208a376a218b37208a37208b376a218c"
-    "37208b37208c376a218d37208c37208d376a218e37208d37208e376a218f37208e37208f376a219037208f37209037"
-    "6a2191372090372091376a2192372091372092376a2193372092372093376a2194372093372094376a219537209437"
-    "2095376a2196372095372096376a2197372096372097376a2198372097372098376a2199372098372099376a219a37"
-    "209937209a376a219b37209a37209b376a219c37209b37209c376a219d37209c37209d376a219e37209d37209e376a"
-    "219f37209e37209f376a21a037209f3720a0376a21a13720a03720a1376a21a23720a13720a2376a21a33720a23720"
-    "a3376a21a43720a33720a4376a21a53720a43720a5376a21a63720a53720a6376a21a73720a63720a7376a21a83720"
-    "a73720a8376a21a93720a83720a9376a21aa3720a93720aa376a21ab3720aa3720ab376a21ac3720ab3720ac376a21"
-    "ad3720ac3720ad376a21ae3720ad3720ae376a21af3720ae3720af376a21b03720af3720b0376a21b13720b03720b1"
-    "376a21b23720b13720b2376a21b33720b23720b3376a21b43720b33720b4376a21b53720b43720b5376a21b63720b5"
-    "3720b6376a21b73720b63720b7376a21b83720b73720b8376a21b93720b83720b9376a21ba3720b93720ba376a21bb"
-    "3720ba3720bb376a21bc3720bb3720bc376a21bd3720bc3720bd376a21be3720bd3720be376a21bf3720be3720bf37"
-    "6a21c03720bf3720c0376a21c13720c03720c1376a21c23720c13720c2376a21c33720c23720c3376a21c43720c337"
-    "20c4376a21c53720c43720c5376a21c63720c53720c6376a21c73720c63720c7376a21c83720c73720c8376a21c937"
-    "20c83720c9376a21ca3720c93720ca376a21cb3720ca3720cb376a21cc3720cb3720cc376a21cd3720cc3720cd376a"
-    "21ce3720cd3720ce376a21cf3720ce3720cf376a21d03720cf3720d0376a21d13720d03720d1376a21d23720d13720"
-    "d2376a21d33720d23720d3376a21d43720d33720d4376a21d53720d43720d5376a21d63720d53720d6376a21d73720"
-    "d63720d7376a21d83720d73720d8376a21d93720d83720d9376a21da3720d93720da376a21db3720da3720db376a21"
-    "dc3720db3720dc376a21dd3720dc3720dd376a21de3720dd3720de376a21df3720de3720df376a21e03720df3720e0"
-    "376a21e13720e03720e1376a21e23720e13720e2376a21e33720e23720e3376a21e43720e33720e4376a21e53720e4"
-    "3720e5376a21e63720e53720e6376a21e73720e63720e7376a21e83720e73720e8376a21e93720e83720e9376a21ea"
-    "3720e93720ea376a21eb3720ea3720eb376a21ec3720eb3720ec376a21ed3720ec3720ed376a21ee3720ed3720ee37"
-    "6a21ef3720ee3720ef376a21f03720ef3720f0376a21f13720f03720f1376a21f23720f13720f2376a21f33720f237"
-    "20f3376a21f43720f33720f4376a21f53720f43720f5376a21f63720f53720f6376a21f73720f63720f7376a21f837"
-    "20f73720f8376a21f93720f83720f9376a21fa3720f93720fa376a21fb3720fa3720fb376a21fc3720fb3720fc376a"
-    "21fd3720fc3720fd376a21fe3720fd3720fe376a21ff3720fe3720ff376a21803820ff372080386a21813820803820"
-    "81386a2182382081382082386a2183382082382083386a2184382083382084386a2185382084382085386a21863820"
-    "85382086386a2187382086382087386a2188382087382088386a2189382088382089386a218a38208938208a386a21"
-    "8b38208a38208b386a218c38208b38208c386a218d38208c38208d386a218e38208d38208e386a218f38208e38208f"
-    "386a219038208f382090386a2191382090382091386a2192382091382092386a2193382092382093386a2194382093"
-    "382094386a2195382094382095386a2196382095382096386a2197382096382097386a2198382097382098386a2199"
-    "382098382099386a219a38209938209a386a219b38209a38209b386a219c38209b38209c386a219d38209c38209d38"
-    "6a219e38209d38209e386a219f38209e38209f386a21a038209f3820a0386a21a13820a03820a1386a21a23820a138"
-    "20a2386a21a33820a23820a3386a21a43820a33820a4386a21a53820a43820a5386a21a63820a53820a6386a21a738"
-    "20a63820a7386a21a83820a73820a8386a21a93820a83820a9386a21aa3820a93820aa386a21ab3820aa3820ab386a"
-    "21ac3820ab3820ac386a21ad3820ac3820ad386a21ae3820ad3820ae386a21af3820ae3820af386a21b03820af3820"
-    "b0386a21b13820b03820b1386a21b23820b13820b2386a21b33820b23820b3386a21b43820b33820b4386a21b53820"
-    "b43820b5386a21b63820b53820b6386a21b73820b63820b7386a21b83820b73820b8386a21b93820b83820b9386a21"
-    "ba3820b93820ba386a21bb3820ba3820bb386a21bc3820bb3820bc386a21bd3820bc3820bd386a21be3820bd3820be"
-    "386a21bf3820be3820bf386a21c03820bf3820c0386a21c13820c03820c1386a21c23820c13820c2386a21c33820c2"
-    "3820c3386a21c43820c33820c4386a21c53820c43820c5386a21c63820c53820c6386a21c73820c63820c7386a21c8"
-    "3820c73820c8386a21c93820c83820c9386a21ca3820c93820ca386a21cb3820ca3820cb386a21cc3820cb3820cc38"
-    "6a21cd3820cc3820cd386a21ce3820cd3820ce386a21cf3820ce3820cf386a21d03820cf3820d0386a21d13820d038"
-    "20d1386a21d23820d13820d2386a21d33820d23820d3386a21d43820d33820d4386a21d53820d43820d5386a21d638"
-    "20d53820d6386a21d73820d63820d7386a21d83820d73820d8386a21d93820d83820d9386a21da3820d93820da386a"
-    "21db3820da3820db386a21dc3820db3820dc386a21dd3820dc3820dd386a21de3820dd3820de386a21df3820de3820"
-    "df386a21e03820df3820e0386a21e13820e03820e1386a21e23820e13820e2386a21e33820e23820e3386a21e43820"
-    "e33820e4386a21e53820e43820e5386a21e63820e53820e6386a21e73820e63820e7386a21e83820e73820e8386a21"
-    "e93820e83820e9386a21ea3820e93820ea386a21eb3820ea3820eb386a21ec3820eb3820ec386a21ed3820ec3820ed"
-    "386a21ee3820ed3820ee386a21ef3820ee3820ef386a21f03820ef3820f0386a21f13820f03820f1386a21f23820f1"
-    "3820f2386a21f33820f23820f3386a21f43820f33820f4386a21f53820f43820f5386a21f63820f53820f6386a21f7"
-    "3820f63820f7386a21f83820f73820f8386a21f93820f83820f9386a21fa3820f93820fa386a21fb3820fa3820fb38"
-    "6a21fc3820fb3820fc386a21fd3820fc3820fd386a21fe3820fd3820fe386a21ff3820fe3820ff386a21803920ff38"
-    "2080396a2181392080392081396a2182392081392082396a2183392082392083396a2184392083392084396a218539"
-    "2084392085396a2186392085392086396a2187392086392087396a2188392087392088396a2189392088392089396a"
-    "218a39208939208a396a218b39208a39208b396a218c39208b39208c396a218d39208c39208d396a218e39208d3920"
-    "8e396a218f39208e39208f396a219039208f392090396a2191392090392091396a2192392091392092396a21933920"
-    "92392093396a2194392093392094396a2195392094392095396a2196392095392096396a2197392096392097396a21"
-    "98392097392098396a2199392098392099396a219a39209939209a396a219b39209a39209b396a219c39209b39209c"
-    "396a219d39209c39209d396a219e39209d39209e396a219f39209e39209f396a21a039209f3920a0396a21a13920a0"
-    "3920a1396a21a23920a13920a2396a21a33920a23920a3396a21a43920a33920a4396a21a53920a43920a5396a21a6"
-    "3920a53920a6396a21a73920a63920a7396a21a83920a73920a8396a21a93920a83920a9396a21aa3920a93920aa39"
-    "6a21ab3920aa3920ab396a21ac3920ab3920ac396a21ad3920ac3920ad396a21ae3920ad3920ae396a21af3920ae39"
-    "20af396a21b03920af3920b0396a21b13920b03920b1396a21b23920b13920b2396a21b33920b23920b3396a21b439"
-    "20b33920b4396a21b53920b43920b5396a21b63920b53920b6396a21b73920b63920b7396a21b83920b73920b8396a"
-    "21b93920b83920b9396a21ba3920b93920ba396a21bb3920ba3920bb396a21bc3920bb3920bc396a21bd3920bc3920"
-    "bd396a21be3920bd3920be396a21bf3920be3920bf396a21c03920bf3920c0396a21c13920c03920c1396a21c23920"
-    "c13920c2396a21c33920c23920c3396a21c43920c33920c4396a21c53920c43920c5396a21c63920c53920c6396a21"
-    "c73920c63920c7396a21c83920c73920c8396a21c93920c83920c9396a21ca3920c93920ca396a21cb3920ca3920cb"
-    "396a21cc3920cb3920cc396a21cd3920cc3920cd396a21ce3920cd3920ce396a21cf3920ce3920cf396a21d03920cf"
-    "3920d0396a21d13920d03920d1396a21d23920d13920d2396a21d33920d23920d3396a21d43920d33920d4396a21d5"
-    "3920d43920d5396a21d63920d53920d6396a21d73920d63920d7396a21d83920d73920d8396a21d93920d83920d939"
-    "6a21da3920d93920da396a21db3920da3920db396a21dc3920db3920dc396a21dd3920dc3920dd396a21de3920dd39"
-    "20de396a21df3920de3920df396a21e03920df3920e0396a21e13920e03920e1396a21e23920e13920e2396a21e339"
-    "20e23920e3396a21e43920e33920e4396a21e53920e43920e5396a21e63920e53920e6396a21e73920e63920e7396a"
-    "21e83920e73920e8396a21e93920e83920e9396a21ea3920e93920ea396a21eb3920ea3920eb396a21ec3920eb3920"
-    "ec396a21ed3920ec3920ed396a21ee3920ed3920ee396a21ef3920ee3920ef396a21f03920ef3920f0396a21f13920"
-    "f03920f1396a21f23920f13920f2396a21f33920f23920f3396a21f43920f33920f4396a21f53920f43920f5396a21"
-    "f63920f53920f6396a21f73920f63920f7396a21f83920f73920f8396a21f93920f83920f9396a21fa3920f93920fa"
-    "396a21fb3920fa3920fb396a21fc3920fb3920fc396a21fd3920fc3920fd396a21fe3920fd3920fe396a21ff3920fe"
-    "3920ff396a21803a20ff3920803a6a21813a20803a20813a6a21823a20813a20823a6a21833a20823a20833a6a2184"
-    "3a20833a20843a6a21853a20843a20853a6a21863a20853a20863a6a21873a20863a20873a6a21883a20873a20883a"
-    "6a21893a20883a20893a6a218a3a20893a208a3a6a218b3a208a3a208b3a6a218c3a208b3a208c3a6a218d3a208c3a"
-    "208d3a6a218e3a208d3a208e3a6a218f3a208e3a208f3a6a21903a208f3a20903a6a21913a20903a20913a6a21923a"
-    "20913a20923a6a21933a20923a20933a6a21943a20933a20943a6a21953a20943a20953a6a21963a20953a20963a6a"
-    "21973a20963a20973a6a21983a20973a20983a6a21993a20983a20993a6a219a3a20993a209a3a6a219b3a209a3a20"
-    "9b3a6a219c3a209b3a209c3a6a219d3a209c3a209d3a6a219e3a209d3a209e3a6a219f3a209e3a209f3a6a21a03a20"
-    "9f3a20a03a6a21a13a20a03a20a13a6a21a23a20a13a20a23a6a21a33a20a23a20a33a6a21a43a20a33a20a43a6a21"
-    "a53a20a43a20a53a6a21a63a20a53a20a63a6a21a73a20a63a20a73a6a21a83a20a73a20a83a6a21a93a20a83a20a9"
-    "3a6a21aa3a20a93a20aa3a6a21ab3a20aa3a20ab3a6a21ac3a20ab3a20ac3a6a21ad3a20ac3a20ad3a6a21ae3a20ad"
-    "3a20ae3a6a21af3a20ae3a20af3a6a21b03a20af3a20b03a6a21b13a20b03a20b13a6a21b23a20b13a20b23a6a21b3"
-    "3a20b23a20b33a6a21b43a20b33a20b43a6a21b53a20b43a20b53a6a21b63a20b53a20b63a6a21b73a20b63a20b73a"
-    "6a21b83a20b73a20b83a6a21b93a20b83a20b93a6a21ba3a20b93a20ba3a6a21bb3a20ba3a20bb3a6a21bc3a20bb3a"
-    "20bc3a6a21bd3a20bc3a20bd3a6a21be3a20bd3a20be3a6a21bf3a20be3a20bf3a6a21c03a20bf3a20c03a6a21c13a"
-    "20c03a20c13a6a21c23a20c13a20c23a6a21c33a20c23a20c33a6a21c43a20c33a20c43a6a21c53a20c43a20c53a6a"
-    "21c63a20c53a20c63a6a21c73a20c63a20c73a6a21c83a20c73a20c83a6a21c93a20c83a20c93a6a21ca3a20c93a20"
-    "ca3a6a21cb3a20ca3a20cb3a6a21cc3a20cb3a20cc3a6a21cd3a20cc3a20cd3a6a21ce3a20cd3a20ce3a6a21cf3a20"
-    "ce3a20cf3a6a21d03a20cf3a20d03a6a21d13a20d03a20d13a6a21d23a20d13a20d23a6a21d33a20d23a20d33a6a21"
-    "d43a20d33a20d43a6a21d53a20d43a20d53a6a21d63a20d53a20d63a6a21d73a20d63a20d73a6a21d83a20d73a20d8"
-    "3a6a21d93a20d83a20d93a6a21da3a20d93a20da3a6a21db3a20da3a20db3a6a21dc3a20db3a20dc3a6a21dd3a20dc"
-    "3a20dd3a6a21de3a20dd3a20de3a6a21df3a20de3a20df3a6a21e03a20df3a20e03a6a21e13a20e03a20e13a6a21e2"
-    "3a20e13a20e23a6a21e33a20e23a20e33a6a21e43a20e33a20e43a6a21e53a20e43a20e53a6a21e63a20e53a20e63a"
-    "6a21e73a20e63a20e73a6a21e83a20e73a20e83a6a21e93a20e83a20e93a6a21ea3a20e93a20ea3a6a21eb3a20ea3a"
-    "20eb3a6a21ec3a20eb3a20ec3a6a21ed3a20ec3a20ed3a6a21ee3a20ed3a20ee3a6a21ef3a20ee3a20ef3a6a21f03a"
-    "20ef3a20f03a6a21f13a20f03a20f13a6a21f23a20f13a20f23a6a21f33a20f23a20f33a6a21f43a20f33a20f43a6a"
-    "21f53a20f43a20f53a6a21f63a20f53a20f63a6a21f73a20f63a20f73a6a21f83a20f73a20f83a6a21f93a20f83a20"
-    "f93a6a21fa3a20f93a20fa3a6a21fb3a20fa3a20fb3a6a21fc3a20fb3a20fc3a6a21fd3a20fc3a20fd3a6a21fe3a20"
-    "fd3a20fe3a6a21ff3a20fe3a20ff3a6a21803b20ff3a20803b6a21813b20803b20813b6a21823b20813b20823b6a21"
-    "833b20823b20833b6a21843b20833b20843b6a21853b20843b20853b6a21863b20853b20863b6a21873b20863b2087"
-    "3b6a21883b20873b20883b6a21893b20883b20893b6a218a3b20893b208a3b6a218b3b208a3b208b3b6a218c3b208b"
-    "3b208c3b6a218d3b208c3b208d3b6a218e3b208d3b208e3b6a218f3b208e3b208f3b6a21903b208f3b20903b6a2191"
-    "3b20903b20913b6a21923b20913b20923b6a21933b20923b20933b6a21943b20933b20943b6a21953b20943b20953b"
-    "6a21963b20953b20963b6a21973b20963b20973b6a21983b20973b20983b6a21993b20983b20993b6a219a3b20993b"
-    "209a3b6a219b3b209a3b209b3b6a219c3b209b3b209c3b6a219d3b209c3b209d3b6a219e3b209d3b209e3b6a219f3b"
-    "209e3b209f3b6a21a03b209f3b20a03b6a21a13b20a03b20a13b6a21a23b20a13b20a23b6a21a33b20a23b20a33b6a"
-    "21a43b20a33b20a43b6a21a53b20a43b20a53b6a21a63b20a53b20a63b6a21a73b20a63b20a73b6a21a83b20a73b20"
-    "a83b6a21a93b20a83b20a93b6a21aa3b20a93b20aa3b6a21ab3b20aa3b20ab3b6a21ac3b20ab3b20ac3b6a21ad3b20"
-    "ac3b20ad3b6a21ae3b20ad3b20ae3b6a21af3b20ae3b20af3b6a21b03b20af3b20b03b6a21b13b20b03b20b13b6a21"
-    "b23b20b13b20b23b6a21b33b20b23b20b33b6a21b43b20b33b20b43b6a21b53b20b43b20b53b6a21b63b20b53b20b6"
-    "3b6a21b73b20b63b20b73b6a21b83b20b73b20b83b6a21b93b20b83b20b93b6a21ba3b20b93b20ba3b6a21bb3b20ba"
-    "3b20bb3b6a21bc3b20bb3b20bc3b6a21bd3b20bc3b20bd3b6a21be3b20bd3b20be3b6a21bf3b20be3b20bf3b6a21c0"
-    "3b20bf3b20c03b6a21c13b20c03b20c13b6a21c23b20c13b20c23b6a21c33b20c23b20c33b6a21c43b20c33b20c43b"
-    "6a21c53b20c43b20c53b6a21c63b20c53b20c63b6a21c73b20c63b20c73b6a21c83b20c73b20c83b6a21c93b20c83b"
-    "20c93b6a21ca3b20c93b20ca3b6a21cb3b20ca3b20cb3b6a21cc3b20cb3b20cc3b6a21cd3b20cc3b20cd3b6a21ce3b"
-    "20cd3b20ce3b6a21cf3b20ce3b20cf3b6a21d03b20cf3b20d03b6a21d13b20d03b20d13b6a21d23b20d13b20d23b6a"
-    "21d33b20d23b20d33b6a21d43b20d33b20d43b6a21d53b20d43b20d53b6a21d63b20d53b20d63b6a21d73b20d63b20"
-    "d73b6a21d83b20d73b20d83b6a21d93b20d83b20d93b6a21da3b20d93b20da3b6a21db3b20da3b20db3b6a21dc3b20"
-    "db3b20dc3b6a21dd3b20dc3b20dd3b6a21de3b20dd3b20de3b6a21df3b20de3b20df3b6a21e03b20df3b20e03b6a21"
-    "e13b20e03b20e13b6a21e23b20e13b20e23b6a21e33b20e23b20e33b6a21e43b20e33b20e43b6a21e53b20e43b20e5"
-    "3b6a21e63b20e53b20e63b6a21e73b20e63b20e73b6a21e83b20e73b20e83b6a21e93b20e83b20e93b6a21ea3b20e9"
-    "3b20ea3b6a21eb3b20ea3b20eb3b6a21ec3b20eb3b20ec3b6a21ed3b20ec3b20ed3b6a21ee3b20ed3b20ee3b6a21ef"
-    "3b20ee3b20ef3b6a21f03b20ef3b20f03b6a21f13b20f03b20f13b6a21f23b20f13b20f23b6a21f33b20f23b20f33b"
-    "6a21f43b20f33b20f43b6a21f53b20f43b20f53b6a21f63b20f53b20f63b6a21f73b20f63b20f73b6a21f83b20f73b"
-    "20f83b6a21f93b20f83b20f93b6a21fa3b20f93b20fa3b6a21fb3b20fa3b20fb3b6a21fc3b20fb3b20fc3b6a21fd3b"
-    "20fc3b20fd3b6a21fe3b20fd3b20fe3b6a21ff3b20fe3b20ff3b6a21803c20ff3b20803c6a21813c20803c20813c6a"
-    "21823c20813c20823c6a21833c20823c20833c6a21843c20833c20843c6a21853c20843c20853c6a21863c20853c20"
-    "863c6a21873c20863c20873c6a21883c20873c20883c6a21893c20883c20893c6a218a3c20893c208a3c6a218b3c20"
-    "8a3c208b3c6a218c3c208b3c208c3c6a218d3c208c3c208d3c6a218e3c208d3c208e3c6a218f3c208e3c208f3c6a21"
-    "903c208f3c20903c6a21913c20903c20913c6a21923c20913c20923c6a21933c20923c20933c6a21943c20933c2094"
-    "3c6a21953c20943c20953c6a21963c20953c20963c6a21973c20963c20973c6a21983c20973c20983c6a21993c2098"
-    "3c20993c6a219a3c20993c209a3c6a219b3c209a3c209b3c6a219c3c209b3c209c3c6a219d3c209c3c209d3c6a219e"
-    "3c209d3c209e3c6a219f3c209e3c209f3c6a21a03c209f3c20a03c6a21a13c20a03c20a13c6a21a23c20a13c20a23c"
-    "6a21a33c20a23c20a33c6a21a43c20a33c20a43c6a21a53c20a43c20a53c6a21a63c20a53c20a63c6a21a73c20a63c"
-    "20a73c6a21a83c20a73c20a83c6a21a93c20a83c20a93c6a21aa3c20a93c20aa3c6a21ab3c20aa3c20ab3c6a21ac3c"
-    "20ab3c20ac3c6a21ad3c20ac3c20ad3c6a21ae3c20ad3c20ae3c6a21af3c20ae3c20af3c6a21b03c20af3c20b03c6a"
-    "21b13c20b03c20b13c6a21b23c20b13c20b23c6a21b33c20b23c20b33c6a21b43c20b33c20b43c6a21b53c20b43c20"
-    "b53c6a21b63c20b53c20b63c6a21b73c20b63c20b73c6a21b83c20b73c20b83c6a21b93c20b83c20b93c6a21ba3c20"
-    "b93c20ba3c6a21bb3c20ba3c20bb3c6a21bc3c20bb3c20bc3c6a21bd3c20bc3c20bd3c6a21be3c20bd3c20be3c6a21"
-    "bf3c20be3c20bf3c6a21c03c20bf3c20c03c6a21c13c20c03c20c13c6a21c23c20c13c20c23c6a21c33c20c23c20c3"
-    "3c6a21c43c20c33c20c43c6a21c53c20c43c20c53c6a21c63c20c53c20c63c6a21c73c20c63c20c73c6a21c83c20c7"
-    "3c20c83c6a21c93c20c83c20c93c6a21ca3c20c93c20ca3c6a21cb3c20ca3c20cb3c6a21cc3c20cb3c20cc3c6a21cd"
-    "3c20cc3c20cd3c6a21ce3c20cd3c20ce3c6a21cf3c20ce3c20cf3c6a21d03c20cf3c20d03c6a21d13c20d03c20d13c"
-    "6a21d23c20d13c20d23c6a21d33c20d23c20d33c6a21d43c20d33c20d43c6a21d53c20d43c20d53c6a21d63c20d53c"
-    "20d63c6a21d73c20d63c20d73c6a21d83c20d73c20d83c6a21d93c20d83c20d93c6a21da3c20d93c20da3c6a21db3c"
-    "20da3c20db3c6a21dc3c20db3c20dc3c6a21dd3c20dc3c20dd3c6a21de3c20dd3c20de3c6a21df3c20de3c20df3c6a"
-    "21e03c20df3c20e03c6a21e13c20e03c20e13c6a21e23c20e13c20e23c6a21e33c20e23c20e33c6a21e43c20e33c20"
-    "e43c6a21e53c20e43c20e53c6a21e63c20e53c20e63c6a21e73c20e63c20e73c6a21e83c20e73c20e83c6a21e93c20"
-    "e83c20e93c6a21ea3c20e93c20ea3c6a21eb3c20ea3c20eb3c6a21ec3c20eb3c20ec3c6a21ed3c20ec3c20ed3c6a21"
-    "ee3c20ed3c20ee3c6a21ef3c20ee3c20ef3c6a21f03c20ef3c20f03c6a21f13c20f03c20f13c6a21f23c20f13c20f2"
-    "3c6a21f33c20f23c20f33c6a21f43c20f33c20f43c6a21f53c20f43c20f53c6a21f63c20f53c20f63c6a21f73c20f6"
-    "3c20f73c6a21f83c20f73c20f83c6a21f93c20f83c20f93c6a21fa3c20f93c20fa3c6a21fb3c20fa3c20fb3c6a21fc"
-    "3c20fb3c20fc3c6a21fd3c20fc3c20fd3c6a21fe3c20fd3c20fe3c6a21ff3c20fe3c20ff3c6a21803d20ff3c20803d"
-    "6a21813d20803d20813d6a21823d20813d20823d6a21833d20823d20833d6a21843d20833d20843d6a21853d20843d"
-    "20853d6a21863d20853d20863d6a21873d20863d20873d6a21883d20873d20883d6a21893d20883d20893d6a218a3d"
-    "20893d208a3d6a218b3d208a3d208b3d6a218c3d208b3d208c3d6a218d3d208c3d208d3d6a218e3d208d3d208e3d6a"
-    "218f3d208e3d208f3d6a21903d208f3d20903d6a21913d20903d20913d6a21923d20913d20923d6a21933d20923d20"
-    "933d6a21943d20933d20943d6a21953d20943d20953d6a21963d20953d20963d6a21973d20963d20973d6a21983d20"
-    "973d20983d6a21993d20983d20993d6a219a3d20993d209a3d6a219b3d209a3d209b3d6a219c3d209b3d209c3d6a21"
-    "9d3d209c3d209d3d6a219e3d209d3d209e3d6a219f3d209e3d209f3d6a21a03d209f3d20a03d6a21a13d20a03d20a1"
-    "3d6a21a23d20a13d20a23d6a21a33d20a23d20a33d6a21a43d20a33d20a43d6a21a53d20a43d20a53d6a21a63d20a5"
-    "3d20a63d6a21a73d20a63d20a73d6a21a83d20a73d20a83d6a21a93d20a83d20a93d6a21aa3d20a93d20aa3d6a21ab"
-    "3d20aa3d20ab3d6a21ac3d20ab3d20ac3d6a21ad3d20ac3d20ad3d6a21ae3d20ad3d20ae3d6a21af3d20ae3d20af3d"
-    "6a21b03d20af3d20b03d6a21b13d20b03d20b13d6a21b23d20b13d20b23d6a21b33d20b23d20b33d6a21b43d20b33d"
-    "20b43d6a21b53d20b43d20b53d6a21b63d20b53d20b63d6a21b73d20b63d20b73d6a21b83d20b73d20b83d6a21b93d"
-    "20b83d20b93d6a21ba3d20b93d20ba3d6a21bb3d20ba3d20bb3d6a21bc3d20bb3d20bc3d6a21bd3d20bc3d20bd3d6a"
-    "21be3d20bd3d20be3d6a21bf3d20be3d20bf3d6a21c03d20bf3d20c03d6a21c13d20c03d20c13d6a21c23d20c13d20"
-    "c23d6a21c33d20c23d20c33d6a21c43d20c33d20c43d6a21c53d20c43d20c53d6a21c63d20c53d20c63d6a21c73d20"
-    "c63d20c73d6a21c83d20c73d20c83d6a21c93d20c83d20c93d6a21ca3d20c93d20ca3d6a21cb3d20ca3d20cb3d6a21"
-    "cc3d20cb3d20cc3d6a21cd3d20cc3d20cd3d6a21ce3d20cd3d20ce3d6a21cf3d20ce3d20cf3d6a21d03d20cf3d20d0"
-    "3d6a21d13d20d03d20d13d6a21d23d20d13d20d23d6a21d33d20d23d20d33d6a21d43d20d33d20d43d6a21d53d20d4"
-    "3d20d53d6a21d63d20d53d20d63d6a21d73d20d63d20d73d6a21d83d20d73d20d83d6a21d93d20d83d20d93d6a21da"
-    "3d20d93d20da3d6a21db3d20da3d20db3d6a21dc3d20db3d20dc3d6a21dd3d20dc3d20dd3d6a21de3d20dd3d20de3d"
-    "6a21df3d20de3d20df3d6a21e03d20df3d20e03d6a21e13d20e03d20e13d6a21e23d20e13d20e23d6a21e33d20e23d"
-    "20e33d6a21e43d20e33d20e43d6a21e53d20e43d20e53d6a21e63d20e53d20e63d6a21e73d20e63d20e73d6a21e83d"
-    "20e73d20e83d6a21e93d20e83d20e93d6a21ea3d20e93d20ea3d6a21eb3d20ea3d20eb3d6a21ec3d20eb3d20ec3d6a"
-    "21ed3d20ec3d20ed3d6a21ee3d20ed3d20ee3d6a21ef3d20ee3d20ef3d6a21f03d20ef3d20f03d6a21f13d20f03d20"
-    "f13d6a21f23d20f13d20f23d6a21f33d20f23d20f33d6a21f43d20f33d20f43d6a21f53d20f43d20f53d6a21f63d20"
-    "f53d20f63d6a21f73d20f63d20f73d6a21f83d20f73d20f83d6a21f93d20f83d20f93d6a21fa3d20f93d20fa3d6a21"
-    "fb3d20fa3d20fb3d6a21fc3d20fb3d20fc3d6a21fd3d20fc3d20fd3d6a21fe3d20fd3d20fe3d6a21ff3d20fe3d20ff"
-    "3d6a21803e20ff3d20803e6a21813e20803e20813e6a21823e20813e20823e6a21833e20823e20833e6a21843e2083"
-    "3e20843e6a21853e20843e20853e6a21863e20853e20863e6a21873e20863e20873e6a21883e20873e20883e6a2189"
-    "3e20883e20893e6a218a3e20893e208a3e6a218b3e208a3e208b3e6a218c3e208b3e208c3e6a218d3e208c3e208d3e"
-    "6a218e3e208d3e208e3e6a218f3e208e3e208f3e6a21903e208f3e20903e6a21913e20903e20913e6a21923e20913e"
-    "20923e6a21933e20923e20933e6a21943e20933e20943e6a21953e20943e20953e6a21963e20953e20963e6a21973e"
-    "20963e20973e6a21983e20973e20983e6a21993e20983e20993e6a219a3e20993e209a3e6a219b3e209a3e209b3e6a"
-    "219c3e209b3e209c3e6a219d3e209c3e209d3e6a219e3e209d3e209e3e6a219f3e209e3e209f3e6a21a03e209f3e20"
-    "a03e6a21a13e20a03e20a13e6a21a23e20a13e20a23e6a21a33e20a23e20a33e6a21a43e20a33e20a43e6a21a53e20"
-    "a43e20a53e6a21a63e20a53e20a63e6a21a73e20a63e20a73e6a21a83e20a73e20a83e6a21a93e20a83e20a93e6a21"
-    "aa3e20a93e20aa3e6a21ab3e20aa3e20ab3e6a21ac3e20ab3e20ac3e6a21ad3e20ac3e20ad3e6a21ae3e20ad3e20ae"
-    "3e6a21af3e20ae3e20af3e6a21b03e20af3e20b03e6a21b13e20b03e20b13e6a21b23e20b13e20b23e6a21b33e20b2"
-    "3e20b33e6a21b43e20b33e20b43e6a21b53e20b43e20b53e6a21b63e20b53e20b63e6a21b73e20b63e20b73e6a21b8"
-    "3e20b73e20b83e6a21b93e20b83e20b93e6a21ba3e20b93e20ba3e6a21bb3e20ba3e20bb3e6a21bc3e20bb3e20bc3e"
-    "6a21bd3e20bc3e20bd3e6a21be3e20bd3e20be3e6a21bf3e20be3e20bf3e6a21c03e20bf3e20c03e6a21c13e20c03e"
-    "20c13e6a21c23e20c13e20c23e6a21c33e20c23e20c33e6a21c43e20c33e20c43e6a21c53e20c43e20c53e6a21c63e"
-    "20c53e20c63e6a21c73e20c63e20c73e6a21c83e20c73e20c83e6a21c93e20c83e20c93e6a21ca3e20c93e20ca3e6a"
-    "21cb3e20ca3e20cb3e6a21cc3e20cb3e20cc3e6a21cd3e20cc3e20cd3e6a21ce3e20cd3e20ce3e6a21cf3e20ce3e20"
-    "cf3e6a21d03e20cf3e20d03e6a21d13e20d03e20d13e6a21d23e20d13e20d23e6a21d33e20d23e20d33e6a21d43e20"
-    "d33e20d43e6a21d53e20d43e20d53e6a21d63e20d53e20d63e6a21d73e20d63e20d73e6a21d83e20d73e20d83e6a21"
-    "d93e20d83e20d93e6a21da3e20d93e20da3e6a21db3e20da3e20db3e6a21dc3e20db3e20dc3e6a21dd3e20dc3e20dd"
-    "3e6a21de3e20dd3e20de3e6a21df3e20de3e20df3e6a21e03e20df3e20e03e6a21e13e20e03e20e13e6a21e23e20e1"
-    "3e20e23e6a21e33e20e23e20e33e6a21e43e20e33e20e43e6a21e53e20e43e20e53e6a21e63e20e53e20e63e6a21e7"
-    "3e20e63e20e73e6a21e83e20e73e20e83e6a21e93e20e83e20e93e6a21ea3e20e93e20ea3e6a21eb3e20ea3e20eb3e"
-    "6a21ec3e20eb3e20ec3e6a21ed3e20ec3e20ed3e6a21ee3e20ed3e20ee3e6a21ef3e20ee3e20ef3e6a21f03e20ef3e"
-    "20f03e6a21f13e20f03e20f13e6a21f23e20f13e20f23e6a21f33e20f23e20f33e6a21f43e20f33e20f43e6a21f53e"
-    "20f43e20f53e6a21f63e20f53e20f63e6a21f73e20f63e20f73e6a21f83e20f73e20f83e6a21f93e20f83e20f93e6a"
-    "21fa3e20f93e20fa3e6a21fb3e20fa3e20fb3e6a21fc3e20fb3e20fc3e6a21fd3e20fc3e20fd3e6a21fe3e20fd3e20"
-    "fe3e6a21ff3e20fe3e20ff3e6a21803f20ff3e20803f6a21813f20803f20813f6a21823f20813f20823f6a21833f20"
-    "823f20833f6a21843f20833f20843f6a21853f20843f20853f6a21863f20853f20863f6a21873f20863f20873f6a21"
-    "883f20873f20883f6a21893f20883f20893f6a218a3f20893f208a3f6a218b3f208a3f208b3f6a218c3f208b3f208c"
-    "3f6a218d3f208c3f208d3f6a218e3f208d3f208e3f6a218f3f208e3f208f3f6a21903f208f3f20903f6a21913f2090"
-    "3f20913f6a21923f20913f20923f6a21933f20923f20933f6a21943f20933f20943f6a21953f20943f20953f6a2196"
-    "3f20953f20963f6a21973f20963f20973f6a21983f20973f20983f6a21993f20983f20993f6a219a3f20993f209a3f"
-    "6a219b3f209a3f209b3f6a219c3f209b3f209c3f6a219d3f209c3f209d3f6a219e3f209d3f209e3f6a219f3f209e3f"
-    "209f3f6a21a03f209f3f20a03f6a21a13f20a03f20a13f6a21a23f20a13f20a23f6a21a33f20a23f20a33f6a21a43f"
-    "20a33f20a43f6a21a53f20a43f20a53f6a21a63f20a53f20a63f6a21a73f20a63f20a73f6a21a83f20a73f20a83f6a"
-    "21a93f20a83f20a93f6a21aa3f20a93f20aa3f6a21ab3f20aa3f20ab3f6a21ac3f20ab3f20ac3f6a21ad3f20ac3f20"
-    "ad3f6a21ae3f20ad3f20ae3f6a21af3f20ae3f20af3f6a21b03f20af3f20b03f6a21b13f20b03f20b13f6a21b23f20"
-    "b13f20b23f6a21b33f20b23f20b33f6a21b43f20b33f20b43f6a21b53f20b43f20b53f6a21b63f20b53f20b63f6a21"
-    "b73f20b63f20b73f6a21b83f20b73f20b83f6a21b93f20b83f20b93f6a21ba3f20b93f20ba3f6a21bb3f20ba3f20bb"
-    "3f6a21bc3f20bb3f20bc3f6a21bd3f20bc3f20bd3f6a21be3f20bd3f20be3f6a21bf3f20be3f20bf3f6a21c03f20bf"
-    "3f20c03f6a21c13f20c03f20c13f6a21c23f20c13f20c23f6a21c33f20c23f20c33f6a21c43f20c33f20c43f6a21c5"
-    "3f20c43f20c53f6a21c63f20c53f20c63f6a21c73f20c63f20c73f6a21c83f20c73f20c83f6a21c93f20c83f20c93f"
-    "6a21ca3f20c93f20ca3f6a21cb3f20ca3f20cb3f6a21cc3f20cb3f20cc3f6a21cd3f20cc3f20cd3f6a21ce3f20cd3f"
-    "20ce3f6a21cf3f20ce3f20cf3f6a21d03f20cf3f20d03f6a21d13f20d03f20d13f6a21d23f20d13f20d23f6a21d33f"
-    "20d23f20d33f6a21d43f20d33f20d43f6a21d53f20d43f20d53f6a21d63f20d53f20d63f6a21d73f20d63f20d73f6a"
-    "21d83f20d73f20d83f6a21d93f20d83f20d93f6a21da3f20d93f20da3f6a21db3f20da3f20db3f6a21dc3f20db3f20"
-    "dc3f6a21dd3f20dc3f20dd3f6a21de3f20dd3f20de3f6a21df3f20de3f20df3f6a21e03f20df3f20e03f6a21e13f20"
-    "e03f20e13f6a21e23f20e13f20e23f6a21e33f20e23f20e33f6a21e43f20e33f20e43f6a21e53f20e43f20e53f6a21"
-    "e63f20e53f20e63f6a21e73f20e63f20e73f6a21e83f20e73f20e83f6a21e93f20e83f20e93f6a21ea3f20e93f20ea"
-    "3f6a21eb3f20ea3f20eb3f6a21ec3f20eb3f20ec3f6a21ed3f20ec3f20ed3f6a21ee3f20ed3f20ee3f6a21ef3f20ee"
-    "3f20ef3f6a21f03f20ef3f20f03f6a21f13f20f03f20f13f6a21f23f20f13f20f23f6a21f33f20f23f20f33f6a21f4"
-    "3f20f33f20f43f6a21f53f20f43f20f53f6a21f63f20f53f20f63f6a21f73f20f63f20f73f6a21f83f20f73f20f83f"
-    "6a21f93f20f83f20f93f6a21fa3f20f93f20fa3f6a21fb3f20fa3f20fb3f6a21fc3f20fb3f20fc3f6a21fd3f20fc3f"
-    "20fd3f6a21fe3f20fd3f20fe3f6a21ff3f20fe3f20ff3f6a21804020ff3f2080406a2181402080402081406a218240"
-    "2081402082406a2183402082402083406a2184402083402084406a2185402084402085406a2186402085402086406a"
-    "2187402086402087406a2188402087402088406a2189402088402089406a218a40208940208a406a218b40208a4020"
-    "8b406a218c40208b40208c406a218d40208c40208d406a218e40208d40208e406a218f40208e40208f406a21904020"
-    "8f402090406a2191402090402091406a2192402091402092406a2193402092402093406a2194402093402094406a21"
-    "95402094402095406a2196402095402096406a2197402096402097406a2198402097402098406a2199402098402099"
-    "406a219a40209940209a406a219b40209a40209b406a219c40209b40209c406a219d40209c40209d406a219e40209d"
-    "40209e406a219f40209e40209f406a21a040209f4020a0406a21a14020a04020a1406a21a24020a14020a2406a21a3"
-    "4020a24020a3406a21a44020a34020a4406a21a54020a44020a5406a21a64020a54020a6406a21a74020a64020a740"
-    "6a21a84020a74020a8406a21a94020a84020a9406a21aa4020a94020aa406a21ab4020aa4020ab406a21ac4020ab40"
-    "20ac406a21ad4020ac4020ad406a21ae4020ad4020ae406a21af4020ae4020af406a21b04020af4020b0406a21b140"
-    "20b04020b1406a21b24020b14020b2406a21b34020b24020b3406a21b44020b34020b4406a21b54020b44020b5406a"
-    "21b64020b54020b6406a21b74020b64020b7406a21b84020b74020b8406a21b94020b84020b9406a21ba4020b94020"
-    "ba406a21bb4020ba4020bb406a21bc4020bb4020bc406a21bd4020bc4020bd406a21be4020bd4020be406a21bf4020"
-    "be4020bf406a21c04020bf4020c0406a21c14020c04020c1406a21c24020c14020c2406a21c34020c24020c3406a21"
-    "c44020c34020c4406a21c54020c44020c5406a21c64020c54020c6406a21c74020c64020c7406a21c84020c74020c8"
-    "406a21c94020c84020c9406a21ca4020c94020ca406a21cb4020ca4020cb406a21cc4020cb4020cc406a21cd4020cc"
-    "4020cd406a21ce4020cd4020ce406a21cf4020ce4020cf406a21d04020cf4020d0406a21d14020d04020d1406a21d2"
-    "4020d14020d2406a21d34020d24020d3406a21d44020d34020d4406a21d54020d44020d5406a21d64020d54020d640"
-    "6a21d74020d64020d7406a21d84020d74020d8406a21d94020d84020d9406a21da4020d94020da406a21db4020da40"
-    "20db406a21dc4020db4020dc406a21dd4020dc4020dd406a21de4020dd4020de406a21df4020de4020df406a21e040"
-    "20df4020e0406a21e14020e04020e1406a21e24020e14020e2406a21e34020e24020e3406a21e44020e34020e4406a"
-    "21e54020e44020e5406a21e64020e54020e6406a21e74020e64020e7406a21e84020e74020e8406a21e94020e84020"
-    "e9406a21ea4020e94020ea406a21eb4020ea4020eb406a21ec4020eb4020ec406a21ed4020ec4020ed406a21ee4020"
-    "ed4020ee406a21ef4020ee4020ef406a21f04020ef4020f0406a21f14020f04020f1406a21f24020f14020f2406a21"
-    "f34020f24020f3406a21f44020f34020f4406a21f54020f44020f5406a21f64020f54020f6406a21f74020f64020f7"
-    "406a21f84020f74020f8406a21f94020f84020f9406a21fa4020f94020fa406a21fb4020fa4020fb406a21fc4020fb"
-    "4020fc406a21fd4020fc4020fd406a21fe4020fd4020fe406a21ff4020fe4020ff406a21804120ff402080416a2181"
-    "412080412081416a2182412081412082416a2183412082412083416a2184412083412084416a218541208441208541"
-    "6a2186412085412086416a2187412086412087416a2188412087412088416a2189412088412089416a218a41208941"
-    "208a416a218b41208a41208b416a218c41208b41208c416a218d41208c41208d416a218e41208d41208e416a218f41"
-    "208e41208f416a219041208f412090416a2191412090412091416a2192412091412092416a2193412092412093416a"
-    "2194412093412094416a2195412094412095416a2196412095412096416a2197412096412097416a21984120974120"
-    "98416a2199412098412099416a219a41209941209a416a219b41209a41209b416a219c41209b41209c416a219d4120"
-    "9c41209d416a219e41209d41209e416a219f41209e41209f416a21a041209f4120a0416a21a14120a04120a1416a21"
-    "a24120a14120a2416a21a34120a24120a3416a21a44120a34120a4416a21a54120a44120a5416a21a64120a54120a6"
-    "416a21a74120a64120a7416a21a84120a74120a8416a21a94120a84120a9416a21aa4120a94120aa416a21ab4120aa"
-    "4120ab416a21ac4120ab4120ac416a21ad4120ac4120ad416a21ae4120ad4120ae416a21af4120ae4120af416a21b0"
-    "4120af4120b0416a21b14120b04120b1416a21b24120b14120b2416a21b34120b24120b3416a21b44120b34120b441"
-    "6a21b54120b44120b5416a21b64120b54120b6416a21b74120b64120b7416a21b84120b74120b8416a21b94120b841"
-    "20b9416a21ba4120b94120ba416a21bb4120ba4120bb416a21bc4120bb4120bc416a21bd4120bc4120bd416a21be41"
-    "20bd4120be416a21bf4120be4120bf416a21c04120bf4120c0416a21c14120c04120c1416a21c24120c14120c2416a"
-    "21c34120c24120c3416a21c44120c34120c4416a21c54120c44120c5416a21c64120c54120c6416a21c74120c64120"
-    "c7416a21c84120c74120c8416a21c94120c84120c9416a21ca4120c94120ca416a21cb4120ca4120cb416a21cc4120"
-    "cb4120cc416a21cd4120cc4120cd416a21ce4120cd4120ce416a21cf4120ce4120cf416a21d04120cf4120d0416a21"
-    "d14120d04120d1416a21d24120d14120d2416a21d34120d24120d3416a21d44120d34120d4416a21d54120d44120d5"
-    "416a21d64120d54120d6416a21d74120d64120d7416a21d84120d74120d8416a21d94120d84120d9416a21da4120d9"
-    "4120da416a21db4120da4120db416a21dc4120db4120dc416a21dd4120dc4120dd416a21de4120dd4120de416a21df"
-    "4120de4120df416a21e04120df4120e0416a21e14120e04120e1416a21e24120e14120e2416a21e34120e24120e341"
-    "6a21e44120e34120e4416a21e54120e44120e5416a21e64120e54120e6416a21e74120e64120e7416a21e84120e741"
-    "20e8416a21e94120e84120e9416a21ea4120e94120ea416a21eb4120ea4120eb416a21ec4120eb4120ec416a21ed41"
-    "20ec4120ed416a21ee4120ed4120ee416a21ef4120ee4120ef416a21f04120ef4120f0416a21f14120f04120f1416a"
-    "21f24120f14120f2416a21f34120f24120f3416a21f44120f34120f4416a21f54120f44120f5416a21f64120f54120"
-    "f6416a21f74120f64120f7416a21f84120f74120f8416a21f94120f84120f9416a21fa4120f94120fa416a21fb4120"
-    "fa4120fb416a21fc4120fb4120fc416a21fd4120fc4120fd416a21fe4120fd4120fe416a21ff4120fe4120ff416a21"
-    "804220ff412080426a2181422080422081426a2182422081422082426a2183422082422083426a2184422083422084"
-    "426a2185422084422085426a2186422085422086426a2187422086422087426a2188422087422088426a2189422088"
-    "422089426a218a42208942208a426a218b42208a42208b426a218c42208b42208c426a218d42208c42208d426a218e"
-    "42208d42208e426a218f42208e42208f426a219042208f422090426a2191422090422091426a219242209142209242"
-    "6a2193422092422093426a2194422093422094426a2195422094422095426a2196422095422096426a219742209642"
-    "2097426a2198422097422098426a2199422098422099426a219a42209942209a426a219b42209a42209b426a219c42"
-    "209b42209c426a219d42209c42209d426a219e42209d42209e426a219f42209e42209f426a21a042209f4220a0426a"
-    "21a14220a04220a1426a21a24220a14220a2426a21a34220a24220a3426a21a44220a34220a4426a21a54220a44220"
-    "a5426a21a64220a54220a6426a21a74220a64220a7426a21a84220a74220a8426a21a94220a84220a9426a21aa4220"
-    "a94220aa426a21ab4220aa4220ab426a21ac4220ab4220ac426a21ad4220ac4220ad426a21ae4220ad4220ae426a21"
-    "af4220ae4220af426a21b04220af4220b0426a21b14220b04220b1426a21b24220b14220b2426a21b34220b24220b3"
-    "426a21b44220b34220b4426a21b54220b44220b5426a21b64220b54220b6426a21b74220b64220b7426a21b84220b7"
-    "4220b8426a21b94220b84220b9426a21ba4220b94220ba426a21bb4220ba4220bb426a21bc4220bb4220bc426a21bd"
-    "4220bc4220bd426a21be4220bd4220be426a21bf4220be4220bf426a21c04220bf4220c0426a21c14220c04220c142"
-    "6a21c24220c14220c2426a21c34220c24220c3426a21c44220c34220c4426a21c54220c44220c5426a21c64220c542"
-    "20c6426a21c74220c64220c7426a21c84220c74220c8426a21c94220c84220c9426a21ca4220c94220ca426a21cb42"
-    "20ca4220cb426a21cc4220cb4220cc426a21cd4220cc4220cd426a21ce4220cd4220ce426a21cf4220ce4220cf426a"
-    "21d04220cf4220d0426a21d14220d04220d1426a21d24220d14220d2426a21d34220d24220d3426a21d44220d34220"
-    "d4426a21d54220d44220d5426a21d64220d54220d6426a21d74220d64220d7426a21d84220d74220d8426a21d94220"
-    "d84220d9426a21da4220d94220da426a21db4220da4220db426a21dc4220db4220dc426a21dd4220dc4220dd426a21"
-    "de4220dd4220de426a21df4220de4220df426a21e04220df4220e0426a21e14220e04220e1426a21e24220e14220e2"
-    "426a21e34220e24220e3426a21e44220e34220e4426a21e54220e44220e5426a21e64220e54220e6426a21e74220e6"
-    "4220e7426a21e84220e74220e8426a21e94220e84220e9426a21ea4220e94220ea426a21eb4220ea4220eb426a21ec"
-    "4220eb4220ec426a21ed4220ec4220ed426a21ee4220ed4220ee426a21ef4220ee4220ef426a21f04220ef4220f042"
-    "6a21f14220f04220f1426a21f24220f14220f2426a21f34220f24220f3426a21f44220f34220f4426a21f54220f442"
-    "20f5426a21f64220f54220f6426a21f74220f64220f7426a21f84220f74220f8426a21f94220f84220f9426a21fa42"
-    "20f94220fa426a21fb4220fa4220fb426a21fc4220fb4220fc426a21fd4220fc4220fd426a21fe4220fd4220fe426a"
-    "21ff4220fe4220ff426a21804320ff422080436a2181432080432081436a2182432081432082436a21834320824320"
-    "83436a2184432083432084436a2185432084432085436a2186432085432086436a2187432086432087436a21884320"
-    "87432088436a2189432088432089436a218a43208943208a436a218b43208a43208b436a218c43208b43208c436a21"
-    "8d43208c43208d436a218e43208d43208e436a218f43208e43208f436a219043208f432090436a2191432090432091"
-    "436a2192432091432092436a2193432092432093436a2194432093432094436a2195432094432095436a2196432095"
-    "432096436a2197432096432097436a2198432097432098436a2199432098432099436a219a43209943209a436a219b"
-    "43209a43209b436a219c43209b43209c436a219d43209c43209d436a219e43209d43209e436a219f43209e43209f43"
-    "6a21a043209f4320a0436a21a14320a04320a1436a21a24320a14320a2436a21a34320a24320a3436a21a44320a343"
-    "20a4436a21a54320a44320a5436a21a64320a54320a6436a21a74320a64320a7436a21a84320a74320a8436a21a943"
-    "20a84320a9436a21aa4320a94320aa436a21ab4320aa4320ab436a21ac4320ab4320ac436a21ad4320ac4320ad436a"
-    "21ae4320ad4320ae436a21af4320ae4320af436a21b04320af4320b0436a21b14320b04320b1436a21b24320b14320"
-    "b2436a21b34320b24320b3436a21b44320b34320b4436a21b54320b44320b5436a21b64320b54320b6436a21b74320"
-    "b64320b7436a21b84320b74320b8436a21b94320b84320b9436a21ba4320b94320ba436a21bb4320ba4320bb436a21"
-    "bc4320bb4320bc436a21bd4320bc4320bd436a21be4320bd4320be436a21bf4320be4320bf436a21c04320bf4320c0"
-    "436a21c14320c04320c1436a21c24320c14320c2436a21c34320c24320c3436a21c44320c34320c4436a21c54320c4"
-    "4320c5436a21c64320c54320c6436a21c74320c64320c7436a21c84320c74320c8436a21c94320c84320c9436a21ca"
-    "4320c94320ca436a21cb4320ca4320cb436a21cc4320cb4320cc436a21cd4320cc4320cd436a21ce4320cd4320ce43"
-    "6a21cf4320ce4320cf436a21d04320cf4320d0436a21d14320d04320d1436a21d24320d14320d2436a21d34320d243"
-    "20d3436a21d44320d34320d4436a21d54320d44320d5436a21d64320d54320d6436a21d74320d64320d7436a21d843"
-    "20d74320d8436a21d94320d84320d9436a21da4320d94320da436a21db4320da4320db436a21dc4320db4320dc436a"
-    "21dd4320dc4320dd436a21de4320dd4320de436a21df4320de4320df436a21e04320df4320e0436a21e14320e04320"
-    "e1436a21e24320e14320e2436a21e34320e24320e3436a21e44320e34320e4436a21e54320e44320e5436a21e64320"
-    "e54320e6436a21e74320e64320e7436a21e84320e74320e8436a21e94320e84320e9436a21ea4320e94320ea436a21"
-    "eb4320ea4320eb436a21ec4320eb4320ec436a21ed4320ec4320ed436a21ee4320ed4320ee436a21ef4320ee4320ef"
-    "436a21f04320ef4320f0436a21f14320f04320f1436a21f24320f14320f2436a21f34320f24320f3436a21f44320f3"
-    "4320f4436a21f54320f44320f5436a21f64320f54320f6436a21f74320f64320f7436a21f84320f74320f8436a21f9"
-    "4320f84320f9436a21fa4320f94320fa436a21fb4320fa4320fb436a21fc4320fb4320fc436a21fd4320fc4320fd43"
-    "6a21fe4320fd4320fe436a21ff4320fe4320ff436a21804420ff432080446a2181442080442081446a218244208144"
-    "2082446a2183442082442083446a2184442083442084446a2185442084442085446a2186442085442086446a218744"
-    "2086442087446a2188442087442088446a2189442088442089446a218a44208944208a446a218b44208a44208b446a"
-    "218c44208b44208c446a218d44208c44208d446a218e44208d44208e446a218f44208e44208f446a219044208f4420"
-    "90446a2191442090442091446a2192442091442092446a2193442092442093446a2194442093442094446a21954420"
-    "94442095446a2196442095442096446a2197442096442097446a2198442097442098446a2199442098442099446a21"
-    "9a44209944209a446a219b44209a44209b446a219c44209b44209c446a219d44209c44209d446a219e44209d44209e"
-    "446a219f44209e44209f446a21a044209f4420a0446a21a14420a04420a1446a21a24420a14420a2446a21a34420a2"
-    "4420a3446a21a44420a34420a4446a21a54420a44420a5446a21a64420a54420a6446a21a74420a64420a7446a21a8"
-    "4420a74420a8446a21a94420a84420a9446a21aa4420a94420aa446a21ab4420aa4420ab446a21ac4420ab4420ac44"
-    "6a21ad4420ac4420ad446a21ae4420ad4420ae446a21af4420ae4420af446a21b04420af4420b0446a21b14420b044"
-    "20b1446a21b24420b14420b2446a21b34420b24420b3446a21b44420b34420b4446a21b54420b44420b5446a21b644"
-    "20b54420b6446a21b74420b64420b7446a21b84420b74420b8446a21b94420b84420b9446a21ba4420b94420ba446a"
-    "21bb4420ba4420bb446a21bc4420bb4420bc446a21bd4420bc4420bd446a21be4420bd4420be446a21bf4420be4420"
-    "bf446a21c04420bf4420c0446a21c14420c04420c1446a21c24420c14420c2446a21c34420c24420c3446a21c44420"
-    "c34420c4446a21c54420c44420c5446a21c64420c54420c6446a21c74420c64420c7446a21c84420c74420c8446a21"
-    "c94420c84420c9446a21ca4420c94420ca446a21cb4420ca4420cb446a21cc4420cb4420cc446a21cd4420cc4420cd"
-    "446a21ce4420cd4420ce446a21cf4420ce4420cf446a21d04420cf4420d0446a21d14420d04420d1446a21d24420d1"
-    "4420d2446a21d34420d24420d3446a21d44420d34420d4446a21d54420d44420d5446a21d64420d54420d6446a21d7"
-    "4420d64420d7446a21d84420d74420d8446a21d94420d84420d9446a21da4420d94420da446a21db4420da4420db44"
-    "6a21dc4420db4420dc446a21dd4420dc4420dd446a21de4420dd4420de446a21df4420de4420df446a21e04420df44"
-    "20e0446a21e14420e04420e1446a21e24420e14420e2446a21e34420e24420e3446a21e44420e34420e4446a21e544"
-    "20e44420e5446a21e64420e54420e6446a21e74420e64420e7446a21e84420e74420e8446a21e94420e84420e9446a"
-    "21ea4420e94420ea446a21eb4420ea4420eb446a21ec4420eb4420ec446a21ed4420ec4420ed446a21ee4420ed4420"
-    "ee446a21ef4420ee4420ef446a21f04420ef4420f0446a21f14420f04420f1446a21f24420f14420f2446a21f34420"
-    "f24420f3446a21f44420f34420f4446a21f54420f44420f5446a21f64420f54420f6446a21f74420f64420f7446a21"
-    "f84420f74420f8446a21f94420f84420f9446a21fa4420f94420fa446a21fb4420fa4420fb446a21fc4420fb4420fc"
-    "446a21fd4420fc4420fd446a21fe4420fd4420fe446a21ff4420fe4420ff446a21804520ff442080456a2181452080"
-    "452081456a2182452081452082456a2183452082452083456a2184452083452084456a2185452084452085456a2186"
-    "452085452086456a2187452086452087456a2188452087452088456a2189452088452089456a218a45208945208a45"
-    "6a218b45208a45208b456a218c45208b45208c456a218d45208c45208d456a218e45208d45208e456a218f45208e45"
-    "208f456a219045208f452090456a2191452090452091456a2192452091452092456a2193452092452093456a219445"
-    "2093452094456a2195452094452095456a2196452095452096456a2197452096452097456a2198452097452098456a"
-    "2199452098452099456a219a45209945209a456a219b45209a45209b456a219c45209b45209c456a219d45209c4520"
-    "9d456a219e45209d45209e456a219f45209e45209f456a21a045209f4520a0456a21a14520a04520a1456a21a24520"
-    "a14520a2456a21a34520a24520a3456a21a44520a34520a4456a21a54520a44520a5456a21a64520a54520a6456a21"
-    "a74520a64520a7456a21a84520a74520a8456a21a94520a84520a9456a21aa4520a94520aa456a21ab4520aa4520ab"
-    "456a21ac4520ab4520ac456a21ad4520ac4520ad456a21ae4520ad4520ae456a21af4520ae4520af456a21b04520af"
-    "4520b0456a21b14520b04520b1456a21b24520b14520b2456a21b34520b24520b3456a21b44520b34520b4456a21b5"
-    "4520b44520b5456a21b64520b54520b6456a21b74520b64520b7456a21b84520b74520b8456a21b94520b84520b945"
-    "6a21ba4520b94520ba456a21bb4520ba4520bb456a21bc4520bb4520bc456a21bd4520bc4520bd456a21be4520bd45"
-    "20be456a21bf4520be4520bf456a21c04520bf4520c0456a21c14520c04520c1456a21c24520c14520c2456a21c345"
-    "20c24520c3456a21c44520c34520c4456a21c54520c44520c5456a21c64520c54520c6456a21c74520c64520c7456a"
-    "21c84520c74520c8456a21c94520c84520c9456a21ca4520c94520ca456a21cb4520ca4520cb456a21cc4520cb4520"
-    "cc456a21cd4520cc4520cd456a21ce4520cd4520ce456a21cf4520ce4520cf456a21d04520cf4520d0456a21d14520"
-    "d04520d1456a21d24520d14520d2456a21d34520d24520d3456a21d44520d34520d4456a21d54520d44520d5456a21"
-    "d64520d54520d6456a21d74520d64520d7456a21d84520d74520d8456a21d94520d84520d9456a21da4520d94520da"
-    "456a21db4520da4520db456a21dc4520db4520dc456a21dd4520dc4520dd456a21de4520dd4520de456a21df4520de"
-    "4520df456a21e04520df4520e0456a21e14520e04520e1456a21e24520e14520e2456a21e34520e24520e3456a21e4"
-    "4520e34520e4456a21e54520e44520e5456a21e64520e54520e6456a21e74520e64520e7456a21e84520e74520e845"
-    "6a21e94520e84520e9456a21ea4520e94520ea456a21eb4520ea4520eb456a21ec4520eb4520ec456a21ed4520ec45"
-    "20ed456a21ee4520ed4520ee456a21ef4520ee4520ef456a21f04520ef4520f0456a21f14520f04520f1456a21f245"
-    "20f14520f2456a21f34520f24520f3456a21f44520f34520f4456a21f54520f44520f5456a21f64520f54520f6456a"
-    "21f74520f64520f7456a21f84520f74520f8456a21f94520f84520f9456a21fa4520f94520fa456a21fb4520fa4520"
-    "fb456a21fc4520fb4520fc456a21fd4520fc4520fd456a21fe4520fd4520fe456a21ff4520fe4520ff456a21804620"
-    "ff452080466a2181462080462081466a2182462081462082466a2183462082462083466a2184462083462084466a21"
-    "85462084462085466a2186462085462086466a2187462086462087466a2188462087462088466a2189462088462089"
-    "466a218a46208946208a466a218b46208a46208b466a218c46208b46208c466a218d46208c46208d466a218e46208d"
-    "46208e466a218f46208e46208f466a219046208f462090466a2191462090462091466a2192462091462092466a2193"
-    "462092462093466a2194462093462094466a2195462094462095466a2196462095462096466a219746209646209746"
-    "6a2198462097462098466a2199462098462099466a219a46209946209a466a219b46209a46209b466a219c46209b46"
-    "209c466a219d46209c46209d466a219e46209d46209e466a219f46209e46209f466a21a046209f4620a0466a21a146"
-    "20a04620a1466a21a24620a14620a2466a21a34620a24620a3466a21a44620a34620a4466a21a54620a44620a5466a"
-    "21a64620a54620a6466a21a74620a64620a7466a21a84620a74620a8466a21a94620a84620a9466a21aa4620a94620"
-    "aa466a21ab4620aa4620ab466a21ac4620ab4620ac466a21ad4620ac4620ad466a21ae4620ad4620ae466a21af4620"
-    "ae4620af466a21b04620af4620b0466a21b14620b04620b1466a21b24620b14620b2466a21b34620b24620b3466a21"
-    "b44620b34620b4466a21b54620b44620b5466a21b64620b54620b6466a21b74620b64620b7466a21b84620b74620b8"
-    "466a21b94620b84620b9466a21ba4620b94620ba466a21bb4620ba4620bb466a21bc4620bb4620bc466a21bd4620bc"
-    "4620bd466a21be4620bd4620be466a21bf4620be4620bf466a21c04620bf4620c0466a21c14620c04620c1466a21c2"
-    "4620c14620c2466a21c34620c24620c3466a21c44620c34620c4466a21c54620c44620c5466a21c64620c54620c646"
-    "6a21c74620c64620c7466a21c84620c74620c8466a21c94620c84620c9466a21ca4620c94620ca466a21cb4620ca46"
-    "20cb466a21cc4620cb4620cc466a21cd4620cc4620cd466a21ce4620cd4620ce466a21cf4620ce4620cf466a21d046"
-    "20cf4620d0466a21d14620d04620d1466a21d24620d14620d2466a21d34620d24620d3466a21d44620d34620d4466a"
-    "21d54620d44620d5466a21d64620d54620d6466a21d74620d64620d7466a21d84620d74620d8466a21d94620d84620"
-    "d9466a21da4620d94620da466a21db4620da4620db466a21dc4620db4620dc466a21dd4620dc4620dd466a21de4620"
-    "dd4620de466a21df4620de4620df466a21e04620df4620e0466a21e14620e04620e1466a21e24620e14620e2466a21"
-    "e34620e24620e3466a21e44620e34620e4466a21e54620e44620e5466a21e64620e54620e6466a21e74620e64620e7"
-    "466a21e84620e74620e8466a21e94620e84620e9466a21ea4620e94620ea466a21eb4620ea4620eb466a21ec4620eb"
-    "4620ec466a21ed4620ec4620ed466a21ee4620ed4620ee466a21ef4620ee4620ef466a21f04620ef4620f0466a21f1"
-    "4620f04620f1466a21f24620f14620f2466a21f34620f24620f3466a21f44620f34620f4466a21f54620f44620f546"
-    "6a21f64620f54620f6466a21f74620f64620f7466a21f84620f74620f8466a21f94620f84620f9466a21fa4620f946"
-    "20fa466a21fb4620fa4620fb466a21fc4620fb4620fc466a21fd4620fc4620fd466a21fe4620fd4620fe466a21ff46"
-    "20fe4620ff466a21804720ff462080476a2181472080472081476a2182472081472082476a2183472082472083476a"
-    "2184472083472084476a2185472084472085476a2186472085472086476a2187472086472087476a21884720874720"
-    "88476a2189472088472089476a218a47208947208a476a218b47208a47208b476a218c47208b47208c476a218d4720"
-    "8c47208d476a218e47208d47208e476a218f47208e47208f476a219047208f472090476a2191472090472091476a21"
-    "92472091472092476a2193472092472093476a2194472093472094476a2195472094472095476a2196472095472096"
-    "476a2197472096472097476a2198472097472098476a2199472098472099476a219a47209947209a476a219b47209a"
-    "47209b476a219c47209b47209c476a219d47209c47209d476a219e47209d47209e476a219f47209e47209f476a21a0"
-    "47209f4720a0476a21a14720a04720a1476a21a24720a14720a2476a21a34720a24720a3476a21a44720a34720a447"
-    "6a21a54720a44720a5476a21a64720a54720a6476a21a74720a64720a7476a21a84720a74720a8476a21a94720a847"
-    "20a9476a21aa4720a94720aa476a21ab4720aa4720ab476a21ac4720ab4720ac476a21ad4720ac4720ad476a21ae47"
-    "20ad4720ae476a21af4720ae4720af476a21b04720af4720b0476a21b14720b04720b1476a21b24720b14720b2476a"
-    "21b34720b24720b3476a21b44720b34720b4476a21b54720b44720b5476a21b64720b54720b6476a21b74720b64720"
-    "b7476a21b84720b74720b8476a21b94720b84720b9476a21ba4720b94720ba476a21bb4720ba4720bb476a21bc4720"
-    "bb4720bc476a21bd4720bc4720bd476a21be4720bd4720be476a21bf4720be4720bf476a21c04720bf4720c0476a21"
-    "c14720c04720c1476a21c24720c14720c2476a21c34720c24720c3476a21c44720c34720c4476a21c54720c44720c5"
-    "476a21c64720c54720c6476a21c74720c64720c7476a21c84720c74720c8476a21c94720c84720c9476a21ca4720c9"
-    "4720ca476a21cb4720ca4720cb476a21cc4720cb4720cc476a21cd4720cc4720cd476a21ce4720cd4720ce476a21cf"
-    "4720ce4720cf476a21d04720cf4720d0476a21d14720d04720d1476a21d24720d14720d2476a21d34720d24720d347"
-    "6a21d44720d34720d4476a21d54720d44720d5476a21d64720d54720d6476a21d74720d64720d7476a21d84720d747"
-    "20d8476a21d94720d84720d9476a21da4720d94720da476a21db4720da4720db476a21dc4720db4720dc476a21dd47"
-    "20dc4720dd476a21de4720dd4720de476a21df4720de4720df476a21e04720df4720e0476a21e14720e04720e1476a"
-    "21e24720e14720e2476a21e34720e24720e3476a21e44720e34720e4476a21e54720e44720e5476a21e64720e54720"
-    "e6476a21e74720e64720e7476a21e84720e74720e8476a21e94720e84720e9476a21ea4720e94720ea476a21eb4720"
-    "ea4720eb476a21ec4720eb4720ec476a21ed4720ec4720ed476a21ee4720ed4720ee476a21ef4720ee4720ef476a21"
-    "f04720ef4720f0476a21f14720f04720f1476a21f24720f14720f2476a21f34720f24720f3476a21f44720f34720f4"
-    "476a21f54720f44720f5476a21f64720f54720f6476a21f74720f64720f7476a21f84720f74720f8476a21f94720f8"
-    "4720f9476a21fa4720f94720fa476a21fb4720fa4720fb476a21fc4720fb4720fc476a21fd4720fc4720fd476a21fe"
-    "4720fd4720fe476a21ff4720fe4720ff476a21804820ff472080486a2181482080482081486a218248208148208248"
-    "6a2183482082482083486a2184482083482084486a2185482084482085486a2186482085482086486a218748208648"
-    "2087486a2188482087482088486a2189482088482089486a218a48208948208a486a218b48208a48208b486a218c48"
-    "208b48208c486a218d48208c48208d486a218e48208d48208e486a218f48208e48208f486a219048208f482090486a"
-    "2191482090482091486a2192482091482092486a2193482092482093486a2194482093482094486a21954820944820"
-    "95486a2196482095482096486a2197482096482097486a2198482097482098486a2199482098482099486a219a4820"
-    "9948209a486a219b48209a48209b486a219c48209b48209c486a219d48209c48209d486a219e48209d48209e486a21"
-    "9f48209e48209f486a21a048209f4820a0486a21a14820a04820a1486a21a24820a14820a2486a21a34820a24820a3"
-    "486a21a44820a34820a4486a21a54820a44820a5486a21a64820a54820a6486a21a74820a64820a7486a21a84820a7"
-    "4820a8486a21a94820a84820a9486a21aa4820a94820aa486a21ab4820aa4820ab486a21ac4820ab4820ac486a21ad"
-    "4820ac4820ad486a21ae4820ad4820ae486a21af4820ae4820af486a21b04820af4820b0486a21b14820b04820b148"
-    "6a21b24820b14820b2486a21b34820b24820b3486a21b44820b34820b4486a21b54820b44820b5486a21b64820b548"
-    "20b6486a21b74820b64820b7486a21b84820b74820b8486a21b94820b84820b9486a21ba4820b94820ba486a21bb48"
-    "20ba4820bb486a21bc4820bb4820bc486a21bd4820bc4820bd486a21be4820bd4820be486a21bf4820be4820bf486a"
-    "21c04820bf4820c0486a21c14820c04820c1486a21c24820c14820c2486a21c34820c24820c3486a21c44820c34820"
-    "c4486a21c54820c44820c5486a21c64820c54820c6486a21c74820c64820c7486a21c84820c74820c8486a21c94820"
-    "c84820c9486a21ca4820c94820ca486a21cb4820ca4820cb486a21cc4820cb4820cc486a21cd4820cc4820cd486a21"
-    "ce4820cd4820ce486a21cf4820ce4820cf486a21d04820cf4820d0486a21d14820d04820d1486a21d24820d14820d2"
-    "486a21d34820d24820d3486a21d44820d34820d4486a21d54820d44820d5486a21d64820d54820d6486a21d74820d6"
-    "4820d7486a21d84820d74820d8486a21d94820d84820d9486a21da4820d94820da486a21db4820da4820db486a21dc"
-    "4820db4820dc486a21dd4820dc4820dd486a21de4820dd4820de486a21df4820de4820df486a21e04820df4820e048"
-    "6a21e14820e04820e1486a21e24820e14820e2486a21e34820e24820e3486a21e44820e34820e4486a21e54820e448"
-    "20e5486a21e64820e54820e6486a21e74820e64820e7486a21e84820e74820e8486a21e94820e84820e9486a21ea48"
-    "20e94820ea486a21eb4820ea4820eb486a21ec4820eb4820ec486a21ed4820ec4820ed486a21ee4820ed4820ee486a"
-    "21ef4820ee4820ef486a21f04820ef4820f0486a21f14820f04820f1486a21f24820f14820f2486a21f34820f24820"
-    "f3486a21f44820f34820f4486a21f54820f44820f5486a21f64820f54820f6486a21f74820f64820f7486a21f84820"
-    "f74820f8486a21f94820f84820f9486a21fa4820f94820fa486a21fb4820fa4820fb486a21fc4820fb4820fc486a21"
-    "fd4820fc4820fd486a21fe4820fd4820fe486a21ff4820fe4820ff486a21804920ff482080496a2181492080492081"
-    "496a2182492081492082496a2183492082492083496a2184492083492084496a2185492084492085496a2186492085"
-    "492086496a2187492086492087496a2188492087492088496a2189492088492089496a218a49208949208a496a218b"
-    "49208a49208b496a218c49208b49208c496a218d49208c49208d496a218e49208d49208e496a218f49208e49208f49"
-    "6a219049208f492090496a2191492090492091496a2192492091492092496a2193492092492093496a219449209349"
-    "2094496a2195492094492095496a2196492095492096496a2197492096492097496a2198492097492098496a219949"
-    "2098492099496a219a49209949209a496a219b49209a49209b496a219c49209b49209c496a219d49209c49209d496a"
-    "219e49209d49209e496a219f49209e49209f496a21a049209f4920a0496a21a14920a04920a1496a21a24920a14920"
-    "a2496a21a34920a24920a3496a21a44920a34920a4496a21a54920a44920a5496a21a64920a54920a6496a21a74920"
-    "a64920a7496a21a84920a74920a8496a21a94920a84920a9496a21aa4920a94920aa496a21ab4920aa4920ab496a21"
-    "ac4920ab4920ac496a21ad4920ac4920ad496a21ae4920ad4920ae496a21af4920ae4920af496a21b04920af4920b0"
-    "496a21b14920b04920b1496a21b24920b14920b2496a21b34920b24920b3496a21b44920b34920b4496a21b54920b4"
-    "4920b5496a21b64920b54920b6496a21b74920b64920b7496a21b84920b74920b8496a21b94920b84920b9496a21ba"
-    "4920b94920ba496a21bb4920ba4920bb496a21bc4920bb4920bc496a21bd4920bc4920bd496a21be4920bd4920be49"
-    "6a21bf4920be4920bf496a21c04920bf4920c0496a21c14920c04920c1496a21c24920c14920c2496a21c34920c249"
-    "20c3496a21c44920c34920c4496a21c54920c44920c5496a21c64920c54920c6496a21c74920c64920c7496a21c849"
-    "20c74920c8496a21c94920c84920c9496a21ca4920c94920ca496a21cb4920ca4920cb496a21cc4920cb4920cc496a"
-    "21cd4920cc4920cd496a21ce4920cd4920ce496a21cf4920ce4920cf496a21d04920cf4920d0496a21d14920d04920"
-    "d1496a21d24920d14920d2496a21d34920d24920d3496a21d44920d34920d4496a21d54920d44920d5496a21d64920"
-    "d54920d6496a21d74920d64920d7496a21d84920d74920d8496a21d94920d84920d9496a21da4920d94920da496a21"
-    "db4920da4920db496a21dc4920db4920dc496a21dd4920dc4920dd496a21de4920dd4920de496a21df4920de4920df"
-    "496a21e04920df4920e0496a21e14920e04920e1496a21e24920e14920e2496a21e34920e24920e3496a21e44920e3"
-    "4920e4496a21e54920e44920e5496a21e64920e54920e6496a21e74920e64920e7496a21e84920e74920e8496a21e9"
-    "4920e84920e9496a21ea4920e94920ea496a21eb4920ea4920eb496a21ec4920eb4920ec496a21ed4920ec4920ed49"
-    "6a21ee4920ed4920ee496a21ef4920ee4920ef496a21f04920ef4920f0496a21f14920f04920f1496a21f24920f149"
-    "20f2496a21f34920f24920f3496a21f44920f34920f4496a21f54920f44920f5496a21f64920f54920f6496a21f749"
-    "20f64920f7496a21f84920f74920f8496a21f94920f84920f9496a21fa4920f94920fa496a21fb4920fa4920fb496a"
-    "21fc4920fb4920fc496a21fd4920fc4920fd496a21fe4920fd4920fe496a21ff4920fe4920ff496a21804a20ff4920"
-    "804a6a21814a20804a20814a6a21824a20814a20824a6a21834a20824a20834a6a21844a20834a20844a6a21854a20"
-    "844a20854a6a21864a20854a20864a6a21874a20864a20874a6a21884a20874a20884a6a21894a20884a20894a6a21"
-    "8a4a20894a208a4a6a218b4a208a4a208b4a6a218c4a208b4a208c4a6a218d4a208c4a208d4a6a218e4a208d4a208e"
-    "4a6a218f4a208e4a208f4a6a21904a208f4a20904a6a21914a20904a20914a6a21924a20914a20924a6a21934a2092"
-    "4a20934a6a21944a20934a20944a6a21954a20944a20954a6a21964a20954a20964a6a21974a20964a20974a6a2198"
-    "4a20974a20984a6a21994a20984a20994a6a219a4a20994a209a4a6a219b4a209a4a209b4a6a219c4a209b4a209c4a"
-    "6a219d4a209c4a209d4a6a219e4a209d4a209e4a6a219f4a209e4a209f4a6a21a04a209f4a20a04a6a21a14a20a04a"
-    "20a14a6a21a24a20a14a20a24a6a21a34a20a24a20a34a6a21a44a20a34a20a44a6a21a54a20a44a20a54a6a21a64a"
-    "20a54a20a64a6a21a74a20a64a20a74a6a21a84a20a74a20a84a6a21a94a20a84a20a94a6a21aa4a20a94a20aa4a6a"
-    "21ab4a20aa4a20ab4a6a21ac4a20ab4a20ac4a6a21ad4a20ac4a20ad4a6a21ae4a20ad4a20ae4a6a21af4a20ae4a20"
-    "af4a6a21b04a20af4a20b04a6a21b14a20b04a20b14a6a21b24a20b14a20b24a6a21b34a20b24a20b34a6a21b44a20"
-    "b34a20b44a6a21b54a20b44a20b54a6a21b64a20b54a20b64a6a21b74a20b64a20b74a6a21b84a20b74a20b84a6a21"
-    "b94a20b84a20b94a6a21ba4a20b94a20ba4a6a21bb4a20ba4a20bb4a6a21bc4a20bb4a20bc4a6a21bd4a20bc4a20bd"
-    "4a6a21be4a20bd4a20be4a6a21bf4a20be4a20bf4a6a21c04a20bf4a20c04a6a21c14a20c04a20c14a6a21c24a20c1"
-    "4a20c24a6a21c34a20c24a20c34a6a21c44a20c34a20c44a6a21c54a20c44a20c54a6a21c64a20c54a20c64a6a21c7"
-    "4a20c64a20c74a6a21c84a20c74a20c84a6a21c94a20c84a20c94a6a21ca4a20c94a20ca4a6a21cb4a20ca4a20cb4a"
-    "6a21cc4a20cb4a20cc4a6a21cd4a20cc4a20cd4a6a21ce4a20cd4a20ce4a6a21cf4a20ce4a20cf4a6a21d04a20cf4a"
-    "20d04a6a21d14a20d04a20d14a6a21d24a20d14a20d24a6a21d34a20d24a20d34a6a21d44a20d34a20d44a6a21d54a"
-    "20d44a20d54a6a21d64a20d54a20d64a6a21d74a20d64a20d74a6a21d84a20d74a20d84a6a21d94a20d84a20d94a6a"
-    "21da4a20d94a20da4a6a21db4a20da4a20db4a6a21dc4a20db4a20dc4a6a21dd4a20dc4a20dd4a6a21de4a20dd4a20"
-    "de4a6a21df4a20de4a20df4a6a21e04a20df4a20e04a6a21e14a20e04a20e14a6a21e24a20e14a20e24a6a21e34a20"
-    "e24a20e34a6a21e44a20e34a20e44a6a21e54a20e44a20e54a6a21e64a20e54a20e64a6a21e74a20e64a20e74a6a21"
-    "e84a20e74a20e84a6a21e94a20e84a20e94a6a21ea4a20e94a20ea4a6a21eb4a20ea4a20eb4a6a21ec4a20eb4a20ec"
-    "4a6a21ed4a20ec4a20ed4a6a21ee4a20ed4a20ee4a6a21ef4a20ee4a20ef4a6a21f04a20ef4a20f04a6a21f14a20f0"
-    "4a20f14a6a21f24a20f14a20f24a6a21f34a20f24a20f34a6a21f44a20f34a20f44a6a21f54a20f44a20f54a6a21f6"
-    "4a20f54a20f64a6a21f74a20f64a20f74a6a21f84a20f74a20f84a6a21f94a20f84a20f94a6a21fa4a20f94a20fa4a"
-    "6a21fb4a20fa4a20fb4a6a21fc4a20fb4a20fc4a6a21fd4a20fc4a20fd4a6a21fe4a20fd4a20fe4a6a21ff4a20fe4a"
-    "20ff4a6a21804b20ff4a20804b6a21814b20804b20814b6a21824b20814b20824b6a21834b20824b20834b6a21844b"
-    "20834b20844b6a21854b20844b20854b6a21864b20854b20864b6a21874b20864b20874b6a21884b20874b20884b6a"
-    "21894b20884b20894b6a218a4b20894b208a4b6a218b4b208a4b208b4b6a218c4b208b4b208c4b6a218d4b208c4b20"
-    "8d4b6a218e4b208d4b208e4b6a218f4b208e4b208f4b6a21904b208f4b20904b6a21914b20904b20914b6a21924b20"
-    "914b20924b6a21934b20924b20934b6a21944b20934b20944b6a21954b20944b20954b6a21964b20954b20964b6a21"
-    "974b20964b20974b6a21984b20974b20984b6a21994b20984b20994b6a219a4b20994b209a4b6a219b4b209a4b209b"
-    "4b6a219c4b209b4b209c4b6a219d4b209c4b209d4b6a219e4b209d4b209e4b6a219f4b209e4b209f4b6a21a04b209f"
-    "4b20a04b6a21a14b20a04b20a14b6a21a24b20a14b20a24b6a21a34b20a24b20a34b6a21a44b20a34b20a44b6a21a5"
-    "4b20a44b20a54b6a21a64b20a54b20a64b6a21a74b20a64b20a74b6a21a84b20a74b20a84b6a21a94b20a84b20a94b"
-    "6a21aa4b20a94b20aa4b6a21ab4b20aa4b20ab4b6a21ac4b20ab4b20ac4b6a21ad4b20ac4b20ad4b6a21ae4b20ad4b"
-    "20ae4b6a21af4b20ae4b20af4b6a21b04b20af4b20b04b6a21b14b20b04b20b14b6a21b24b20b14b20b24b6a21b34b"
-    "20b24b20b34b6a21b44b20b34b20b44b6a21b54b20b44b20b54b6a21b64b20b54b20b64b6a21b74b20b64b20b74b6a"
-    "21b84b20b74b20b84b6a21b94b20b84b20b94b6a21ba4b20b94b20ba4b6a21bb4b20ba4b20bb4b6a21bc4b20bb4b20"
-    "bc4b6a21bd4b20bc4b20bd4b6a21be4b20bd4b20be4b6a21bf4b20be4b20bf4b6a21c04b20bf4b20c04b6a21c14b20"
-    "c04b20c14b6a21c24b20c14b20c24b6a21c34b20c24b20c34b6a21c44b20c34b20c44b6a21c54b20c44b20c54b6a21"
-    "c64b20c54b20c64b6a21c74b20c64b20c74b6a21c84b20c74b20c84b6a21c94b20c84b20c94b6a21ca4b20c94b20ca"
-    "4b6a21cb4b20ca4b20cb4b6a21cc4b20cb4b20cc4b6a21cd4b20cc4b20cd4b6a21ce4b20cd4b20ce4b6a21cf4b20ce"
-    "4b20cf4b6a21d04b20cf4b20d04b6a21d14b20d04b20d14b6a21d24b20d14b20d24b6a21d34b20d24b20d34b6a21d4"
-    "4b20d34b20d44b6a21d54b20d44b20d54b6a21d64b20d54b20d64b6a21d74b20d64b20d74b6a21d84b20d74b20d84b"
-    "6a21d94b20d84b20d94b6a21da4b20d94b20da4b6a21db4b20da4b20db4b6a21dc4b20db4b20dc4b6a21dd4b20dc4b"
-    "20dd4b6a21de4b20dd4b20de4b6a21df4b20de4b20df4b6a21e04b20df4b20e04b6a21e14b20e04b20e14b6a21e24b"
-    "20e14b20e24b6a21e34b20e24b20e34b6a21e44b20e34b20e44b6a21e54b20e44b20e54b6a21e64b20e54b20e64b6a"
-    "21e74b20e64b20e74b6a21e84b20e74b20e84b6a21e94b20e84b20e94b6a21ea4b20e94b20ea4b6a21eb4b20ea4b20"
-    "eb4b6a21ec4b20eb4b20ec4b6a21ed4b20ec4b20ed4b6a21ee4b20ed4b20ee4b6a21ef4b20ee4b20ef4b6a21f04b20"
-    "ef4b20f04b6a21f14b20f04b20f14b6a21f24b20f14b20f24b6a21f34b20f24b20f34b6a21f44b20f34b20f44b6a21"
-    "f54b20f44b20f54b6a21f64b20f54b20f64b6a21f74b20f64b20f74b6a21f84b20f74b20f84b6a21f94b20f84b20f9"
-    "4b6a21fa4b20f94b20fa4b6a21fb4b20fa4b20fb4b6a21fc4b20fb4b20fc4b6a21fd4b20fc4b20fd4b6a21fe4b20fd"
-    "4b20fe4b6a21ff4b20fe4b20ff4b6a21804c20ff4b20804c6a21814c20804c20814c6a21824c20814c20824c6a2183"
-    "4c20824c20834c6a21844c20834c20844c6a21854c20844c20854c6a21864c20854c20864c6a21874c20864c20874c"
-    "6a21884c20874c20884c6a21894c20884c20894c6a218a4c20894c208a4c6a218b4c208a4c208b4c6a218c4c208b4c"
-    "208c4c6a218d4c208c4c208d4c6a218e4c208d4c208e4c6a218f4c208e4c208f4c6a21904c208f4c20904c6a21914c"
-    "20904c20914c6a21924c20914c20924c6a21934c20924c20934c6a21944c20934c20944c6a21954c20944c20954c6a"
-    "21964c20954c20964c6a21974c20964c20974c6a21984c20974c20984c6a21994c20984c20994c6a219a4c20994c20"
-    "9a4c6a219b4c209a4c209b4c6a219c4c209b4c209c4c6a219d4c209c4c209d4c6a219e4c209d4c209e4c6a219f4c20"
-    "9e4c209f4c6a21a04c209f4c20a04c6a21a14c20a04c20a14c6a21a24c20a14c20a24c6a21a34c20a24c20a34c6a21"
-    "a44c20a34c20a44c6a21a54c20a44c20a54c6a21a64c20a54c20a64c6a21a74c20a64c20a74c6a21a84c20a74c20a8"
-    "4c6a21a94c20a84c20a94c6a21aa4c20a94c20aa4c6a21ab4c20aa4c20ab4c6a21ac4c20ab4c20ac4c6a21ad4c20ac"
-    "4c20ad4c6a21ae4c20ad4c20ae4c6a21af4c20ae4c20af4c6a21b04c20af4c20b04c6a21b14c20b04c20b14c6a21b2"
-    "4c20b14c20b24c6a21b34c20b24c20b34c6a21b44c20b34c20b44c6a21b54c20b44c20b54c6a21b64c20b54c20b64c"
-    "6a21b74c20b64c20b74c6a21b84c20b74c20b84c6a21b94c20b84c20b94c6a21ba4c20b94c20ba4c6a21bb4c20ba4c"
-    "20bb4c6a21bc4c20bb4c20bc4c6a21bd4c20bc4c20bd4c6a21be4c20bd4c20be4c6a21bf4c20be4c20bf4c6a21c04c"
-    "20bf4c20c04c6a21c14c20c04c20c14c6a21c24c20c14c20c24c6a21c34c20c24c20c34c6a21c44c20c34c20c44c6a"
-    "21c54c20c44c20c54c6a21c64c20c54c20c64c6a21c74c20c64c20c74c6a21c84c20c74c20c84c6a21c94c20c84c20"
-    "c94c6a21ca4c20c94c20ca4c6a21cb4c20ca4c20cb4c6a21cc4c20cb4c20cc4c6a21cd4c20cc4c20cd4c6a21ce4c20"
-    "cd4c20ce4c6a21cf4c20ce4c20cf4c6a21d04c20cf4c20d04c6a21d14c20d04c20d14c6a21d24c20d14c20d24c6a21"
-    "d34c20d24c20d34c6a21d44c20d34c20d44c6a21d54c20d44c20d54c6a21d64c20d54c20d64c6a21d74c20d64c20d7"
-    "4c6a21d84c20d74c20d84c6a21d94c20d84c20d94c6a21da4c20d94c20da4c6a21db4c20da4c20db4c6a21dc4c20db"
-    "4c20dc4c6a21dd4c20dc4c20dd4c6a21de4c20dd4c20de4c6a21df4c20de4c20df4c6a21e04c20df4c20e04c6a21e1"
-    "4c20e04c20e14c6a21e24c20e14c20e24c6a21e34c20e24c20e34c6a21e44c20e34c20e44c6a21e54c20e44c20e54c"
-    "6a21e64c20e54c20e64c6a21e74c20e64c20e74c6a21e84c20e74c20e84c6a21e94c20e84c20e94c6a21ea4c20e94c"
-    "20ea4c6a21eb4c20ea4c20eb4c6a21ec4c20eb4c20ec4c6a21ed4c20ec4c20ed4c6a21ee4c20ed4c20ee4c6a21ef4c"
-    "20ee4c20ef4c6a21f04c20ef4c20f04c6a21f14c20f04c20f14c6a21f24c20f14c20f24c6a21f34c20f24c20f34c6a"
-    "21f44c20f34c20f44c6a21f54c20f44c20f54c6a21f64c20f54c20f64c6a21f74c20f64c20f74c6a21f84c20f74c20"
-    "f84c6a21f94c20f84c20f94c6a21fa4c20f94c20fa4c6a21fb4c20fa4c20fb4c6a21fc4c20fb4c20fc4c6a21fd4c20"
-    "fc4c20fd4c6a21fe4c20fd4c20fe4c6a21ff4c20fe4c20ff4c6a21804d20ff4c20804d6a21814d20804d20814d6a21"
-    "824d20814d20824d6a21834d20824d20834d6a21844d20834d20844d6a21854d20844d20854d6a21864d20854d2086"
-    "4d6a21874d20864d20874d6a21884d20874d20884d6a21894d20884d20894d6a218a4d20894d208a4d6a218b4d208a"
-    "4d208b4d6a218c4d208b4d208c4d6a218d4d208c4d208d4d6a218e4d208d4d208e4d6a218f4d208e4d208f4d6a2190"
-    "4d208f4d20904d6a21914d20904d20914d6a21924d20914d20924d6a21934d20924d20934d6a21944d20934d20944d"
-    "6a21954d20944d20954d6a21964d20954d20964d6a21974d20964d20974d6a21984d20974d20984d6a21994d20984d"
-    "20994d6a219a4d20994d209a4d6a219b4d209a4d209b4d6a219c4d209b4d209c4d6a219d4d209c4d209d4d6a219e4d"
-    "209d4d209e4d6a219f4d209e4d209f4d6a21a04d209f4d20a04d6a21a14d20a04d20a14d6a21a24d20a14d20a24d6a"
-    "21a34d20a24d20a34d6a21a44d20a34d20a44d6a21a54d20a44d20a54d6a21a64d20a54d20a64d6a21a74d20a64d20"
-    "a74d6a21a84d20a74d20a84d6a21a94d20a84d20a94d6a21aa4d20a94d20aa4d6a21ab4d20aa4d20ab4d6a21ac4d20"
-    "ab4d20ac4d6a21ad4d20ac4d20ad4d6a21ae4d20ad4d20ae4d6a21af4d20ae4d20af4d6a21b04d20af4d20b04d6a21"
-    "b14d20b04d20b14d6a21b24d20b14d20b24d6a21b34d20b24d20b34d6a21b44d20b34d20b44d6a21b54d20b44d20b5"
-    "4d6a21b64d20b54d20b64d6a21b74d20b64d20b74d6a21b84d20b74d20b84d6a21b94d20b84d20b94d6a21ba4d20b9"
-    "4d20ba4d6a21bb4d20ba4d20bb4d6a21bc4d20bb4d20bc4d6a21bd4d20bc4d20bd4d6a21be4d20bd4d20be4d6a21bf"
-    "4d20be4d20bf4d6a21c04d20bf4d20c04d6a21c14d20c04d20c14d6a21c24d20c14d20c24d6a21c34d20c24d20c34d"
-    "6a21c44d20c34d20c44d6a21c54d20c44d20c54d6a21c64d20c54d20c64d6a21c74d20c64d20c74d6a21c84d20c74d"
-    "20c84d6a21c94d20c84d20c94d6a21ca4d20c94d20ca4d6a21cb4d20ca4d20cb4d6a21cc4d20cb4d20cc4d6a21cd4d"
-    "20cc4d20cd4d6a21ce4d20cd4d20ce4d6a21cf4d20ce4d20cf4d6a21d04d20cf4d20d04d6a21d14d20d04d20d14d6a"
-    "21d24d20d14d20d24d6a21d34d20d24d20d34d6a21d44d20d34d20d44d6a21d54d20d44d20d54d6a21d64d20d54d20"
-    "d64d6a21d74d20d64d20d74d6a21d84d20d74d20d84d6a21d94d20d84d20d94d6a21da4d20d94d20da4d6a21db4d20"
-    "da4d20db4d6a21dc4d20db4d20dc4d6a21dd4d20dc4d20dd4d6a21de4d20dd4d20de4d6a21df4d20de4d20df4d6a21"
-    "e04d20df4d20e04d6a21e14d20e04d20e14d6a21e24d20e14d20e24d6a21e34d20e24d20e34d6a21e44d20e34d20e4"
-    "4d6a21e54d20e44d20e54d6a21e64d20e54d20e64d6a21e74d20e64d20e74d6a21e84d20e74d20e84d6a21e94d20e8"
-    "4d20e94d6a21ea4d20e94d20ea4d6a21eb4d20ea4d20eb4d6a21ec4d20eb4d20ec4d6a21ed4d20ec4d20ed4d6a21ee"
-    "4d20ed4d20ee4d6a21ef4d20ee4d20ef4d6a21f04d20ef4d20f04d6a21f14d20f04d20f14d6a21f24d20f14d20f24d"
-    "6a21f34d20f24d20f34d6a21f44d20f34d20f44d6a21f54d20f44d20f54d6a21f64d20f54d20f64d6a21f74d20f64d"
-    "20f74d6a21f84d20f74d20f84d6a21f94d20f84d20f94d6a21fa4d20f94d20fa4d6a21fb4d20fa4d20fb4d6a21fc4d"
-    "20fb4d20fc4d6a21fd4d20fc4d20fd4d6a21fe4d20fd4d20fe4d6a21ff4d20fe4d20ff4d6a21804e20ff4d20804e6a"
-    "21814e20804e20814e6a21824e20814e20824e6a21834e20824e20834e6a21844e20834e20844e6a21854e20844e20"
-    "854e6a21864e20854e20864e6a21874e20864e20874e6a21884e20874e20884e6a21894e20884e20894e6a218a4e20"
-    "894e208a4e6a218b4e208a4e208b4e6a218c4e208b4e208c4e6a218d4e208c4e208d4e6a218e4e208d4e208e4e6a21"
-    "8f4e208f4e0b";
diff --git a/src/test/app/wasm_fixtures/fixtures.cpp b/src/test/app/wasm_fixtures/fixtures.cpp
deleted file mode 100644
index 48ddacd218..0000000000
--- a/src/test/app/wasm_fixtures/fixtures.cpp
+++ /dev/null
@@ -1,1510 +0,0 @@
-// TODO: consider moving these to separate files (and figure out the build)
-
-#include 
-
-#include 
-#include 
-#include 
-
-namespace wasm_constants {
-
-namespace {
-
-void
-appendU32Leb(std::vector& out, uint32_t value)
-{
-    do
-    {
-        auto byte = static_cast(value & 0x7f);
-        value >>= 7;
-        if (value != 0u)
-            byte |= 0x80;
-        out.push_back(byte);
-    } while (value != 0u);
-}
-
-void
-appendSection(std::vector& out, uint8_t section, std::vector const& payload)
-{
-    out.push_back(section);
-    appendU32Leb(out, payload.size());
-    out.insert(out.end(), payload.begin(), payload.end());
-}
-
-void
-appendBytes(std::vector& out, auto const& bytes)
-{
-    for (auto byte : bytes)
-        out.push_back(byte);
-}
-
-std::vector
-baseModule()
-{
-    std::vector out;
-    appendBytes(out, kWasmHeader);
-    appendBytes(out, kTypeEmptyFunc);
-    appendBytes(out, kFuncTypE0);
-    appendBytes(out, kExportFinish);
-    return out;
-}
-
-}  // namespace
-
-std::vector
-generateCodeBlob(uint32_t numInstructions)
-{
-    auto out = baseModule();
-
-    std::vector body;
-    body.push_back(0x00);
-    body.insert(body.end(), numInstructions, kInstrNop);
-    body.push_back(kInstrEnd);
-
-    std::vector codePayload;
-    codePayload.push_back(0x01);
-    appendU32Leb(codePayload, body.size());
-    codePayload.insert(codePayload.end(), body.begin(), body.end());
-
-    appendSection(out, kSectionCode, codePayload);
-    return out;
-}
-
-std::vector
-generateDataBlob(uint32_t dataSize)
-{
-    std::vector out;
-    appendBytes(out, kWasmHeader);
-    appendBytes(out, kTypeEmptyFunc);
-    appendBytes(out, kFuncTypE0);
-
-    std::vector memoryPayload;
-    memoryPayload.push_back(0x01);
-    memoryPayload.push_back(0x00);
-    appendU32Leb(memoryPayload, (dataSize + 65'535) / 65'536);
-    appendSection(out, kSectionMemory, memoryPayload);
-
-    appendBytes(out, kExportFinish);
-
-    std::vector codePayload;
-    codePayload.push_back(0x01);
-    appendU32Leb(codePayload, sizeof(kEmptyBody));
-    appendBytes(codePayload, kEmptyBody);
-    appendSection(out, kSectionCode, codePayload);
-
-    std::vector dataPayload;
-    dataPayload.push_back(0x01);
-    dataPayload.push_back(0x00);
-    appendBytes(dataPayload, kDataOffsetZero);
-    appendU32Leb(dataPayload, dataSize);
-    dataPayload.insert(dataPayload.end(), dataSize, kDataFillByte);
-    appendSection(out, kSectionData, dataPayload);
-
-    return out;
-}
-
-}  // namespace wasm_constants
-
-extern std::string const kFibWasmHex =
-    "0061736d0100000001090260000060017f017f030302000105030100020638097f004180080b7f004180080b7f0041"
-    "80080b7f00418088040b7f004180080b7f00418088040b7f00418080080b7f0041000b7f0041010b07a7010c066d65"
-    "6d6f72790200115f5f7761736d5f63616c6c5f63746f727300000366696200010c5f5f64736f5f68616e646c650300"
-    "0a5f5f646174615f656e6403010b5f5f737461636b5f6c6f7703020c5f5f737461636b5f6869676803030d5f5f676c"
-    "6f62616c5f6261736503040b5f5f686561705f6261736503050a5f5f686561705f656e6403060d5f5f6d656d6f7279"
-    "5f6261736503070c5f5f7461626c655f6261736503080a440202000b3f01017f200045044041000f0b200041034804"
-    "4041010f0b200041026a21000340200041036b100120016a2101200041026b220041044a0d000b200141016a0b007f"
-    "0970726f647563657273010c70726f6365737365642d62790105636c616e675f31392e312e352d776173692d73646b"
-    "202868747470733a2f2f6769746875622e636f6d2f6c6c766d2f6c6c766d2d70726f6a656374206162346235613264"
-    "62353832393538616631656533303861373930636664623432626432343732302900490f7461726765745f66656174"
-    "75726573042b0f6d757461626c652d676c6f62616c732b087369676e2d6578742b0f7265666572656e63652d747970"
-    "65732b0a6d756c746976616c7565";
-
-extern std::string const kLedgerSqnWasmHex =
-    "0061736d01000000010e0360027f7f017f6000006000017f02120103656e760a6c6467725f696e6465780000030302"
-    "01020503010002063f0a7f01418088040b7f004180080b7f004180080b7f004180080b7f00418088040b7f00418008"
-    "0b7f00418088040b7f00418080080b7f0041000b7f0041010b07b1010c066d656d6f72790200115f5f7761736d5f63"
-    "616c6c5f63746f727300010d657363726f775f66696e69736800020c5f5f64736f5f68616e646c6503010a5f5f6461"
-    "74615f656e6403020b5f5f737461636b5f6c6f7703030c5f5f737461636b5f6869676803040d5f5f676c6f62616c5f"
-    "6261736503050b5f5f686561705f6261736503060a5f5f686561705f656e6403070d5f5f6d656d6f72795f62617365"
-    "03080c5f5f7461626c655f6261736503090a3d0202000b3801037f230041106b220024002000410c6a410410002101"
-    "200028020c2102200041106a2400200141054100200241054f1b20014100481b0b007f0970726f647563657273010c"
-    "70726f6365737365642d62790105636c616e675f31392e312e352d776173692d73646b202868747470733a2f2f6769"
-    "746875622e636f6d2f6c6c766d2f6c6c766d2d70726f6a656374206162346235613264623538323935386166316565"
-    "33303861373930636664623432626432343732302900490f7461726765745f6665617475726573042b0f6d75746162"
-    "6c652d676c6f62616c732b087369676e2d6578742b0f7265666572656e63652d74797065732b0a6d756c746976616c"
-    "7565";
-
-extern std::string const kAllHostFunctionsWasmHex =
-    "0061736d0100000001550c60027f7f017f60037f7f7f017f60047f7f7f7f017f60017f017f60067f7f7f7f7f7f017f"
-    "60037f7f7f0060057f7f7f7f7f0060087f7f7f7f7f7f7f7f017f60057f7f7f7f7f017f60017f0060027f7f00600001"
-    "7f02b1041808686f73745f6c69620874785f6669656c64000108686f73745f6c6962057472616365000608686f7374"
-    "5f6c69620a6c6467725f696e646578000008686f73745f6c696210706172656e745f6c6467725f74696d6500000868"
-    "6f73745f6c696210706172656e745f6c6467725f68617368000008686f73745f6c69620874785f696e6e6572000208"
-    "686f73745f6c69620a74785f6172725f6c656e000308686f73745f6c69621074785f696e6e65725f6172725f6c656e"
-    "000008686f73745f6c69620d686f6d655f6c655f6669656c64000108686f73745f6c69620d686f6d655f6c655f696e"
-    "6e6572000208686f73745f6c69620f686f6d655f6c655f6172725f6c656e000308686f73745f6c696215686f6d655f"
-    "6c655f696e6e65725f6172725f6c656e000008686f73745f6c69620863616368655f6c65000108686f73745f6c6962"
-    "0d63726564656e7469616c5f6964000708686f73745f6c696209657363726f775f6964000408686f73745f6c696209"
-    "6f7261636c655f6964000408686f73745f6c69620b7368613531325f68616c66000208686f73745f6c6962076e6674"
-    "5f757269000408686f73745f6c6962087365745f64617461000008686f73745f6c69620a6c655f6172725f6c656e00"
-    "0008686f73745f6c69620e6163636f756e74726f6f745f6964000208686f73745f6c6962106c655f696e6e65725f61"
-    "72725f6c656e000108686f73745f6c6962086c655f6669656c64000208686f73745f6c6962086c655f696e6e657200"
-    "08030b0a090a05050b000101030005030100110619037f01418080c0000b7f0041c698c0000b7f0041d098c0000b07"
-    "3504066d656d6f727902000d657363726f775f66696e697368001c0a5f5f646174615f656e6403010b5f5f68656170"
-    "5f6261736503020acf210a9d0101027f230041206b2201240020014200370310200142003703082001410036021820"
-    "00027f024041818020200141086a41141000220241004e0440200241144b0d01200241144704402000418180808078"
-    "36020441010c030b20002001280218360011200020012903103700092000200129030837000141000c020b20002002"
-    "36020441010c010b2000417336020441010b3a0000200141206a24000b5a01017f230041106b2202240020012d0000"
-    "41014604402002200134020437030841df97c000410b4101200241086a41081001000b200020012800113600102000"
-    "200129000937000820002001290001370000200241106a24000b1900200241214f0440000b20002002360204200020"
-    "013602000b1900200241094f0440000b20002002360204200020013602000bdd1e01077f230041c0036b2200240041"
-    "ea97c000411b4107410141001001418598c0004119410741014100100141b583c000412b4107410141001001200041"
-    "003602500240024002400240024002400240200041d0006a41041002220141004a044020002000280250220141ff81"
-    "fc0771410878200141187841ff81fc077172ad3703c00141e083c00041174101200041c0016a220241081001200041"
-    "003602800120004180016a41041003220141004c0d012000200028028001220141ff81fc0771410878200141187841"
-    "ff81fc077172ad3703c00141f783c00041134101200241081001200042003703d801200042003703d0012000420037"
-    "03c801200042003703c0012002412010042201412047044020002001ac3703a00141bd84c000412b4101200041a001"
-    "6a4108100141997f21030c080b418a84c00041134106200041c0016a220441201001419d84c0004120410741014100"
-    "100141aa85c000412e4107410141001001200041003602b001200042003703a801200042003703a001418180202000"
-    "41a0016a22024114100022014114470d0241d885c00041144104200241141001200042003703384188801820004138"
-    "6a22024108100022014108470d03200042083703c00141ec85c00041174101200441081001418386c0004128410620"
-    "02410810012000410036027841848008200041f8006a22024104100022014104470d0441ab86c00041154106200241"
-    "04100120004100360051200041013a005020004100360054200042003703d801200042003703d001200042003703c8"
-    "01200042003703c0010240200041d0006a4108200441201005220141004e044020002001ad3703800141c086c00041"
-    "14410120004180016a41081001200041306a20042001101a41d486c000410d41062000280230200028023410010c01"
-    "0b20002001ac3703800141e186c0004129410120004180016a410810010b20004183803c1006ac37038001418a87c0"
-    "004115410120004180016a22024108100120004189803c1006ac37038001419f87c000411341012002410810010240"
-    "200041d0006a41081007220141004e044020002001ad3703800141b287c000411441012002410810010c010b200020"
-    "01ac3703800141c687c000412d410120004180016a410810010b41f387c0004123410741014100100141e792c00041"
-    "3341074101410010012000420037033841828018200041386a220141081008220241004c0d05200241084604402000"
-    "42083703c001419a93c000412b4101200041c0016a4108100141c593c000412f41062001410810010c070b20002002"
-    "ad3703c00141f493c000412f4101200041c0016a41081001200041286a200041386a2002101b41a394c00041174106"
-    "2000280228200028022c10010c060b20002001ac3703c001418d85c000411d4101200041c0016a41081001419b7f21"
-    "030c060b20002001ac3703c00141e884c00041254101200041c0016a41081001419a7f21030c050b20002001ac3703"
-    "c001418289c000412a4101200041c0016a4108100141b77e21030c040b20002001ac3703c00141c188c00041c10041"
-    "01200041c0016a4108100141b67e21030c030b20002001ac3703c001419688c000412b4101200041c0016a41081001"
-    "41b57e21030c020b20002002ac3703c00141ba94c00041c5004101200041c0016a410810010b200041003602b00120"
-    "0042003703a801200042003703a001024041818020200041a0016a220241141008220141004a044041ff94c000411e"
-    "41042002411410010c010b20002001ac3703c001419d95c00041334101200041c0016a410810010b20004100360051"
-    "200041013a005020004100360054200042003703d801200042003703d001200042003703c801200042003703c00102"
-    "40200041d0006a4108200041c0016a220141201009220241004e044020002002ad3703800141d095c000411c410120"
-    "004180016a41081001200041206a20012002101a41ec95c000411541062000280220200028022410010c010b200020"
-    "02ac37038001418196c0004139410120004180016a410810010b20004183803c100aac3703800141ba96c000412441"
-    "0120004180016a2201410810010240200041d0006a4108100b220241004e044020002002ad3703800141de96c00041"
-    "1c41012001410810010c010b20002002ac3703800141fa96c000413d410120004180016a410810010b41b797c00041"
-    "28410741014100100141ac89c000412f4107410141001001200041c0016a2204101820004180016a22012004101920"
-    "0042003703b801200042003703b001200042003703a801200042003703a001024002400240024002402001200041a0"
-    "016a2202101d22014120460440200241204100100c220541004a044020002005ad3703c00141db89c0004123410120"
-    "0441081001200042003703782005200041f8006a22014108101e220241004c0d0220024108460440200042083703c0"
-    "0141fe89c000412a410120044108100141a88ac000412e41062001410810010c060b20002002ad3703c00141d68ac0"
-    "00412e4101200041c0016a41081001200041186a200041f8006a2002101b41848bc000411641062000280218200028"
-    "021c10010c050b20002005ac3703c00141bc8dc000413c4101200041c0016a220141081001200042003703d8012000"
-    "42003703d001200042003703c801200042003703c001410120014120101e22014100480d020c030b20002001ac3703"
-    "c001419090c000412e4101200041c0016a4108100141ef7c21030c050b20002002ac3703c001419a8bc000412b4101"
-    "200041c0016a410810010c020b20002001ac37035041f88dc00041c1004101200041d0006a410810010b2000410036"
-    "0039200041013a00382000410036003c4101200041386a200041c0016a101f2201410048044020002001ac37035041"
-    "b98ec00041354101200041d0006a410810010b410110202201410048044020002001ac37035041ee8ec00041324101"
-    "200041d0006a410810010b4101200041386a10212201410048044020002001ac37035041a08fc00041394101200041"
-    "d0006a410810010b41d98fc000413741074101410010010c010b20004100360039200041013a00382000410036003c"
-    "200042003703d801200042003703d001200042003703c801200042003703c00102402005200041386a200041c0016a"
-    "2201101f220241004e044020002002ad37035041c58bc000411b4101200041d0006a41081001200041106a20012002"
-    "101a41e08bc000411441062000280210200028021410010c010b20002002ac37035041f48bc00041314101200041d0"
-    "006a410810010b200020051020ac37035041a58cc00041234101200041d0006a22014108100102402005200041386a"
-    "1021220241004e044020002002ad37035041c88cc000411b41012001410810010c010b20002002ac37035041e38cc0"
-    "0041354101200041d0006a410810010b41988dc000412441074101410010010b41be90c000412f4107410141001001"
-    "200041c0016a22011018200041386a2204200110192000420037036820004200370360200042003703582000420037"
-    "0350024002402004200041d0006a2202101d2201412046044041ed90c000410f410620024120100120004200370398"
-    "012000420037039001200042003703880120004200370380010240200441142004411441fc90c00041092000418001"
-    "6a22014120100d220241004a0440200041086a20012002101a418491c000411241062000280208200028020c10010c"
-    "010b20002002ac3703c001419691c000413c4101200041c0016a410810010b200042003703b801200042003703b001"
-    "200042003703a801200042003703a00120004180808cc07e360270200041386a22044114200041f0006a4104200041"
-    "a0016a22024120100e22014120470d0141d291c000410e4106200241201001200042003703d801200042003703d001"
-    "200042003703c801200042003703c001200041808080d00236027420044114200041f4006a4104200041c0016a4120"
-    "100f2201412047044020002001ac370378419292c000411c4101200041f8006a4108100141887c21030c040b41e091"
-    "c000410e4106200041c0016a22044120100141ee91c00041244107410141001001418080c000412541074101410010"
-    "0120004200370398012000420037039001200042003703880120004200370380010240024041a580c0004117200041"
-    "80016a2202412010102201412046044041bc80c000410b410641a580c0004117100141c780c0004111410620024120"
-    "100120041018200041d0006a220620041019200042003703b801200042003703b001200042003703a8012000420037"
-    "03a00102404100200422036b410371220220036a220520034d0d0020020440200221010340200341003a0000200341"
-    "016a2103200141016b22010d000b0b200241016b4107490d000340200341003a0000200341076a41003a0000200341"
-    "066a41003a0000200341056a41003a0000200341046a41003a0000200341036a41003a0000200341026a41003a0000"
-    "200341016a41003a0000200341086a22032005470d000b0b200541800220026b2201417c716a220320054b04400340"
-    "20054100360200200541046a22052003490d000b0b024020032001410371220120036a22024f0d0020012205044003"
-    "40200341003a0000200341016a2103200541016b22050d000b0b200141016b4107490d000340200341003a00002003"
-    "41076a41003a0000200341066a41003a0000200341056a41003a0000200341046a41003a0000200341036a41003a00"
-    "00200341026a41003a0000200341016a41003a0000200341086a22032002470d000b0b20064114200041a0016a4120"
-    "20044180021011220141004c0d0120002001ad37033841d880c00041104101200041386a4108100120014181024f0d"
-    "0541e880c000410941062004200110010c020b20002001ac3703c00141e381c00041224101200041c0016a41081001"
-    "41a77b21030c050b20002001ac37033841f180c000412e4101200041386a410810010b419f81c0004112410641b181"
-    "c000410710012000422a3703384101210341b881c00041114101200041386a4108100141c981c000411a4107410141"
-    "001001418582c0004129410741014100100141ae82c000412810122201412847044020002001ac3703c001419b83c0"
-    "00411a4101200041c0016a4108100141c37a21030c040b41d682c0004127410641ae82c0004128100141fd82c00041"
-    "1e4107410141001001419e98c000412841074101410010010c030b20002001ac3703c00141ca92c000411d41012000"
-    "41c0016a41081001418b7c21030c020b20002001ac3703c00141ae92c000411c4101200041c0016a4108100141897c"
-    "21030c010b000b200041c0036a240020030b0c00200041142001412010140b0e002000418280182001200210160b0e"
-    "002000200141082002412010170b0a0020004183803c10130b0a0020002001410810150b0bd0180100418080c0000b"
-    "c6182d2d2d2043617465676f727920363a205574696c6974792046756e6374696f6e73202d2d2d48656c6c6f2c2058"
-    "52504c205741534d20776f726c6421496e70757420646174613a5348413531322068616c6620686173683a4e465420"
-    "64617461206c656e6774683a4e465420646174613a494e464f3a206765745f6e6674206661696c6564202865787065"
-    "63746564202d206e6f2073756368204e4654293a54657374207472616365206d6573736167657061796c6f61645465"
-    "7374206e756d626572207472616365535543434553533a205574696c6974792066756e6374696f6e734552524f523a"
-    "20636f6d707574655f7368613531325f68616c66206661696c65643a2d2d2d2043617465676f727920373a20446174"
-    "61205570646174652046756e6374696f6e73202d2d2d55706461746564206c656467657220656e7472792064617461"
-    "2066726f6d205741534d20746573745375636365737366756c6c792075706461746564206c656467657220656e7472"
-    "7920776974683a535543434553533a2044617461207570646174652066756e6374696f6e734552524f523a20757064"
-    "6174655f64617461206661696c65643a2d2d2d2043617465676f727920313a204c6564676572204865616465722046"
-    "756e6374696f6e73202d2d2d4c65646765722073657175656e6365206e756d6265723a506172656e74206c65646765"
-    "722074696d653a506172656e74206c656467657220686173683a535543434553533a204c6564676572206865616465"
-    "722066756e6374696f6e734552524f523a206765745f706172656e745f6c65646765725f686173682077726f6e6720"
-    "6c656e6774683a4552524f523a206765745f706172656e745f6c65646765725f74696d65206661696c65643a455252"
-    "4f523a206765745f6c65646765725f73716e206661696c65643a2d2d2d2043617465676f727920323a205472616e73"
-    "616374696f6e20446174612046756e6374696f6e73202d2d2d5472616e73616374696f6e204163636f756e743a5472"
-    "616e73616374696f6e20466565206c656e6774683a5472616e73616374696f6e20466565202873657269616c697a65"
-    "642058525020616d6f756e74293a5472616e73616374696f6e2053657175656e63653a4e6573746564206669656c64"
-    "206c656e6774683a4e6573746564206669656c643a494e464f3a206765745f74785f6e65737465645f6669656c6420"
-    "6e6f74206170706c696361626c653a5369676e657273206172726179206c656e6774683a4d656d6f73206172726179"
-    "206c656e6774683a4e6573746564206172726179206c656e6774683a494e464f3a206765745f74785f6e6573746564"
-    "5f61727261795f6c656e206e6f74206170706c696361626c653a535543434553533a205472616e73616374696f6e20"
-    "646174612066756e6374696f6e734552524f523a206765745f74785f6669656c642853657175656e6365292077726f"
-    "6e67206c656e6774683a4552524f523a206765745f74785f6669656c6428466565292077726f6e67206c656e677468"
-    "20286578706563746564203820627974657320666f7220585250293a4552524f523a206765745f74785f6669656c64"
-    "284163636f756e74292077726f6e67206c656e6774683a2d2d2d2043617465676f727920343a20416e79204c656467"
-    "6572204f626a6563742046756e6374696f6e73202d2d2d5375636365737366756c6c7920636163686564206f626a65"
-    "637420696e20736c6f743a436163686564206f626a6563742062616c616e6365206c656e677468202858525020616d"
-    "6f756e74293a436163686564206f626a6563742062616c616e6365202873657269616c697a65642058525020616d6f"
-    "756e74293a436163686564206f626a6563742062616c616e6365206c656e67746820286e6f6e2d58525020616d6f75"
-    "6e74293a436163686564206f626a6563742062616c616e63653a494e464f3a206765745f6c65646765725f6f626a5f"
-    "6669656c642842616c616e636529206661696c65643a436163686564206e6573746564206669656c64206c656e6774"
-    "683a436163686564206e6573746564206669656c643a494e464f3a206765745f6c65646765725f6f626a5f6e657374"
-    "65645f6669656c64206e6f74206170706c696361626c653a436163686564206f626a656374205369676e6572732061"
-    "72726179206c656e6774683a436163686564206e6573746564206172726179206c656e6774683a494e464f3a206765"
-    "745f6c65646765725f6f626a5f6e65737465645f61727261795f6c656e206e6f74206170706c696361626c653a5355"
-    "43434553533a20416e79206c6564676572206f626a6563742066756e6374696f6e73494e464f3a2063616368655f6c"
-    "65646765725f6f626a206661696c65642028657870656374656420776974682074657374206669787475726573293a"
-    "494e464f3a206765745f6c65646765725f6f626a5f6669656c64206661696c65642061732065787065637465642028"
-    "6e6f20636163686564206f626a656374293a494e464f3a206765745f6c65646765725f6f626a5f6e65737465645f66"
-    "69656c64206661696c65642061732065787065637465643a494e464f3a206765745f6c65646765725f6f626a5f6172"
-    "7261795f6c656e206661696c65642061732065787065637465643a494e464f3a206765745f6c65646765725f6f626a"
-    "5f6e65737465645f61727261795f6c656e206661696c65642061732065787065637465643a535543434553533a2041"
-    "6e79206c6564676572206f626a6563742066756e6374696f6e732028696e7465726661636520746573746564294552"
-    "524f523a206163636f756e74726f6f745f6964206661696c656420666f722063616368696e6720746573743a2d2d2d"
-    "2043617465676f727920353a204b65796c65742047656e65726174696f6e2046756e6374696f6e73202d2d2d416363"
-    "6f756e74206b65796c65743a546573745479706543726564656e7469616c206b65796c65743a494e464f3a20637265"
-    "64656e7469616c5f6b65796c6574206661696c656420286578706563746564202d20696e7465726661636520697373"
-    "7565293a457363726f77206b65796c65743a4f7261636c65206b65796c65743a535543434553533a204b65796c6574"
-    "2067656e65726174696f6e2066756e6374696f6e734552524f523a206f7261636c655f6b65796c6574206661696c65"
-    "643a4552524f523a20657363726f775f6b65796c6574206661696c65643a4552524f523a206163636f756e74726f6f"
-    "745f6964206661696c65643a2d2d2d2043617465676f727920333a2043757272656e74204c6564676572204f626a65"
-    "63742046756e6374696f6e73202d2d2d43757272656e74206f626a6563742062616c616e6365206c656e6774682028"
-    "58525020616d6f756e74293a43757272656e74206f626a6563742062616c616e6365202873657269616c697a656420"
-    "58525020616d6f756e74293a43757272656e74206f626a6563742062616c616e6365206c656e67746820286e6f6e2d"
-    "58525020616d6f756e74293a43757272656e74206f626a6563742062616c616e63653a494e464f3a206765745f6375"
-    "7272656e745f6c65646765725f6f626a5f6669656c642842616c616e636529206661696c656420286d617920626520"
-    "6578706563746564293a43757272656e74206c6564676572206f626a656374206163636f756e743a494e464f3a2067"
-    "65745f63757272656e745f6c65646765725f6f626a5f6669656c64284163636f756e7429206661696c65643a437572"
-    "72656e74206e6573746564206669656c64206c656e6774683a43757272656e74206e6573746564206669656c643a49"
-    "4e464f3a206765745f63757272656e745f6c65646765725f6f626a5f6e65737465645f6669656c64206e6f74206170"
-    "706c696361626c653a43757272656e74206f626a656374205369676e657273206172726179206c656e6774683a4375"
-    "7272656e74206e6573746564206172726179206c656e6774683a494e464f3a206765745f63757272656e745f6c6564"
-    "6765725f6f626a5f6e65737465645f61727261795f6c656e206e6f74206170706c696361626c653a53554343455353"
-    "3a2043757272656e74206c6564676572206f626a6563742066756e6374696f6e736572726f725f636f64653d3d3d3d"
-    "20484f53542046554e4354494f4e532054455354203d3d3d54657374696e6720323620686f73742066756e6374696f"
-    "6e73535543434553533a20416c6c20686f73742066756e6374696f6e2074657374732070617373656421004d097072"
-    "6f64756365727302086c616e6775616765010452757374000c70726f6365737365642d6279010572757374631d312e"
-    "39352e30202835393830373631366520323032362d30342d313429002c0f7461726765745f6665617475726573022b"
-    "0f6d757461626c652d676c6f62616c732b087369676e2d657874";
-
-extern std::string const kDeepRecursionHex =
-    "0061736d010000000105016000017f030201000608017f0141c0843d0b0711010d657363726f775f66696e69736800"
-    "000a16011400230045044041010f0b230041016b240010000b";
-
-extern std::string const kAllKeyletsWasmHex =
-    "0061736d0100000001500a60067f7f7f7f7f7f017f60047f7f7f7f017f60087f7f7f7f7f7f7f7f017f60047f7f7f7f"
-    "0060037f7f7f017f60037f7f7e017f60057f7f7f7f7f017f6000017f60037f7f7f0060067f7f7f7f7f7e00029f0418"
-    "08686f73745f6c69620974726163655f6e756d000508686f73745f6c6962057472616365000608686f73745f6c6962"
-    "0863616368655f6c65000408686f73745f6c6962086c655f6669656c64000108686f73745f6c69620d686f6d655f6c"
-    "655f6669656c64000408686f73745f6c69620a74726163655f61636374000108686f73745f6c69620e6163636f756e"
-    "74726f6f745f6964000108686f73745f6c69620c74727573746c696e655f6964000208686f73745f6c696206616d6d"
-    "5f6964000008686f73745f6c696208636865636b5f6964000008686f73745f6c69620d63726564656e7469616c5f69"
-    "64000208686f73745f6c69620b64656c65676174655f6964000008686f73745f6c6962126465706f7369745f707265"
-    "617574685f6964000008686f73745f6c6962066469645f6964000108686f73745f6c696209657363726f775f696400"
-    "0008686f73745f6c69620f6d70745f69737375616e63655f6964000008686f73745f6c69620a6d70746f6b656e5f69"
-    "64000008686f73745f6c69620c6e66745f6f666665725f6964000008686f73745f6c6962086f666665725f69640000"
-    "08686f73745f6c69620a7061796368616e5f6964000208686f73745f6c6962167065726d697373696f6e65645f646f"
-    "6d61696e5f6964000008686f73745f6c69620a7369676e6572735f6964000108686f73745f6c6962097469636b6574"
-    "5f6964000008686f73745f6c6962087661756c745f6964000003070603030307080905030100110619037f01418080"
-    "c0000b7f0041c28ac0000b7f0041d08ac0000b073504066d656d6f727902000d657363726f775f66696e697368001b"
-    "0a5f5f646174615f656e6403010b5f5f686561705f6261736503020ae8370614002000200120022003418280204282"
-    "8020101d0b140020002001200220034181802042818020101d0bd10302017f017e230041a0016b2204240002402001"
-    "2d0000410146044041d780c000411620012802042201ac10001a200041013a0000200020013602040c010b20044118"
-    "6a200141196a290000370300200441106a200141116a290000370300200441086a200141096a290000370300200420"
-    "012900013703002002200320044120410110011a2004412041001002220141004c044041d080c00041072001ac1000"
-    "1a200041013a0000200020013602040c010b418b80c000410f4285801410001a20014185801420044180016a412010"
-    "032201412047044041af80c0004115417f20012001417f4e1b2201ac10001a200041013a0000200020013602040c01"
-    "0b200441c2006a20044182016a2d00003a0000200441f0006a20044197016a2900002205370300200441286a220120"
-    "04418f016a290000370300200441306a22022005370300200441386a22032004419f016a2d00003a0000200420042f"
-    "0080013b014020042004290087013703202004200428008301360043200441df006a20032d00003a0000200441d700"
-    "6a2002290300370000200441cf006a20012903003700002004200429032037004741c480c000410c200441406b4120"
-    "410110011a20004180023b01000b200441a0016a24000bd32c02097f027e23004180076b2200240041ed80c0004123"
-    "41014100410010011a02402000027f02404181802020004190016a220741141004220641144604402000410e6a2000"
-    "4192016a22032d00003a000020002000290097013703e80120002000419c016a22012900003700ed01200020002f00"
-    "90013b010c200020002903e8013703d806200020002900ed013700dd06200020002800930136000f200041186a2000"
-    "2900dd06370000200020002903d806370013419081c00041082000410c6a2204411410051a41838020200741141004"
-    "22064114470d03200041226a20032d00003a000020002000290097013703e801200020012900003700ed0120002000"
-    "2f0090013b0120200020002903e8013703d806200020002900ed013700dd0620002000280093013600232000412c6a"
-    "20002900dd06370000200020002903d806370027419881c000410c200041206a411410051a200041a8016a22034200"
-    "370300200041a0016a2201420037030020004198016a42003703002000420037039001200441142007412010062204"
-    "4120460d01024020044100480440200020043602380c010b2000417f3602380b41010c020b0c020b200041cd006a20"
-    "03290300370000200041c5006a20012903003700002000413d6a20004198016a290300370000200020002903900137"
-    "003541000b3a003420004190016a200041346a41a481c00041071019024020002d0090014101460440200028029401"
-    "2106419c8ac0004112420510001a0c010b4100210641ab81c000413541014100410010011a200041e6006a41c4003a"
-    "0000200041e0006a4100360200200041eb006a41003a0000200041d5a6013b01642000420037035820004100360067"
-    "200041a8016a22044200370300200041a0016a2203420037030020004198016a220142003703002000420037039001"
-    "02402000410c6a4114200041206a4114200041d8006a411420004190016a4120100722074120470440024020074100"
-    "480440200020073602700c010b2000417f3602700b410121060c010b20004185016a2004290300370000200041fd00"
-    "6a2003290300370000200041f5006a2001290300370000200020002903900137006d0b200020063a006c2000419001"
-    "6a200041ec006a41e081c0004109101a20002d00900141014604402000280294012106419c8ac0004112420510001a"
-    "0c010b4100210641e981c000413741014100410010011a200041f8016a200041306a2204280100360200200041f001"
-    "6a200041286a220329010037030020004184026a200041e0006a290300220a3702002000418c026a200041e8006a28"
-    "02002201360200200020002901203703e8012000200029035822093702fc01200041e8066a22052001360200200041"
-    "e0066a2207200a370300200020093703d806200041f4066a2003290100370200200041fc066a200428010036020020"
-    "0020002901203702ec0620004190026a200041d8066a22034128101c20004194016a200041e8016a41d000101c2000"
-    "410136029001200041f0066a220142003703002005420037030020074200370300200042003703d806024041ae8ac0"
-    "004114200041bc016a412820034120100822034120470440024020034100480440200020033602ec010c010b200041"
-    "7f3602ec010b410121060c010b20004181026a2001290300370000200041f9016a2005290300370000200041f1016a"
-    "2007290300370000200020002903d8063700e9010b200020063a00e801200041bc026a200041e8016a41a082c00041"
-    "03101920002d00bc02410146044020002802c0022106419c8ac0004112420610001a0c010b4100210641a382c00041"
-    "3141014100410010011a200041063602d80620004180026a22044200370300200041f8016a22034200370300200041"
-    "f0016a22014200370300200042003703e80102402000410c6a4114200041d8066a4104200041e8016a412010092207"
-    "4120470440024020074100480440200020073602c8020c010b2000417f3602c8020b410121060c010b200041dd026a"
-    "2004290300370000200041d5026a2003290300370000200041cd026a2001290300370000200020002903e8013700c5"
-    "020b200020063a00c402200041e8016a200041c4026a41d482c0004105101920002d00e801410146044020002802ec"
-    "012106419c8ac0004112420610001a0c010b41d982c000413341014100410010011a20004180026a42003703002000"
-    "41f8016a4200370300200041f0016a4200370300200042003703e801024002402000410c6a2201411420014114418c"
-    "83c0004112200041e8016a4120100a2201412047044041d780c0004116417f20012001417f4e1b2206ac10001a0c01"
-    "0b200041da066a20002d00ea013a0000200041f0026a200041f7016a290000220a370300200041f8026a200041ff01"
-    "6a290000220937030020004180036a20004187026a2d000022013a0000200041e7066a200a370000200041ef066a20"
-    "09370000200041f7066a20013a0000200020002f01e8013b01d806200020002900ef0122093703e802200020002800"
-    "eb013600db06200020093700df06419e83c000410a200041d8066a22014120410110011a2001412041001002220641"
-    "004c044041d080c00041072006ac10001a0c010b418b80c000410f4298802010001a200641988020200041e8016a41"
-    "14100322014114460d0141af80c0004115417f20012001417f4e1b2206ac10001a0b419c8ac0004112420710001a0c"
-    "010b419a80c000411541014100410010011a41a883c000413841014100410010011a230041206b2208240020084118"
-    "6a22074200370300200841106a22044200370300200841086a220342003703002008420037030020004184036a2201"
-    "027f2000410c6a22064114200041206a2202411420084120100b220541204704400240200541004804402001200536"
-    "02040c010b2001417f3602040b41010c010b20012008290300370001200141196a2007290300370000200141116a20"
-    "04290300370000200141096a200329030037000041000b3a0000200841206a2400200041e8016a2205200141e083c0"
-    "004108101920002d00e80145044041e883c000413641014100410010011a230041206b22082400200841186a220742"
-    "00370300200841106a22044200370300200841086a2203420037030020084200370300200041a8036a2201027f2006"
-    "41142002411420084120100c22024120470440024020024100480440200120023602040c010b2001417f3602040b41"
-    "010c010b20012008290300370001200141196a2007290300370000200141116a2004290300370000200141096a2003"
-    "29030037000041000b3a0000200841206a240020052001419e84c000410e101920002d00e801410146044020002802"
-    "ec012106419c8ac0004112420910001a0c020b41ac84c000413c41014100410010011a230041206b22022400200241"
-    "186a22074200370300200241106a22044200370300200241086a2203420037030020024200370300200041cc036a22"
-    "01027f2000410c6a411420024120100d22054120470440024020054100480440200120053602040c010b2001417f36"
-    "02040b41010c010b20012002290300370001200141196a2007290300370000200141116a2004290300370000200141"
-    "096a200329030037000041000b3a0000200241206a2400200041e8016a200141e884c0004103101920002d00e80141"
-    "0146044020002802ec012106419c8ac0004112420a10001a0c020b41eb84c000413141014100410010011a23004130"
-    "6b220224002002410b36020c200241286a22074200370300200241206a22044200370300200241186a220342003703"
-    "0020024200370310200041f0036a2201027f2000410c6a41142002410c6a4104200241106a4120100e220541204704"
-    "40024020054100480440200120053602040c010b2001417f3602040b41010c010b2001200229031037000120014119"
-    "6a2007290300370000200141116a2004290300370000200141096a200329030037000041000b3a0000200241306a24"
-    "00200041e8016a2001419c85c0004106101920002d00e801410146044020002802ec012106419c8ac0004112420b10"
-    "001a0c020b41a285c000413441014100410010011a230041306b220224002002410c36020c200241286a2207420037"
-    "0300200241206a22044200370300200241186a220342003703002002420037031020004194046a2201027f2000410c"
-    "6a41142002410c6a4104200241106a4120100f22054120470440024020054100480440200120053602040c010b2001"
-    "417f3602040b41010c010b20012002290310370001200141196a2007290300370000200141116a2004290300370000"
-    "200141096a200329030037000041000b3a0000200241306a2400200041fc016a2000411c6a280100360200200041f4"
-    "016a200041146a2901003702002000200029010c3702ec01200041808080e0003602e801200041d8066a2103230041"
-    "406a22042400024020012d0000410146044041d780c000411620012802042201ac10001a200341013a000020032001"
-    "3602040c010b200441206a200141196a290000370300200441186a200141116a290000370300200441106a20014109"
-    "6a2900003703002004200129000137030841d685c000410b200441086a22014120410110011a024002402001412041"
-    "001002220141004c044041d080c00041072001ac10001a0c010b418b80c000410f4284802010001a20014184802020"
-    "04412c6a4114100322014114460d0141af80c0004115417f20012001417f4e1b2201ac10001a0b200341013a000020"
-    "0320013602040c010b419a80c000411541014100410010011a20034180023b01000b200441406b240020002d00d806"
-    "410146044020002802dc062106419c8ac0004112420c10001a0c020b41e185c000413941014100410010011a230041"
-    "206b22022400200241186a22074200370300200241106a22044200370300200241086a220342003703002002420037"
-    "0300200041b8046a2201027f200041e8016a4118200041206a41142002412010102205412047044002402005410048"
-    "0440200120053602040c010b2001417f3602040b41010c010b20012002290300370001200141196a20072903003700"
-    "00200141116a2004290300370000200141096a200329030037000041000b3a0000200241206a2400200041d8066a20"
-    "01419a86c0004107101920002d00d806410146044020002802dc062106419c8ac0004112420d10001a0c020b41a186"
-    "c000413541014100410010011a230041306b220224002002410636020c200241286a22074200370300200241206a22"
-    "044200370300200241186a2203420037030020024200370310200041dc046a2201027f200041206a41142002410c6a"
-    "4104200241106a4120101122054120470440024020054100480440200120053602040c010b2001417f3602040b4101"
-    "0c010b20012002290310370001200141196a2007290300370000200141116a2004290300370000200141096a200329"
-    "030037000041000b3a0000200241306a2400200041d8066a200141d686c000410c101820002d00d806410146044020"
-    "002802dc062106419c8ac0004112420d10001a0c020b41e286c000413a41014100410010011a230041306b22022400"
-    "2002410d36020c200241286a22074200370300200241206a22044200370300200241186a2203420037030020024200"
-    "37031020004180056a2201027f2000410c6a41142002410c6a4104200241106a412010122205412047044002402005"
-    "4100480440200120053602040c010b2001417f3602040b41010c010b20012002290310370001200141196a20072903"
-    "00370000200141116a2004290300370000200141096a200329030037000041000b3a0000200241306a2400200041d8"
-    "066a2001419c87c0004105101920002d00d806410146044020002802dc062106419c8ac0004112420d10001a0c020b"
-    "41a187c000413341014100410010011a230041306b220224002002410e36020c200241286a22074200370300200241"
-    "206a22044200370300200241186a2203420037030020024200370310200041a4056a2201027f2000410c6a41142000"
-    "41206a41142002410c6a4104200241106a4120101322054120470440024020054100480440200120053602040c010b"
-    "2001417f3602040b41010c010b20012002290310370001200141196a2007290300370000200141116a200429030037"
-    "0000200141096a200329030037000041000b3a0000200241306a2400200041d8066a200141d487c000410a10192000"
-    "2d00d806410146044020002802dc062106419c8ac0004112420e10001a0c020b41de87c00041384101410041001001"
-    "1a230041306b220224002002410f36020c200241286a22074200370300200241206a22044200370300200241186a22"
-    "03420037030020024200370310200041c8056a2201027f2000410c6a41142002410c6a4104200241106a4120101422"
-    "054120470440024020054100480440200120053602040c010b2001417f3602040b41010c010b200120022903103700"
-    "01200141196a2007290300370000200141116a2004290300370000200141096a200329030037000041000b3a000020"
-    "0241306a2400200041d8066a2001419688c0004112101820002d00d806410146044020002802dc062106419c8ac000"
-    "4112420f10001a0c020b41a888c00041c00041014100410010011a230041206b22022400200241186a220742003703"
-    "00200241106a22044200370300200241086a2203420037030020024200370300200041ec056a2201027f2000410c6a"
-    "411420024120101522054120470440024020054100480440200120053602040c010b2001417f3602040b41010c010b"
-    "20012002290300370001200141196a2007290300370000200141116a2004290300370000200141096a200329030037"
-    "000041000b3a0000200241206a2400200041d8066a200141e888c000410a101a20002d00d806410146044020002802"
-    "dc062106419c8ac0004112421010001a0c020b41f288c000413841014100410010011a230041306b22022400200241"
-    "1236020c200241286a22074200370300200241206a22044200370300200241186a2203420037030020024200370310"
-    "20004190066a2201027f2000410c6a41142002410c6a4104200241106a412010162205412047044002402005410048"
-    "0440200120053602040c010b2001417f3602040b41010c010b20012002290310370001200141196a20072903003700"
-    "00200141116a2004290300370000200141096a200329030037000041000b3a0000200241306a2400200041d8066a20"
-    "0141aa89c0004106101920002d00d806410146044020002802dc062106419c8ac0004112421210001a0c020b410121"
-    "0641b089c000413441014100410010011a230041306b220224002002411336020c200241286a220742003703002002"
-    "41206a22044200370300200241186a2203420037030020024200370310200041b4066a2201027f2000410c6a411420"
-    "02410c6a4104200241106a4120101722054120470440024020054100480440200120053602040c010b2001417f3602"
-    "040b41010c010b20012002290310370001200141196a2007290300370000200141116a200429030037000020014109"
-    "6a200329030037000041000b3a0000200241306a2400200041d8066a200141e489c0004105101920002d00d8064101"
-    "46044020002802dc062106419c8ac0004112421310001a0c020b41e989c000413341014100410010011a0c010b2000"
-    "2802ec012106419c8ac0004112420810001a0b20004180076a240020060f0b418080c000410b417f20062006417f4e"
-    "1bac1000000bfd0401067f200241104f0440024020002000410020006b41037122056a22044f0d0020012103200504"
-    "40200521060340200020032d00003a0000200341016a2103200041016a2100200641016b22060d000b0b200541016b"
-    "4107490d000340200020032d00003a0000200041016a200341016a2d00003a0000200041026a200341026a2d00003a"
-    "0000200041036a200341036a2d00003a0000200041046a200341046a2d00003a0000200041056a200341056a2d0000"
-    "3a0000200041066a200341066a2d00003a0000200041076a200341076a2d00003a0000200341086a2103200041086a"
-    "22002004470d000b0b2004200220056b2207417c7122086a21000240200120056a2206410371450440200020044d0d"
-    "0120062101034020042001280200360200200141046a2101200441046a22042000490d000b0c010b200020044d0d00"
-    "2006410374220541187121032006417c71220241046a2101410020056b411871210520022802002102034020042002"
-    "2003762001280200220220057472360200200141046a2101200441046a22042000490d000b0b200741037121022006"
-    "20086a21010b02402000200020026a22064f0d002002410771220304400340200020012d00003a0000200141016a21"
-    "01200041016a2100200341016b22030d000b0b200241016b4107490d000340200020012d00003a0000200041016a20"
-    "0141016a2d00003a0000200041026a200141026a2d00003a0000200041036a200141036a2d00003a0000200041046a"
-    "200141046a2d00003a0000200041056a200141056a2d00003a0000200041066a200141066a2d00003a000020004107"
-    "6a200141076a2d00003a0000200141086a2101200041086a22002006470d000b0b0b940201017f230041406a220624"
-    "00024020012d0000410146044041d780c000411620012802042201ac10001a200041013a0000200020013602040c01"
-    "0b200641206a200141196a290000370300200641186a200141116a290000370300200641106a200141096a29000037"
-    "03002006200129000137030820022003200641086a22014120410110011a024002402001412041001002220141004c"
-    "044041d080c00041072001ac10001a0c010b418b80c000410f200510001a200120042006412c6a4114100322014114"
-    "460d0141af80c0004115417f20012001417f4e1b2201ac10001a0b200041013a0000200020013602040c010b419a80"
-    "c000411541014100410010011a20004180023b01000b200641406b24000b0bb80a0100418080c0000bae0a6572726f"
-    "725f636f64653d47657474696e67206669656c643a204669656c6420646174613a207265747269657665644572726f"
-    "722067657474696e67206669656c643a204669656c6420646174613a204572726f723a204572726f72206765747469"
-    "6e67206b65796c65743a202424242424205354415254494e47205741534d20455845435554494f4e20242424242441"
-    "63636f756e743a44657374696e6174696f6e3a4163636f756e744163636f756e74206f626a65637420657869737473"
-    "2c2070726f63656564696e67207769746820657363726f772066696e6973682e54727573746c696e6554727573746c"
-    "696e65206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f772066696e6973"
-    "682e414d4d414d4d206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f7720"
-    "66696e6973682e436865636b436865636b206f626a656374206578697374732c2070726f63656564696e6720776974"
-    "6820657363726f772066696e6973682e7465726d73616e64636f6e646974696f6e7343726564656e7469616c437265"
-    "64656e7469616c206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f772066"
-    "696e6973682e44656c656761746544656c6567617465206f626a656374206578697374732c2070726f63656564696e"
-    "67207769746820657363726f772066696e6973682e4465706f736974507265617574684465706f7369745072656175"
-    "7468206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f772066696e697368"
-    "2e444944444944206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f772066"
-    "696e6973682e457363726f77457363726f77206f626a656374206578697374732c2070726f63656564696e67207769"
-    "746820657363726f772066696e6973682e4d505449737375616e63654d505449737375616e6365206f626a65637420"
-    "6578697374732c2070726f63656564696e67207769746820657363726f772066696e6973682e4d50546f6b656e4d50"
-    "546f6b656e206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f772066696e"
-    "6973682e4e46546f6b656e4f666665724e46546f6b656e4f66666572206f626a656374206578697374732c2070726f"
-    "63656564696e67207769746820657363726f772066696e6973682e4f666665724f66666572206f626a656374206578"
-    "697374732c2070726f63656564696e67207769746820657363726f772066696e6973682e5061794368616e6e656c50"
-    "61794368616e6e656c206f626a656374206578697374732c2070726f63656564696e67207769746820657363726f77"
-    "2066696e6973682e5065726d697373696f6e6564446f6d61696e5065726d697373696f6e6564446f6d61696e206f62"
-    "6a656374206578697374732c2070726f63656564696e67207769746820657363726f772066696e6973682e5369676e"
-    "65724c6973745369676e65724c697374206f626a656374206578697374732c2070726f63656564696e672077697468"
-    "20657363726f772066696e6973682e5469636b65745469636b6574206f626a656374206578697374732c2070726f63"
-    "656564696e67207769746820657363726f772066696e6973682e5661756c745661756c74206f626a65637420657869"
-    "7374732c2070726f63656564696e67207769746820657363726f772066696e6973682e43757272656e742073657120"
-    "76616c75653a004d0970726f64756365727302086c616e6775616765010452757374000c70726f6365737365642d62"
-    "79010572757374631d312e38372e30202831373036376539616320323032352d30352d303929002c0f746172676574"
-    "5f6665617475726573022b0f6d757461626c652d676c6f62616c732b087369676e2d657874";
-
-extern std::string const kCodecovTestsWasmHex =
-    "0061736d01000000015c0c60067f7f7f7f7f7f017f60027f7f017f60047f7f7f7f017f60037f7f7f017f60077f7f7f"
-    "7f7f7f7f017f60087f7f7f7f7f7f7f7f017f60057f7f7f7f7f017f60017f017f60057f7f7f7f7f0060047f7f7f7f00"
-    "60017f006000017f02ee093708686f73745f6c6962057472616365000808686f73745f6c69620a6c6467725f696e64"
-    "6578000108686f73745f6c696210706172656e745f6c6467725f74696d65000108686f73745f6c696210706172656e"
-    "745f6c6467725f68617368000108686f73745f6c696208626173655f666565000108686f73745f6c696211616d656e"
-    "646d656e745f656e61626c6564000108686f73745f6c69620874785f6669656c64000308686f73745f6c69620e6163"
-    "636f756e74726f6f745f6964000208686f73745f6c69620863616368655f6c65000308686f73745f6c69620d686f6d"
-    "655f6c655f6669656c64000308686f73745f6c6962086c655f6669656c64000208686f73745f6c69620874785f696e"
-    "6e6572000208686f73745f6c69620d686f6d655f6c655f696e6e6572000208686f73745f6c6962086c655f696e6e65"
-    "72000608686f73745f6c69620a74785f6172725f6c656e000708686f73745f6c69620f686f6d655f6c655f6172725f"
-    "6c656e000708686f73745f6c69620a6c655f6172725f6c656e000108686f73745f6c69621074785f696e6e65725f61"
-    "72725f6c656e000108686f73745f6c696215686f6d655f6c655f696e6e65725f6172725f6c656e000108686f73745f"
-    "6c6962106c655f696e6e65725f6172725f6c656e000308686f73745f6c6962087365745f64617461000108686f7374"
-    "5f6c69620b7368613531325f68616c66000208686f73745f6c696209636865636b5f736967000008686f73745f6c69"
-    "62076e66745f757269000008686f73745f6c69620a6e66745f697373756572000208686f73745f6c6962096e66745f"
-    "7461786f6e000208686f73745f6c6962096e66745f666c616773000108686f73745f6c69620c6e66745f786665725f"
-    "666565000108686f73745f6c69620a6e66745f73657269616c000208686f73745f6c696208636865636b5f69640000"
-    "08686f73745f6c69620f666c6f61745f66726f6d5f75696e74000608686f73745f6c69620c74727573746c696e655f"
-    "6964000508686f73745f6c696206616d6d5f6964000008686f73745f6c69620d63726564656e7469616c5f69640005"
-    "08686f73745f6c69620a6d70746f6b656e5f6964000008686f73745f6c696209666c6f61745f636d70000208686f73"
-    "745f6c696209666c6f61745f616464000408686f73745f6c696209666c6f61745f737562000408686f73745f6c6962"
-    "0a666c6f61745f6d756c74000408686f73745f6c696209666c6f61745f646976000408686f73745f6c69620a666c6f"
-    "61745f726f6f74000008686f73745f6c696209666c6f61745f706f77000008686f73745f6c696209657363726f775f"
-    "6964000008686f73745f6c69620f6d70745f69737375616e63655f6964000008686f73745f6c69620c6e66745f6f66"
-    "6665725f6964000008686f73745f6c6962086f666665725f6964000008686f73745f6c6962096f7261636c655f6964"
-    "000008686f73745f6c69620a7061796368616e5f6964000508686f73745f6c6962167065726d697373696f6e65645f"
-    "646f6d61696e5f6964000008686f73745f6c6962097469636b65745f6964000008686f73745f6c6962087661756c74"
-    "5f6964000008686f73745f6c69620b64656c65676174655f6964000008686f73745f6c6962126465706f7369745f70"
-    "7265617574685f6964000008686f73745f6c6962066469645f6964000208686f73745f6c69620a7369676e6572735f"
-    "69640002030403090a0b05030100110619037f01418080c0000b7f0041da98c0000b7f0041e098c0000b073504066d"
-    "656d6f727902000d657363726f775f66696e69736800390a5f5f646174615f656e6403010b5f5f686561705f626173"
-    "6503020ac32e037201017f230041106b22042400024002402000200147044020022003410741014100100020004100"
-    "480d0120042000ad3703080c020b20042000ac370308200220034101200441086a41081000200441106a24000f0b20"
-    "042000ac3703080b418080c000410b4101200441086a41081000000b2801017f230041106b2201240020012000ac37"
-    "030841be91c000410b4101200141086a41081000000ba42d02087f017e230041a0026b2200240041c991c000412341"
-    "0741014100100020004100360260200041e0006a220141041001410441a090c000410a103720004100360260200141"
-    "041002410441908bc00041101037200042003703782000420037037020004200370368200042003703602001412010"
-    "03412041f180c0004110103720004100360260200141041004410441ff83c000410810372000428182848890a0c080"
-    "013703202000428182848890a0c080013703182000428182848890a0c080013703102000428182848890a0c0800137"
-    "030841ec91c000410e1005410141fa91c00041111037200041086a41201005410141fa91c000411110372000410036"
-    "02702000420037036820004200370360024002404181802020014114100622014100480d00200141144b0440417321"
-    "010c010b20014114460d0141818080807821010b20011038000b2000200029006c3700fd01200020002900673703f8"
-    "01200020002d00623a002e200020002f01603b012c2000200028006336002f200020002903f8013700332000200029"
-    "00fd013700382000420037037820004200370370200042003703682000420037036002402000412c6a4114200041e0"
-    "006a4120100722014120470440200141004e0d0120011038000b200020002d00623a0042200020002f01603b014020"
-    "00200029006f22083703800220002000280063360043200020002900673700472000200837004f2000200029007737"
-    "0057200020002d007f3a005f200041406b4120410010084101418b92c0004108103720004100360270200042003703"
-    "682000420037036041818020200041e0006a220241141009411441d38dc000410d1037200041003602702000420037"
-    "03682000420037036041014181802020024114100a4114418784c0004108103702404100200041e4006a22046b4103"
-    "71220320046a220120044d0d0020030440200321050340200441003a0000200441016a2104200541016b22050d000b"
-    "0b200341016b4107490d000340200441003a0000200441076a41003a0000200441066a41003a0000200441056a4100"
-    "3a0000200441046a41003a0000200441036a41003a0000200441026a41003a0000200441016a41003a000020044108"
-    "6a22042001470d000b0b2001413c20036b2203417c716a220420014b0440034020014100360200200141046a220120"
-    "04490d000b0b024020042003410371220320046a22054f0d002003220104400340200441003a0000200441016a2104"
-    "200141016b22010d000b0b200341016b4107490d000340200441003a0000200441076a41003a0000200441066a4100"
-    "3a0000200441056a41003a0000200441046a41003a0000200441036a41003a0000200441026a41003a000020044101"
-    "6a41003a0000200441086a22042005470d000b0b200041043602a00120004181802036026020004100360288022000"
-    "420037038002200042003703f80120024104200041f8016a22014114100b411441a280c00041081037200041003602"
-    "88022000420037038002200042003703f801200220002802a00120014114100c411441d084c000410d103720004100"
-    "360288022000420037038002200042003703f8014101200220002802a00120014114100d411441b08dc00041081037"
-    "4189803c100e4120419392c000410a10374189803c100f4120419d92c000410f103741014189803c1010412041ac92"
-    "c000410a1037200220002802a0011011412041b692c00041101037200220002802a0011012412041c692c000411510"
-    "374101200220002802a0011013412041db92c000411010372000412c6a220341141014411441eb92c0004108103720"
-    "0042003703900220004200370388022000420037038002200042003703f801200220002802a0012001412010154120"
-    "418f84c000410b103741f392c000410c41ff92c000410b418a93c000410e10164101419893c0004109103720002000"
-    "2903203703c001200020002903183703b801200020002903103703b001200020002903083703a801200041003b0188"
-    "022000420037038002200042003703f80120034114200041a8016a22054120200141121017411241d18fc000410710"
-    "3720004100360288022000420037038002200042003703f80120054120200141141018411441ac8fc000410a103720"
-    "0041003602f801200541202001410410194104419790c0004109103720054120101a410841a193c000410910372005"
-    "4120101b410a41aa93c000410c1037200041003602f8012005412020014104101c410441ba83c000410a103741b693"
-    "c000410d410420034114100041b693c000410d410541c393c0004108100041b693c000410d410541cb93c000410810"
-    "00417f41041003417141d393c00041181037200041003602f8012001417f1003417141a888c0004118103720004100"
-    "3a00fa01200041003b01f801200141031003417d41e790c000411e1037200041003602f8012001418094ebdc031003"
-    "417341bd8ec000411d10374102100e416f41eb93c00041191037417f20002802a00110114171418494c00041181037"
-    "2002417f10114171419c94c0004118103720024181081011417441b494c00041191037200041e094ebdc036a220420"
-    "002802a0011011417341cd94c000411810372000420037039002200042003703880220004200370380022000420037"
-    "03f801200341142004410820014120101d417341cc8cc0004114103720004200370390022000420037038802200042"
-    "0037038002200042003703f801200341142003411420014120101d417141918ec00041161037200042003703900220"
-    "004200370388022000420037038002200042003703f80120044108200141204100101e4173418b80c0004117103720"
-    "0042003703900220004200370388022000420037038002200042003703f801200220002802a001200141204100101e"
-    "417141a485c00041201037200420002802a00141011008417341e594c00041101037200220002802a0014101100841"
-    "7141f594c00041121037200042003703900220004200370388022000420037038002200042003703f8012004200028"
-    "02a001200141201007417341a78ec00041161037200042003703900220004200370388022000420037038002200042"
-    "003703f801200220002802a0012001412010074171418e83c000411810372000420037039002200042003703880220"
-    "00420037038002200042003703f8012003411420034114200420002802a00120014120101f4173418591c000411d10"
-    "37200042003703900220004200370388022000420037038002200042003703f8012003411420034114200220002802"
-    "a00120014120101f4171419581c000411f103720004200370390022000420037038802200042003703800220004200"
-    "3703f80141c698c0004114200420002802a001200141201020417341dc8ac000411510372000420037039002200042"
-    "00370388022000420037038002200042003703f80141c698c0004114200220002802a0012001412010204171418e89"
-    "c000411b1037200042003703900220004200370388022000420037038002200042003703f80141c698c00041144187"
-    "95c0004114200141201020417141a38ac0004125103720004200370390022000420037038802200042003703800220"
-    "0042003703f801419b95c000412841c698c00041142001412010204171418887c000412110372000200028013c3602"
-    "dc01200020002901343702d4012000200029012c3702cc01200041808080083602c801200041003b01f801200041c8"
-    "016a2207411841c698c0004114200141021020417141be80c000410a10372000422a3703e001200420002802a00141"
-    "01200041e0016a41081000200041003b01f8014102200141021006416f41b481c00041171037200041003b01f80141"
-    "02200141021009416f41f68ec000411c1037200041003b01f8014101410220014102100a416f41b586c00041171037"
-    "4102100e416f41eb93c000411910374102100f416f41c395c000411e1037410141021010416f41e195c00041191037"
-    "41ec91c0004181081005417441fa95c000411f103741ec91c00041c10010054174419996c000411a1037200041003b"
-    "01f801200241810820014102100b417441a987c00041161037200041003b01f801200241810820014102100c417441"
-    "aa90c000411b1037200041003b01f8014101200241810820014102100d417441db88c0004116103720024181081011"
-    "417441b396c000411e103720024181081012417441d196c00041231037410120024181081013417441f496c000411e"
-    "1037200241810810144174419297c0004116103741b693c00041810841ff92c000410b418a93c000410e1016417441"
-    "9893c0004109103741b693c000410d41ff92c000418108418a93c000410e10164174419893c0004109103741b693c0"
-    "00410d41ff92c000410b418a93c00041810810164174419893c00041091037200041003b01f8012002418108200141"
-    "021015417441c483c00041191037200041003b01f80141c698c00041810841c698c0004114200141021020417441dd"
-    "82c00041141037200041003b01f80120034114200341142002418108200141021021417441cc86c000411b10372000"
-    "41003b01f801200741810820034114200141021022417441c389c000411e103741b693c000410d4107200420002802"
-    "a0011000200042d487b6f4c7d4b1c0003700ec0141b693c000410d4103200041ec95ebdc036a22054108100041b693"
-    "c000410d4105200420002802a001100020054108200041ec016a220441081023417341a897c0004114103720044108"
-    "200541081023417341bc97c00041141037200041003b01f80120054108200441082001410241001024417341e08dc0"
-    "0041141037200041003b01f801200441082005410820014102410010244173418181c00041141037200041003b01f8"
-    "0120054108200441082001410241001025417341aa80c00041141037200041003b01f8012004410820054108200141"
-    "0241001025417341e08cc00041141037200041003b01f80120054108200441082001410241001026417341bb84c000"
-    "41151037200041003b01f80120044108200541082001410241001026417341c98bc00041151037200041003b01f801"
-    "20054108200441082001410241001027417341a683c00041141037200041003b01f801200441082005410820014102"
-    "41001027417341c88ac00041141037200041003b01f80120054108410320014102410010284173419488c000411410"
-    "37200041003b01f8012005410841032001410241001029417341ff85c0004113103720004200370390022000420037"
-    "0388022000420037038002200042003703f801200341142003411420014120102a417141c088c000411b1037200042"
-    "003703900220004200370388022000420037038002200042003703f801200341142003411420014120102b417141bc"
-    "82c00041211037200042003703900220004200370388022000420037038002200042003703f8012003411420034114"
-    "20014120102c417141928dc000411e1037200042003703900220004200370388022000420037038002200042003703"
-    "f801200341142003411420014120102d417141928fc000411a10372000420037039002200042003703880220004200"
-    "37038002200042003703f801200341142003411420014120102e417141b88dc000411b103720004200370390022000"
-    "4200370388022000420037038002200042003703f80120034114200341142003411420014120102f417141a291c000"
-    "411c1037200042003703900220004200370388022000420037038002200042003703f8012003411420034114200141"
-    "201030417141ef81c00041281037200042003703900220004200370388022000420037038002200042003703f80120"
-    "03411420034114200141201031417141b68fc000411b10372000420037039002200042003703880220004200370380"
-    "02200042003703f8012003411420034114200141201032417141a989c000411a1037200220002802a0014100100841"
-    "7141d097c000411b1037200041003b01f80120034114200220002802a001200141021017417141de87c000411a1037"
-    "200041003b01f801200220002802a001200141021018417141e285c000411d1037200041003b01f801200220002802"
-    "a001200141021019417141da8ec000411c1037200220002802a001101a417141eb97c000411c1037200220002802a0"
-    "01101b4171418798c000411f1037200041003602f801200220002802a00120014104101c417141f48dc000411d1037"
-    "200041003b01f801200220002802a001200141021007417141ff89c00041241037200041808080083602f401200041"
-    "003b01f801200220002802a001200041f4016a2205410420014102101d417141f48cc000411e1037200041003b01f8"
-    "01200220002802a00122062003411420022006200141021021417141dd84c00041241037200041003b01f801200341"
-    "14200220002802a001220620022006200141021021417141cb81c00041241037200041003b01f801200220002802a0"
-    "0120034114200141021033417141dd83c00041221037200041003b01f80120034114200220002802a0012001410210"
-    "33417141de8bc00041221037200041003b01f801200220002802a00120034114200141021034417141c880c0004129"
-    "1037200041003b01f80120034114200220002802a001200141021034417141a08bc00041291037200041003b01f801"
-    "200220002802a001200141021035417141f887c000411c1037200041003b01f801200220002802a001200541042001"
-    "4102102a417141f18ac000411f1037200041003b01f801200220002802a00120034114418795c00041142001410210"
-    "1f4171419286c00041231037200041003b01f80120034114200220002802a001418795c000411420014102101f4171"
-    "418185c00041231037200041003b01f801200220002802a0012005410420014102102b4171419782c0004125103720"
-    "0041003b01f80120074118200220002802a001200141021022417141ac8cc00041201037200041003b01f801200220"
-    "002802a0012005410420014102102c417141c590c00041221037200041003b01f801200220002802a0012005410420"
-    "014102102d417141e189c000411e1037200041003b01f801200220002802a0012005410420014102102e417141bf87"
-    "c000411f1037200041003b01f801200220002802a001200341142005410420014102102f417141e786c00041211037"
-    "200041003b01f80120034114200220002802a0012005410420014102102f4171419a84c00041211037200041003b01"
-    "f801200220002802a00120054104200141021030417141808cc000412c1037200041003b01f801200220002802a001"
-    "200141021036417141f78fc00041201037200041003b01f801200220002802a00120054104200141021031417141d8"
-    "8fc000411f1037200041003b01f801200220002802a00120054104200141021032417141c485c000411e1037200041"
-    "003b01f801200220002802a00141a698c0004120200141021017417141f182c000411d103741b693c000410d410420"
-    "0220002802a001100041b6a7abdd03410d410741a698c0004120100041b6a7abdd03410d410320044108100041b6a7"
-    "abdd03410d410420034114100041b6a7abdd03410d410541cb93c00041081000200220002802a00141072002418108"
-    "1000200042013703f8012002418108410120014108100041b693c000418108410320044108100041b693c000418108"
-    "410420034114100041b693c000418108410541cb93c0004108100041b693c000410d4105200220002802a001100020"
-    "0041003b019e02200220002802a001200341142000419e026a41021022417141f188c000411d103741b693c000410d"
-    "41e300200220002802a0011000410141004104200341141000200041a0026a240041010f0b000b0bb1180200418080"
-    "c0000b9b1554455354204641494c4544666c6f61745f66726f6d5f75696e745f6c656e5f6f6f6274785f696e6e6572"
-    "666c6f61745f7375625f6f6f625f736c69636531616d6d5f69645f6d70746465706f7369745f707265617574685f69"
-    "645f77726f6e675f73697a655f6163636f756e745f696431706172656e745f6c6467725f68617368666c6f61745f61"
-    "64645f6f6f625f736c6963653274727573746c696e655f69645f77726f6e675f6c656e5f63757272656e637974785f"
-    "6669656c645f696e76616c69645f736669656c6463726564656e7469616c5f69645f77726f6e675f73697a655f6163"
-    "636f756e745f6964327065726d697373696f6e65645f646f6d61696e5f69645f77726f6e675f73697a655f75696e74"
-    "33326d70745f69737375616e63655f69645f77726f6e675f73697a655f6163636f756e745f69646d70745f69737375"
-    "616e63655f69645f77726f6e675f73697a655f75696e743332616d6d5f69645f746f6f5f6269675f736c6963656e66"
-    "745f7572695f77726f6e675f73697a655f6163636f756e745f69646163636f756e74726f6f745f69645f77726f6e67"
-    "5f6c656e666c6f61745f6469765f6f6f625f736c696365316e66745f73657269616c7368613531325f68616c665f74"
-    "6f6f5f6269675f736c69636564656c65676174655f69645f77726f6e675f73697a655f6163636f756e745f69643162"
-    "6173655f6665656c655f6669656c647368613531325f68616c667061796368616e5f69645f77726f6e675f73697a65"
-    "5f6163636f756e745f696432666c6f61745f6d756c745f6f6f625f736c69636531686f6d655f6c655f696e6e657263"
-    "726564656e7469616c5f69645f77726f6e675f73697a655f6163636f756e745f69643174727573746c696e655f6964"
-    "5f77726f6e675f73697a655f6163636f756e745f696432666c6f61745f66726f6d5f75696e745f77726f6e675f6c65"
-    "6e5f75696e7436347661756c745f69645f77726f6e675f73697a655f6163636f756e745f69646e66745f6973737565"
-    "725f77726f6e675f73697a655f75696e74323536666c6f61745f706f775f6f6f625f736c69636574727573746c696e"
-    "655f69645f77726f6e675f73697a655f6163636f756e745f6964316c655f6669656c645f696e76616c69645f736669"
-    "656c6463726564656e7469616c5f69645f746f6f5f6269675f736c6963657061796368616e5f69645f77726f6e675f"
-    "73697a655f6163636f756e745f696431616d6d5f69645f6c656e5f77726f6e675f7872705f63757272656e63795f6c"
-    "656e74785f696e6e65725f746f6f5f6269675f736c6963656f7261636c655f69645f77726f6e675f73697a655f6163"
-    "636f756e745f69646e66745f7572695f77726f6e675f73697a655f75696e743235366469645f69645f77726f6e675f"
-    "73697a655f6163636f756e745f6964666c6f61745f726f6f745f6f6f625f736c696365706172656e745f6c6467725f"
-    "686173685f6e65675f6c656e657363726f775f69645f77726f6e675f73697a655f75696e7433326c655f696e6e6572"
-    "5f746f6f5f6269675f736c6963656d70746f6b656e5f69645f6d707469645f77726f6e675f6c656e677468616d6d5f"
-    "69645f6c656e5f77726f6e675f6c656e5f6173736574327661756c745f69645f77726f6e675f73697a655f75696e74"
-    "33326d70746f6b656e5f69645f746f6f5f6269675f736c6963655f6d707469646f666665725f69645f77726f6e675f"
-    "73697a655f6163636f756e745f69646163636f756e74726f6f745f69645f77726f6e675f73697a655f6163636f756e"
-    "745f6964616d6d5f69645f6c656e5f77726f6e675f6e6f6e5f7872705f63757272656e63795f6c656e666c6f61745f"
-    "6469765f6f6f625f736c69636532616d6d5f69645f6c656e5f6f6f625f617373657432657363726f775f69645f7772"
-    "6f6e675f73697a655f6163636f756e745f6964706172656e745f6c6467725f74696d656465706f7369745f70726561"
-    "7574685f69645f77726f6e675f73697a655f6163636f756e745f696432666c6f61745f6d756c745f6f6f625f736c69"
-    "63653264656c65676174655f69645f77726f6e675f73697a655f6163636f756e745f6964327065726d697373696f6e"
-    "65645f646f6d61696e5f69645f77726f6e675f73697a655f6163636f756e745f69646d70746f6b656e5f69645f7772"
-    "6f6e675f73697a655f6163636f756e745f6964636865636b5f69645f6f6f625f6c656e5f753332666c6f61745f7375"
-    "625f6f6f625f736c69636532636865636b5f69645f77726f6e675f73697a655f6163636f756e745f69646e66745f6f"
-    "666665725f69645f77726f6e675f73697a655f75696e7433326c655f696e6e65726f7261636c655f69645f77726f6e"
-    "675f73697a655f75696e743332686f6d655f6c655f6669656c64666c6f61745f6164645f6f6f625f736c696365316e"
-    "66745f73657269616c5f77726f6e675f73697a655f75696e74323536636865636b5f69645f77726f6e675f6c656e5f"
-    "7533326163636f756e74726f6f745f69645f6c656e5f6f6f62706172656e745f6c6467725f686173685f6c656e5f74"
-    "6f6f5f6c6f6e676e66745f7461786f6e5f77726f6e675f73697a655f75696e74323536686f6d655f6c655f6669656c"
-    "645f696e76616c69645f736669656c646f666665725f69645f77726f6e675f73697a655f75696e7433326e66745f69"
-    "73737565727469636b65745f69645f77726f6e675f73697a655f75696e7433326e66745f7572697469636b65745f69"
-    "645f77726f6e675f73697a655f6163636f756e745f69647369676e6572735f69645f77726f6e675f73697a655f6163"
-    "636f756e745f69646e66745f7461786f6e6c6467725f696e646578686f6d655f6c655f696e6e65725f746f6f5f6269"
-    "675f736c6963656e66745f6f666665725f69645f77726f6e675f73697a655f6163636f756e745f6964706172656e74"
-    "5f6c6467725f686173685f6275665f746f6f5f736d616c6c74727573746c696e655f69645f6c656e5f6f6f625f6375"
-    "7272656e63797061796368616e5f69645f77726f6e675f73697a655f75696e7433326572726f725f636f64653d2424"
-    "242424205354415254494e47205741534d20455845435554494f4e202424242424746573745f616d656e646d656e74"
-    "616d656e646d656e745f656e61626c656463616368655f6c6574785f6172725f6c656e686f6d655f6c655f6172725f"
-    "6c656e6c655f6172725f6c656e74785f696e6e65725f6172725f6c656e686f6d655f6c655f696e6e65725f6172725f"
-    "6c656e6c655f696e6e65725f6172725f6c656e7365745f6461746174657374206d6573736167657465737420707562"
-    "6b657974657374207369676e6174757265636865636b5f7369676e66745f666c6167736e66745f786665725f666565"
-    "74657374696e67207472616365400000000000005f4000000000000000706172656e745f6c6467725f686173685f6e"
-    "65675f70747274785f6172725f6c656e5f696e76616c69645f736669656c6474785f696e6e65725f6172725f6c656e"
-    "5f6e65675f70747274785f696e6e65725f6172725f6c656e5f6e65675f6c656e74785f696e6e65725f6172725f6c65"
-    "6e5f746f6f5f6c6f6e6774785f696e6e65725f6172725f6c656e5f7074725f6f6f6263616368655f6c655f7074725f"
-    "6f6f6263616368655f6c655f77726f6e675f6c656e55534430303030303030303030303030303030300041c395c000"
-    "0b8303686f6d655f6c655f6172725f6c656e5f696e76616c69645f736669656c646c655f6172725f6c656e5f696e76"
-    "616c69645f736669656c64616d656e646d656e745f656e61626c65645f746f6f5f6269675f736c696365616d656e64"
-    "6d656e745f656e61626c65645f746f6f5f6c6f6e6774785f696e6e65725f6172725f6c656e5f746f6f5f6269675f73"
-    "6c696365686f6d655f6c655f696e6e65725f6172725f6c656e5f746f6f5f6269675f736c6963656c655f696e6e6572"
-    "5f6172725f6c656e5f746f6f5f6269675f736c6963657365745f646174615f746f6f5f6269675f736c696365666c6f"
-    "61745f636d705f6f6f625f736c69636531666c6f61745f636d705f6f6f625f736c6963653263616368655f6c655f77"
-    "726f6e675f73697a655f75696e743235366e66745f666c6167735f77726f6e675f73697a655f75696e743235366e66"
-    "745f786665725f6665655f77726f6e675f73697a655f75696e74323536303030303030303030303030303030303030"
-    "3030303030303030303030303031004d0970726f64756365727302086c616e6775616765010452757374000c70726f"
-    "6365737365642d6279010572757374631d312e39352e30202835393830373631366520323032362d30342d31342900"
-    "2c0f7461726765745f6665617475726573022b0f6d757461626c652d676c6f62616c732b087369676e2d657874";
-
-extern std::string const kFloatTestsWasmHex =
-    "0061736d0100000001490960057f7f7f7f7f017f60077f7f7f7f7f7f7f017f60067f7f7f7f7f7f017f60047e7f7f7f"
-    "017f60057e7f7f7f7f017f60047f7f7f7f017f60037f7f7e017f60037f7f7f006000017f02ea021008686f73745f6c"
-    "6962057472616365000008686f73745f6c69620e666c6f61745f66726f6d5f696e74000308686f73745f6c69620f66"
-    "6c6f61745f66726f6d5f75696e74000003656e7613666c6f61745f66726f6d5f6d616e745f657870000408686f7374"
-    "5f6c696209666c6f61745f636d70000508686f73745f6c696209666c6f61745f616464000108686f73745f6c696209"
-    "666c6f61745f737562000108686f73745f6c69620a666c6f61745f6d756c74000108686f73745f6c696209666c6f61"
-    "745f646976000108686f73745f6c696209666c6f61745f706f77000208686f73745f6c69620974726163655f6e756d"
-    "000608686f73745f6c69620a666c6f61745f726f6f74000203656e760c666c6f61745f746f5f696e74000003656e76"
-    "11666c6f61745f746f5f6d616e745f657870000203656e7613666c6f61745f66726f6d5f7374616d6f756e74000003"
-    "656e7613666c6f61745f66726f6d5f73746e756d6265720000030302070805030100110619037f01418080c0000b7f"
-    "00418599c0000b7f00419099c0000b073504066d656d6f727902000d657363726f775f66696e69736800110a5f5f64"
-    "6174615f656e6403010b5f5f686561705f6261736503020aec20021f002000200141014100410010001a418080c000"
-    "41022002410c410110001a0bc920020c7f017e230041f0006b2200240041ee8ac000411d41014100410010001a2000"
-    "4100360268200042003703600240428ce000200041e0006a2202410c410010012201410c460440418b8bc000411720"
-    "02101041a28bc000411e2002410c410110001a0c010b41c08bc000411e41014100410010001a0b2000428ce0003703"
-    "500240200041d0006a4108200041e0006a2202410c41001002410c4604402001410c46210741de8bc0004117200210"
-    "100c010b41f58bc000411e41014100410010001a0b024042fb004102200041e0006a2201410c41001003410c460440"
-    "41938cc0004121200110100c010b41b48cc000412841014100410010001a410021070b41dc8cc000411541be80c000"
-    "101041f18cc0004116418881c0001010418280c000411741014100410010001a200041003602682000420037036002"
-    "404201200041e0006a2202410c410010012201410c460440419980c000410f200210100c010b41a880c00041164101"
-    "4100410010001a0b027f200041e0006a410c41be80c000410c100445044041ca80c000411b41014100410010001a20"
-    "01410c460c010b41e580c000412341014100410010001a41000b21080240200041e0006a410c418881c000410c1004"
-    "4101460440419481c000412341014100410010001a0c010b4100210841b781c000412c41014100410010001a0b0240"
-    "418881c000410c200041e0006a410c1004410246044041e381c000412341014100410010001a0c010b410021084186"
-    "82c000412c41014100410010001a0b419c93c000412041014100410010001a200041c680c000280000360258200041"
-    "be80c000290000370350410921030340200041d0006a2201410c41be80c000410c2001410c410010051a200341016b"
-    "22030d000b2000410036026820004200370360420a200041e0006a410c41001001410c46220945044041bc93c00041"
-    "1741014100410010001a0b0240200041e0006a410c200041d0006a410c100445044041d393c0004114410141004100"
-    "10001a0c010b4100210941e793c000411641014100410010001a0b410b21030340200041d0006a2201410c41be80c0"
-    "00410c2001410c410010061a200341016b22030d000b02402001410c418881c000410c100445044041fd93c0004119"
-    "41014100410010001a0c010b41002109419694c000411b41014100410010001a0b41878dc000411f41014100410010"
-    "001a2000410036021020004200370308420a200041086a410c410010011a200041c680c000280000360220200041be"
-    "80c000290000370318410621030340200041186a2201410c200041086a410c2001410c410010071a200341016b2203"
-    "0d000b200041003602582000420037035042c0843d200041d0006a2202410c410010011a02402002410c2001410c10"
-    "04220145044041a68dc000411941014100410010001a0c010b41bf8dc000411b41014100410010001a0b200145210a"
-    "410721030340200041186a2201410c200041086a410c2001410c410010081a200341016b22030d000b200041003602"
-    "68200042003703604201417f200041e0006a2202410c410010031a02402001410c2002410c100445044041da8dc000"
-    "411741014100410010001a0c010b41f18dc000411941014100410010001a4100210a0b41b282c00041174101410041"
-    "0010001a200041003602202000420037031841be80c000410c4103200041186a2202410c410010091a41c982c00041"
-    "1220021010418881c000410c41062002410c410010091a41db82c00041182002101020004100360258200042003703"
-    "504209200041d0006a2201410c410010011a2001410c41022002410c410010091a41f382c000411420021010200141"
-    "0c41002002410c410010091a418783c00041172002101020004100360268200042003703604200200041e0006a2203"
-    "410c410010011a2003410c41022002410c410010091a419e83c00041142002101041b283c00041382003410c410020"
-    "02410c41001009ac100a1a41ea83c000411841014100410010001a200041003602202000420037031842092002410c"
-    "410010011a20004100360258200042003703502002410c41022001410c4100100b1a418284c0004112200110102002"
-    "410c41032001410c4100100b1a419484c000411220011010200041003602682000420037036042c0843d2003410c41"
-    "0010011a2003410c41032001410c4100100b1a41a684c0004118200110102003410c41062001410c4100100b1a41be"
-    "84c000411c2001101041da84c000411a41014100410010001a20004100360258200042003703502000410036026820"
-    "004200370360420a2003410c410010011a41be80c000410c2003410c2001410c410010081a41f484c0004119200110"
-    "1041be80c000410c2001410c2001410c410010081a418d85c000410f2001101002402003410c2001410c1004220b45"
-    "0440419c85c000411441014100410010001a0c010b41b085c000411641014100410010001a0b4100210141c685c000"
-    "411a41014100410010001a20004200370308024041be80c000410c200041086a41084100100c220241084604402000"
-    "290308220c42015104404101210141e085c000411741014100410010001a0c020b41f785c000411941014100410010"
-    "001a419086c0004108200c100a1a0c010b419886c000412441014100410010001a41bc86c000410f2002ac100a1a0b"
-    "410021030240418881c000410c200041086a41084100100c220241084604402000290308220c427f51044041cb86c0"
-    "00411841014100410010001a200121030c020b41e386c000411a41014100410010001a419086c0004108200c100a1a"
-    "0c010b41fd86c000412541014100410010001a41bc86c000410f2002ac100a1a0b4100210120004100360220200042"
-    "0037031842ffffffffffffffffff00200041186a2202410c410010011a02402002410c200041086a41084100100c22"
-    "0241084604402000290308220c42ffffffffffffffffff0051044041a287c000411e41014100410010001a20032101"
-    "0c020b41c087c000412041014100410010001a41e087c000410d42ffffffffffffffffff00100a1a419086c0004108"
-    "200c100a1a0c010b41ed87c000412b41014100410010001a41bc86c000410f2002ac100a1a0b410021022000410036"
-    "0258200042003703504200200041d0006a2203410c410010011a02402003410c200041086a41084100100c22034108"
-    "4604402000290308220c500440419888c000411741014100410010001a200121020c020b41af88c000411941014100"
-    "410010001a419086c0004108200c100a1a0c010b41c888c000412441014100410010001a41bc86c000410f2003ac10"
-    "0a1a0b4100210320004100360268200042003703604201417f200041e0006a2201410c410010031a02402001410c20"
-    "0041086a41084100100c220141084604402000290308220c50044041ec88c000412541014100410010001a20022103"
-    "0c020b419189c000412741014100410010001a419086c0004108200c100a1a0c010b41b889c0004132410141004100"
-    "10001a41bc86c000410f2001ac100a1a0b0240200041e0006a410c200041086a41084101100c220141084604402000"
-    "290308220c50044041ea89c000412741014100410010001a0c020b4100210341918ac000412941014100410010001a"
-    "419086c0004108200c100a1a0c010b4100210341ba8ac000413441014100410010001a41bc86c000410f2001ac100a"
-    "1a0b41002101418a8ec000411f41014100410010001a2000420037032820004100360234024041be80c000410c2000"
-    "41286a4108200041346a4104100d2202410c46044020002802342202416e462000290328220c42808090bbbad6adf0"
-    "0d517145044041c58ec000411e41014100410010001a41e38ec000412f200c100a1a41928fc000411f2002ac100a1a"
-    "0c020b4101210141a98ec000411c41014100410010001a0c010b41b18fc000412941014100410010001a41bc86c000"
-    "410f2002ac100a1a0b2000420037033841002102200041003602440240418881c000410c200041386a4108200041c4"
-    "006a4104100d2204410c46044020002802442204416e462000290338220c428080f0c4c5a9d28f72517145044041f7"
-    "8fc000411f41014100410010001a419690c0004130200c100a1a41928fc000411f2004ac100a1a0c020b41da8fc000"
-    "411d41014100410010001a200121020c010b41c690c000412a41014100410010001a41bc86c000410f2004ac100a1a"
-    "0b410021012000410036025820004200370350420a200041d0006a2204410c410010011a2000420037030820004100"
-    "36024802402004410c200041086a4108200041c8006a4104100d2204410c46044020002802482204416f4620002903"
-    "08220c42808090bbbad6adf00d5171450440418d91c000411f41014100410010001a41e38ec000412f200c100a1a41"
-    "ac91c000411f2004ac100a1a0c020b41f090c000411d41014100410010001a200221010c010b41cb91c000412a4101"
-    "4100410010001a41bc86c000410f2004ac100a1a0b4100210220004100360268200042003703604200200041e0006a"
-    "2204410c410010011a200042003703182000410036024c02402004410c200041186a4108200041cc006a4104100d22"
-    "04410c4604402000290318220c50200028024c22044180808080784671450440419192c000411e4101410041001000"
-    "1a41af92c000411d200c100a1a41cc92c00041272004ac100a1a0c020b41f591c000411c41014100410010001a2001"
-    "21020c010b41f392c000412941014100410010001a41bc86c000410f2004ac100a1a0b4100210141b194c000412141"
-    "014100410010001a200042c0808080d0a0fdf000370018200041003602682000420037036002400240200041186a41"
-    "08200041e0006a2205410c4100100e2204410c46044041d294c000412220051010200042003703502005410c200041"
-    "d0006a41084100100c22044108470d012000290350220c4280c2d72f5104404101210141f494c000411d4101410041"
-    "0010001a0c030b419195c000411f41014100410010001a41b095c000411c200c100a1a0c020b418096c000411f4101"
-    "4100410010001a419f96c00041102004ac100a1a0c010b41cc95c000413441014100410010001a41bc86c000410f20"
-    "04ac100a1a0b4100210441af96c000412141014100410010001a200041ffffff8f7f36005820004281eceedce494cc"
-    "f9c000370050200041003602682000420037036002400240200041d0006a410c200041e0006a2206410c4100100f22"
-    "05410c46044041d096c000411c20061010200042003703182006410c200041186a41084100100c22054108470d0120"
-    "00290318220c42fb005104404101210441ec96c000411b41014100410010001a0c030b418797c000411d4101410041"
-    "0010001a41a497c0004116200c100a1a0c020b41ec97c000411d41014100410010001a419f96c00041102005ac100a"
-    "1a0c010b41ba97c000413241014100410010001a41bc86c000410f2005ac100a1a0b41002105024041be80c000410c"
-    "200041e0006a2206410c4100100f410c460440418998c000411a200610102006410c41be80c000410c100445044041"
-    "a398c000412041014100410010001a200421050c020b41c398c000412241014100410010001a0c010b41e598c00041"
-    "2041014100410010001a0b200041f0006a2400200b452007200871200971200a71200371200271200171200571710b"
-    "0b8f190100418080c0000b851920200a24242420746573745f666c6f61745f636d70202424242020666c6f61742066"
-    "726f6d20313a2020666c6f61742066726f6d20313a206661696c65640de0b6b3a7640000ffffffee2020666c6f6174"
-    "2066726f6d2031203d3d20464c4f41545f4f4e452020666c6f61742066726f6d203120213d20464c4f41545f4f4e45"
-    "2c206661696c6564f21f494c589c0000ffffffee2020666c6f61742066726f6d2031203e20464c4f41545f4e454741"
-    "544956455f4f4e452020666c6f61742066726f6d203120213e20464c4f41545f4e454741544956455f4f4e452c2066"
-    "61696c65642020464c4f41545f4e454741544956455f4f4e45203c20666c6f61742066726f6d20312020464c4f4154"
-    "5f4e454741544956455f4f4e4520213c20666c6f61742066726f6d20312c206661696c65640a24242420746573745f"
-    "666c6f61745f706f77202424242020666c6f61742063756265206f6620313a2020666c6f61742036746820706f7765"
-    "72206f66202d313a2020666c6f617420737175617265206f6620393a2020666c6f61742030746820706f776572206f"
-    "6620393a2020666c6f617420737175617265206f6620303a2020666c6f61742030746820706f776572206f66203020"
-    "28657870656374696e6720494e56414c49445f504152414d53206572726f72293a0a24242420746573745f666c6f61"
-    "745f726f6f74202424242020666c6f61742073717274206f6620393a2020666c6f61742063627274206f6620393a20"
-    "20666c6f61742063627274206f6620313030303030303a2020666c6f61742036746820726f6f74206f662031303030"
-    "3030303a0a24242420746573745f666c6f61745f696e76657274202424242020696e76657274206120666c6f617420"
-    "66726f6d2031303a2020696e7665727420616761696e3a2020696e766572742074776963653a20676f6f642020696e"
-    "766572742074776963653a206661696c65640a24242420746573745f666c6f61745f746f5f696e7420242424202066"
-    "6c6f61745f746f5f696e742831293a20676f6f642020666c6f61745f746f5f696e742831293a206661696c65642020"
-    "2020676f743a2020666c6f61745f746f5f696e742831293a206661696c65642077697468206572726f722020202065"
-    "72726f7220636f64653a2020666c6f61745f746f5f696e74282d31293a20676f6f642020666c6f61745f746f5f696e"
-    "74282d31293a206661696c65642020666c6f61745f746f5f696e74282d31293a206661696c65642077697468206572"
-    "726f722020666c6f61745f746f5f696e74286936343a3a4d4158293a20676f6f642020666c6f61745f746f5f696e74"
-    "286936343a3a4d4158293a206661696c65642020202065787065637465643a2020666c6f61745f746f5f696e742869"
-    "36343a3a4d4158293a206661696c65642077697468206572726f722020666c6f61745f746f5f696e742830293a2067"
-    "6f6f642020666c6f61745f746f5f696e742830293a206661696c65642020666c6f61745f746f5f696e742830293a20"
-    "6661696c65642077697468206572726f722020666c6f61745f746f5f696e7428302e312c20746f5f6e656172657374"
-    "293a20676f6f642020666c6f61745f746f5f696e7428302e312c20746f5f6e656172657374293a206661696c656420"
-    "20666c6f61745f746f5f696e7428302e312c20746f5f6e656172657374293a206661696c6564207769746820657272"
-    "6f722020666c6f61745f746f5f696e7428302e312c20746f77617264735f7a65726f293a20676f6f642020666c6f61"
-    "745f746f5f696e7428302e312c20746f77617264735f7a65726f293a206661696c65642020666c6f61745f746f5f69"
-    "6e7428302e312c20746f77617264735f7a65726f293a206661696c65642077697468206572726f720a242424207465"
-    "73745f666c6f61745f66726f6d5f7761736d202424242020666c6f61742066726f6d206936342031323330303a2020"
-    "666c6f61742066726f6d20693634203132333030206173204845583a2020666c6f61742066726f6d20693634203132"
-    "3330303a206661696c65642020666c6f61742066726f6d207536342031323330303a2020666c6f61742066726f6d20"
-    "7536342031323330303a206661696c65642020666c6f61742066726f6d2065787020322c206d616e74697373612031"
-    "32333a2020666c6f61742066726f6d2065787020322c206d616e7469737361203132333a206661696c65642020666c"
-    "6f61742066726f6d20636f6e737420313a2020666c6f61742066726f6d20636f6e7374202d313a0a24242420746573"
-    "745f666c6f61745f6d756c745f6469766964652024242420207265706561746564206d756c7469706c793a20676f6f"
-    "6420207265706561746564206d756c7469706c793a206661696c656420207265706561746564206469766964653a20"
-    "676f6f6420207265706561746564206469766964653a206661696c65640a24242420746573745f666c6f61745f746f"
-    "5f6d616e745f657870202424242020666c6f61745f746f5f6d616e745f6578702831293a20676f6f642020666c6f61"
-    "745f746f5f6d616e745f6578702831293a206661696c6564202020206578706563746564206d616e74697373612031"
-    "3030303030303030303030303030303030302c20676f743a202020206578706563746564206578706f6e656e74202d"
-    "31382c20676f743a2020666c6f61745f746f5f6d616e745f6578702831293a206661696c6564207769746820657272"
-    "6f722020666c6f61745f746f5f6d616e745f657870282d31293a20676f6f642020666c6f61745f746f5f6d616e745f"
-    "657870282d31293a206661696c6564202020206578706563746564206d616e7469737361202d313030303030303030"
-    "303030303030303030302c20676f743a2020666c6f61745f746f5f6d616e745f657870282d31293a206661696c6564"
-    "2077697468206572726f722020666c6f61745f746f5f6d616e745f657870283130293a20676f6f642020666c6f6174"
-    "5f746f5f6d616e745f657870283130293a206661696c6564202020206578706563746564206578706f6e656e74202d"
-    "31372c20676f743a2020666c6f61745f746f5f6d616e745f657870283130293a206661696c65642077697468206572"
-    "726f722020666c6f61745f746f5f6d616e745f6578702830293a20676f6f642020666c6f61745f746f5f6d616e745f"
-    "6578702830293a206661696c6564202020206578706563746564206d616e746973736120302c20676f743a20202020"
-    "6578706563746564206578706f6e656e74202d323134373438333634382c20676f743a2020666c6f61745f746f5f6d"
-    "616e745f6578702830293a206661696c65642077697468206572726f720a24242420746573745f666c6f61745f6164"
-    "645f7375627472616374202424242020666c6f61742066726f6d2031303a206661696c656420207265706561746564"
-    "206164643a20676f6f6420207265706561746564206164643a206661696c6564202072657065617465642073756274"
-    "726163743a20676f6f64202072657065617465642073756274726163743a206661696c65640a24242420746573745f"
-    "666c6f61745f66726f6d5f7374616d6f756e74202424242020666c6f61742066726f6d2058525020616d6f756e7420"
-    "2831303020585250293a202058525020616d6f756e7420636f6e76657273696f6e3a20676f6f64202058525020616d"
-    "6f756e7420636f6e76657273696f6e3a206661696c6564202020206578706563746564203130303030303030302c20"
-    "676f743a202058525020616d6f756e7420636f6e76657273696f6e3a206661696c6564202d20666c6f61745f746f5f"
-    "696e74206572726f722020666c6f61742066726f6d2058525020616d6f756e743a206661696c656420202020726573"
-    "756c745f73697a653a0a24242420746573745f666c6f61745f66726f6d5f73746e756d626572202424242020666c6f"
-    "61742066726f6d2053544e756d6265722028313233293a202053544e756d62657220636f6e76657273696f6e3a2067"
-    "6f6f64202053544e756d62657220636f6e76657273696f6e3a206661696c6564202020206578706563746564203132"
-    "332c20676f743a202053544e756d62657220636f6e76657273696f6e3a206661696c6564202d20666c6f61745f746f"
-    "5f696e74206572726f722020666c6f61742066726f6d2053544e756d6265723a206661696c65642020666c6f617420"
-    "66726f6d2053544e756d626572202831293a202053544e756d626572283129203d3d20464c4f41545f4f4e453a2067"
-    "6f6f64202053544e756d626572283129203d3d20464c4f41545f4f4e453a206661696c65642020666c6f6174206672"
-    "6f6d2053544e756d6265722831293a206661696c6564004d0970726f64756365727302086c616e6775616765010452"
-    "757374000c70726f6365737365642d6279010572757374631d312e39352e3020283539383037363136652032303236"
-    "2d30342d313429002c0f7461726765745f6665617475726573022b0f6d757461626c652d676c6f62616c732b087369"
-    "676e2d657874";
-
-extern std::string const kFloat0Hex =
-    "0061736d0100000001290560057f7f7f7f7f017f60047e7f7f7f017f60077f7f7f7f7f7f7f017f60047f7f7f7f017f"
-    "6000017f02560408686f73745f6c6962057472616365000008686f73745f6c69620e666c6f61745f66726f6d5f696e"
-    "74000108686f73745f6c696209666c6f61745f737562000208686f73745f6c696209666c6f61745f636d7000030302"
-    "010405030100110619037f01418080c0000b7f0041e980c0000b7f0041f080c0000b073504066d656d6f727902000d"
-    "657363726f775f66696e69736800040a5f5f646174615f656e6403010b5f5f686561705f6261736503020acc0101c9"
-    "0101027f230041206b22002400418080c000411541014100410010001a200041003602082000420037030020004100"
-    "3602182000420037031002400240420a2000410c41001001410c4604402000410c2000410c200041106a2201410c41"
-    "001002410c470d012001410c419580c000410c100345044041a180c000411a41014100410010001a0c030b41bb80c0"
-    "00411941014100410010001a0c020b41d480c000411541014100410010001a0c010b41d480c0004115410141004100"
-    "10001a0b200041206a240041010b0b720100418080c0000b690a24242420746573745f666c6f61745f302024242400"
-    "00000000000000800000002020464c4f41545f5a45524f20636f6d706172653a20676f6f642020464c4f41545f5a45"
-    "524f20636f6d706172653a206261642020666c6f61742031302d31303a206661696c6564004d0970726f6475636572"
-    "7302086c616e6775616765010452757374000c70726f6365737365642d6279010572757374631d312e39352e302028"
-    "35393830373631366520323032362d30342d313429002c0f7461726765745f6665617475726573022b0f6d75746162"
-    "6c652d676c6f62616c732b087369676e2d657874";
-
-extern std::string const kDisabledFloatHex =
-    "0061736d010000000108026000006000017f03030200010503010002063e0a7f004180080b7f004180080b7f004180"
-    "100b7f004180100b7f00418090040b7f004180080b7f00418090040b7f00418080080b7f0041000b7f0041010b07b7"
-    "010d066d656d6f72790200115f5f7761736d5f63616c6c5f63746f727300000d657363726f775f66696e6973680001"
-    "0362756603000c5f5f64736f5f68616e646c6503010a5f5f646174615f656e6403020b5f5f737461636b5f6c6f7703"
-    "030c5f5f737461636b5f6869676803040d5f5f676c6f62616c5f6261736503050b5f5f686561705f6261736503060a"
-    "5f5f686561705f656e6403070d5f5f6d656d6f72795f6261736503080c5f5f7461626c655f6261736503090a150202"
-    "000b100043000000c54300200045931a41010b";
-
-extern std::string const kMemoryPointerAtLimitHex =
-    "0061736d010000000105016000017f0302010005030100010711010d657363726f775f66696e69736800000a0e010c"
-    "0041ffff032d00001a41010b";
-
-extern std::string const kMemoryPointerOverLimitHex =
-    "0061736d010000000105016000017f0302010005030100010711010d657363726f775f66696e69736800000a0e010c"
-    "00418080042d00001a41010b";
-
-extern std::string const kMemoryOffsetOverLimitHex =
-    "0061736d010000000105016000017f030201000503010001071a02066d656d6f727902000d657363726f775f66696e"
-    "69736800000a0e010c00410028028080041a41010b";
-
-extern std::string const kMemoryEndOfWordOverLimitHex =
-    "0061736d010000000105016000017f030201000503010001071a02066d656d6f727902000d657363726f775f66696e"
-    "69736800000a0e010c0041feff032802001a41010b";
-
-extern std::string const kMemoryGrow0To1PageHex =
-    "0061736d010000000105016000017f030201000503010000071a02066d656d6f727902000d657363726f775f66696e"
-    "69736800000a0b010900410140001a41010b";
-
-extern std::string const kMemoryGrow1To0PageHex =
-    "0061736d010000000105016000017f030201000503010001071a02066d656d6f727902000d657363726f775f66696e"
-    "69736800000a13011100417f4000417f460440417f0f0b41010b";
-
-extern std::string const kMemoryLastByteOf8MbHex =
-    "0061736d010000000105016000017f030201000506010180018001071a02066d656d6f727902000d657363726f775f"
-    "66696e69736800000a0f010d0041ffffff032d00001a41010b";
-
-extern std::string const kMemoryGrow1MoreThan8MbHex =
-    "0061736d010000000105016000017f03020100050401008001071a02066d656d6f727902000d657363726f775f6669"
-    "6e69736800000a1301110041014000417f460440417f0f0b41010b";
-
-extern std::string const kMemoryGrow0MoreThan8MbHex =
-    "0061736d010000000105016000017f03020100050401008001071a02066d656d6f727902000d657363726f775f6669"
-    "6e69736800000a1301110041004000417f460440417f0f0b41010b";
-
-extern std::string const kMemoryInit1MoreThan8MbHex =
-    "0061736d010000000105016000017f030201000506010181018101071a02066d656d6f727902000d657363726f775f"
-    "66696e69736800000a0f010d0041ffffff032d00001a41010b";
-
-extern std::string const kMemoryNegativeAddressHex =
-    "0061736d010000000105016000017f030201000506010180018001071a02066d656d6f727902000d657363726f775f"
-    "66696e69736800000a0c010a00417f2d00001a41010b";
-
-extern std::string const kTable64ElementsHex =
-    "0061736d010000000108026000006000017f03030200010404017000400711010d657363726f775f66696e69736800"
-    "010946010041000b400000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "00000000000000000000000000000000000000000000000000000a090202000b040041010b";
-
-extern std::string const kTable65ElementsHex =
-    "0061736d010000000108026000006000017f03030200010404017000410711010d657363726f775f66696e69736800"
-    "010947010041000b410000000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000000000000000000000000a090202000b040041010b";
-
-extern std::string const kTable2TablesHex =
-    "0061736d010000000108026000006000017f030302000104090270010101700101010711010d657363726f775f6669"
-    "6e6973680001090f020041000b0100020141000b0001000a090202000b040041010b";
-
-extern std::string const kTable0ElementsHex =
-    "0061736d010000000105016000017f030201000404017000000711010d657363726f775f66696e69736800000a0601"
-    "040041010b";
-
-extern std::string const kTableUintMaxHex =
-    "0061736d010000000105016000017f030201000408017000ffffffff0f0711010d657363726f775f66696e69736800"
-    "000a0601040041010b";
-
-extern std::string const kProposalMutableGlobalHex =
-    "0061736d010000000105016000017f030201000606017f0141000b071b0207636f756e74657203000d657363726f77"
-    "5f66696e69736800000a0d010b00230041016a240041010b";
-
-extern std::string const kProposalGcStructNewHex =
-    "0061736d01000000010b026000017f5f027f017f01030201000711010d657363726f775f66696e69736800000a0a01"
-    "0800fb01011a41010b";
-
-extern std::string const kProposalMultiValueHex =
-    "0061736d010000000110036000027f7f6000017f60027f7f017f03030200010711010d657363726f775f66696e6973"
-    "6800010a14020600410a41140b0b00100002026a411e460b0b";
-
-extern std::string const kProposalSignExtHex =
-    "0061736d010000000105016000017f030201000711010d657363726f775f66696e69736800000a0b01090041ff01c0"
-    "417f460b";
-
-extern std::string const kProposalFloatToIntHex =
-    "0061736d010000000105016000017f030201000711010d657363726f775f66696e69736800000a1201100043f90215"
-    "50fc0041ffffffff07460b";
-
-extern std::string const kProposalBulkMemoryHex =
-    "0061736d010000000105016000017f030201000503010001071a02066d656d6f727902000d657363726f775f66696e"
-    "69736800000a1f011d004100412a3a000041e40041004101fc0a000041e4002d0000412a460b";
-
-extern std::string const kProposalRefTypesHex =
-    "0061736d010000000105016000017f020f0103656e76057461626c65016f0001030201000711010d657363726f775f"
-    "66696e69736800000a0c010a004100d06f260041010b";
-
-extern std::string const kProposalTailCallHex =
-    "0061736d010000000105016000017f03030200000711010d657363726f775f66696e69736800010a0b02040041010b"
-    "040012000b";
-
-extern std::string const kProposalExtendedConstHex =
-    "0061736d010000000105016000017f030201000609017f00410a41206a0b0711010d657363726f775f66696e697368"
-    "00000a090107002300412a460b";
-
-extern std::string const kProposalMultiMemoryHex =
-    "0061736d010000000105016000017f03020100050502000000010711010d657363726f775f66696e69736800000a06"
-    "0104003f010b";
-
-extern std::string const kProposalCustomPageSizesHex =
-    "0061736d010000000105016000017f030201000504010801000711010d657363726f775f66696e69736800000a0601"
-    "040041010b";
-
-extern std::string const kProposalMemory64Hex =
-    "0061736d010000000105016000017f0302010005030104010711010d657363726f775f66696e69736800000a10010e"
-    "004200412a3a00003f004201510b";
-
-extern std::string const kProposalWideArithmeticHex =
-    "0061736d010000000105016000017f030201000711010d657363726f775f66696e69736800000a0e010c0042014202"
-    "fc161a1a41010b";
-
-extern std::string const kTrapDivideBy0Hex =
-    "0061736d010000000105016000017f030201000711010d657363726f775f66696e69736800000a0c010a00412a4100"
-    "6d1a41010b";
-
-extern std::string const kTrapIntOverflowHex =
-    "0061736d010000000105016000017f030201000711010d657363726f775f66696e69736800000a0d010b0041808080"
-    "8078417f6d0b";
-
-extern std::string const kTrapUnreachableHex =
-    "0061736d010000000105016000017f030201000711010d657363726f775f66696e69736800000a070105000041010"
-    "b";
-
-extern std::string const kTrapNullCallHex =
-    "0061736d010000000105016000017f030201000404017000010711010d657363726f775f66696e69736800000a0901"
-    "070041001100000b";
-
-extern std::string const kTrapFuncSigMismatchHex =
-    "0061736d010000000108026000006000017f03030200010404017000010711010d657363726f775f66696e69736800"
-    "010907010041000b01000a0d020300010b070041001101000b";
-
-extern std::string const kWasiGetTimeHex =
-    "0061736d01000000010c0260037f7e7f017f6000017f02290116776173695f736e617073686f745f70726576696577"
-    "310e636c6f636b5f74696d655f6765740000030201010503010001071a02066d656d6f727902000d657363726f775f"
-    "66696e69736800010a16011400410042e8074100100045047f410105417f0b0b";
-
-extern std::string const kWasiPrintHex =
-    "0061736d01000000010d0260047f7f7f7f017f6000017f02230116776173695f736e617073686f745f707265766965"
-    "77310866645f77726974650000030201010503010001071a02066d656d6f727902000d657363726f775f66696e6973"
-    "6800010a1d011b01017f411821004101410041012000100045047f410105417f0b0b0b1e030041100b0648656c6c6f"
-    "0a0041000b04100000000041040b0406000000";
-
-// The following several wasm hex strings are for testing wasm section
-// corruption cases. They are illegal hence do not have corresponding
-// rust or wat sources.
-// Wasm code magic number is "0061736d", and the only valid version is 1.
-
-extern std::string const kBadMagicNumberHex = "1061736d01000000";
-extern std::string const kBadVersionNumberHex = "0061736d02000000";
-
-// Corruption Test: lyingHeader
-// Scenario: A section declares it is 2GB long, but the file ends immediately.
-// Attack: Buffer pre-allocation DoS (OOM).
-// # Magic (00 61 73 6d) + Version (01 00 00 00)
-// data = b'\x00\x61\x73\x6d\x01\x00\x00\x00'
-// # Type Section (ID 1)
-// # Size: LEB128 encoded 2GB (0x80 0x80 0x80 0x80 0x08)
-// data += b'\x01\x80\x80\x80\x80\x08'
-extern std::string const kLyingHeaderHex = "0061736d01000000018080808008";
-
-// Corruption Test: neverEndingNumber
-// Scenario: An LEB128 integer that never has a stop bit (byte < 0x80).
-// Attack: Infinite loop in parser or read out of bounds.
-// data = b'\x00\x61\x73\x6d\x01\x00\x00\x00'
-// # Type Section (ID 1), Size 5
-// data += b'\x01\x05'
-// # Vector count: Infinite stream of 0x80 (100 bytes)
-// data += b'\x80' * 100
-extern std::string const kNeverEndingNumberHex =
-    "0061736d01000000010580808080808080808080808080808080808080808080808080808080808080808080808080"
-    "8080808080808080808080808080808080808080808080808080808080808080808080808080808080808080808080"
-    "80808080808080808080808080808080";
-
-// Corruption Test: vectorLie
-// Scenario: A vector declares it has 4 billion items, but provides none.
-// Attack: Vector pre-allocation DoS (OOM).
-// data = b'\x00\x61\x73\x6d\x01\x00\x00\x00'
-// # Type Section (ID 1)
-// # Size 5 (just enough for the count bytes)
-// data += b'\x01\x05'
-// # Vector Count: 0xFF 0xFF 0xFF 0xFF 0x0F (4,294,967,295 items)
-// data += b'\xff\xff\xff\xff\x0f'
-// # No actual items follow...
-extern std::string const kVectorLieHex = "0061736d010000000105ffffffff0f";
-
-// Corruption Test: sectionOrdering
-// Scenario: Sections appear out of order
-//           (Code section before Function section).
-// Attack: Parser state confusion / potential null pointer deref.
-// data = b'\x00\x61\x73\x6d\x01\x00\x00\x00'
-// # Code Section (ID 10) - usually last
-// # Size 2, Count 0
-// data += b'\x0a\x02\x00\x0b'
-// # Function Section (ID 3) - usually 3rd
-// data += b'\x03\x02\x00\x00'
-extern std::string const kSectionOrderingHex = "0061736d010000000a02000b03020000";
-
-// Corruption Test: ghostPayload
-// Scenario: Valid headers, but file is truncated in the middle of a payload.
-// Attack: Read out of bounds panic.
-// data = b'\x00\x61\x73\x6d\x01\x00\x00\x00'
-// # Type Section (ID 1), Size 10
-// data += b'\x01\x0a'
-// # Content: Count 1
-// data += b'\x01'
-// # Start of a type definition (0x60 = func)
-// data += b'\x60'
-// # File ends abruptly here (missing params/results)
-extern std::string const kGhostPayloadHex = "0061736d01000000010a0160";
-
-// Corruption Test: junkAfterSection
-// Scenario: Section declares size X, but logical content finishes at X-5.
-// Attack: Validation bypass if parser stops early,
-//         or panic if strict check missing.
-// data = b'\x00\x61\x73\x6d\x01\x00\x00\x00'
-// # Type Section (ID 1), Size 10 bytes
-// data += b'\x01\x0a'
-// # Real content: Count 1, (func -> void) = 4 bytes
-// # \x01 (count) \x60 (func) \x00 (0 params) \x00 (0 results)
-// data += b'\x01\x60\x00\x00'
-// # Remaining 6 bytes are junk padding within the section size
-// data += b'\x00' * 6
-extern std::string const kJunkAfterSectionHex = "0061736d01000000010a01600000000000000000";
-
-// Corruption Test: invalidSectionId
-// Scenario: A section ID that doesn't exist (0xFF).
-// Attack: Default case handling / unhandled enum variant.
-// data = b'\x00\x61\x73\x6d\x01\x00\x00\x00'
-// # Section ID 0xFF, Size 1
-// data += b'\xff\x01\x00'
-extern std::string const kInvalidSectionIdHex = "0061736d01000000ff0100";
-
-// Corruption Test: localVariableBomb
-// Scenario: A function declares 4 billion local variables.
-// Attack: Stack Overflow / OOM during function init (memset).
-// data = b'\x00\x61\x73\x6d\x01\x00\x00\x00'
-// # 1. Type Section: (func) -> ()
-// data += b'\x01\x04\x01\x60\x00\x00'
-// # 3. Function Section: 1 function of type 0
-// data += b'\x03\x02\x01\x00'
-// # 10. Code Section
-// # ID 10, Size 15 (estimated), Count 1
-// data += b'\x0a\x0f\x01'
-// # Function Body Size: 13 bytes
-// data += b'\x0d'
-// # Local Declarations Count: 1 entry
-// data += b'\x01'
-// # The Bomb: 4,294,967,295 locals of type i32
-// # Count: 0xFF 0xFF 0xFF 0xFF 0x0F
-// # Type: 0x7F (i32)
-// data += b'\xff\xff\xff\xff\x0f\x7f'
-// # Instruction: end (0x0b)
-// data += b'\x0b'
-extern std::string const kLocalVariableBombHex =
-    "0061736d01000000010401600000030201000a0f010d01ffffffff0f7f0b";
-
-extern std::string const kInfiniteLoopWasmHex =
-    "0061736d010000000108026000006000017f030302000105030100020638097f004180080b7f004180080b7f004180"
-    "080b7f00418088040b7f004180080b7f00418088040b7f00418080080b7f0041000b7f0041010b07a8010c066d656d"
-    "6f72790200115f5f7761736d5f63616c6c5f63746f72730000046c6f6f7000010c5f5f64736f5f68616e646c650300"
-    "0a5f5f646174615f656e6403010b5f5f737461636b5f6c6f7703020c5f5f737461636b5f6869676803030d5f5f676c"
-    "6f62616c5f6261736503040b5f5f686561705f6261736503050a5f5f686561705f656e6403060d5f5f6d656d6f7279"
-    "5f6261736503070c5f5f7461626c655f6261736503080a270202000b220041fc87044100360200034041fc870441fc"
-    "870428020041016a3602000c000b000b007f0970726f647563657273010c70726f6365737365642d62790105636c61"
-    "6e675f31392e312e352d776173692d73646b202868747470733a2f2f6769746875622e636f6d2f6c6c766d2f6c6c76"
-    "6d2d70726f6a6563742061623462356132646235383239353861663165653330386137393063666462343262643234"
-    "3732302900490f7461726765745f6665617475726573042b0f6d757461626c652d676c6f62616c732b087369676e2d"
-    "6578742b0f7265666572656e63652d74797065732b0a6d756c746976616c7565";
-
-extern std::string const kStartLoopHex =
-    "0061736d010000000108026000006000017f030302000107190205737461727400000d657363726f775f66696e6973"
-    "6800010801000a0e02070003400c000b0b040041010b";
-
-extern std::string const kBadAlignWasmHex =
-    "0061736d01000000011b046000017f60057f7f7f7f7f017f60067f7f7f7f7f7f017f60000002260203656e760f666c"
-    "6f61745f66726f6d5f75696e74000103656e7608636865636b5f6964000203050403000000050301000306470b7f00"
-    "4180080b7f00418088020b7f004180080b7f00418088040b7f00418088040b7f00418088080b7f004180080b7f0041"
-    "8088080b7f004180800c0b7f0041000b7f0041010b07cc0110066d656d6f72790200115f5f7761736d5f63616c6c5f"
-    "63746f72730002057465737431000307655f64617461310300057465737432000407655f6461746132030104746573"
-    "7400050c5f5f64736f5f68616e646c6503020a5f5f646174615f656e6403030b5f5f737461636b5f6c6f7703040c5f"
-    "5f737461636b5f6869676803050d5f5f676c6f62616c5f6261736503060b5f5f686561705f6261736503070a5f5f68"
-    "6561705f656e6403080d5f5f6d656d6f72795f6261736503090c5f5f7461626c655f62617365030a0a99020402000b"
-    "2801017f418108427f370000418108410841a308410c41001000220041a40828020020004100481b0b5f01017f419a"
-    "88024191a4cca00136010041928802428994ace0d0c1c38710370100418a88024281848ca0d0c0c183083701004181"
-    "8802417f360000418a8802411441818802410441a3880241201001220041a4880228020020004100481b0b8a010103"
-    "7f418108427f370000418108410841a308410c410010002100419a88024191a4cca00136010041928802428994ace0"
-    "d0c1c38710370100418a88024281848ca0d0c0c1830837010041818802417f36000041a4082802002101418a880241"
-    "1441818802410441a3880241201001220241a4880228020020024100481b2000200120004100481b6a0b007f097072"
-    "6f647563657273010c70726f6365737365642d62790105636c616e675f31392e312e352d776173692d73646b202868"
-    "747470733a2f2f6769746875622e636f6d2f6c6c766d2f6c6c766d2d70726f6a656374206162346235613264623538"
-    "32393538616631656533303861373930636664623432626432343732302900490f7461726765745f66656174757265"
-    "73042b0f6d757461626c652d676c6f62616c732b087369676e2d6578742b0f7265666572656e63652d74797065732b"
-    "0a6d756c746976616c7565";
-
-extern std::string const kThousandParamsHex =
-    "0061736d0100000001f1070260000060e8077f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f017f030302000105030100020638097f"
-    "004180080b7f004180080b7f004180080b7f00418088040b7f004180080b7f00418088040b7f00418080080b7f0041"
-    "000b7f0041010b07a8010c066d656d6f72790200115f5f7761736d5f63616c6c5f63746f7273000004746573740001"
-    "0c5f5f64736f5f68616e646c6503000a5f5f646174615f656e6403010b5f5f737461636b5f6c6f7703020c5f5f7374"
-    "61636b5f6869676803030d5f5f676c6f62616c5f6261736503040b5f5f686561705f6261736503050a5f5f68656170"
-    "5f656e6403060d5f5f6d656d6f72795f6261736503070c5f5f7461626c655f6261736503080aa71e0202000ba11e00"
-    "200020016a20026a20036a20046a20056a20066a20076a20086a20096a200a6a200b6a200c6a200d6a200e6a200f6a"
-    "20106a20116a20126a20136a20146a20156a20166a20176a20186a20196a201a6a201b6a201c6a201d6a201e6a201f"
-    "6a20206a20216a20226a20236a20246a20256a20266a20276a20286a20296a202a6a202b6a202c6a202d6a202e6a20"
-    "2f6a20306a20316a20326a20336a20346a20356a20366a20376a20386a20396a203a6a203b6a203c6a203d6a203e6a"
-    "203f6a20406a20416a20426a20436a20446a20456a20466a20476a20486a20496a204a6a204b6a204c6a204d6a204e"
-    "6a204f6a20506a20516a20526a20536a20546a20556a20566a20576a20586a20596a205a6a205b6a205c6a205d6a20"
-    "5e6a205f6a20606a20616a20626a20636a20646a20656a20666a20676a20686a20696a206a6a206b6a206c6a206d6a"
-    "206e6a206f6a20706a20716a20726a20736a20746a20756a20766a20776a20786a20796a207a6a207b6a207c6a207d"
-    "6a207e6a207f6a2080016a2081016a2082016a2083016a2084016a2085016a2086016a2087016a2088016a2089016a"
-    "208a016a208b016a208c016a208d016a208e016a208f016a2090016a2091016a2092016a2093016a2094016a209501"
-    "6a2096016a2097016a2098016a2099016a209a016a209b016a209c016a209d016a209e016a209f016a20a0016a20a1"
-    "016a20a2016a20a3016a20a4016a20a5016a20a6016a20a7016a20a8016a20a9016a20aa016a20ab016a20ac016a20"
-    "ad016a20ae016a20af016a20b0016a20b1016a20b2016a20b3016a20b4016a20b5016a20b6016a20b7016a20b8016a"
-    "20b9016a20ba016a20bb016a20bc016a20bd016a20be016a20bf016a20c0016a20c1016a20c2016a20c3016a20c401"
-    "6a20c5016a20c6016a20c7016a20c8016a20c9016a20ca016a20cb016a20cc016a20cd016a20ce016a20cf016a20d0"
-    "016a20d1016a20d2016a20d3016a20d4016a20d5016a20d6016a20d7016a20d8016a20d9016a20da016a20db016a20"
-    "dc016a20dd016a20de016a20df016a20e0016a20e1016a20e2016a20e3016a20e4016a20e5016a20e6016a20e7016a"
-    "20e8016a20e9016a20ea016a20eb016a20ec016a20ed016a20ee016a20ef016a20f0016a20f1016a20f2016a20f301"
-    "6a20f4016a20f5016a20f6016a20f7016a20f8016a20f9016a20fa016a20fb016a20fc016a20fd016a20fe016a20ff"
-    "016a2080026a2081026a2082026a2083026a2084026a2085026a2086026a2087026a2088026a2089026a208a026a20"
-    "8b026a208c026a208d026a208e026a208f026a2090026a2091026a2092026a2093026a2094026a2095026a2096026a"
-    "2097026a2098026a2099026a209a026a209b026a209c026a209d026a209e026a209f026a20a0026a20a1026a20a202"
-    "6a20a3026a20a4026a20a5026a20a6026a20a7026a20a8026a20a9026a20aa026a20ab026a20ac026a20ad026a20ae"
-    "026a20af026a20b0026a20b1026a20b2026a20b3026a20b4026a20b5026a20b6026a20b7026a20b8026a20b9026a20"
-    "ba026a20bb026a20bc026a20bd026a20be026a20bf026a20c0026a20c1026a20c2026a20c3026a20c4026a20c5026a"
-    "20c6026a20c7026a20c8026a20c9026a20ca026a20cb026a20cc026a20cd026a20ce026a20cf026a20d0026a20d102"
-    "6a20d2026a20d3026a20d4026a20d5026a20d6026a20d7026a20d8026a20d9026a20da026a20db026a20dc026a20dd"
-    "026a20de026a20df026a20e0026a20e1026a20e2026a20e3026a20e4026a20e5026a20e6026a20e7026a20e8026a20"
-    "e9026a20ea026a20eb026a20ec026a20ed026a20ee026a20ef026a20f0026a20f1026a20f2026a20f3026a20f4026a"
-    "20f5026a20f6026a20f7026a20f8026a20f9026a20fa026a20fb026a20fc026a20fd026a20fe026a20ff026a208003"
-    "6a2081036a2082036a2083036a2084036a2085036a2086036a2087036a2088036a2089036a208a036a208b036a208c"
-    "036a208d036a208e036a208f036a2090036a2091036a2092036a2093036a2094036a2095036a2096036a2097036a20"
-    "98036a2099036a209a036a209b036a209c036a209d036a209e036a209f036a20a0036a20a1036a20a2036a20a3036a"
-    "20a4036a20a5036a20a6036a20a7036a20a8036a20a9036a20aa036a20ab036a20ac036a20ad036a20ae036a20af03"
-    "6a20b0036a20b1036a20b2036a20b3036a20b4036a20b5036a20b6036a20b7036a20b8036a20b9036a20ba036a20bb"
-    "036a20bc036a20bd036a20be036a20bf036a20c0036a20c1036a20c2036a20c3036a20c4036a20c5036a20c6036a20"
-    "c7036a20c8036a20c9036a20ca036a20cb036a20cc036a20cd036a20ce036a20cf036a20d0036a20d1036a20d2036a"
-    "20d3036a20d4036a20d5036a20d6036a20d7036a20d8036a20d9036a20da036a20db036a20dc036a20dd036a20de03"
-    "6a20df036a20e0036a20e1036a20e2036a20e3036a20e4036a20e5036a20e6036a20e7036a20e8036a20e9036a20ea"
-    "036a20eb036a20ec036a20ed036a20ee036a20ef036a20f0036a20f1036a20f2036a20f3036a20f4036a20f5036a20"
-    "f6036a20f7036a20f8036a20f9036a20fa036a20fb036a20fc036a20fd036a20fe036a20ff036a2080046a2081046a"
-    "2082046a2083046a2084046a2085046a2086046a2087046a2088046a2089046a208a046a208b046a208c046a208d04"
-    "6a208e046a208f046a2090046a2091046a2092046a2093046a2094046a2095046a2096046a2097046a2098046a2099"
-    "046a209a046a209b046a209c046a209d046a209e046a209f046a20a0046a20a1046a20a2046a20a3046a20a4046a20"
-    "a5046a20a6046a20a7046a20a8046a20a9046a20aa046a20ab046a20ac046a20ad046a20ae046a20af046a20b0046a"
-    "20b1046a20b2046a20b3046a20b4046a20b5046a20b6046a20b7046a20b8046a20b9046a20ba046a20bb046a20bc04"
-    "6a20bd046a20be046a20bf046a20c0046a20c1046a20c2046a20c3046a20c4046a20c5046a20c6046a20c7046a20c8"
-    "046a20c9046a20ca046a20cb046a20cc046a20cd046a20ce046a20cf046a20d0046a20d1046a20d2046a20d3046a20"
-    "d4046a20d5046a20d6046a20d7046a20d8046a20d9046a20da046a20db046a20dc046a20dd046a20de046a20df046a"
-    "20e0046a20e1046a20e2046a20e3046a20e4046a20e5046a20e6046a20e7046a20e8046a20e9046a20ea046a20eb04"
-    "6a20ec046a20ed046a20ee046a20ef046a20f0046a20f1046a20f2046a20f3046a20f4046a20f5046a20f6046a20f7"
-    "046a20f8046a20f9046a20fa046a20fb046a20fc046a20fd046a20fe046a20ff046a2080056a2081056a2082056a20"
-    "83056a2084056a2085056a2086056a2087056a2088056a2089056a208a056a208b056a208c056a208d056a208e056a"
-    "208f056a2090056a2091056a2092056a2093056a2094056a2095056a2096056a2097056a2098056a2099056a209a05"
-    "6a209b056a209c056a209d056a209e056a209f056a20a0056a20a1056a20a2056a20a3056a20a4056a20a5056a20a6"
-    "056a20a7056a20a8056a20a9056a20aa056a20ab056a20ac056a20ad056a20ae056a20af056a20b0056a20b1056a20"
-    "b2056a20b3056a20b4056a20b5056a20b6056a20b7056a20b8056a20b9056a20ba056a20bb056a20bc056a20bd056a"
-    "20be056a20bf056a20c0056a20c1056a20c2056a20c3056a20c4056a20c5056a20c6056a20c7056a20c8056a20c905"
-    "6a20ca056a20cb056a20cc056a20cd056a20ce056a20cf056a20d0056a20d1056a20d2056a20d3056a20d4056a20d5"
-    "056a20d6056a20d7056a20d8056a20d9056a20da056a20db056a20dc056a20dd056a20de056a20df056a20e0056a20"
-    "e1056a20e2056a20e3056a20e4056a20e5056a20e6056a20e7056a20e8056a20e9056a20ea056a20eb056a20ec056a"
-    "20ed056a20ee056a20ef056a20f0056a20f1056a20f2056a20f3056a20f4056a20f5056a20f6056a20f7056a20f805"
-    "6a20f9056a20fa056a20fb056a20fc056a20fd056a20fe056a20ff056a2080066a2081066a2082066a2083066a2084"
-    "066a2085066a2086066a2087066a2088066a2089066a208a066a208b066a208c066a208d066a208e066a208f066a20"
-    "90066a2091066a2092066a2093066a2094066a2095066a2096066a2097066a2098066a2099066a209a066a209b066a"
-    "209c066a209d066a209e066a209f066a20a0066a20a1066a20a2066a20a3066a20a4066a20a5066a20a6066a20a706"
-    "6a20a8066a20a9066a20aa066a20ab066a20ac066a20ad066a20ae066a20af066a20b0066a20b1066a20b2066a20b3"
-    "066a20b4066a20b5066a20b6066a20b7066a20b8066a20b9066a20ba066a20bb066a20bc066a20bd066a20be066a20"
-    "bf066a20c0066a20c1066a20c2066a20c3066a20c4066a20c5066a20c6066a20c7066a20c8066a20c9066a20ca066a"
-    "20cb066a20cc066a20cd066a20ce066a20cf066a20d0066a20d1066a20d2066a20d3066a20d4066a20d5066a20d606"
-    "6a20d7066a20d8066a20d9066a20da066a20db066a20dc066a20dd066a20de066a20df066a20e0066a20e1066a20e2"
-    "066a20e3066a20e4066a20e5066a20e6066a20e7066a20e8066a20e9066a20ea066a20eb066a20ec066a20ed066a20"
-    "ee066a20ef066a20f0066a20f1066a20f2066a20f3066a20f4066a20f5066a20f6066a20f7066a20f8066a20f9066a"
-    "20fa066a20fb066a20fc066a20fd066a20fe066a20ff066a2080076a2081076a2082076a2083076a2084076a208507"
-    "6a2086076a2087076a2088076a2089076a208a076a208b076a208c076a208d076a208e076a208f076a2090076a2091"
-    "076a2092076a2093076a2094076a2095076a2096076a2097076a2098076a2099076a209a076a209b076a209c076a20"
-    "9d076a209e076a209f076a20a0076a20a1076a20a2076a20a3076a20a4076a20a5076a20a6076a20a7076a20a8076a"
-    "20a9076a20aa076a20ab076a20ac076a20ad076a20ae076a20af076a20b0076a20b1076a20b2076a20b3076a20b407"
-    "6a20b5076a20b6076a20b7076a20b8076a20b9076a20ba076a20bb076a20bc076a20bd076a20be076a20bf076a20c0"
-    "076a20c1076a20c2076a20c3076a20c4076a20c5076a20c6076a20c7076a20c8076a20c9076a20ca076a20cb076a20"
-    "cc076a20cd076a20ce076a20cf076a20d0076a20d1076a20d2076a20d3076a20d4076a20d5076a20d6076a20d7076a"
-    "20d8076a20d9076a20da076a20db076a20dc076a20dd076a20de076a20df076a20e0076a20e1076a20e2076a20e307"
-    "6a20e4076a20e5076a20e6076a20e7076a0b007f0970726f647563657273010c70726f6365737365642d6279010563"
-    "6c616e675f31392e312e352d776173692d73646b202868747470733a2f2f6769746875622e636f6d2f6c6c766d2f6c"
-    "6c766d2d70726f6a656374206162346235613264623538323935386166316565333038613739306366646234326264"
-    "32343732302900490f7461726765745f6665617475726573042b0f6d757461626c652d676c6f62616c732b08736967"
-    "6e2d6578742b0f7265666572656e63652d74797065732b0a6d756c746976616c7565";
-
-extern std::string const kThousand1ParamsHex =
-    "0061736d0100000001f2070260000060e9077f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"
-    "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f017f03030200010503010002063809"
-    "7f004180080b7f004180080b7f004180080b7f00418088040b7f004180080b7f00418088040b7f00418080080b7f00"
-    "41000b7f0041010b07a8010c066d656d6f72790200115f5f7761736d5f63616c6c5f63746f72730000047465737400"
-    "010c5f5f64736f5f68616e646c6503000a5f5f646174615f656e6403010b5f5f737461636b5f6c6f7703020c5f5f73"
-    "7461636b5f6869676803030d5f5f676c6f62616c5f6261736503040b5f5f686561705f6261736503050a5f5f686561"
-    "705f656e6403060d5f5f6d656d6f72795f6261736503070c5f5f7461626c655f6261736503080aab1e0202000ba51e"
-    "00200020016a20026a20036a20046a20056a20066a20076a20086a20096a200a6a200b6a200c6a200d6a200e6a200f"
-    "6a20106a20116a20126a20136a20146a20156a20166a20176a20186a20196a201a6a201b6a201c6a201d6a201e6a20"
-    "1f6a20206a20216a20226a20236a20246a20256a20266a20276a20286a20296a202a6a202b6a202c6a202d6a202e6a"
-    "202f6a20306a20316a20326a20336a20346a20356a20366a20376a20386a20396a203a6a203b6a203c6a203d6a203e"
-    "6a203f6a20406a20416a20426a20436a20446a20456a20466a20476a20486a20496a204a6a204b6a204c6a204d6a20"
-    "4e6a204f6a20506a20516a20526a20536a20546a20556a20566a20576a20586a20596a205a6a205b6a205c6a205d6a"
-    "205e6a205f6a20606a20616a20626a20636a20646a20656a20666a20676a20686a20696a206a6a206b6a206c6a206d"
-    "6a206e6a206f6a20706a20716a20726a20736a20746a20756a20766a20776a20786a20796a207a6a207b6a207c6a20"
-    "7d6a207e6a207f6a2080016a2081016a2082016a2083016a2084016a2085016a2086016a2087016a2088016a208901"
-    "6a208a016a208b016a208c016a208d016a208e016a208f016a2090016a2091016a2092016a2093016a2094016a2095"
-    "016a2096016a2097016a2098016a2099016a209a016a209b016a209c016a209d016a209e016a209f016a20a0016a20"
-    "a1016a20a2016a20a3016a20a4016a20a5016a20a6016a20a7016a20a8016a20a9016a20aa016a20ab016a20ac016a"
-    "20ad016a20ae016a20af016a20b0016a20b1016a20b2016a20b3016a20b4016a20b5016a20b6016a20b7016a20b801"
-    "6a20b9016a20ba016a20bb016a20bc016a20bd016a20be016a20bf016a20c0016a20c1016a20c2016a20c3016a20c4"
-    "016a20c5016a20c6016a20c7016a20c8016a20c9016a20ca016a20cb016a20cc016a20cd016a20ce016a20cf016a20"
-    "d0016a20d1016a20d2016a20d3016a20d4016a20d5016a20d6016a20d7016a20d8016a20d9016a20da016a20db016a"
-    "20dc016a20dd016a20de016a20df016a20e0016a20e1016a20e2016a20e3016a20e4016a20e5016a20e6016a20e701"
-    "6a20e8016a20e9016a20ea016a20eb016a20ec016a20ed016a20ee016a20ef016a20f0016a20f1016a20f2016a20f3"
-    "016a20f4016a20f5016a20f6016a20f7016a20f8016a20f9016a20fa016a20fb016a20fc016a20fd016a20fe016a20"
-    "ff016a2080026a2081026a2082026a2083026a2084026a2085026a2086026a2087026a2088026a2089026a208a026a"
-    "208b026a208c026a208d026a208e026a208f026a2090026a2091026a2092026a2093026a2094026a2095026a209602"
-    "6a2097026a2098026a2099026a209a026a209b026a209c026a209d026a209e026a209f026a20a0026a20a1026a20a2"
-    "026a20a3026a20a4026a20a5026a20a6026a20a7026a20a8026a20a9026a20aa026a20ab026a20ac026a20ad026a20"
-    "ae026a20af026a20b0026a20b1026a20b2026a20b3026a20b4026a20b5026a20b6026a20b7026a20b8026a20b9026a"
-    "20ba026a20bb026a20bc026a20bd026a20be026a20bf026a20c0026a20c1026a20c2026a20c3026a20c4026a20c502"
-    "6a20c6026a20c7026a20c8026a20c9026a20ca026a20cb026a20cc026a20cd026a20ce026a20cf026a20d0026a20d1"
-    "026a20d2026a20d3026a20d4026a20d5026a20d6026a20d7026a20d8026a20d9026a20da026a20db026a20dc026a20"
-    "dd026a20de026a20df026a20e0026a20e1026a20e2026a20e3026a20e4026a20e5026a20e6026a20e7026a20e8026a"
-    "20e9026a20ea026a20eb026a20ec026a20ed026a20ee026a20ef026a20f0026a20f1026a20f2026a20f3026a20f402"
-    "6a20f5026a20f6026a20f7026a20f8026a20f9026a20fa026a20fb026a20fc026a20fd026a20fe026a20ff026a2080"
-    "036a2081036a2082036a2083036a2084036a2085036a2086036a2087036a2088036a2089036a208a036a208b036a20"
-    "8c036a208d036a208e036a208f036a2090036a2091036a2092036a2093036a2094036a2095036a2096036a2097036a"
-    "2098036a2099036a209a036a209b036a209c036a209d036a209e036a209f036a20a0036a20a1036a20a2036a20a303"
-    "6a20a4036a20a5036a20a6036a20a7036a20a8036a20a9036a20aa036a20ab036a20ac036a20ad036a20ae036a20af"
-    "036a20b0036a20b1036a20b2036a20b3036a20b4036a20b5036a20b6036a20b7036a20b8036a20b9036a20ba036a20"
-    "bb036a20bc036a20bd036a20be036a20bf036a20c0036a20c1036a20c2036a20c3036a20c4036a20c5036a20c6036a"
-    "20c7036a20c8036a20c9036a20ca036a20cb036a20cc036a20cd036a20ce036a20cf036a20d0036a20d1036a20d203"
-    "6a20d3036a20d4036a20d5036a20d6036a20d7036a20d8036a20d9036a20da036a20db036a20dc036a20dd036a20de"
-    "036a20df036a20e0036a20e1036a20e2036a20e3036a20e4036a20e5036a20e6036a20e7036a20e8036a20e9036a20"
-    "ea036a20eb036a20ec036a20ed036a20ee036a20ef036a20f0036a20f1036a20f2036a20f3036a20f4036a20f5036a"
-    "20f6036a20f7036a20f8036a20f9036a20fa036a20fb036a20fc036a20fd036a20fe036a20ff036a2080046a208104"
-    "6a2082046a2083046a2084046a2085046a2086046a2087046a2088046a2089046a208a046a208b046a208c046a208d"
-    "046a208e046a208f046a2090046a2091046a2092046a2093046a2094046a2095046a2096046a2097046a2098046a20"
-    "99046a209a046a209b046a209c046a209d046a209e046a209f046a20a0046a20a1046a20a2046a20a3046a20a4046a"
-    "20a5046a20a6046a20a7046a20a8046a20a9046a20aa046a20ab046a20ac046a20ad046a20ae046a20af046a20b004"
-    "6a20b1046a20b2046a20b3046a20b4046a20b5046a20b6046a20b7046a20b8046a20b9046a20ba046a20bb046a20bc"
-    "046a20bd046a20be046a20bf046a20c0046a20c1046a20c2046a20c3046a20c4046a20c5046a20c6046a20c7046a20"
-    "c8046a20c9046a20ca046a20cb046a20cc046a20cd046a20ce046a20cf046a20d0046a20d1046a20d2046a20d3046a"
-    "20d4046a20d5046a20d6046a20d7046a20d8046a20d9046a20da046a20db046a20dc046a20dd046a20de046a20df04"
-    "6a20e0046a20e1046a20e2046a20e3046a20e4046a20e5046a20e6046a20e7046a20e8046a20e9046a20ea046a20eb"
-    "046a20ec046a20ed046a20ee046a20ef046a20f0046a20f1046a20f2046a20f3046a20f4046a20f5046a20f6046a20"
-    "f7046a20f8046a20f9046a20fa046a20fb046a20fc046a20fd046a20fe046a20ff046a2080056a2081056a2082056a"
-    "2083056a2084056a2085056a2086056a2087056a2088056a2089056a208a056a208b056a208c056a208d056a208e05"
-    "6a208f056a2090056a2091056a2092056a2093056a2094056a2095056a2096056a2097056a2098056a2099056a209a"
-    "056a209b056a209c056a209d056a209e056a209f056a20a0056a20a1056a20a2056a20a3056a20a4056a20a5056a20"
-    "a6056a20a7056a20a8056a20a9056a20aa056a20ab056a20ac056a20ad056a20ae056a20af056a20b0056a20b1056a"
-    "20b2056a20b3056a20b4056a20b5056a20b6056a20b7056a20b8056a20b9056a20ba056a20bb056a20bc056a20bd05"
-    "6a20be056a20bf056a20c0056a20c1056a20c2056a20c3056a20c4056a20c5056a20c6056a20c7056a20c8056a20c9"
-    "056a20ca056a20cb056a20cc056a20cd056a20ce056a20cf056a20d0056a20d1056a20d2056a20d3056a20d4056a20"
-    "d5056a20d6056a20d7056a20d8056a20d9056a20da056a20db056a20dc056a20dd056a20de056a20df056a20e0056a"
-    "20e1056a20e2056a20e3056a20e4056a20e5056a20e6056a20e7056a20e8056a20e9056a20ea056a20eb056a20ec05"
-    "6a20ed056a20ee056a20ef056a20f0056a20f1056a20f2056a20f3056a20f4056a20f5056a20f6056a20f7056a20f8"
-    "056a20f9056a20fa056a20fb056a20fc056a20fd056a20fe056a20ff056a2080066a2081066a2082066a2083066a20"
-    "84066a2085066a2086066a2087066a2088066a2089066a208a066a208b066a208c066a208d066a208e066a208f066a"
-    "2090066a2091066a2092066a2093066a2094066a2095066a2096066a2097066a2098066a2099066a209a066a209b06"
-    "6a209c066a209d066a209e066a209f066a20a0066a20a1066a20a2066a20a3066a20a4066a20a5066a20a6066a20a7"
-    "066a20a8066a20a9066a20aa066a20ab066a20ac066a20ad066a20ae066a20af066a20b0066a20b1066a20b2066a20"
-    "b3066a20b4066a20b5066a20b6066a20b7066a20b8066a20b9066a20ba066a20bb066a20bc066a20bd066a20be066a"
-    "20bf066a20c0066a20c1066a20c2066a20c3066a20c4066a20c5066a20c6066a20c7066a20c8066a20c9066a20ca06"
-    "6a20cb066a20cc066a20cd066a20ce066a20cf066a20d0066a20d1066a20d2066a20d3066a20d4066a20d5066a20d6"
-    "066a20d7066a20d8066a20d9066a20da066a20db066a20dc066a20dd066a20de066a20df066a20e0066a20e1066a20"
-    "e2066a20e3066a20e4066a20e5066a20e6066a20e7066a20e8066a20e9066a20ea066a20eb066a20ec066a20ed066a"
-    "20ee066a20ef066a20f0066a20f1066a20f2066a20f3066a20f4066a20f5066a20f6066a20f7066a20f8066a20f906"
-    "6a20fa066a20fb066a20fc066a20fd066a20fe066a20ff066a2080076a2081076a2082076a2083076a2084076a2085"
-    "076a2086076a2087076a2088076a2089076a208a076a208b076a208c076a208d076a208e076a208f076a2090076a20"
-    "91076a2092076a2093076a2094076a2095076a2096076a2097076a2098076a2099076a209a076a209b076a209c076a"
-    "209d076a209e076a209f076a20a0076a20a1076a20a2076a20a3076a20a4076a20a5076a20a6076a20a7076a20a807"
-    "6a20a9076a20aa076a20ab076a20ac076a20ad076a20ae076a20af076a20b0076a20b1076a20b2076a20b3076a20b4"
-    "076a20b5076a20b6076a20b7076a20b8076a20b9076a20ba076a20bb076a20bc076a20bd076a20be076a20bf076a20"
-    "c0076a20c1076a20c2076a20c3076a20c4076a20c5076a20c6076a20c7076a20c8076a20c9076a20ca076a20cb076a"
-    "20cc076a20cd076a20ce076a20cf076a20d0076a20d1076a20d2076a20d3076a20d4076a20d5076a20d6076a20d707"
-    "6a20d8076a20d9076a20da076a20db076a20dc076a20dd076a20de076a20df076a20e0076a20e1076a20e2076a20e3"
-    "076a20e4076a20e5076a20e6076a20e7076a20e8076a0b007f0970726f647563657273010c70726f6365737365642d"
-    "62790105636c616e675f31392e312e352d776173692d73646b202868747470733a2f2f6769746875622e636f6d2f6c"
-    "6c766d2f6c6c766d2d70726f6a65637420616234623561326462353832393538616631656533303861373930636664"
-    "623432626432343732302900490f7461726765745f6665617475726573042b0f6d757461626c652d676c6f62616c73"
-    "2b087369676e2d6578742b0f7265666572656e63652d74797065732b0a6d756c746976616c7565";
-
-extern std::string const kOpcReservedHex =
-    "0061736d010000000105016000017f03030200000404017000010503010001060b027f0141000b7e0142000b071401"
-    "10616c6c5f696e737472756374696f6e7300010907010041000b01000a53020400412a0b4c02017f017e0101010101"
-    "0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101"
-    "01010101010101010101010101010101410b0b0b0a010041000b0474657374";
-
-extern std::string const kImpExpHex =
-    "0061736d0100000001100360027f7f017f6000017f60017f017f02330203656e760e6765745f6c65646765725f7371"
-    "6e000003656e76166765745f706172656e745f6c65646765725f686173680000030403010201050301000107310406"
-    "6d656d6f72790200096578705f66756e63310002096578705f66756e633200030c746573745f696d706f7274730004"
-    "0a2b03040041010b0700200041026c0b1c01027f4120410410001a41202802002100410041201001210120000b";
-
-extern std::string const kUpdateDataWasmHex =
-    "0061736d01000000010e0360027f7f017f6000006000017f02100103656e76087365745f6461746100000303020102"
-    "0503010002063f0a7f01419088040b7f004180080b7f004185080b7f004190080b7f00419088040b7f004180080b7f"
-    "00419088040b7f00418080080b7f0041000b7f0041010b07b1010c066d656d6f72790200115f5f7761736d5f63616c"
-    "6c5f63746f727300010d657363726f775f66696e69736800020c5f5f64736f5f68616e646c6503010a5f5f64617461"
-    "5f656e6403020b5f5f737461636b5f6c6f7703030c5f5f737461636b5f6869676803040d5f5f676c6f62616c5f6261"
-    "736503050b5f5f686561705f6261736503060a5f5f686561705f656e6403070d5f5f6d656d6f72795f626173650308"
-    "0c5f5f7461626c655f6261736503090a3f0202000b3a01017f230041106b220024002000410c6a4184082d00003a00"
-    "002000418008280000360208200041086a410410001a200041106a240041807e0b0b0b01004180080b044461746100"
-    "7f0970726f647563657273010c70726f6365737365642d62790105636c616e675f31392e312e352d776173692d7364"
-    "6b202868747470733a2f2f6769746875622e636f6d2f6c6c766d2f6c6c766d2d70726f6a6563742061623462356132"
-    "6462353832393538616631656533303861373930636664623432626432343732302900490f7461726765745f666561"
-    "7475726573042b0f6d757461626c652d676c6f62616c732b087369676e2d6578742b0f7265666572656e63652d7479"
-    "7065732b0a6d756c746976616c7565";
diff --git a/src/test/app/wasm_fixtures/fixtures.h b/src/test/app/wasm_fixtures/fixtures.h
deleted file mode 100644
index ad373e1565..0000000000
--- a/src/test/app/wasm_fixtures/fixtures.h
+++ /dev/null
@@ -1,157 +0,0 @@
-#pragma once
-
-// TODO: consider moving these to separate files (and figure out the build)
-
-#include 
-#include 
-#include 
-
-// WASM binary format constants and helpers for building test modules
-namespace wasm_constants {
-
-// Magic + version header
-uint8_t const kWasmHeader[] = {
-    0x00,
-    0x61,
-    0x73,
-    0x6d,  // magic: \0asm
-    0x01,
-    0x00,
-    0x00,
-    0x00  // version: 1
-};
-
-// Type section: () -> ()
-uint8_t const kTypeEmptyFunc[] = {0x01, 0x04, 0x01, 0x60, 0x00, 0x00};
-
-// Function section: one function using type 0
-uint8_t const kFuncTypE0[] = {0x03, 0x02, 0x01, 0x00};
-
-// Export section: export func 0 as "escrow_finish"
-uint8_t const kExportFinish[] = {
-    0x07,
-    0x11,
-    0x01,
-    0x0d,
-    'e',
-    's',
-    'c',
-    'r',
-    'o',
-    'w',
-    '_',
-    'f',
-    'i',
-    'n',
-    'i',
-    's',
-    'h',
-    0x00,
-    0x00};
-
-// Empty function body: 0 locals, end
-uint8_t const kEmptyBody[] = {0x00, 0x0b};
-
-// Data segment offset: i32.const 0, end
-uint8_t const kDataOffsetZero[] = {0x41, 0x00, 0x0b};
-
-// Section IDs
-uint8_t const kSectionMemory = 0x05;
-uint8_t const kSectionCode = 0x0a;
-uint8_t const kSectionData = 0x0b;
-
-// Instructions
-uint8_t const kInstrNop = 0x01;
-uint8_t const kInstrEnd = 0x0b;
-
-// Fill byte for data section bloat
-uint8_t const kDataFillByte = 0xEE;
-
-// Generator for WASM module with large code section (many NOPs)
-std::vector
-generateCodeBlob(uint32_t numInstructions);
-
-// Generator for WASM module with large data section
-std::vector
-generateDataBlob(uint32_t dataSize);
-
-}  // namespace wasm_constants
-
-extern std::string const kLedgerSqnWasmHex;
-extern std::string const kAllHostFunctionsWasmHex;
-extern std::string const kAllKeyletsWasmHex;
-extern std::string const kCodecovTestsWasmHex;
-
-extern std::string const kFibWasmHex;
-
-extern std::string const kFloatTestsWasmHex;
-extern std::string const kFloat0Hex;
-extern std::string const kDisabledFloatHex;
-
-extern std::string const kMemoryPointerAtLimitHex;
-extern std::string const kMemoryPointerOverLimitHex;
-extern std::string const kMemoryOffsetOverLimitHex;
-extern std::string const kMemoryEndOfWordOverLimitHex;
-extern std::string const kMemoryGrow0To1PageHex;
-extern std::string const kMemoryGrow1To0PageHex;
-extern std::string const kMemoryLastByteOf8MbHex;
-extern std::string const kMemoryGrow1MoreThan8MbHex;
-extern std::string const kMemoryGrow0MoreThan8MbHex;
-extern std::string const kMemoryInit1MoreThan8MbHex;
-extern std::string const kMemoryNegativeAddressHex;
-
-extern std::string const kTable64ElementsHex;
-extern std::string const kTable65ElementsHex;
-extern std::string const kTable2TablesHex;
-extern std::string const kTable0ElementsHex;
-extern std::string const kTableUintMaxHex;
-
-extern std::string const kProposalMutableGlobalHex;
-extern std::string const kProposalGcStructNewHex;
-extern std::string const kProposalMultiValueHex;
-extern std::string const kProposalSignExtHex;
-extern std::string const kProposalFloatToIntHex;
-extern std::string const kProposalBulkMemoryHex;
-extern std::string const kProposalRefTypesHex;
-extern std::string const kProposalTailCallHex;
-extern std::string const kProposalExtendedConstHex;
-extern std::string const kProposalMultiMemoryHex;
-extern std::string const kProposalCustomPageSizesHex;
-extern std::string const kProposalMemory64Hex;
-extern std::string const kProposalWideArithmeticHex;
-
-extern std::string const kTrapDivideBy0Hex;
-extern std::string const kTrapIntOverflowHex;
-extern std::string const kTrapUnreachableHex;
-extern std::string const kTrapNullCallHex;
-extern std::string const kTrapFuncSigMismatchHex;
-
-extern std::string const kWasiGetTimeHex;
-extern std::string const kWasiPrintHex;
-
-extern std::string const kBadMagicNumberHex;
-extern std::string const kBadVersionNumberHex;
-extern std::string const kLyingHeaderHex;
-extern std::string const kNeverEndingNumberHex;
-extern std::string const kVectorLieHex;
-extern std::string const kSectionOrderingHex;
-extern std::string const kGhostPayloadHex;
-extern std::string const kJunkAfterSectionHex;
-extern std::string const kInvalidSectionIdHex;
-extern std::string const kLocalVariableBombHex;
-
-extern std::string const kDeepRecursionHex;
-extern std::string const kInfiniteLoopWasmHex;
-extern std::string const kStartLoopHex;
-
-extern std::string const kBadAlignWasmHex;
-
-extern std::string const kThousandParamsHex;
-extern std::string const kThousand1ParamsHex;
-extern std::string const kLocals10kHex;
-extern std::string const kFunctions5kHex;
-
-extern std::string const kOpcReservedHex;
-
-extern std::string const kImpExpHex;
-extern std::string const kUpdateDataWasmHex;
diff --git a/src/test/app/wasm_fixtures/float_0/Cargo.lock b/src/test/app/wasm_fixtures/float_0/Cargo.lock
deleted file mode 100644
index 690b1b51f3..0000000000
--- a/src/test/app/wasm_fixtures/float_0/Cargo.lock
+++ /dev/null
@@ -1,171 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "block-buffer"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
-dependencies = [
- "hybrid-array",
-]
-
-[[package]]
-name = "bs58"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
-dependencies = [
- "tinyvec",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "const-oid"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
-
-[[package]]
-name = "cpufeatures"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "crypto-common"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
-dependencies = [
- "hybrid-array",
-]
-
-[[package]]
-name = "digest"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
-dependencies = [
- "block-buffer",
- "const-oid",
- "crypto-common",
-]
-
-[[package]]
-name = "float_0"
-version = "0.0.1"
-dependencies = [
- "xrpl-wasm-stdlib",
-]
-
-[[package]]
-name = "hybrid-array"
-version = "0.4.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
-dependencies = [
- "typenum",
-]
-
-[[package]]
-name = "libc"
-version = "0.2.183"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.106"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.45"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "sha2"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
-[[package]]
-name = "syn"
-version = "2.0.117"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "tinyvec"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
-dependencies = [
- "tinyvec_macros",
-]
-
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
-
-[[package]]
-name = "typenum"
-version = "1.20.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.24"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
-
-[[package]]
-name = "xrpl-macros"
-version = "0.1.0"
-source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=renames#6b35fe45ac70bad38914e7f319d31d7947e05e25"
-dependencies = [
- "bs58",
- "proc-macro2",
- "quote",
- "sha2",
- "syn",
-]
-
-[[package]]
-name = "xrpl-wasm-stdlib"
-version = "0.8.0"
-source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=renames#6b35fe45ac70bad38914e7f319d31d7947e05e25"
-dependencies = [
- "xrpl-macros",
-]
diff --git a/src/test/app/wasm_fixtures/float_0/Cargo.toml b/src/test/app/wasm_fixtures/float_0/Cargo.toml
deleted file mode 100644
index 95254f2e2b..0000000000
--- a/src/test/app/wasm_fixtures/float_0/Cargo.toml
+++ /dev/null
@@ -1,21 +0,0 @@
-[package]
-name = "float_0"
-version = "0.0.1"
-edition = "2024"
-
-# This empty workspace definition keeps this project independent of the parent workspace
-[workspace]
-
-[lib]
-crate-type = ["cdylib"]
-
-[profile.release]
-lto = true
-opt-level = 's'
-panic = "abort"
-
-[dependencies]
-xrpl-std = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-wasm-stdlib", branch = "renames" }
-
-[profile.dev]
-panic = "abort"
diff --git a/src/test/app/wasm_fixtures/float_0/src/lib.rs b/src/test/app/wasm_fixtures/float_0/src/lib.rs
deleted file mode 100644
index 5f6c8f0770..0000000000
--- a/src/test/app/wasm_fixtures/float_0/src/lib.rs
+++ /dev/null
@@ -1,70 +0,0 @@
-#![cfg_attr(target_arch = "wasm32", no_std)]
-
-use xrpl_std::host::trace::trace;
-use xrpl_std::host::{float_cmp, float_from_int, float_sub, FLOAT_ROUNDING_MODES_TO_NEAREST};
-
-// Float size constant (8 bytes mantissa + 4 bytes exponent)
-const FLOAT_SIZE: usize = 12;
-
-// FLOAT_ZERO constant
-const FLOAT_ZERO: [u8; FLOAT_SIZE] = [
-    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
-];
-
-#[unsafe(no_mangle)]
-pub extern "C" fn escrow_finish() -> i32 {
-    let _ = trace("\n$$$ test_float_0 $$$");
-
-    // Test: 10 - 10 should equal 0
-    let mut f10: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    let mut f_result: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-
-    // Create float from 10
-    if FLOAT_SIZE as i32
-        != unsafe {
-            float_from_int(
-                10,
-                f10.as_mut_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        }
-    {
-        let _ = trace("  float 10-10: failed");
-        return 1;
-    }
-
-    // Subtract: 10 - 10 = 0
-    if FLOAT_SIZE as i32
-        != unsafe {
-            float_sub(
-                f10.as_ptr(),
-                FLOAT_SIZE,
-                f10.as_ptr(),
-                FLOAT_SIZE,
-                f_result.as_mut_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        }
-    {
-        let _ = trace("  float 10-10: failed");
-        return 1;
-    }
-
-    // Compare result with FLOAT_ZERO constant
-    if 0 == unsafe {
-        float_cmp(
-            f_result.as_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ZERO.as_ptr(),
-            FLOAT_SIZE,
-        )
-    } {
-        let _ = trace("  FLOAT_ZERO compare: good");
-    } else {
-        let _ = trace("  FLOAT_ZERO compare: bad");
-    }
-
-    1
-}
diff --git a/src/test/app/wasm_fixtures/float_tests/Cargo.lock b/src/test/app/wasm_fixtures/float_tests/Cargo.lock
deleted file mode 100644
index 92158d3262..0000000000
--- a/src/test/app/wasm_fixtures/float_tests/Cargo.lock
+++ /dev/null
@@ -1,171 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 4
-
-[[package]]
-name = "block-buffer"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
-dependencies = [
- "hybrid-array",
-]
-
-[[package]]
-name = "bs58"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
-dependencies = [
- "tinyvec",
-]
-
-[[package]]
-name = "cfg-if"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
-
-[[package]]
-name = "const-oid"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
-
-[[package]]
-name = "cpufeatures"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
-dependencies = [
- "libc",
-]
-
-[[package]]
-name = "crypto-common"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
-dependencies = [
- "hybrid-array",
-]
-
-[[package]]
-name = "digest"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
-dependencies = [
- "block-buffer",
- "const-oid",
- "crypto-common",
-]
-
-[[package]]
-name = "float_tests"
-version = "0.0.1"
-dependencies = [
- "xrpl-wasm-stdlib",
-]
-
-[[package]]
-name = "hybrid-array"
-version = "0.4.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
-dependencies = [
- "typenum",
-]
-
-[[package]]
-name = "libc"
-version = "0.2.177"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.103"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.41"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "sha2"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
-dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
-]
-
-[[package]]
-name = "syn"
-version = "2.0.108"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "tinyvec"
-version = "1.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
-dependencies = [
- "tinyvec_macros",
-]
-
-[[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
-
-[[package]]
-name = "typenum"
-version = "1.20.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
-
-[[package]]
-name = "xrpl-macros"
-version = "0.1.0"
-source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=renames#6b35fe45ac70bad38914e7f319d31d7947e05e25"
-dependencies = [
- "bs58",
- "proc-macro2",
- "quote",
- "sha2",
- "syn",
-]
-
-[[package]]
-name = "xrpl-wasm-stdlib"
-version = "0.8.0"
-source = "git+https://github.com/ripple/xrpl-wasm-stdlib.git?branch=renames#6b35fe45ac70bad38914e7f319d31d7947e05e25"
-dependencies = [
- "xrpl-macros",
-]
diff --git a/src/test/app/wasm_fixtures/float_tests/Cargo.toml b/src/test/app/wasm_fixtures/float_tests/Cargo.toml
deleted file mode 100644
index d4f70f1afc..0000000000
--- a/src/test/app/wasm_fixtures/float_tests/Cargo.toml
+++ /dev/null
@@ -1,21 +0,0 @@
-[package]
-name = "float_tests"
-version = "0.0.1"
-edition = "2024"
-
-# This empty workspace definition keeps this project independent of the parent workspace
-[workspace]
-
-[lib]
-crate-type = ["cdylib"]
-
-[profile.release]
-lto = true
-opt-level = 's'
-panic = "abort"
-
-[dependencies]
-xrpl-std = { git = "https://github.com/ripple/xrpl-wasm-stdlib.git", package = "xrpl-wasm-stdlib", branch = "renames" }
-
-[profile.dev]
-panic = "abort"
diff --git a/src/test/app/wasm_fixtures/float_tests/src/lib.rs b/src/test/app/wasm_fixtures/float_tests/src/lib.rs
deleted file mode 100644
index b5e6aa6185..0000000000
--- a/src/test/app/wasm_fixtures/float_tests/src/lib.rs
+++ /dev/null
@@ -1,1112 +0,0 @@
-#![allow(unused_imports)]
-#![allow(unused_variables)]
-#![cfg_attr(target_arch = "wasm32", no_std)]
-
-#[cfg(not(target_arch = "wasm32"))]
-extern crate std;
-
-use xrpl_std::core::locator::Locator;
-use xrpl_std::decode_hex_32;
-use xrpl_std::host::trace::DataRepr::AsHex;
-use xrpl_std::host::trace::{trace, trace_data, trace_num, DataRepr};
-use xrpl_std::host::{
-    cache_le, float_add, float_cmp, float_div, float_from_int, float_from_uint, float_mult,
-    float_pow, float_root, float_sub, le_field, le_inner, le_inner_arr_len,
-    FLOAT_ROUNDING_MODES_TO_NEAREST,
-};
-use xrpl_std::sfield;
-use xrpl_std::sfield::{
-    Account, AccountTxnID, Balance, Domain, EmailHash, Flags, LedgerEntryType, MessageKey,
-    OwnerCount, PreviousTxnID, PreviousTxnLgrSeq, RegularKey, Sequence, TicketCount, TransferRate,
-};
-
-// External host functions not yet in xrpl_std
-unsafe extern "C" {
-    #[link_name = "float_from_stamount"]
-    fn float_from_stamount(
-        amount_ptr: *const u8,
-        amount_len: i32,
-        out_ptr: *mut u8,
-        out_len: i32,
-        rounding: i32,
-    ) -> i32;
-
-    #[link_name = "float_from_stnumber"]
-    fn float_from_stnumber(
-        number_ptr: *const u8,
-        number_len: i32,
-        out_ptr: *mut u8,
-        out_len: i32,
-        rounding: i32,
-    ) -> i32;
-
-    #[link_name = "float_to_int"]
-    fn float_to_int(
-        float_ptr: *const u8,
-        float_len: i32,
-        out_ptr: *mut u8,
-        out_len: i32,
-        rounding: i32,
-    ) -> i32;
-
-    #[link_name = "float_to_mant_exp"]
-    fn float_to_mant_exp(
-        float_ptr: *const u8,
-        float_len: i32,
-        mantissa_ptr: *mut u8,
-        mantissa_len: i32,
-        exponent_ptr: *mut u8,
-        exponent_len: i32,
-    ) -> i32;
-
-    #[link_name = "float_from_mant_exp"]
-    fn float_from_mant_exp(
-        mantissa: i64,
-        exponent: i32,
-        out_ptr: *mut u8,
-        out_len: i32,
-        rounding: i32,
-    ) -> i32;
-}
-
-// Float size constant (8 bytes mantissa + 4 bytes exponent)
-const FLOAT_SIZE: usize = 12;
-
-// Float constants (8 bytes mantissa + 4 bytes exponent, big-endian)
-// FLOAT_ONE: mantissa=0x0DE0B6B3A7640000 (10^18), exponent=0xFFFFFFEE (-18)
-const FLOAT_ONE: [u8; FLOAT_SIZE] = [
-    0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE,
-];
-// FLOAT_NEGATIVE_ONE: mantissa=0xF21F494C589C0000 (-10^18), exponent=0xFFFFFFEE (-18)
-const FLOAT_NEGATIVE_ONE: [u8; FLOAT_SIZE] = [
-    0xF2, 0x1F, 0x49, 0x4C, 0x58, 0x9C, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE,
-];
-
-// Helper function to trace floats
-fn trace_float(msg: &str, f: &[u8; FLOAT_SIZE]) {
-    let _ = trace(msg);
-    let _ = trace_data("  ", f, AsHex);
-}
-
-fn test_float_from_wasm() -> bool {
-    let _ = trace("\n$$$ test_float_from_wasm $$$");
-    let mut all_pass = true;
-
-    let mut f: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    if FLOAT_SIZE as i32
-        == unsafe {
-            float_from_int(
-                12300,
-                f.as_mut_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        }
-    {
-        let _ = trace_float("  float from i64 12300:", &f);
-        let _ = trace_data("  float from i64 12300 as HEX:", &f, AsHex);
-    } else {
-        let _ = trace("  float from i64 12300: failed");
-        all_pass = false;
-    }
-
-    let u64_value: u64 = 12300;
-    if FLOAT_SIZE as i32
-        == unsafe {
-            float_from_uint(
-                &u64_value as *const u64 as *const u8,
-                8,
-                f.as_mut_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        }
-    {
-        let _ = trace_float("  float from u64 12300:", &f);
-    } else {
-        let _ = trace("  float from u64 12300: failed");
-        all_pass = false;
-    }
-
-    if FLOAT_SIZE as i32
-        == unsafe {
-            float_from_mant_exp(
-                123,
-                2,
-                f.as_mut_ptr(),
-                FLOAT_SIZE as i32,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        }
-    {
-        let _ = trace_float("  float from exp 2, mantissa 123:", &f);
-    } else {
-        let _ = trace("  float from exp 2, mantissa 123: failed");
-        all_pass = false;
-    }
-
-    let _ = trace_float("  float from const 1:", &FLOAT_ONE);
-    let _ = trace_float("  float from const -1:", &FLOAT_NEGATIVE_ONE);
-
-    all_pass
-}
-
-fn test_float_cmp() -> bool {
-    let _ = trace("\n$$$ test_float_cmp $$$");
-    let mut all_pass = true;
-
-    let mut f1: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    if FLOAT_SIZE as i32
-        != unsafe {
-            float_from_int(
-                1,
-                f1.as_mut_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        }
-    {
-        let _ = trace("  float from 1: failed");
-        all_pass = false;
-    } else {
-        let _ = trace_float("  float from 1:", &f1);
-    }
-
-    if 0 == unsafe { float_cmp(f1.as_ptr(), FLOAT_SIZE, FLOAT_ONE.as_ptr(), FLOAT_SIZE) } {
-        let _ = trace("  float from 1 == FLOAT_ONE");
-    } else {
-        let _ = trace("  float from 1 != FLOAT_ONE, failed");
-        all_pass = false;
-    }
-
-    if 1 == unsafe {
-        float_cmp(
-            f1.as_ptr(),
-            FLOAT_SIZE,
-            FLOAT_NEGATIVE_ONE.as_ptr(),
-            FLOAT_SIZE,
-        )
-    } {
-        let _ = trace("  float from 1 > FLOAT_NEGATIVE_ONE");
-    } else {
-        let _ = trace("  float from 1 !> FLOAT_NEGATIVE_ONE, failed");
-        all_pass = false;
-    }
-
-    if 2 == unsafe {
-        float_cmp(
-            FLOAT_NEGATIVE_ONE.as_ptr(),
-            FLOAT_SIZE,
-            f1.as_ptr(),
-            FLOAT_SIZE,
-        )
-    } {
-        let _ = trace("  FLOAT_NEGATIVE_ONE < float from 1");
-    } else {
-        let _ = trace("  FLOAT_NEGATIVE_ONE !< float from 1, failed");
-        all_pass = false;
-    }
-
-    all_pass
-}
-
-fn test_float_add_subtract() -> bool {
-    let _ = trace("\n$$$ test_float_add_subtract $$$");
-    let mut all_pass = true;
-
-    let mut f_compute: [u8; FLOAT_SIZE] = FLOAT_ONE;
-    for i in 0..9 {
-        unsafe {
-            float_add(
-                f_compute.as_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ONE.as_ptr(),
-                FLOAT_SIZE,
-                f_compute.as_mut_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        };
-        // let _ = trace_float("  float:", &f_compute);
-    }
-    let mut f10: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    if FLOAT_SIZE as i32
-        != unsafe {
-            float_from_int(
-                10,
-                f10.as_mut_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        }
-    {
-        let _ = trace("  float from 10: failed");
-        all_pass = false;
-    }
-
-    if 0 == unsafe { float_cmp(f10.as_ptr(), FLOAT_SIZE, f_compute.as_ptr(), FLOAT_SIZE) } {
-        let _ = trace("  repeated add: good");
-    } else {
-        let _ = trace("  repeated add: failed");
-        all_pass = false;
-    }
-
-    for i in 0..11 {
-        unsafe {
-            float_sub(
-                f_compute.as_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ONE.as_ptr(),
-                FLOAT_SIZE,
-                f_compute.as_mut_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        };
-    }
-    if 0 == unsafe {
-        float_cmp(
-            f_compute.as_ptr(),
-            FLOAT_SIZE,
-            FLOAT_NEGATIVE_ONE.as_ptr(),
-            FLOAT_SIZE,
-        )
-    } {
-        let _ = trace("  repeated subtract: good");
-    } else {
-        let _ = trace("  repeated subtract: failed");
-        all_pass = false;
-    }
-
-    all_pass
-}
-
-fn test_float_mult_divide() -> bool {
-    let _ = trace("\n$$$ test_float_mult_divide $$$");
-    let mut all_pass = true;
-
-    let mut f10: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_int(
-            10,
-            f10.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let mut f_compute: [u8; FLOAT_SIZE] = FLOAT_ONE;
-    for i in 0..6 {
-        unsafe {
-            float_mult(
-                f_compute.as_ptr(),
-                FLOAT_SIZE,
-                f10.as_ptr(),
-                FLOAT_SIZE,
-                f_compute.as_mut_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        };
-        // let _ = trace_float("  float:", &f_compute);
-    }
-    let mut f1000000: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_int(
-            1000000,
-            f1000000.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-
-    if 0 == unsafe {
-        float_cmp(
-            f1000000.as_ptr(),
-            FLOAT_SIZE,
-            f_compute.as_ptr(),
-            FLOAT_SIZE,
-        )
-    } {
-        let _ = trace("  repeated multiply: good");
-    } else {
-        let _ = trace("  repeated multiply: failed");
-        all_pass = false;
-    }
-
-    for i in 0..7 {
-        unsafe {
-            float_div(
-                f_compute.as_ptr(),
-                FLOAT_SIZE,
-                f10.as_ptr(),
-                FLOAT_SIZE,
-                f_compute.as_mut_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        };
-    }
-    let mut f01: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_mant_exp(
-            1,
-            -1,
-            f01.as_mut_ptr(),
-            FLOAT_SIZE as i32,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-
-    if 0 == unsafe { float_cmp(f_compute.as_ptr(), FLOAT_SIZE, f01.as_ptr(), FLOAT_SIZE) } {
-        let _ = trace("  repeated divide: good");
-    } else {
-        let _ = trace("  repeated divide: failed");
-        all_pass = false;
-    }
-
-    all_pass
-}
-
-fn test_float_pow() -> bool {
-    let _ = trace("\n$$$ test_float_pow $$$");
-    let mut all_pass = true;
-
-    let mut f_compute: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_pow(
-            FLOAT_ONE.as_ptr(),
-            FLOAT_SIZE,
-            3,
-            f_compute.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let _ = trace_float("  float cube of 1:", &f_compute);
-
-    unsafe {
-        float_pow(
-            FLOAT_NEGATIVE_ONE.as_ptr(),
-            FLOAT_SIZE,
-            6,
-            f_compute.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let _ = trace_float("  float 6th power of -1:", &f_compute);
-
-    let mut f9: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_int(
-            9,
-            f9.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    unsafe {
-        float_pow(
-            f9.as_ptr(),
-            FLOAT_SIZE,
-            2,
-            f_compute.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let _ = trace_float("  float square of 9:", &f_compute);
-
-    unsafe {
-        float_pow(
-            f9.as_ptr(),
-            FLOAT_SIZE,
-            0,
-            f_compute.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let _ = trace_float("  float 0th power of 9:", &f_compute);
-
-    let mut f0: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_int(
-            0,
-            f0.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    unsafe {
-        float_pow(
-            f0.as_ptr(),
-            FLOAT_SIZE,
-            2,
-            f_compute.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let _ = trace_float("  float square of 0:", &f_compute);
-
-    let r = unsafe {
-        float_pow(
-            f0.as_ptr(),
-            FLOAT_SIZE,
-            0,
-            f_compute.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let _ = trace_num(
-        "  float 0th power of 0 (expecting INVALID_PARAMS error):",
-        r as i64,
-    );
-
-    all_pass
-}
-
-fn test_float_root() -> bool {
-    let _ = trace("\n$$$ test_float_root $$$");
-    let mut all_pass = true;
-
-    let mut f9: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_int(
-            9,
-            f9.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let mut f_compute: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_root(
-            f9.as_ptr(),
-            FLOAT_SIZE,
-            2,
-            f_compute.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let _ = trace_float("  float sqrt of 9:", &f_compute);
-    unsafe {
-        float_root(
-            f9.as_ptr(),
-            FLOAT_SIZE,
-            3,
-            f_compute.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let _ = trace_float("  float cbrt of 9:", &f_compute);
-
-    let mut f1000000: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_int(
-            1000000,
-            f1000000.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    unsafe {
-        float_root(
-            f1000000.as_ptr(),
-            FLOAT_SIZE,
-            3,
-            f_compute.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let _ = trace_float("  float cbrt of 1000000:", &f_compute);
-    unsafe {
-        float_root(
-            f1000000.as_ptr(),
-            FLOAT_SIZE,
-            6,
-            f_compute.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let _ = trace_float("  float 6th root of 1000000:", &f_compute);
-
-    all_pass
-}
-
-fn test_float_invert() -> bool {
-    let _ = trace("\n$$$ test_float_invert $$$");
-    let mut all_pass = true;
-
-    let mut f_compute: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    let mut f10: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_int(
-            10,
-            f10.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    unsafe {
-        float_div(
-            FLOAT_ONE.as_ptr(),
-            FLOAT_SIZE,
-            f10.as_ptr(),
-            FLOAT_SIZE,
-            f_compute.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let _ = trace_float("  invert a float from 10:", &f_compute);
-    unsafe {
-        float_div(
-            FLOAT_ONE.as_ptr(),
-            FLOAT_SIZE,
-            f_compute.as_ptr(),
-            FLOAT_SIZE,
-            f_compute.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let _ = trace_float("  invert again:", &f_compute);
-
-    // if f10's value is 7, then invert twice won't match the original value
-    if 0 == unsafe { float_cmp(f10.as_ptr(), FLOAT_SIZE, f_compute.as_ptr(), FLOAT_SIZE) } {
-        let _ = trace("  invert twice: good");
-    } else {
-        let _ = trace("  invert twice: failed");
-        all_pass = false;
-    }
-
-    all_pass
-}
-
-fn test_float_to_int() -> bool {
-    let _ = trace("\n$$$ test_float_to_int $$$");
-    let mut all_pass = true;
-    let mut result: [u8; 8] = [0u8; 8];
-
-    // Test converting FLOAT_ONE (value 1) to int
-    let ret = unsafe {
-        float_to_int(
-            FLOAT_ONE.as_ptr(),
-            FLOAT_SIZE as i32,
-            result.as_mut_ptr(),
-            8,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    if ret == 8 {
-        let number = i64::from_le_bytes(result);
-        if number == 1 {
-            let _ = trace("  float_to_int(1): good");
-        } else {
-            let _ = trace("  float_to_int(1): failed");
-            let _ = trace_num("    got:", number);
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float_to_int(1): failed with error");
-        let _ = trace_num("    error code:", ret as i64);
-        all_pass = false;
-    }
-
-    // Test converting FLOAT_NEGATIVE_ONE (value -1) to int
-    let ret = unsafe {
-        float_to_int(
-            FLOAT_NEGATIVE_ONE.as_ptr(),
-            FLOAT_SIZE as i32,
-            result.as_mut_ptr(),
-            8,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    if ret == 8 {
-        let number = i64::from_le_bytes(result);
-        if number == -1 {
-            let _ = trace("  float_to_int(-1): good");
-        } else {
-            let _ = trace("  float_to_int(-1): failed");
-            let _ = trace_num("    got:", number);
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float_to_int(-1): failed with error");
-        let _ = trace_num("    error code:", ret as i64);
-        all_pass = false;
-    }
-
-    // Test converting a larger number (i64::MAX)
-    let test_val: i64 = i64::MAX;
-    let mut f_max: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_int(
-            test_val,
-            f_max.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let ret = unsafe {
-        float_to_int(
-            f_max.as_ptr(),
-            FLOAT_SIZE as i32,
-            result.as_mut_ptr(),
-            8,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    if ret == 8 {
-        let number = i64::from_le_bytes(result);
-        if number == test_val {
-            let _ = trace("  float_to_int(i64::MAX): good");
-        } else {
-            let _ = trace("  float_to_int(i64::MAX): failed");
-            let _ = trace_num("    expected:", test_val);
-            let _ = trace_num("    got:", number);
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float_to_int(i64::MAX): failed with error");
-        let _ = trace_num("    error code:", ret as i64);
-        all_pass = false;
-    }
-
-    // Test converting zero
-    let mut f0: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_int(
-            0,
-            f0.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let ret = unsafe {
-        float_to_int(
-            f0.as_ptr(),
-            FLOAT_SIZE as i32,
-            result.as_mut_ptr(),
-            8,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    if ret == 8 {
-        let number = i64::from_le_bytes(result);
-        if number == 0 {
-            let _ = trace("  float_to_int(0): good");
-        } else {
-            let _ = trace("  float_to_int(0): failed");
-            let _ = trace_num("    got:", number);
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float_to_int(0): failed with error");
-        let _ = trace_num("    error code:", ret as i64);
-        all_pass = false;
-    }
-
-    // Test rounding with fractional value (0.1)
-    let mut f01: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_mant_exp(
-            1,
-            -1,
-            f01.as_mut_ptr(),
-            FLOAT_SIZE as i32,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    let ret = unsafe {
-        float_to_int(
-            f01.as_ptr(),
-            FLOAT_SIZE as i32,
-            result.as_mut_ptr(),
-            8 as i32,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-    if ret == 8 as i32 {
-        let number = i64::from_le_bytes(result);
-        if number == 0 {
-            let _ = trace("  float_to_int(0.1, to_nearest): good");
-        } else {
-            let _ = trace("  float_to_int(0.1, to_nearest): failed");
-            let _ = trace_num("    got:", number);
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float_to_int(0.1, to_nearest): failed with error");
-        let _ = trace_num("    error code:", ret as i64);
-        all_pass = false;
-    }
-
-    // Test rounding mode 1 (towards_zero)
-    let ret = unsafe {
-        float_to_int(
-            f01.as_ptr(),
-            FLOAT_SIZE as i32,
-            result.as_mut_ptr(),
-            8 as i32,
-            1,
-        )
-    };
-    if ret == 8 as i32 {
-        let number = i64::from_le_bytes(result);
-        if number == 0 {
-            let _ = trace("  float_to_int(0.1, towards_zero): good");
-        } else {
-            let _ = trace("  float_to_int(0.1, towards_zero): failed");
-            let _ = trace_num("    got:", number);
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float_to_int(0.1, towards_zero): failed with error");
-        let _ = trace_num("    error code:", ret as i64);
-        all_pass = false;
-    }
-
-    all_pass
-}
-
-fn test_float_to_mant_exp() -> bool {
-    let _ = trace("\n$$$ test_float_to_mant_exp $$$");
-    let mut all_pass = true;
-
-    // Test with FLOAT_ONE (value 1)
-    let mut mantissa_bytes: [u8; 8] = [0u8; 8];
-    let mut exponent_bytes: [u8; 4] = [0u8; 4];
-    let result = unsafe {
-        float_to_mant_exp(
-            FLOAT_ONE.as_ptr(),
-            FLOAT_SIZE as i32,
-            mantissa_bytes.as_mut_ptr(),
-            8,
-            exponent_bytes.as_mut_ptr(),
-            4,
-        )
-    };
-
-    if result == FLOAT_SIZE as i32 {
-        let mantissa = i64::from_le_bytes(mantissa_bytes);
-        let exponent = i32::from_le_bytes(exponent_bytes);
-        if mantissa == 1000000000000000000 && exponent == -18 {
-            let _ = trace("  float_to_mant_exp(1): good");
-        } else {
-            let _ = trace("  float_to_mant_exp(1): failed");
-            let _ = trace_num("    expected mantissa 1000000000000000000, got:", mantissa);
-            let _ = trace_num("    expected exponent -18, got:", exponent as i64);
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float_to_mant_exp(1): failed with error");
-        let _ = trace_num("    error code:", result as i64);
-        all_pass = false;
-    }
-
-    // Test with FLOAT_NEGATIVE_ONE (value -1)
-    let mut mantissa_bytes: [u8; 8] = [0u8; 8];
-    let mut exponent_bytes: [u8; 4] = [0u8; 4];
-    let result = unsafe {
-        float_to_mant_exp(
-            FLOAT_NEGATIVE_ONE.as_ptr(),
-            FLOAT_SIZE as i32,
-            mantissa_bytes.as_mut_ptr(),
-            8,
-            exponent_bytes.as_mut_ptr(),
-            4,
-        )
-    };
-
-    if result == FLOAT_SIZE as i32 {
-        let mantissa = i64::from_le_bytes(mantissa_bytes);
-        let exponent = i32::from_le_bytes(exponent_bytes);
-        if mantissa == -1000000000000000000 && exponent == -18 {
-            let _ = trace("  float_to_mant_exp(-1): good");
-        } else {
-            let _ = trace("  float_to_mant_exp(-1): failed");
-            let _ = trace_num("    expected mantissa -1000000000000000000, got:", mantissa);
-            let _ = trace_num("    expected exponent -18, got:", exponent as i64);
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float_to_mant_exp(-1): failed with error");
-        let _ = trace_num("    error code:", result as i64);
-        all_pass = false;
-    }
-
-    // Test with a float created from int (10)
-    let mut f10: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_int(
-            10,
-            f10.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-
-    let mut mantissa_bytes: [u8; 8] = [0u8; 8];
-    let mut exponent_bytes: [u8; 4] = [0u8; 4];
-    let result = unsafe {
-        float_to_mant_exp(
-            f10.as_ptr(),
-            FLOAT_SIZE as i32,
-            mantissa_bytes.as_mut_ptr(),
-            8,
-            exponent_bytes.as_mut_ptr(),
-            4,
-        )
-    };
-
-    if result == FLOAT_SIZE as i32 {
-        let mantissa = i64::from_le_bytes(mantissa_bytes);
-        let exponent = i32::from_le_bytes(exponent_bytes);
-        if mantissa == 1000000000000000000 && exponent == -17 {
-            let _ = trace("  float_to_mant_exp(10): good");
-        } else {
-            let _ = trace("  float_to_mant_exp(10): failed");
-            let _ = trace_num("    expected mantissa 1000000000000000000, got:", mantissa);
-            let _ = trace_num("    expected exponent -17, got:", exponent as i64);
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float_to_mant_exp(10): failed with error");
-        let _ = trace_num("    error code:", result as i64);
-        all_pass = false;
-    }
-
-    // Test with zero
-    let mut f0: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    unsafe {
-        float_from_int(
-            0,
-            f0.as_mut_ptr(),
-            FLOAT_SIZE,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-
-    let mut mantissa_bytes: [u8; 8] = [0u8; 8];
-    let mut exponent_bytes: [u8; 4] = [0u8; 4];
-    let result = unsafe {
-        float_to_mant_exp(
-            f0.as_ptr(),
-            FLOAT_SIZE as i32,
-            mantissa_bytes.as_mut_ptr(),
-            8,
-            exponent_bytes.as_mut_ptr(),
-            4,
-        )
-    };
-
-    if result == FLOAT_SIZE as i32 {
-        let mantissa = i64::from_le_bytes(mantissa_bytes);
-        let exponent = i32::from_le_bytes(exponent_bytes);
-        if mantissa == 0 && exponent == -2147483648 {
-            let _ = trace("  float_to_mant_exp(0): good");
-        } else {
-            let _ = trace("  float_to_mant_exp(0): failed");
-            let _ = trace_num("    expected mantissa 0, got:", mantissa);
-            let _ = trace_num("    expected exponent -2147483648, got:", exponent as i64);
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float_to_mant_exp(0): failed with error");
-        let _ = trace_num("    error code:", result as i64);
-        all_pass = false;
-    }
-
-    all_pass
-}
-
-fn test_float_from_stamount() -> bool {
-    let _ = trace("\n$$$ test_float_from_stamount $$$");
-    let mut all_pass = true;
-
-    // STAmount is serialized as:
-    // - 1 byte: type/flags
-    // - 8 bytes: amount (for XRP) or mantissa (for IOU)
-    // - For IOU: additional currency and issuer fields
-
-    // Create an XRP amount: 100 XRP = 100,000,000 drops
-    // XRP format: bit 62 clear (not IOU), bit 63 clear (not negative)
-    // Amount in drops: 100,000,000 = 0x05F5E100
-    let xrp_amount: [u8; 8] = [0x40, 0x00, 0x00, 0x00, 0x05, 0xF5, 0xE1, 0x00];
-
-    let mut f_result: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    let result_size = unsafe {
-        float_from_stamount(
-            xrp_amount.as_ptr(),
-            8,
-            f_result.as_mut_ptr(),
-            FLOAT_SIZE as i32,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-
-    if result_size == FLOAT_SIZE as i32 {
-        let _ = trace_float("  float from XRP amount (100 XRP):", &f_result);
-
-        // Convert back to int to verify
-        let mut int_bytes: [u8; 8] = [0u8; 8];
-        let ret = unsafe {
-            float_to_int(
-                f_result.as_ptr(),
-                FLOAT_SIZE as i32,
-                int_bytes.as_mut_ptr(),
-                8,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        };
-        if ret == 8 {
-            let int_val = i64::from_le_bytes(int_bytes);
-            if int_val == 100000000 {
-                let _ = trace("  XRP amount conversion: good");
-            } else {
-                let _ = trace("  XRP amount conversion: failed");
-                let _ = trace_num("    expected 100000000, got:", int_val);
-                all_pass = false;
-            }
-        } else {
-            let _ = trace("  XRP amount conversion: failed - float_to_int error");
-            let _ = trace_num("    error code:", ret as i64);
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float from XRP amount: failed");
-        let _ = trace_num("    result_size:", result_size as i64);
-        all_pass = false;
-    }
-
-    all_pass
-}
-
-fn test_float_from_stnumber() -> bool {
-    let _ = trace("\n$$$ test_float_from_stnumber $$$");
-    let mut all_pass = true;
-
-    // STNumber is serialized as:
-    // - 8 bytes: mantissa (big-endian signed int64)
-    // - 4 bytes: exponent (big-endian signed int32)
-
-    // Create STNumber for value 123 (mantissa=123*10^18, exponent=-18)
-    // mantissa = 123000000000000000000 = 0x6ADF37F675EF6B28000
-    // But we need to fit in int64, so use mantissa=123*10^15, exponent=-15
-    // 123*10^15 = 123000000000000000 = 0x01B69B4BA630F34000
-    let stnumber_123: [u8; 12] = [
-        0x01, 0xB6, 0x9B, 0x4B, 0xA6, 0x30, 0xF3, 0x40, // mantissa
-        0xFF, 0xFF, 0xFF, 0xF1, // exponent = -15
-    ];
-
-    let mut f_result: [u8; FLOAT_SIZE] = [0u8; FLOAT_SIZE];
-    let result_size = unsafe {
-        float_from_stnumber(
-            stnumber_123.as_ptr(),
-            12,
-            f_result.as_mut_ptr(),
-            FLOAT_SIZE as i32,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-
-    if result_size == FLOAT_SIZE as i32 {
-        let _ = trace_float("  float from STNumber (123):", &f_result);
-
-        // Convert back to int to verify
-        let mut int_bytes: [u8; 8] = [0u8; 8];
-        let ret = unsafe {
-            float_to_int(
-                f_result.as_ptr(),
-                FLOAT_SIZE as i32,
-                int_bytes.as_mut_ptr(),
-                8,
-                FLOAT_ROUNDING_MODES_TO_NEAREST,
-            )
-        };
-        if ret == 8 {
-            let int_val = i64::from_le_bytes(int_bytes);
-            if int_val == 123 {
-                let _ = trace("  STNumber conversion: good");
-            } else {
-                let _ = trace("  STNumber conversion: failed");
-                let _ = trace_num("    expected 123, got:", int_val);
-                all_pass = false;
-            }
-        } else {
-            let _ = trace("  STNumber conversion: failed - float_to_int error");
-            let _ = trace_num("    error code:", ret as i64);
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float from STNumber: failed");
-        let _ = trace_num("    result_size:", result_size as i64);
-        all_pass = false;
-    }
-
-    // Test with FLOAT_ONE constant (which is already in STNumber format)
-    let result_size = unsafe {
-        float_from_stnumber(
-            FLOAT_ONE.as_ptr(),
-            FLOAT_SIZE as i32,
-            f_result.as_mut_ptr(),
-            FLOAT_SIZE as i32,
-            FLOAT_ROUNDING_MODES_TO_NEAREST,
-        )
-    };
-
-    if result_size == FLOAT_SIZE as i32 {
-        let _ = trace_float("  float from STNumber (1):", &f_result);
-
-        // Should match FLOAT_ONE
-        if 0 == unsafe {
-            float_cmp(
-                f_result.as_ptr(),
-                FLOAT_SIZE,
-                FLOAT_ONE.as_ptr(),
-                FLOAT_SIZE,
-            )
-        } {
-            let _ = trace("  STNumber(1) == FLOAT_ONE: good");
-        } else {
-            let _ = trace("  STNumber(1) == FLOAT_ONE: failed");
-            all_pass = false;
-        }
-    } else {
-        let _ = trace("  float from STNumber(1): failed");
-        all_pass = false;
-    }
-
-    all_pass
-}
-
-#[unsafe(no_mangle)]
-pub extern "C" fn escrow_finish() -> i32 {
-    let mut all_pass = true;
-    all_pass &= test_float_from_wasm();
-    all_pass &= test_float_cmp();
-    all_pass &= test_float_add_subtract();
-    all_pass &= test_float_mult_divide();
-    all_pass &= test_float_pow();
-    all_pass &= test_float_root();
-    all_pass &= test_float_invert();
-    all_pass &= test_float_to_int();
-    all_pass &= test_float_to_mant_exp();
-    all_pass &= test_float_from_stamount();
-    all_pass &= test_float_from_stnumber();
-
-    if all_pass {
-        1
-    } else {
-        0
-    }
-}
diff --git a/src/test/app/wasm_fixtures/infiniteLoop.c b/src/test/app/wasm_fixtures/infiniteLoop.c
deleted file mode 100644
index ba84a92ac1..0000000000
--- a/src/test/app/wasm_fixtures/infiniteLoop.c
+++ /dev/null
@@ -1,7 +0,0 @@
-int loop()
-{
-  int volatile x = 0;
-  while (1)
-    x++;
-  return x;
-}
diff --git a/src/test/app/wasm_fixtures/ledgerSqn.c b/src/test/app/wasm_fixtures/ledgerSqn.c
deleted file mode 100644
index 0f4c27af7d..0000000000
--- a/src/test/app/wasm_fixtures/ledgerSqn.c
+++ /dev/null
@@ -1,14 +0,0 @@
-#include 
-
-int32_t ldgr_index(uint8_t *, int32_t);
-
-int escrow_finish()
-{
-  uint32_t sqn;
-  int32_t result = ldgr_index((uint8_t *)&sqn, sizeof(sqn));
-
-  if (result < 0)
-    return result;
-
-  return sqn >= 5 ? 5 : 0;
-}
diff --git a/src/test/app/wasm_fixtures/thousand1_params.c b/src/test/app/wasm_fixtures/thousand1_params.c
deleted file mode 100644
index 1a281461c4..0000000000
--- a/src/test/app/wasm_fixtures/thousand1_params.c
+++ /dev/null
@@ -1,264 +0,0 @@
-// clang-format off
-
-#include 
-
-int32_t test(
-  int32_t p0, int32_t p1, int32_t p2, int32_t p3, int32_t p4, int32_t p5, int32_t p6, int32_t p7
-, int32_t p8, int32_t p9, int32_t p10, int32_t p11, int32_t p12, int32_t p13, int32_t p14, int32_t p15
-, int32_t p16, int32_t p17, int32_t p18, int32_t p19, int32_t p20, int32_t p21, int32_t p22, int32_t p23
-, int32_t p24, int32_t p25, int32_t p26, int32_t p27, int32_t p28, int32_t p29, int32_t p30, int32_t p31
-, int32_t p32, int32_t p33, int32_t p34, int32_t p35, int32_t p36, int32_t p37, int32_t p38, int32_t p39
-, int32_t p40, int32_t p41, int32_t p42, int32_t p43, int32_t p44, int32_t p45, int32_t p46, int32_t p47
-, int32_t p48, int32_t p49, int32_t p50, int32_t p51, int32_t p52, int32_t p53, int32_t p54, int32_t p55
-, int32_t p56, int32_t p57, int32_t p58, int32_t p59, int32_t p60, int32_t p61, int32_t p62, int32_t p63
-, int32_t p64, int32_t p65, int32_t p66, int32_t p67, int32_t p68, int32_t p69, int32_t p70, int32_t p71
-, int32_t p72, int32_t p73, int32_t p74, int32_t p75, int32_t p76, int32_t p77, int32_t p78, int32_t p79
-, int32_t p80, int32_t p81, int32_t p82, int32_t p83, int32_t p84, int32_t p85, int32_t p86, int32_t p87
-, int32_t p88, int32_t p89, int32_t p90, int32_t p91, int32_t p92, int32_t p93, int32_t p94, int32_t p95
-, int32_t p96, int32_t p97, int32_t p98, int32_t p99, int32_t p100, int32_t p101, int32_t p102, int32_t p103
-, int32_t p104, int32_t p105, int32_t p106, int32_t p107, int32_t p108, int32_t p109, int32_t p110, int32_t p111
-, int32_t p112, int32_t p113, int32_t p114, int32_t p115, int32_t p116, int32_t p117, int32_t p118, int32_t p119
-, int32_t p120, int32_t p121, int32_t p122, int32_t p123, int32_t p124, int32_t p125, int32_t p126, int32_t p127
-, int32_t p128, int32_t p129, int32_t p130, int32_t p131, int32_t p132, int32_t p133, int32_t p134, int32_t p135
-, int32_t p136, int32_t p137, int32_t p138, int32_t p139, int32_t p140, int32_t p141, int32_t p142, int32_t p143
-, int32_t p144, int32_t p145, int32_t p146, int32_t p147, int32_t p148, int32_t p149, int32_t p150, int32_t p151
-, int32_t p152, int32_t p153, int32_t p154, int32_t p155, int32_t p156, int32_t p157, int32_t p158, int32_t p159
-, int32_t p160, int32_t p161, int32_t p162, int32_t p163, int32_t p164, int32_t p165, int32_t p166, int32_t p167
-, int32_t p168, int32_t p169, int32_t p170, int32_t p171, int32_t p172, int32_t p173, int32_t p174, int32_t p175
-, int32_t p176, int32_t p177, int32_t p178, int32_t p179, int32_t p180, int32_t p181, int32_t p182, int32_t p183
-, int32_t p184, int32_t p185, int32_t p186, int32_t p187, int32_t p188, int32_t p189, int32_t p190, int32_t p191
-, int32_t p192, int32_t p193, int32_t p194, int32_t p195, int32_t p196, int32_t p197, int32_t p198, int32_t p199
-, int32_t p200, int32_t p201, int32_t p202, int32_t p203, int32_t p204, int32_t p205, int32_t p206, int32_t p207
-, int32_t p208, int32_t p209, int32_t p210, int32_t p211, int32_t p212, int32_t p213, int32_t p214, int32_t p215
-, int32_t p216, int32_t p217, int32_t p218, int32_t p219, int32_t p220, int32_t p221, int32_t p222, int32_t p223
-, int32_t p224, int32_t p225, int32_t p226, int32_t p227, int32_t p228, int32_t p229, int32_t p230, int32_t p231
-, int32_t p232, int32_t p233, int32_t p234, int32_t p235, int32_t p236, int32_t p237, int32_t p238, int32_t p239
-, int32_t p240, int32_t p241, int32_t p242, int32_t p243, int32_t p244, int32_t p245, int32_t p246, int32_t p247
-, int32_t p248, int32_t p249, int32_t p250, int32_t p251, int32_t p252, int32_t p253, int32_t p254, int32_t p255
-, int32_t p256, int32_t p257, int32_t p258, int32_t p259, int32_t p260, int32_t p261, int32_t p262, int32_t p263
-, int32_t p264, int32_t p265, int32_t p266, int32_t p267, int32_t p268, int32_t p269, int32_t p270, int32_t p271
-, int32_t p272, int32_t p273, int32_t p274, int32_t p275, int32_t p276, int32_t p277, int32_t p278, int32_t p279
-, int32_t p280, int32_t p281, int32_t p282, int32_t p283, int32_t p284, int32_t p285, int32_t p286, int32_t p287
-, int32_t p288, int32_t p289, int32_t p290, int32_t p291, int32_t p292, int32_t p293, int32_t p294, int32_t p295
-, int32_t p296, int32_t p297, int32_t p298, int32_t p299, int32_t p300, int32_t p301, int32_t p302, int32_t p303
-, int32_t p304, int32_t p305, int32_t p306, int32_t p307, int32_t p308, int32_t p309, int32_t p310, int32_t p311
-, int32_t p312, int32_t p313, int32_t p314, int32_t p315, int32_t p316, int32_t p317, int32_t p318, int32_t p319
-, int32_t p320, int32_t p321, int32_t p322, int32_t p323, int32_t p324, int32_t p325, int32_t p326, int32_t p327
-, int32_t p328, int32_t p329, int32_t p330, int32_t p331, int32_t p332, int32_t p333, int32_t p334, int32_t p335
-, int32_t p336, int32_t p337, int32_t p338, int32_t p339, int32_t p340, int32_t p341, int32_t p342, int32_t p343
-, int32_t p344, int32_t p345, int32_t p346, int32_t p347, int32_t p348, int32_t p349, int32_t p350, int32_t p351
-, int32_t p352, int32_t p353, int32_t p354, int32_t p355, int32_t p356, int32_t p357, int32_t p358, int32_t p359
-, int32_t p360, int32_t p361, int32_t p362, int32_t p363, int32_t p364, int32_t p365, int32_t p366, int32_t p367
-, int32_t p368, int32_t p369, int32_t p370, int32_t p371, int32_t p372, int32_t p373, int32_t p374, int32_t p375
-, int32_t p376, int32_t p377, int32_t p378, int32_t p379, int32_t p380, int32_t p381, int32_t p382, int32_t p383
-, int32_t p384, int32_t p385, int32_t p386, int32_t p387, int32_t p388, int32_t p389, int32_t p390, int32_t p391
-, int32_t p392, int32_t p393, int32_t p394, int32_t p395, int32_t p396, int32_t p397, int32_t p398, int32_t p399
-, int32_t p400, int32_t p401, int32_t p402, int32_t p403, int32_t p404, int32_t p405, int32_t p406, int32_t p407
-, int32_t p408, int32_t p409, int32_t p410, int32_t p411, int32_t p412, int32_t p413, int32_t p414, int32_t p415
-, int32_t p416, int32_t p417, int32_t p418, int32_t p419, int32_t p420, int32_t p421, int32_t p422, int32_t p423
-, int32_t p424, int32_t p425, int32_t p426, int32_t p427, int32_t p428, int32_t p429, int32_t p430, int32_t p431
-, int32_t p432, int32_t p433, int32_t p434, int32_t p435, int32_t p436, int32_t p437, int32_t p438, int32_t p439
-, int32_t p440, int32_t p441, int32_t p442, int32_t p443, int32_t p444, int32_t p445, int32_t p446, int32_t p447
-, int32_t p448, int32_t p449, int32_t p450, int32_t p451, int32_t p452, int32_t p453, int32_t p454, int32_t p455
-, int32_t p456, int32_t p457, int32_t p458, int32_t p459, int32_t p460, int32_t p461, int32_t p462, int32_t p463
-, int32_t p464, int32_t p465, int32_t p466, int32_t p467, int32_t p468, int32_t p469, int32_t p470, int32_t p471
-, int32_t p472, int32_t p473, int32_t p474, int32_t p475, int32_t p476, int32_t p477, int32_t p478, int32_t p479
-, int32_t p480, int32_t p481, int32_t p482, int32_t p483, int32_t p484, int32_t p485, int32_t p486, int32_t p487
-, int32_t p488, int32_t p489, int32_t p490, int32_t p491, int32_t p492, int32_t p493, int32_t p494, int32_t p495
-, int32_t p496, int32_t p497, int32_t p498, int32_t p499, int32_t p500, int32_t p501, int32_t p502, int32_t p503
-, int32_t p504, int32_t p505, int32_t p506, int32_t p507, int32_t p508, int32_t p509, int32_t p510, int32_t p511
-, int32_t p512, int32_t p513, int32_t p514, int32_t p515, int32_t p516, int32_t p517, int32_t p518, int32_t p519
-, int32_t p520, int32_t p521, int32_t p522, int32_t p523, int32_t p524, int32_t p525, int32_t p526, int32_t p527
-, int32_t p528, int32_t p529, int32_t p530, int32_t p531, int32_t p532, int32_t p533, int32_t p534, int32_t p535
-, int32_t p536, int32_t p537, int32_t p538, int32_t p539, int32_t p540, int32_t p541, int32_t p542, int32_t p543
-, int32_t p544, int32_t p545, int32_t p546, int32_t p547, int32_t p548, int32_t p549, int32_t p550, int32_t p551
-, int32_t p552, int32_t p553, int32_t p554, int32_t p555, int32_t p556, int32_t p557, int32_t p558, int32_t p559
-, int32_t p560, int32_t p561, int32_t p562, int32_t p563, int32_t p564, int32_t p565, int32_t p566, int32_t p567
-, int32_t p568, int32_t p569, int32_t p570, int32_t p571, int32_t p572, int32_t p573, int32_t p574, int32_t p575
-, int32_t p576, int32_t p577, int32_t p578, int32_t p579, int32_t p580, int32_t p581, int32_t p582, int32_t p583
-, int32_t p584, int32_t p585, int32_t p586, int32_t p587, int32_t p588, int32_t p589, int32_t p590, int32_t p591
-, int32_t p592, int32_t p593, int32_t p594, int32_t p595, int32_t p596, int32_t p597, int32_t p598, int32_t p599
-, int32_t p600, int32_t p601, int32_t p602, int32_t p603, int32_t p604, int32_t p605, int32_t p606, int32_t p607
-, int32_t p608, int32_t p609, int32_t p610, int32_t p611, int32_t p612, int32_t p613, int32_t p614, int32_t p615
-, int32_t p616, int32_t p617, int32_t p618, int32_t p619, int32_t p620, int32_t p621, int32_t p622, int32_t p623
-, int32_t p624, int32_t p625, int32_t p626, int32_t p627, int32_t p628, int32_t p629, int32_t p630, int32_t p631
-, int32_t p632, int32_t p633, int32_t p634, int32_t p635, int32_t p636, int32_t p637, int32_t p638, int32_t p639
-, int32_t p640, int32_t p641, int32_t p642, int32_t p643, int32_t p644, int32_t p645, int32_t p646, int32_t p647
-, int32_t p648, int32_t p649, int32_t p650, int32_t p651, int32_t p652, int32_t p653, int32_t p654, int32_t p655
-, int32_t p656, int32_t p657, int32_t p658, int32_t p659, int32_t p660, int32_t p661, int32_t p662, int32_t p663
-, int32_t p664, int32_t p665, int32_t p666, int32_t p667, int32_t p668, int32_t p669, int32_t p670, int32_t p671
-, int32_t p672, int32_t p673, int32_t p674, int32_t p675, int32_t p676, int32_t p677, int32_t p678, int32_t p679
-, int32_t p680, int32_t p681, int32_t p682, int32_t p683, int32_t p684, int32_t p685, int32_t p686, int32_t p687
-, int32_t p688, int32_t p689, int32_t p690, int32_t p691, int32_t p692, int32_t p693, int32_t p694, int32_t p695
-, int32_t p696, int32_t p697, int32_t p698, int32_t p699, int32_t p700, int32_t p701, int32_t p702, int32_t p703
-, int32_t p704, int32_t p705, int32_t p706, int32_t p707, int32_t p708, int32_t p709, int32_t p710, int32_t p711
-, int32_t p712, int32_t p713, int32_t p714, int32_t p715, int32_t p716, int32_t p717, int32_t p718, int32_t p719
-, int32_t p720, int32_t p721, int32_t p722, int32_t p723, int32_t p724, int32_t p725, int32_t p726, int32_t p727
-, int32_t p728, int32_t p729, int32_t p730, int32_t p731, int32_t p732, int32_t p733, int32_t p734, int32_t p735
-, int32_t p736, int32_t p737, int32_t p738, int32_t p739, int32_t p740, int32_t p741, int32_t p742, int32_t p743
-, int32_t p744, int32_t p745, int32_t p746, int32_t p747, int32_t p748, int32_t p749, int32_t p750, int32_t p751
-, int32_t p752, int32_t p753, int32_t p754, int32_t p755, int32_t p756, int32_t p757, int32_t p758, int32_t p759
-, int32_t p760, int32_t p761, int32_t p762, int32_t p763, int32_t p764, int32_t p765, int32_t p766, int32_t p767
-, int32_t p768, int32_t p769, int32_t p770, int32_t p771, int32_t p772, int32_t p773, int32_t p774, int32_t p775
-, int32_t p776, int32_t p777, int32_t p778, int32_t p779, int32_t p780, int32_t p781, int32_t p782, int32_t p783
-, int32_t p784, int32_t p785, int32_t p786, int32_t p787, int32_t p788, int32_t p789, int32_t p790, int32_t p791
-, int32_t p792, int32_t p793, int32_t p794, int32_t p795, int32_t p796, int32_t p797, int32_t p798, int32_t p799
-, int32_t p800, int32_t p801, int32_t p802, int32_t p803, int32_t p804, int32_t p805, int32_t p806, int32_t p807
-, int32_t p808, int32_t p809, int32_t p810, int32_t p811, int32_t p812, int32_t p813, int32_t p814, int32_t p815
-, int32_t p816, int32_t p817, int32_t p818, int32_t p819, int32_t p820, int32_t p821, int32_t p822, int32_t p823
-, int32_t p824, int32_t p825, int32_t p826, int32_t p827, int32_t p828, int32_t p829, int32_t p830, int32_t p831
-, int32_t p832, int32_t p833, int32_t p834, int32_t p835, int32_t p836, int32_t p837, int32_t p838, int32_t p839
-, int32_t p840, int32_t p841, int32_t p842, int32_t p843, int32_t p844, int32_t p845, int32_t p846, int32_t p847
-, int32_t p848, int32_t p849, int32_t p850, int32_t p851, int32_t p852, int32_t p853, int32_t p854, int32_t p855
-, int32_t p856, int32_t p857, int32_t p858, int32_t p859, int32_t p860, int32_t p861, int32_t p862, int32_t p863
-, int32_t p864, int32_t p865, int32_t p866, int32_t p867, int32_t p868, int32_t p869, int32_t p870, int32_t p871
-, int32_t p872, int32_t p873, int32_t p874, int32_t p875, int32_t p876, int32_t p877, int32_t p878, int32_t p879
-, int32_t p880, int32_t p881, int32_t p882, int32_t p883, int32_t p884, int32_t p885, int32_t p886, int32_t p887
-, int32_t p888, int32_t p889, int32_t p890, int32_t p891, int32_t p892, int32_t p893, int32_t p894, int32_t p895
-, int32_t p896, int32_t p897, int32_t p898, int32_t p899, int32_t p900, int32_t p901, int32_t p902, int32_t p903
-, int32_t p904, int32_t p905, int32_t p906, int32_t p907, int32_t p908, int32_t p909, int32_t p910, int32_t p911
-, int32_t p912, int32_t p913, int32_t p914, int32_t p915, int32_t p916, int32_t p917, int32_t p918, int32_t p919
-, int32_t p920, int32_t p921, int32_t p922, int32_t p923, int32_t p924, int32_t p925, int32_t p926, int32_t p927
-, int32_t p928, int32_t p929, int32_t p930, int32_t p931, int32_t p932, int32_t p933, int32_t p934, int32_t p935
-, int32_t p936, int32_t p937, int32_t p938, int32_t p939, int32_t p940, int32_t p941, int32_t p942, int32_t p943
-, int32_t p944, int32_t p945, int32_t p946, int32_t p947, int32_t p948, int32_t p949, int32_t p950, int32_t p951
-, int32_t p952, int32_t p953, int32_t p954, int32_t p955, int32_t p956, int32_t p957, int32_t p958, int32_t p959
-, int32_t p960, int32_t p961, int32_t p962, int32_t p963, int32_t p964, int32_t p965, int32_t p966, int32_t p967
-, int32_t p968, int32_t p969, int32_t p970, int32_t p971, int32_t p972, int32_t p973, int32_t p974, int32_t p975
-, int32_t p976, int32_t p977, int32_t p978, int32_t p979, int32_t p980, int32_t p981, int32_t p982, int32_t p983
-, int32_t p984, int32_t p985, int32_t p986, int32_t p987, int32_t p988, int32_t p989, int32_t p990, int32_t p991
-, int32_t p992, int32_t p993, int32_t p994, int32_t p995, int32_t p996, int32_t p997, int32_t p998, int32_t p999
-, int32_t p1000
-)
-{
-    int32_t x;
-    x = p0 + p1 + p2 + p3 + p4 + p5 + p6 + p7
- + p8 + p9 + p10 + p11 + p12 + p13 + p14 + p15
- + p16 + p17 + p18 + p19 + p20 + p21 + p22 + p23
- + p24 + p25 + p26 + p27 + p28 + p29 + p30 + p31
- + p32 + p33 + p34 + p35 + p36 + p37 + p38 + p39
- + p40 + p41 + p42 + p43 + p44 + p45 + p46 + p47
- + p48 + p49 + p50 + p51 + p52 + p53 + p54 + p55
- + p56 + p57 + p58 + p59 + p60 + p61 + p62 + p63
- + p64 + p65 + p66 + p67 + p68 + p69 + p70 + p71
- + p72 + p73 + p74 + p75 + p76 + p77 + p78 + p79
- + p80 + p81 + p82 + p83 + p84 + p85 + p86 + p87
- + p88 + p89 + p90 + p91 + p92 + p93 + p94 + p95
- + p96 + p97 + p98 + p99 + p100 + p101 + p102 + p103
- + p104 + p105 + p106 + p107 + p108 + p109 + p110 + p111
- + p112 + p113 + p114 + p115 + p116 + p117 + p118 + p119
- + p120 + p121 + p122 + p123 + p124 + p125 + p126 + p127
- + p128 + p129 + p130 + p131 + p132 + p133 + p134 + p135
- + p136 + p137 + p138 + p139 + p140 + p141 + p142 + p143
- + p144 + p145 + p146 + p147 + p148 + p149 + p150 + p151
- + p152 + p153 + p154 + p155 + p156 + p157 + p158 + p159
- + p160 + p161 + p162 + p163 + p164 + p165 + p166 + p167
- + p168 + p169 + p170 + p171 + p172 + p173 + p174 + p175
- + p176 + p177 + p178 + p179 + p180 + p181 + p182 + p183
- + p184 + p185 + p186 + p187 + p188 + p189 + p190 + p191
- + p192 + p193 + p194 + p195 + p196 + p197 + p198 + p199
- + p200 + p201 + p202 + p203 + p204 + p205 + p206 + p207
- + p208 + p209 + p210 + p211 + p212 + p213 + p214 + p215
- + p216 + p217 + p218 + p219 + p220 + p221 + p222 + p223
- + p224 + p225 + p226 + p227 + p228 + p229 + p230 + p231
- + p232 + p233 + p234 + p235 + p236 + p237 + p238 + p239
- + p240 + p241 + p242 + p243 + p244 + p245 + p246 + p247
- + p248 + p249 + p250 + p251 + p252 + p253 + p254 + p255
- + p256 + p257 + p258 + p259 + p260 + p261 + p262 + p263
- + p264 + p265 + p266 + p267 + p268 + p269 + p270 + p271
- + p272 + p273 + p274 + p275 + p276 + p277 + p278 + p279
- + p280 + p281 + p282 + p283 + p284 + p285 + p286 + p287
- + p288 + p289 + p290 + p291 + p292 + p293 + p294 + p295
- + p296 + p297 + p298 + p299 + p300 + p301 + p302 + p303
- + p304 + p305 + p306 + p307 + p308 + p309 + p310 + p311
- + p312 + p313 + p314 + p315 + p316 + p317 + p318 + p319
- + p320 + p321 + p322 + p323 + p324 + p325 + p326 + p327
- + p328 + p329 + p330 + p331 + p332 + p333 + p334 + p335
- + p336 + p337 + p338 + p339 + p340 + p341 + p342 + p343
- + p344 + p345 + p346 + p347 + p348 + p349 + p350 + p351
- + p352 + p353 + p354 + p355 + p356 + p357 + p358 + p359
- + p360 + p361 + p362 + p363 + p364 + p365 + p366 + p367
- + p368 + p369 + p370 + p371 + p372 + p373 + p374 + p375
- + p376 + p377 + p378 + p379 + p380 + p381 + p382 + p383
- + p384 + p385 + p386 + p387 + p388 + p389 + p390 + p391
- + p392 + p393 + p394 + p395 + p396 + p397 + p398 + p399
- + p400 + p401 + p402 + p403 + p404 + p405 + p406 + p407
- + p408 + p409 + p410 + p411 + p412 + p413 + p414 + p415
- + p416 + p417 + p418 + p419 + p420 + p421 + p422 + p423
- + p424 + p425 + p426 + p427 + p428 + p429 + p430 + p431
- + p432 + p433 + p434 + p435 + p436 + p437 + p438 + p439
- + p440 + p441 + p442 + p443 + p444 + p445 + p446 + p447
- + p448 + p449 + p450 + p451 + p452 + p453 + p454 + p455
- + p456 + p457 + p458 + p459 + p460 + p461 + p462 + p463
- + p464 + p465 + p466 + p467 + p468 + p469 + p470 + p471
- + p472 + p473 + p474 + p475 + p476 + p477 + p478 + p479
- + p480 + p481 + p482 + p483 + p484 + p485 + p486 + p487
- + p488 + p489 + p490 + p491 + p492 + p493 + p494 + p495
- + p496 + p497 + p498 + p499 + p500 + p501 + p502 + p503
- + p504 + p505 + p506 + p507 + p508 + p509 + p510 + p511
- + p512 + p513 + p514 + p515 + p516 + p517 + p518 + p519
- + p520 + p521 + p522 + p523 + p524 + p525 + p526 + p527
- + p528 + p529 + p530 + p531 + p532 + p533 + p534 + p535
- + p536 + p537 + p538 + p539 + p540 + p541 + p542 + p543
- + p544 + p545 + p546 + p547 + p548 + p549 + p550 + p551
- + p552 + p553 + p554 + p555 + p556 + p557 + p558 + p559
- + p560 + p561 + p562 + p563 + p564 + p565 + p566 + p567
- + p568 + p569 + p570 + p571 + p572 + p573 + p574 + p575
- + p576 + p577 + p578 + p579 + p580 + p581 + p582 + p583
- + p584 + p585 + p586 + p587 + p588 + p589 + p590 + p591
- + p592 + p593 + p594 + p595 + p596 + p597 + p598 + p599
- + p600 + p601 + p602 + p603 + p604 + p605 + p606 + p607
- + p608 + p609 + p610 + p611 + p612 + p613 + p614 + p615
- + p616 + p617 + p618 + p619 + p620 + p621 + p622 + p623
- + p624 + p625 + p626 + p627 + p628 + p629 + p630 + p631
- + p632 + p633 + p634 + p635 + p636 + p637 + p638 + p639
- + p640 + p641 + p642 + p643 + p644 + p645 + p646 + p647
- + p648 + p649 + p650 + p651 + p652 + p653 + p654 + p655
- + p656 + p657 + p658 + p659 + p660 + p661 + p662 + p663
- + p664 + p665 + p666 + p667 + p668 + p669 + p670 + p671
- + p672 + p673 + p674 + p675 + p676 + p677 + p678 + p679
- + p680 + p681 + p682 + p683 + p684 + p685 + p686 + p687
- + p688 + p689 + p690 + p691 + p692 + p693 + p694 + p695
- + p696 + p697 + p698 + p699 + p700 + p701 + p702 + p703
- + p704 + p705 + p706 + p707 + p708 + p709 + p710 + p711
- + p712 + p713 + p714 + p715 + p716 + p717 + p718 + p719
- + p720 + p721 + p722 + p723 + p724 + p725 + p726 + p727
- + p728 + p729 + p730 + p731 + p732 + p733 + p734 + p735
- + p736 + p737 + p738 + p739 + p740 + p741 + p742 + p743
- + p744 + p745 + p746 + p747 + p748 + p749 + p750 + p751
- + p752 + p753 + p754 + p755 + p756 + p757 + p758 + p759
- + p760 + p761 + p762 + p763 + p764 + p765 + p766 + p767
- + p768 + p769 + p770 + p771 + p772 + p773 + p774 + p775
- + p776 + p777 + p778 + p779 + p780 + p781 + p782 + p783
- + p784 + p785 + p786 + p787 + p788 + p789 + p790 + p791
- + p792 + p793 + p794 + p795 + p796 + p797 + p798 + p799
- + p800 + p801 + p802 + p803 + p804 + p805 + p806 + p807
- + p808 + p809 + p810 + p811 + p812 + p813 + p814 + p815
- + p816 + p817 + p818 + p819 + p820 + p821 + p822 + p823
- + p824 + p825 + p826 + p827 + p828 + p829 + p830 + p831
- + p832 + p833 + p834 + p835 + p836 + p837 + p838 + p839
- + p840 + p841 + p842 + p843 + p844 + p845 + p846 + p847
- + p848 + p849 + p850 + p851 + p852 + p853 + p854 + p855
- + p856 + p857 + p858 + p859 + p860 + p861 + p862 + p863
- + p864 + p865 + p866 + p867 + p868 + p869 + p870 + p871
- + p872 + p873 + p874 + p875 + p876 + p877 + p878 + p879
- + p880 + p881 + p882 + p883 + p884 + p885 + p886 + p887
- + p888 + p889 + p890 + p891 + p892 + p893 + p894 + p895
- + p896 + p897 + p898 + p899 + p900 + p901 + p902 + p903
- + p904 + p905 + p906 + p907 + p908 + p909 + p910 + p911
- + p912 + p913 + p914 + p915 + p916 + p917 + p918 + p919
- + p920 + p921 + p922 + p923 + p924 + p925 + p926 + p927
- + p928 + p929 + p930 + p931 + p932 + p933 + p934 + p935
- + p936 + p937 + p938 + p939 + p940 + p941 + p942 + p943
- + p944 + p945 + p946 + p947 + p948 + p949 + p950 + p951
- + p952 + p953 + p954 + p955 + p956 + p957 + p958 + p959
- + p960 + p961 + p962 + p963 + p964 + p965 + p966 + p967
- + p968 + p969 + p970 + p971 + p972 + p973 + p974 + p975
- + p976 + p977 + p978 + p979 + p980 + p981 + p982 + p983
- + p984 + p985 + p986 + p987 + p988 + p989 + p990 + p991
- + p992 + p993 + p994 + p995 + p996 + p997 + p998 + p999
- + p1000;
-    return x;
-}
-
-// clang-format on
diff --git a/src/test/app/wasm_fixtures/thousand_params.c b/src/test/app/wasm_fixtures/thousand_params.c
deleted file mode 100644
index d934ca38c8..0000000000
--- a/src/test/app/wasm_fixtures/thousand_params.c
+++ /dev/null
@@ -1,262 +0,0 @@
-// clang-format off
-
-#include 
-
-int32_t test(
-  int32_t p0, int32_t p1, int32_t p2, int32_t p3, int32_t p4, int32_t p5, int32_t p6, int32_t p7
-, int32_t p8, int32_t p9, int32_t p10, int32_t p11, int32_t p12, int32_t p13, int32_t p14, int32_t p15
-, int32_t p16, int32_t p17, int32_t p18, int32_t p19, int32_t p20, int32_t p21, int32_t p22, int32_t p23
-, int32_t p24, int32_t p25, int32_t p26, int32_t p27, int32_t p28, int32_t p29, int32_t p30, int32_t p31
-, int32_t p32, int32_t p33, int32_t p34, int32_t p35, int32_t p36, int32_t p37, int32_t p38, int32_t p39
-, int32_t p40, int32_t p41, int32_t p42, int32_t p43, int32_t p44, int32_t p45, int32_t p46, int32_t p47
-, int32_t p48, int32_t p49, int32_t p50, int32_t p51, int32_t p52, int32_t p53, int32_t p54, int32_t p55
-, int32_t p56, int32_t p57, int32_t p58, int32_t p59, int32_t p60, int32_t p61, int32_t p62, int32_t p63
-, int32_t p64, int32_t p65, int32_t p66, int32_t p67, int32_t p68, int32_t p69, int32_t p70, int32_t p71
-, int32_t p72, int32_t p73, int32_t p74, int32_t p75, int32_t p76, int32_t p77, int32_t p78, int32_t p79
-, int32_t p80, int32_t p81, int32_t p82, int32_t p83, int32_t p84, int32_t p85, int32_t p86, int32_t p87
-, int32_t p88, int32_t p89, int32_t p90, int32_t p91, int32_t p92, int32_t p93, int32_t p94, int32_t p95
-, int32_t p96, int32_t p97, int32_t p98, int32_t p99, int32_t p100, int32_t p101, int32_t p102, int32_t p103
-, int32_t p104, int32_t p105, int32_t p106, int32_t p107, int32_t p108, int32_t p109, int32_t p110, int32_t p111
-, int32_t p112, int32_t p113, int32_t p114, int32_t p115, int32_t p116, int32_t p117, int32_t p118, int32_t p119
-, int32_t p120, int32_t p121, int32_t p122, int32_t p123, int32_t p124, int32_t p125, int32_t p126, int32_t p127
-, int32_t p128, int32_t p129, int32_t p130, int32_t p131, int32_t p132, int32_t p133, int32_t p134, int32_t p135
-, int32_t p136, int32_t p137, int32_t p138, int32_t p139, int32_t p140, int32_t p141, int32_t p142, int32_t p143
-, int32_t p144, int32_t p145, int32_t p146, int32_t p147, int32_t p148, int32_t p149, int32_t p150, int32_t p151
-, int32_t p152, int32_t p153, int32_t p154, int32_t p155, int32_t p156, int32_t p157, int32_t p158, int32_t p159
-, int32_t p160, int32_t p161, int32_t p162, int32_t p163, int32_t p164, int32_t p165, int32_t p166, int32_t p167
-, int32_t p168, int32_t p169, int32_t p170, int32_t p171, int32_t p172, int32_t p173, int32_t p174, int32_t p175
-, int32_t p176, int32_t p177, int32_t p178, int32_t p179, int32_t p180, int32_t p181, int32_t p182, int32_t p183
-, int32_t p184, int32_t p185, int32_t p186, int32_t p187, int32_t p188, int32_t p189, int32_t p190, int32_t p191
-, int32_t p192, int32_t p193, int32_t p194, int32_t p195, int32_t p196, int32_t p197, int32_t p198, int32_t p199
-, int32_t p200, int32_t p201, int32_t p202, int32_t p203, int32_t p204, int32_t p205, int32_t p206, int32_t p207
-, int32_t p208, int32_t p209, int32_t p210, int32_t p211, int32_t p212, int32_t p213, int32_t p214, int32_t p215
-, int32_t p216, int32_t p217, int32_t p218, int32_t p219, int32_t p220, int32_t p221, int32_t p222, int32_t p223
-, int32_t p224, int32_t p225, int32_t p226, int32_t p227, int32_t p228, int32_t p229, int32_t p230, int32_t p231
-, int32_t p232, int32_t p233, int32_t p234, int32_t p235, int32_t p236, int32_t p237, int32_t p238, int32_t p239
-, int32_t p240, int32_t p241, int32_t p242, int32_t p243, int32_t p244, int32_t p245, int32_t p246, int32_t p247
-, int32_t p248, int32_t p249, int32_t p250, int32_t p251, int32_t p252, int32_t p253, int32_t p254, int32_t p255
-, int32_t p256, int32_t p257, int32_t p258, int32_t p259, int32_t p260, int32_t p261, int32_t p262, int32_t p263
-, int32_t p264, int32_t p265, int32_t p266, int32_t p267, int32_t p268, int32_t p269, int32_t p270, int32_t p271
-, int32_t p272, int32_t p273, int32_t p274, int32_t p275, int32_t p276, int32_t p277, int32_t p278, int32_t p279
-, int32_t p280, int32_t p281, int32_t p282, int32_t p283, int32_t p284, int32_t p285, int32_t p286, int32_t p287
-, int32_t p288, int32_t p289, int32_t p290, int32_t p291, int32_t p292, int32_t p293, int32_t p294, int32_t p295
-, int32_t p296, int32_t p297, int32_t p298, int32_t p299, int32_t p300, int32_t p301, int32_t p302, int32_t p303
-, int32_t p304, int32_t p305, int32_t p306, int32_t p307, int32_t p308, int32_t p309, int32_t p310, int32_t p311
-, int32_t p312, int32_t p313, int32_t p314, int32_t p315, int32_t p316, int32_t p317, int32_t p318, int32_t p319
-, int32_t p320, int32_t p321, int32_t p322, int32_t p323, int32_t p324, int32_t p325, int32_t p326, int32_t p327
-, int32_t p328, int32_t p329, int32_t p330, int32_t p331, int32_t p332, int32_t p333, int32_t p334, int32_t p335
-, int32_t p336, int32_t p337, int32_t p338, int32_t p339, int32_t p340, int32_t p341, int32_t p342, int32_t p343
-, int32_t p344, int32_t p345, int32_t p346, int32_t p347, int32_t p348, int32_t p349, int32_t p350, int32_t p351
-, int32_t p352, int32_t p353, int32_t p354, int32_t p355, int32_t p356, int32_t p357, int32_t p358, int32_t p359
-, int32_t p360, int32_t p361, int32_t p362, int32_t p363, int32_t p364, int32_t p365, int32_t p366, int32_t p367
-, int32_t p368, int32_t p369, int32_t p370, int32_t p371, int32_t p372, int32_t p373, int32_t p374, int32_t p375
-, int32_t p376, int32_t p377, int32_t p378, int32_t p379, int32_t p380, int32_t p381, int32_t p382, int32_t p383
-, int32_t p384, int32_t p385, int32_t p386, int32_t p387, int32_t p388, int32_t p389, int32_t p390, int32_t p391
-, int32_t p392, int32_t p393, int32_t p394, int32_t p395, int32_t p396, int32_t p397, int32_t p398, int32_t p399
-, int32_t p400, int32_t p401, int32_t p402, int32_t p403, int32_t p404, int32_t p405, int32_t p406, int32_t p407
-, int32_t p408, int32_t p409, int32_t p410, int32_t p411, int32_t p412, int32_t p413, int32_t p414, int32_t p415
-, int32_t p416, int32_t p417, int32_t p418, int32_t p419, int32_t p420, int32_t p421, int32_t p422, int32_t p423
-, int32_t p424, int32_t p425, int32_t p426, int32_t p427, int32_t p428, int32_t p429, int32_t p430, int32_t p431
-, int32_t p432, int32_t p433, int32_t p434, int32_t p435, int32_t p436, int32_t p437, int32_t p438, int32_t p439
-, int32_t p440, int32_t p441, int32_t p442, int32_t p443, int32_t p444, int32_t p445, int32_t p446, int32_t p447
-, int32_t p448, int32_t p449, int32_t p450, int32_t p451, int32_t p452, int32_t p453, int32_t p454, int32_t p455
-, int32_t p456, int32_t p457, int32_t p458, int32_t p459, int32_t p460, int32_t p461, int32_t p462, int32_t p463
-, int32_t p464, int32_t p465, int32_t p466, int32_t p467, int32_t p468, int32_t p469, int32_t p470, int32_t p471
-, int32_t p472, int32_t p473, int32_t p474, int32_t p475, int32_t p476, int32_t p477, int32_t p478, int32_t p479
-, int32_t p480, int32_t p481, int32_t p482, int32_t p483, int32_t p484, int32_t p485, int32_t p486, int32_t p487
-, int32_t p488, int32_t p489, int32_t p490, int32_t p491, int32_t p492, int32_t p493, int32_t p494, int32_t p495
-, int32_t p496, int32_t p497, int32_t p498, int32_t p499, int32_t p500, int32_t p501, int32_t p502, int32_t p503
-, int32_t p504, int32_t p505, int32_t p506, int32_t p507, int32_t p508, int32_t p509, int32_t p510, int32_t p511
-, int32_t p512, int32_t p513, int32_t p514, int32_t p515, int32_t p516, int32_t p517, int32_t p518, int32_t p519
-, int32_t p520, int32_t p521, int32_t p522, int32_t p523, int32_t p524, int32_t p525, int32_t p526, int32_t p527
-, int32_t p528, int32_t p529, int32_t p530, int32_t p531, int32_t p532, int32_t p533, int32_t p534, int32_t p535
-, int32_t p536, int32_t p537, int32_t p538, int32_t p539, int32_t p540, int32_t p541, int32_t p542, int32_t p543
-, int32_t p544, int32_t p545, int32_t p546, int32_t p547, int32_t p548, int32_t p549, int32_t p550, int32_t p551
-, int32_t p552, int32_t p553, int32_t p554, int32_t p555, int32_t p556, int32_t p557, int32_t p558, int32_t p559
-, int32_t p560, int32_t p561, int32_t p562, int32_t p563, int32_t p564, int32_t p565, int32_t p566, int32_t p567
-, int32_t p568, int32_t p569, int32_t p570, int32_t p571, int32_t p572, int32_t p573, int32_t p574, int32_t p575
-, int32_t p576, int32_t p577, int32_t p578, int32_t p579, int32_t p580, int32_t p581, int32_t p582, int32_t p583
-, int32_t p584, int32_t p585, int32_t p586, int32_t p587, int32_t p588, int32_t p589, int32_t p590, int32_t p591
-, int32_t p592, int32_t p593, int32_t p594, int32_t p595, int32_t p596, int32_t p597, int32_t p598, int32_t p599
-, int32_t p600, int32_t p601, int32_t p602, int32_t p603, int32_t p604, int32_t p605, int32_t p606, int32_t p607
-, int32_t p608, int32_t p609, int32_t p610, int32_t p611, int32_t p612, int32_t p613, int32_t p614, int32_t p615
-, int32_t p616, int32_t p617, int32_t p618, int32_t p619, int32_t p620, int32_t p621, int32_t p622, int32_t p623
-, int32_t p624, int32_t p625, int32_t p626, int32_t p627, int32_t p628, int32_t p629, int32_t p630, int32_t p631
-, int32_t p632, int32_t p633, int32_t p634, int32_t p635, int32_t p636, int32_t p637, int32_t p638, int32_t p639
-, int32_t p640, int32_t p641, int32_t p642, int32_t p643, int32_t p644, int32_t p645, int32_t p646, int32_t p647
-, int32_t p648, int32_t p649, int32_t p650, int32_t p651, int32_t p652, int32_t p653, int32_t p654, int32_t p655
-, int32_t p656, int32_t p657, int32_t p658, int32_t p659, int32_t p660, int32_t p661, int32_t p662, int32_t p663
-, int32_t p664, int32_t p665, int32_t p666, int32_t p667, int32_t p668, int32_t p669, int32_t p670, int32_t p671
-, int32_t p672, int32_t p673, int32_t p674, int32_t p675, int32_t p676, int32_t p677, int32_t p678, int32_t p679
-, int32_t p680, int32_t p681, int32_t p682, int32_t p683, int32_t p684, int32_t p685, int32_t p686, int32_t p687
-, int32_t p688, int32_t p689, int32_t p690, int32_t p691, int32_t p692, int32_t p693, int32_t p694, int32_t p695
-, int32_t p696, int32_t p697, int32_t p698, int32_t p699, int32_t p700, int32_t p701, int32_t p702, int32_t p703
-, int32_t p704, int32_t p705, int32_t p706, int32_t p707, int32_t p708, int32_t p709, int32_t p710, int32_t p711
-, int32_t p712, int32_t p713, int32_t p714, int32_t p715, int32_t p716, int32_t p717, int32_t p718, int32_t p719
-, int32_t p720, int32_t p721, int32_t p722, int32_t p723, int32_t p724, int32_t p725, int32_t p726, int32_t p727
-, int32_t p728, int32_t p729, int32_t p730, int32_t p731, int32_t p732, int32_t p733, int32_t p734, int32_t p735
-, int32_t p736, int32_t p737, int32_t p738, int32_t p739, int32_t p740, int32_t p741, int32_t p742, int32_t p743
-, int32_t p744, int32_t p745, int32_t p746, int32_t p747, int32_t p748, int32_t p749, int32_t p750, int32_t p751
-, int32_t p752, int32_t p753, int32_t p754, int32_t p755, int32_t p756, int32_t p757, int32_t p758, int32_t p759
-, int32_t p760, int32_t p761, int32_t p762, int32_t p763, int32_t p764, int32_t p765, int32_t p766, int32_t p767
-, int32_t p768, int32_t p769, int32_t p770, int32_t p771, int32_t p772, int32_t p773, int32_t p774, int32_t p775
-, int32_t p776, int32_t p777, int32_t p778, int32_t p779, int32_t p780, int32_t p781, int32_t p782, int32_t p783
-, int32_t p784, int32_t p785, int32_t p786, int32_t p787, int32_t p788, int32_t p789, int32_t p790, int32_t p791
-, int32_t p792, int32_t p793, int32_t p794, int32_t p795, int32_t p796, int32_t p797, int32_t p798, int32_t p799
-, int32_t p800, int32_t p801, int32_t p802, int32_t p803, int32_t p804, int32_t p805, int32_t p806, int32_t p807
-, int32_t p808, int32_t p809, int32_t p810, int32_t p811, int32_t p812, int32_t p813, int32_t p814, int32_t p815
-, int32_t p816, int32_t p817, int32_t p818, int32_t p819, int32_t p820, int32_t p821, int32_t p822, int32_t p823
-, int32_t p824, int32_t p825, int32_t p826, int32_t p827, int32_t p828, int32_t p829, int32_t p830, int32_t p831
-, int32_t p832, int32_t p833, int32_t p834, int32_t p835, int32_t p836, int32_t p837, int32_t p838, int32_t p839
-, int32_t p840, int32_t p841, int32_t p842, int32_t p843, int32_t p844, int32_t p845, int32_t p846, int32_t p847
-, int32_t p848, int32_t p849, int32_t p850, int32_t p851, int32_t p852, int32_t p853, int32_t p854, int32_t p855
-, int32_t p856, int32_t p857, int32_t p858, int32_t p859, int32_t p860, int32_t p861, int32_t p862, int32_t p863
-, int32_t p864, int32_t p865, int32_t p866, int32_t p867, int32_t p868, int32_t p869, int32_t p870, int32_t p871
-, int32_t p872, int32_t p873, int32_t p874, int32_t p875, int32_t p876, int32_t p877, int32_t p878, int32_t p879
-, int32_t p880, int32_t p881, int32_t p882, int32_t p883, int32_t p884, int32_t p885, int32_t p886, int32_t p887
-, int32_t p888, int32_t p889, int32_t p890, int32_t p891, int32_t p892, int32_t p893, int32_t p894, int32_t p895
-, int32_t p896, int32_t p897, int32_t p898, int32_t p899, int32_t p900, int32_t p901, int32_t p902, int32_t p903
-, int32_t p904, int32_t p905, int32_t p906, int32_t p907, int32_t p908, int32_t p909, int32_t p910, int32_t p911
-, int32_t p912, int32_t p913, int32_t p914, int32_t p915, int32_t p916, int32_t p917, int32_t p918, int32_t p919
-, int32_t p920, int32_t p921, int32_t p922, int32_t p923, int32_t p924, int32_t p925, int32_t p926, int32_t p927
-, int32_t p928, int32_t p929, int32_t p930, int32_t p931, int32_t p932, int32_t p933, int32_t p934, int32_t p935
-, int32_t p936, int32_t p937, int32_t p938, int32_t p939, int32_t p940, int32_t p941, int32_t p942, int32_t p943
-, int32_t p944, int32_t p945, int32_t p946, int32_t p947, int32_t p948, int32_t p949, int32_t p950, int32_t p951
-, int32_t p952, int32_t p953, int32_t p954, int32_t p955, int32_t p956, int32_t p957, int32_t p958, int32_t p959
-, int32_t p960, int32_t p961, int32_t p962, int32_t p963, int32_t p964, int32_t p965, int32_t p966, int32_t p967
-, int32_t p968, int32_t p969, int32_t p970, int32_t p971, int32_t p972, int32_t p973, int32_t p974, int32_t p975
-, int32_t p976, int32_t p977, int32_t p978, int32_t p979, int32_t p980, int32_t p981, int32_t p982, int32_t p983
-, int32_t p984, int32_t p985, int32_t p986, int32_t p987, int32_t p988, int32_t p989, int32_t p990, int32_t p991
-, int32_t p992, int32_t p993, int32_t p994, int32_t p995, int32_t p996, int32_t p997, int32_t p998, int32_t p999
-)
-{
-    int32_t x;
-    x = p0 + p1 + p2 + p3 + p4 + p5 + p6 + p7
- + p8 + p9 + p10 + p11 + p12 + p13 + p14 + p15
- + p16 + p17 + p18 + p19 + p20 + p21 + p22 + p23
- + p24 + p25 + p26 + p27 + p28 + p29 + p30 + p31
- + p32 + p33 + p34 + p35 + p36 + p37 + p38 + p39
- + p40 + p41 + p42 + p43 + p44 + p45 + p46 + p47
- + p48 + p49 + p50 + p51 + p52 + p53 + p54 + p55
- + p56 + p57 + p58 + p59 + p60 + p61 + p62 + p63
- + p64 + p65 + p66 + p67 + p68 + p69 + p70 + p71
- + p72 + p73 + p74 + p75 + p76 + p77 + p78 + p79
- + p80 + p81 + p82 + p83 + p84 + p85 + p86 + p87
- + p88 + p89 + p90 + p91 + p92 + p93 + p94 + p95
- + p96 + p97 + p98 + p99 + p100 + p101 + p102 + p103
- + p104 + p105 + p106 + p107 + p108 + p109 + p110 + p111
- + p112 + p113 + p114 + p115 + p116 + p117 + p118 + p119
- + p120 + p121 + p122 + p123 + p124 + p125 + p126 + p127
- + p128 + p129 + p130 + p131 + p132 + p133 + p134 + p135
- + p136 + p137 + p138 + p139 + p140 + p141 + p142 + p143
- + p144 + p145 + p146 + p147 + p148 + p149 + p150 + p151
- + p152 + p153 + p154 + p155 + p156 + p157 + p158 + p159
- + p160 + p161 + p162 + p163 + p164 + p165 + p166 + p167
- + p168 + p169 + p170 + p171 + p172 + p173 + p174 + p175
- + p176 + p177 + p178 + p179 + p180 + p181 + p182 + p183
- + p184 + p185 + p186 + p187 + p188 + p189 + p190 + p191
- + p192 + p193 + p194 + p195 + p196 + p197 + p198 + p199
- + p200 + p201 + p202 + p203 + p204 + p205 + p206 + p207
- + p208 + p209 + p210 + p211 + p212 + p213 + p214 + p215
- + p216 + p217 + p218 + p219 + p220 + p221 + p222 + p223
- + p224 + p225 + p226 + p227 + p228 + p229 + p230 + p231
- + p232 + p233 + p234 + p235 + p236 + p237 + p238 + p239
- + p240 + p241 + p242 + p243 + p244 + p245 + p246 + p247
- + p248 + p249 + p250 + p251 + p252 + p253 + p254 + p255
- + p256 + p257 + p258 + p259 + p260 + p261 + p262 + p263
- + p264 + p265 + p266 + p267 + p268 + p269 + p270 + p271
- + p272 + p273 + p274 + p275 + p276 + p277 + p278 + p279
- + p280 + p281 + p282 + p283 + p284 + p285 + p286 + p287
- + p288 + p289 + p290 + p291 + p292 + p293 + p294 + p295
- + p296 + p297 + p298 + p299 + p300 + p301 + p302 + p303
- + p304 + p305 + p306 + p307 + p308 + p309 + p310 + p311
- + p312 + p313 + p314 + p315 + p316 + p317 + p318 + p319
- + p320 + p321 + p322 + p323 + p324 + p325 + p326 + p327
- + p328 + p329 + p330 + p331 + p332 + p333 + p334 + p335
- + p336 + p337 + p338 + p339 + p340 + p341 + p342 + p343
- + p344 + p345 + p346 + p347 + p348 + p349 + p350 + p351
- + p352 + p353 + p354 + p355 + p356 + p357 + p358 + p359
- + p360 + p361 + p362 + p363 + p364 + p365 + p366 + p367
- + p368 + p369 + p370 + p371 + p372 + p373 + p374 + p375
- + p376 + p377 + p378 + p379 + p380 + p381 + p382 + p383
- + p384 + p385 + p386 + p387 + p388 + p389 + p390 + p391
- + p392 + p393 + p394 + p395 + p396 + p397 + p398 + p399
- + p400 + p401 + p402 + p403 + p404 + p405 + p406 + p407
- + p408 + p409 + p410 + p411 + p412 + p413 + p414 + p415
- + p416 + p417 + p418 + p419 + p420 + p421 + p422 + p423
- + p424 + p425 + p426 + p427 + p428 + p429 + p430 + p431
- + p432 + p433 + p434 + p435 + p436 + p437 + p438 + p439
- + p440 + p441 + p442 + p443 + p444 + p445 + p446 + p447
- + p448 + p449 + p450 + p451 + p452 + p453 + p454 + p455
- + p456 + p457 + p458 + p459 + p460 + p461 + p462 + p463
- + p464 + p465 + p466 + p467 + p468 + p469 + p470 + p471
- + p472 + p473 + p474 + p475 + p476 + p477 + p478 + p479
- + p480 + p481 + p482 + p483 + p484 + p485 + p486 + p487
- + p488 + p489 + p490 + p491 + p492 + p493 + p494 + p495
- + p496 + p497 + p498 + p499 + p500 + p501 + p502 + p503
- + p504 + p505 + p506 + p507 + p508 + p509 + p510 + p511
- + p512 + p513 + p514 + p515 + p516 + p517 + p518 + p519
- + p520 + p521 + p522 + p523 + p524 + p525 + p526 + p527
- + p528 + p529 + p530 + p531 + p532 + p533 + p534 + p535
- + p536 + p537 + p538 + p539 + p540 + p541 + p542 + p543
- + p544 + p545 + p546 + p547 + p548 + p549 + p550 + p551
- + p552 + p553 + p554 + p555 + p556 + p557 + p558 + p559
- + p560 + p561 + p562 + p563 + p564 + p565 + p566 + p567
- + p568 + p569 + p570 + p571 + p572 + p573 + p574 + p575
- + p576 + p577 + p578 + p579 + p580 + p581 + p582 + p583
- + p584 + p585 + p586 + p587 + p588 + p589 + p590 + p591
- + p592 + p593 + p594 + p595 + p596 + p597 + p598 + p599
- + p600 + p601 + p602 + p603 + p604 + p605 + p606 + p607
- + p608 + p609 + p610 + p611 + p612 + p613 + p614 + p615
- + p616 + p617 + p618 + p619 + p620 + p621 + p622 + p623
- + p624 + p625 + p626 + p627 + p628 + p629 + p630 + p631
- + p632 + p633 + p634 + p635 + p636 + p637 + p638 + p639
- + p640 + p641 + p642 + p643 + p644 + p645 + p646 + p647
- + p648 + p649 + p650 + p651 + p652 + p653 + p654 + p655
- + p656 + p657 + p658 + p659 + p660 + p661 + p662 + p663
- + p664 + p665 + p666 + p667 + p668 + p669 + p670 + p671
- + p672 + p673 + p674 + p675 + p676 + p677 + p678 + p679
- + p680 + p681 + p682 + p683 + p684 + p685 + p686 + p687
- + p688 + p689 + p690 + p691 + p692 + p693 + p694 + p695
- + p696 + p697 + p698 + p699 + p700 + p701 + p702 + p703
- + p704 + p705 + p706 + p707 + p708 + p709 + p710 + p711
- + p712 + p713 + p714 + p715 + p716 + p717 + p718 + p719
- + p720 + p721 + p722 + p723 + p724 + p725 + p726 + p727
- + p728 + p729 + p730 + p731 + p732 + p733 + p734 + p735
- + p736 + p737 + p738 + p739 + p740 + p741 + p742 + p743
- + p744 + p745 + p746 + p747 + p748 + p749 + p750 + p751
- + p752 + p753 + p754 + p755 + p756 + p757 + p758 + p759
- + p760 + p761 + p762 + p763 + p764 + p765 + p766 + p767
- + p768 + p769 + p770 + p771 + p772 + p773 + p774 + p775
- + p776 + p777 + p778 + p779 + p780 + p781 + p782 + p783
- + p784 + p785 + p786 + p787 + p788 + p789 + p790 + p791
- + p792 + p793 + p794 + p795 + p796 + p797 + p798 + p799
- + p800 + p801 + p802 + p803 + p804 + p805 + p806 + p807
- + p808 + p809 + p810 + p811 + p812 + p813 + p814 + p815
- + p816 + p817 + p818 + p819 + p820 + p821 + p822 + p823
- + p824 + p825 + p826 + p827 + p828 + p829 + p830 + p831
- + p832 + p833 + p834 + p835 + p836 + p837 + p838 + p839
- + p840 + p841 + p842 + p843 + p844 + p845 + p846 + p847
- + p848 + p849 + p850 + p851 + p852 + p853 + p854 + p855
- + p856 + p857 + p858 + p859 + p860 + p861 + p862 + p863
- + p864 + p865 + p866 + p867 + p868 + p869 + p870 + p871
- + p872 + p873 + p874 + p875 + p876 + p877 + p878 + p879
- + p880 + p881 + p882 + p883 + p884 + p885 + p886 + p887
- + p888 + p889 + p890 + p891 + p892 + p893 + p894 + p895
- + p896 + p897 + p898 + p899 + p900 + p901 + p902 + p903
- + p904 + p905 + p906 + p907 + p908 + p909 + p910 + p911
- + p912 + p913 + p914 + p915 + p916 + p917 + p918 + p919
- + p920 + p921 + p922 + p923 + p924 + p925 + p926 + p927
- + p928 + p929 + p930 + p931 + p932 + p933 + p934 + p935
- + p936 + p937 + p938 + p939 + p940 + p941 + p942 + p943
- + p944 + p945 + p946 + p947 + p948 + p949 + p950 + p951
- + p952 + p953 + p954 + p955 + p956 + p957 + p958 + p959
- + p960 + p961 + p962 + p963 + p964 + p965 + p966 + p967
- + p968 + p969 + p970 + p971 + p972 + p973 + p974 + p975
- + p976 + p977 + p978 + p979 + p980 + p981 + p982 + p983
- + p984 + p985 + p986 + p987 + p988 + p989 + p990 + p991
- + p992 + p993 + p994 + p995 + p996 + p997 + p998 + p999;
-    return x;
-}
-
-// clang-format on
diff --git a/src/test/app/wasm_fixtures/updateData.c b/src/test/app/wasm_fixtures/updateData.c
deleted file mode 100644
index 6eb70666b9..0000000000
--- a/src/test/app/wasm_fixtures/updateData.c
+++ /dev/null
@@ -1,11 +0,0 @@
-#include 
-
-int32_t set_data(uint8_t const *, int32_t);
-
-int escrow_finish()
-{
-  uint8_t buf[] = "Data";
-  set_data(buf, sizeof(buf) - 1);
-
-  return -256;
-}
diff --git a/src/test/app/wasm_fixtures/wat/custom_page_sizes.wat b/src/test/app/wasm_fixtures/wat/custom_page_sizes.wat
deleted file mode 100644
index c0bf1c3a11..0000000000
--- a/src/test/app/wasm_fixtures/wat/custom_page_sizes.wat
+++ /dev/null
@@ -1,13 +0,0 @@
-(module
-  ;; Define a memory with 1 initial page.
-  ;; CRITICAL: We explicitly set the page size to 1 byte.
-  ;; Standard Wasm implies (pagesize 65536).
-  (memory 1 (pagesize 1))
-
-  (func $escrow_finish (result i32)
-    ;; If this module instantiates, the runtime accepted the custom page size.
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/deep_recursion.wat b/src/test/app/wasm_fixtures/wat/deep_recursion.wat
deleted file mode 100644
index efe20cf53d..0000000000
--- a/src/test/app/wasm_fixtures/wat/deep_recursion.wat
+++ /dev/null
@@ -1,29 +0,0 @@
-(module
-  ;; Define a Mutable Global Variable to act as our counter.
-  ;; We initialize it to 1,000,000.
-  (global $counter (mut i32) (i32.const 1000000))
-
-  (func $escrow_finish (result i32)
-    ;; 1. Check if counter == 0 (Base Case)
-    global.get $counter
-    i32.eqz
-    if
-      ;; If counter is 0, we are done. Return 1.
-      i32.const 1
-      return
-    end
-
-    ;; 2. Decrement the Global Counter
-    global.get $counter
-    i32.const 1
-    i32.sub
-    global.set $counter
-
-    ;; 3. Recursive Step: Call SELF
-    ;; This puts an i32 (1) on the stack when it returns.
-    call $escrow_finish
-  )
-
-  ;; Export the only function we have
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/functions_5k.zip b/src/test/app/wasm_fixtures/wat/functions_5k.zip
deleted file mode 100644
index e261760545..0000000000
Binary files a/src/test/app/wasm_fixtures/wat/functions_5k.zip and /dev/null differ
diff --git a/src/test/app/wasm_fixtures/wat/locals_10k.zip b/src/test/app/wasm_fixtures/wat/locals_10k.zip
deleted file mode 100644
index 22aad2db51..0000000000
Binary files a/src/test/app/wasm_fixtures/wat/locals_10k.zip and /dev/null differ
diff --git a/src/test/app/wasm_fixtures/wat/memory64.wat b/src/test/app/wasm_fixtures/wat/memory64.wat
deleted file mode 100644
index 3273af1e40..0000000000
--- a/src/test/app/wasm_fixtures/wat/memory64.wat
+++ /dev/null
@@ -1,21 +0,0 @@
-(module
-  ;; Define a 64-bit memory (index type i64)
-  ;; Start with 1 page.
-  (memory i64 1)
-
-  (func $escrow_finish (result i32)
-    ;; 1. Perform a store using a 64-bit address.
-    ;;    Even if the value is small (0), the type MUST be i64.
-    i64.const 0     ;; Address (64-bit)
-    i32.const 42    ;; Value (32-bit)
-    i32.store8      ;; Opcode doesn't change, but validation rules do.
-
-    ;; 2. check memory size
-    ;;    memory.size now returns an i64.
-    memory.size
-    i64.const 1
-    i64.eq          ;; Returns i32 (1 if true)
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/memory_end_of_word_over_limit.wat b/src/test/app/wasm_fixtures/wat/memory_end_of_word_over_limit.wat
deleted file mode 100644
index 855307ddf4..0000000000
--- a/src/test/app/wasm_fixtures/wat/memory_end_of_word_over_limit.wat
+++ /dev/null
@@ -1,28 +0,0 @@
-(module
-  ;; 1. Define Memory: 1 Page = 64KB = 65,536 bytes
-  (memory 1)
-
-  ;; Export memory so the host can inspect it if needed
-  (export "memory" (memory 0))
-
-  (func $test_straddle (result i32)
-    ;; Push the address onto the stack.
-    ;; 65534 is valid, but it is only 2 bytes away from the end.
-    i32.const 65534
-
-    ;; Attempt to load an i32 (4 bytes) from that address.
-    ;; This requires bytes 65534, 65535, 65536, and 65537.
-    ;; Since 65536 is the first invalid byte, this MUST trap.
-    i32.load
-
-    ;; Clean up the stack.
-    ;; The load pushed a value, but we don't care what it is.
-    drop
-
-    ;; Return 1 to signal "I survived the memory access"
-    i32.const 1
-  )
-
-  ;; Export the function so you can call it from your host (JS, Python, etc.)
-  (export "escrow_finish" (func $test_straddle))
-)
diff --git a/src/test/app/wasm_fixtures/wat/memory_grow_0_page_more_than_8MB.wat b/src/test/app/wasm_fixtures/wat/memory_grow_0_page_more_than_8MB.wat
deleted file mode 100644
index 777f3062bf..0000000000
--- a/src/test/app/wasm_fixtures/wat/memory_grow_0_page_more_than_8MB.wat
+++ /dev/null
@@ -1,29 +0,0 @@
-(module
-  ;; Start at your limit: 128 pages (8MB)
-  (memory 128)
-  (export "memory" (memory 0))
-
-  (func $try_grow_beyond_limit (result i32)
-    ;; Attempt to grow by 0 page
-    i32.const 0
-    memory.grow
-
-    ;; memory.grow returns:
-    ;;   -1  if the growth failed (Correct behavior for your limit)
-    ;;   128 (old size) if growth succeeded (Means limit was bypassed)
-
-    ;; Check if result == -1
-    i32.const -1
-    i32.eq
-    if
-      ;; Growth FAILED (Host blocked it). Return -1.
-      i32.const -1
-      return
-    end
-
-    ;; Growth SUCCEEDED (Host allowed it). Return 1.
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $try_grow_beyond_limit))
-)
diff --git a/src/test/app/wasm_fixtures/wat/memory_grow_0_to_1.wat b/src/test/app/wasm_fixtures/wat/memory_grow_0_to_1.wat
deleted file mode 100644
index 54a4193927..0000000000
--- a/src/test/app/wasm_fixtures/wat/memory_grow_0_to_1.wat
+++ /dev/null
@@ -1,26 +0,0 @@
-(module
-  ;; 1. Define Memory: Start with 0 pages
-  (memory 0)
-
-  ;; Export memory to host
-  (export "memory" (memory 0))
-
-  (func $grow_from_zero (result i32)
-    ;; We have 0 pages. We want to add 1 page.
-    ;; Push delta (1) onto stack.
-    i32.const 1
-
-    ;; Grow the memory.
-    ;; If successful: memory becomes 64KB, returns old size (0).
-    ;; If failed: memory stays 0, returns -1.
-    memory.grow
-
-    ;; Drop the return value of memory.grow
-    drop
-
-    ;; Return 1 (as requested)
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $grow_from_zero))
-)
diff --git a/src/test/app/wasm_fixtures/wat/memory_grow_1_page_more_than_8MB.wat b/src/test/app/wasm_fixtures/wat/memory_grow_1_page_more_than_8MB.wat
deleted file mode 100644
index 540b112178..0000000000
--- a/src/test/app/wasm_fixtures/wat/memory_grow_1_page_more_than_8MB.wat
+++ /dev/null
@@ -1,29 +0,0 @@
-(module
-  ;; Start at your limit: 128 pages (8MB)
-  (memory 128)
-  (export "memory" (memory 0))
-
-  (func $try_grow_beyond_limit (result i32)
-    ;; Attempt to grow by 1 page
-    i32.const 1
-    memory.grow
-
-    ;; memory.grow returns:
-    ;;   -1  if the growth failed (Correct behavior for your limit)
-    ;;   128 (old size) if growth succeeded (Means limit was bypassed)
-
-    ;; Check if result == -1
-    i32.const -1
-    i32.eq
-    if
-      ;; Growth FAILED (Host blocked it). Return -1.
-      i32.const -1
-      return
-    end
-
-    ;; Growth SUCCEEDED (Host allowed it). Return 1.
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $try_grow_beyond_limit))
-)
diff --git a/src/test/app/wasm_fixtures/wat/memory_grow_1_to_0.wat b/src/test/app/wasm_fixtures/wat/memory_grow_1_to_0.wat
deleted file mode 100644
index cc4d161153..0000000000
--- a/src/test/app/wasm_fixtures/wat/memory_grow_1_to_0.wat
+++ /dev/null
@@ -1,33 +0,0 @@
-(module
-  ;; 1. Define Memory: Start with 1 page (64KB)
-  (memory 1)
-
-  ;; Export memory to host
-  (export "memory" (memory 0))
-
-  (func $grow_negative (result i32)
-    ;; The user pushed -1. In Wasm, this is interpreted as unsigned MAX_UINT32.
-    ;; This is requesting to add 4,294,967,295 pages (approx 256 TB).
-    ;; A secure runtime MUST fail this request (return -1) without crashing.
-    i32.const -1
-
-    ;; Grow the memory.
-    ;; Returns: old_size if success, -1 if failure.
-    memory.grow
-
-    ;; Check if result == -1 (Failure)
-    i32.const -1
-    i32.eq
-    if
-        ;; If memory.grow returned -1, we return -1 to signal "Correctly failed".
-        i32.const -1
-        return
-    end
-
-    ;; If we are here, memory.grow somehow SUCCEEDED (Vulnerability).
-    ;; We return 1 to signal "Unexpected Success".
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $grow_negative))
-)
diff --git a/src/test/app/wasm_fixtures/wat/memory_init_1_page_more_than_8MB.wat b/src/test/app/wasm_fixtures/wat/memory_init_1_page_more_than_8MB.wat
deleted file mode 100644
index f1bbee6c61..0000000000
--- a/src/test/app/wasm_fixtures/wat/memory_init_1_page_more_than_8MB.wat
+++ /dev/null
@@ -1,27 +0,0 @@
-(module
-  ;; Define memory: 129 pages (> 8MB limit) min, 129 pages max
-  (memory 129 129)
-
-  ;; Export memory so host can verify size
-  (export "memory" (memory 0))
-
-  ;; access last byte of 8MB limit
-  (func $access_last_byte (result i32)
-    ;; Math: 128 pages * 64,536 bytes/page = 8,388,608 bytes
-    ;; Valid indices: 0 to 8,388,607
-
-    ;; Push the address of the LAST valid byte
-    i32.const 8388607
-
-    ;; Load byte from that address
-    i32.load8_u
-
-    ;; Drop the value (we don't care what it is, just that we could read it)
-    drop
-
-    ;; Return 1 to indicate success
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $access_last_byte))
-)
diff --git a/src/test/app/wasm_fixtures/wat/memory_last_byte_of_8MB.wat b/src/test/app/wasm_fixtures/wat/memory_last_byte_of_8MB.wat
deleted file mode 100644
index e00f5e1239..0000000000
--- a/src/test/app/wasm_fixtures/wat/memory_last_byte_of_8MB.wat
+++ /dev/null
@@ -1,26 +0,0 @@
-(module
-  ;; Define memory: 128 pages (8MB) min, 128 pages max
-  (memory 128 128)
-
-  ;; Export memory so host can verify size
-  (export "memory" (memory 0))
-
-  (func $access_last_byte (result i32)
-    ;; Math: 128 pages * 64,536 bytes/page = 8,388,608 bytes
-    ;; Valid indices: 0 to 8,388,607
-
-    ;; Push the address of the LAST valid byte
-    i32.const 8388607
-
-    ;; Load byte from that address
-    i32.load8_u
-
-    ;; Drop the value (we don't care what it is, just that we could read it)
-    drop
-
-    ;; Return 1 to indicate success
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $access_last_byte))
-)
diff --git a/src/test/app/wasm_fixtures/wat/memory_negative_address.wat b/src/test/app/wasm_fixtures/wat/memory_negative_address.wat
deleted file mode 100644
index 6e26a07108..0000000000
--- a/src/test/app/wasm_fixtures/wat/memory_negative_address.wat
+++ /dev/null
@@ -1,23 +0,0 @@
-(module
-  ;; Define memory: 128 pages (8MB) min, 128 pages max
-  (memory 128 128)
-
-  ;; Export memory so host can verify size
-  (export "memory" (memory 0))
-
-  (func $access_last_byte (result i32)
-    ;; Push a negative address
-    i32.const -1
-
-    ;; Load byte from that address
-    i32.load8_u
-
-    ;; Drop the value
-    drop
-
-    ;; Return 1 to indicate success
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $access_last_byte))
-)
diff --git a/src/test/app/wasm_fixtures/wat/memory_offset_over_limit.wat b/src/test/app/wasm_fixtures/wat/memory_offset_over_limit.wat
deleted file mode 100644
index 20a401e3d2..0000000000
--- a/src/test/app/wasm_fixtures/wat/memory_offset_over_limit.wat
+++ /dev/null
@@ -1,27 +0,0 @@
-(module
-  ;; 1. Define Memory: 1 Page = 64KB
-  (memory 1)
-
-  (export "memory" (memory 0))
-
-  (func $test_offset_overflow (result i32)
-    ;; 1. Push the base address onto the stack.
-    ;; We use '0', which is the safest, most valid address possible.
-    i32.const 0
-
-    ;; 2. Attempt to load using a static offset.
-    ;; syntax: i32.load offset=N align=N
-    ;; We set the offset to 65536 (the size of the memory).
-    ;; The effective address becomes 0 + 65536 = 65536.
-    i32.load offset=65536
-
-    ;; Clean up the stack.
-    ;; The load pushed a value, but we don't care what it is.
-    drop
-
-    ;; Return 1 to signal "I survived the memory access"
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $test_offset_overflow))
-)
diff --git a/src/test/app/wasm_fixtures/wat/memory_pointer_at_limit.wat b/src/test/app/wasm_fixtures/wat/memory_pointer_at_limit.wat
deleted file mode 100644
index e4432e6fdd..0000000000
--- a/src/test/app/wasm_fixtures/wat/memory_pointer_at_limit.wat
+++ /dev/null
@@ -1,22 +0,0 @@
-(module
-  ;; Define 1 page of memory (64KB = 65,536 bytes)
-  (memory 1)
-
-  (func $read_edge (result i32)
-    ;; Push the index of the LAST valid byte
-    i32.const 65535
-
-    ;; Load 1 byte (unsigned)
-    i32.load8_u
-
-    ;; Clean up the stack.
-    ;; The load pushed a value, but we don't care what it is.
-    drop
-
-    ;; Return 1 to signal "I survived the memory access"
-    i32.const 1
-  )
-
-  ;; Export as "escrow_finish" as requested
-  (export "escrow_finish" (func $read_edge))
-)
diff --git a/src/test/app/wasm_fixtures/wat/memory_pointer_over_limit.wat b/src/test/app/wasm_fixtures/wat/memory_pointer_over_limit.wat
deleted file mode 100644
index 906468f308..0000000000
--- a/src/test/app/wasm_fixtures/wat/memory_pointer_over_limit.wat
+++ /dev/null
@@ -1,23 +0,0 @@
-(module
-   ;; Define 1 page of memory (64KB = 65,536 bytes)
-   (memory 1)
-
-   (func $read_overflow (result i32)
-     ;; Push the index of the FIRST invalid byte
-     ;; Memory is 0..65535, so 65536 is out of bounds.
-     i32.const 65536
-
-     ;; Load 1 byte (unsigned)
-     i32.load8_u
-
-     ;; Clean up the stack.
-     ;; The load pushed a value, but we don't care what it is.
-     drop
-
-     ;; Return 1 to signal "I survived the memory access"
-     i32.const 1
-   )
-
-   ;; Export as "escrow_finish" as requested
-   (export "escrow_finish" (func $read_overflow))
- )
diff --git a/src/test/app/wasm_fixtures/wat/multi_memory.wat b/src/test/app/wasm_fixtures/wat/multi_memory.wat
deleted file mode 100644
index 67fc5e76aa..0000000000
--- a/src/test/app/wasm_fixtures/wat/multi_memory.wat
+++ /dev/null
@@ -1,16 +0,0 @@
-(module
-  ;; Memory 0: Index 0 (Empty)
-  (memory 0)
-
-  ;; Memory 1: Index 1 (Size 1 page)
-  ;; If multi-memory is disabled, this line causes a validation error (max 1 memory).
-  (memory 1)
-
-  (func $escrow_finish (result i32)
-    ;; Query size of Memory Index 1.
-    ;; Should return 1 (success).
-    memory.size 1
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/opc_reserved.wat b/src/test/app/wasm_fixtures/wat/opc_reserved.wat
deleted file mode 100644
index 0bc61b52c3..0000000000
--- a/src/test/app/wasm_fixtures/wat/opc_reserved.wat
+++ /dev/null
@@ -1,98 +0,0 @@
-(module
-
-  ;; Type for call_indirect
-  (type (func (result i32)))
-
-  ;; Memory and table declarations
-  (memory 1)
-  (table 1 funcref)
-  (data (i32.const 0) "test")
-  (elem (i32.const 0) $test_func)
-
-  ;; Global declarations
-  (global $g0 (mut i32) (i32.const 0))
-  (global $g1 (mut i64) (i64.const 0))
-
-  ;; Test function for call/call_indirect
-  (func $test_func (result i32)
-    i32.const 42
-  )
-
-
-  ;; Main function with all instructions in hex order
-  (func $all_instructions (export "all_instructions") (result i32)
-    (local $l0 i32)
-    (local $l1 i64)
-
-    ;; 0x01: nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    nop
-    i32.const 11
-  )
-)
diff --git a/src/test/app/wasm_fixtures/wat/proposal_bulk_memory.wat b/src/test/app/wasm_fixtures/wat/proposal_bulk_memory.wat
deleted file mode 100644
index ce59868ce6..0000000000
--- a/src/test/app/wasm_fixtures/wat/proposal_bulk_memory.wat
+++ /dev/null
@@ -1,25 +0,0 @@
-(module
-  ;; Define 1 page of memory
-  (memory 1)
-  (export "memory" (memory 0))
-
-  (func $test_bulk_ops (result i32)
-    ;; Setup: Write value 42 at index 0 so we have something to copy
-    (i32.store8 (i32.const 0) (i32.const 42))
-
-    ;; Test memory.copy (Opcode 0xFC 0x0A)
-    ;; Copy 1 byte from offset 0 to offset 100
-    (memory.copy
-      (i32.const 100) ;; Destination Offset
-      (i32.const 0)   ;; Source Offset
-      (i32.const 1)   ;; Size (bytes)
-    )
-
-    ;; Verify: Read byte at offset 100. Should be 42.
-    (i32.load8_u (i32.const 100))
-    (i32.const 42)
-    i32.eq
-  )
-
-  (export "escrow_finish" (func $test_bulk_ops))
-)
diff --git a/src/test/app/wasm_fixtures/wat/proposal_extended_const.wat b/src/test/app/wasm_fixtures/wat/proposal_extended_const.wat
deleted file mode 100644
index e296a468f0..0000000000
--- a/src/test/app/wasm_fixtures/wat/proposal_extended_const.wat
+++ /dev/null
@@ -1,15 +0,0 @@
-(module
-  ;; 1. Define a global using an EXTENDED constant expression.
-  ;;    MVP only allows (i32.const X).
-  ;;    This proposal allows (i32.add (i32.const X) (i32.const Y)).
-  (global $g i32 (i32.add (i32.const 10) (i32.const 32)))
-
-  (func $escrow_finish (result i32)
-    ;; 2. verify the global equals 42
-    global.get $g
-    i32.const 42
-    i32.eq
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/proposal_float_to_int.wat b/src/test/app/wasm_fixtures/wat/proposal_float_to_int.wat
deleted file mode 100644
index 367735f5e7..0000000000
--- a/src/test/app/wasm_fixtures/wat/proposal_float_to_int.wat
+++ /dev/null
@@ -1,18 +0,0 @@
-(module
-  (func $test_saturation (result i32)
-    ;; 1. Push a float that is too big for a 32-bit integer
-    ;; 1e10 (10 billion) > 2.14 billion (Max i32)
-    f32.const 1.0e10
-
-    ;; 2. Attempt saturating conversion (Opcode 0xFC 0x00)
-    ;; If supported: Clamps to MAX_I32.
-    ;; If disabled: Validation error (unknown instruction).
-    i32.trunc_sat_f32_s
-
-    ;; 3. Check if result is MAX_I32 (2147483647)
-    i32.const 2147483647
-    i32.eq
-  )
-
-  (export "escrow_finish" (func $test_saturation))
-)
diff --git a/src/test/app/wasm_fixtures/wat/proposal_gc_struct_new.wat b/src/test/app/wasm_fixtures/wat/proposal_gc_struct_new.wat
deleted file mode 100644
index bc33b7fada..0000000000
--- a/src/test/app/wasm_fixtures/wat/proposal_gc_struct_new.wat
+++ /dev/null
@@ -1,12 +0,0 @@
-;; generated by wasm-tools print gc_test.wasm that has the following hex
-;; 0061736d01000000010b026000017f5f027f017f0103020100070a010666696e69736800000a0a010800fb01011a41010b
-(module
-  (type (;0;) (func (result i32)))
-  (type (;1;) (struct (field (mut i32)) (field (mut i32))))
-  (export "escrow_finish" (func 0))
-  (func (;0;) (type 0) (result i32)
-    struct.new_default 1
-    drop
-    i32.const 1
-  )
-)
diff --git a/src/test/app/wasm_fixtures/wat/proposal_multi_value.wat b/src/test/app/wasm_fixtures/wat/proposal_multi_value.wat
deleted file mode 100644
index 23f1691872..0000000000
--- a/src/test/app/wasm_fixtures/wat/proposal_multi_value.wat
+++ /dev/null
@@ -1,22 +0,0 @@
-(module
-  ;; 1. Function returning TWO values (Multi-Value feature)
-  (func $get_numbers (result i32 i32)
-    i32.const 10
-    i32.const 20
-  )
-
-  (func $escrow_finish (result i32)
-    ;; Call pushes [10, 20] onto the stack
-    call $get_numbers
-
-    ;; 2. Block taking TWO parameters (Multi-Value feature)
-    ;;    It consumes the [10, 20] from the stack.
-    block (param i32 i32) (result i32)
-      i32.add       ;; 10 + 20 = 30
-      i32.const 30  ;; Expected result
-      i32.eq        ;; Compare: returns 1 if equal
-    end
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/proposal_mutable_global.wat b/src/test/app/wasm_fixtures/wat/proposal_mutable_global.wat
deleted file mode 100644
index 82a5d35203..0000000000
--- a/src/test/app/wasm_fixtures/wat/proposal_mutable_global.wat
+++ /dev/null
@@ -1,25 +0,0 @@
-(module
-  ;; Define a mutable global initialized to 0
-  (global $counter (mut i32) (i32.const 0))
-
-  ;; EXPORTING a mutable global is the key feature of this proposal.
-  ;; In strict MVP, exported globals had to be immutable (const).
-  (export "counter" (global $counter))
-
-  (func $escrow_finish (result i32)
-    ;; 1. Get current value
-    global.get $counter
-
-    ;; 2. Add 1
-    i32.const 1
-    i32.add
-
-    ;; 3. Set new value (Mutation)
-    global.set $counter
-
-    ;; 4. Return 1 for success
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/proposal_ref_types.wat b/src/test/app/wasm_fixtures/wat/proposal_ref_types.wat
deleted file mode 100644
index 8cba4edf29..0000000000
--- a/src/test/app/wasm_fixtures/wat/proposal_ref_types.wat
+++ /dev/null
@@ -1,18 +0,0 @@
-(module
-  ;; Import a table from the host that holds externrefs
-  (import "env" "table" (table 1 externref))
-
-  (func $test_ref_types (result i32)
-    ;; Store a null externref into the table at index 0
-    ;; If reference_types is disabled, 'externref' and 'ref.null' will fail parsing.
-    (table.set
-      (i32.const 0)       ;; Index
-      (ref.null extern)   ;; Value (Null External Reference)
-    )
-
-    ;; Return 1 (Success)
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $test_ref_types))
-)
diff --git a/src/test/app/wasm_fixtures/wat/proposal_sign_ext.wat b/src/test/app/wasm_fixtures/wat/proposal_sign_ext.wat
deleted file mode 100644
index 9c8fdd1980..0000000000
--- a/src/test/app/wasm_fixtures/wat/proposal_sign_ext.wat
+++ /dev/null
@@ -1,18 +0,0 @@
-(module
-  (func $test_sign_ext (result i32)
-    ;; Push 255 (0x000000FF) onto the stack
-    i32.const 255
-
-    ;; Sign-extend from 8-bit to 32-bit
-    ;; If 255 is treated as an i8, it is -1.
-    ;; Result should be -1 (0xFFFFFFFF).
-    ;; Without this proposal, this opcode (0xC0) causes a validation error.
-    i32.extend8_s
-
-    ;; Check if result is -1
-    i32.const -1
-    i32.eq
-  )
-
-  (export "escrow_finish" (func $test_sign_ext))
-)
diff --git a/src/test/app/wasm_fixtures/wat/proposal_stringref.wat b/src/test/app/wasm_fixtures/wat/proposal_stringref.wat
deleted file mode 100644
index c9f8030faf..0000000000
--- a/src/test/app/wasm_fixtures/wat/proposal_stringref.wat
+++ /dev/null
@@ -1 +0,0 @@
-;;hard to generate
diff --git a/src/test/app/wasm_fixtures/wat/proposal_tail_call.wat b/src/test/app/wasm_fixtures/wat/proposal_tail_call.wat
deleted file mode 100644
index 193fb0aeb2..0000000000
--- a/src/test/app/wasm_fixtures/wat/proposal_tail_call.wat
+++ /dev/null
@@ -1,15 +0,0 @@
-(module
-  ;; Define a simple function we can tail-call
-  (func $target (result i32)
-    i32.const 1
-  )
-
-  (func $escrow_finish (result i32)
-    ;; Try to use the 'return_call' instruction (Opcode 0x12)
-    ;; If Tail Call proposal is disabled, this fails to Compile/Validate.
-    ;; If enabled, it jumps to $target, which returns 1.
-    return_call $target
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/start_loop.wat b/src/test/app/wasm_fixtures/wat/start_loop.wat
deleted file mode 100644
index 241774e53e..0000000000
--- a/src/test/app/wasm_fixtures/wat/start_loop.wat
+++ /dev/null
@@ -1,22 +0,0 @@
-(module
-  ;; Function 1: The Infinite Loop
-  (func $run_forever
-    (loop $infinite
-      br $infinite
-    )
-  )
-
-  ;; Function 2: Finish
-  (func $escrow_finish (result i32)
-    i32.const 1
-  )
-
-  ;; 1. EXPORT the functions (optional, if you want to call them later)
-  (export "start" (func $run_forever))
-  (export "escrow_finish" (func $escrow_finish))
-
-  ;; 2. The special start section
-  ;; This tells the VM: "Run function $run_forever immediately
-  ;; when this module is instantiated."
-  (start $run_forever)
-)
diff --git a/src/test/app/wasm_fixtures/wat/table_0_elements.wat b/src/test/app/wasm_fixtures/wat/table_0_elements.wat
deleted file mode 100644
index 9b8a5408d2..0000000000
--- a/src/test/app/wasm_fixtures/wat/table_0_elements.wat
+++ /dev/null
@@ -1,10 +0,0 @@
-(module
-  ;; Define a table with exactly 0 entries
-  (table 0 funcref)
-
-  ;; Standard finish function
-  (func $escrow_finish (result i32)
-    i32.const 1
-  )
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/table_2_tables.wat b/src/test/app/wasm_fixtures/wat/table_2_tables.wat
deleted file mode 100644
index 4e4e013ce3..0000000000
--- a/src/test/app/wasm_fixtures/wat/table_2_tables.wat
+++ /dev/null
@@ -1,24 +0,0 @@
-(module
-  ;; Define a dummy function to put in the tables
-  (func $dummy)
-
-  ;; TABLE 0: The default table (allowed in MVP)
-  ;; Size: 1 initial, 1 max
-  (table $t0 1 1 funcref)
-
-  ;; Initialize Table 0 at index 0
-  (elem (table $t0) (i32.const 0) $dummy)
-
-  ;; TABLE 1: The second table (Requires Reference Types proposal)
-  ;; If strict MVP is enforced, the parser should error here.
-  (table $t1 1 1 funcref)
-
-  ;; Initialize Table 1 at index 0
-  (elem (table $t1) (i32.const 0) $dummy)
-
-  (func $escrow_finish (result i32)
-    ;; If we successfully loaded a module with 2 tables, return 1.
-    i32.const 1
-  )
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/table_64_elements.wat b/src/test/app/wasm_fixtures/wat/table_64_elements.wat
deleted file mode 100644
index 3221571fe9..0000000000
--- a/src/test/app/wasm_fixtures/wat/table_64_elements.wat
+++ /dev/null
@@ -1,25 +0,0 @@
-(module
-  ;; Define a table with exactly 64 entries
-  (table 64 funcref)
-
-  ;; A dummy function to reference
-  (func $dummy)
-
-  ;; Initialize the table at offset 0 with 64 references to $dummy
-  (elem (i32.const 0)
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 8
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 16
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 24
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 32
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 40
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 48
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 56
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 64
-  )
-
-  ;; Standard finish function
-  (func $escrow_finish (result i32)
-    i32.const 1
-  )
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/table_65_elements.wat b/src/test/app/wasm_fixtures/wat/table_65_elements.wat
deleted file mode 100644
index aa0688c56f..0000000000
--- a/src/test/app/wasm_fixtures/wat/table_65_elements.wat
+++ /dev/null
@@ -1,25 +0,0 @@
-(module
-  ;; Define a table with exactly 65 entries
-  (table 65 funcref)
-
-  ;; A dummy function to reference
-  (func $dummy)
-
-  ;; Initialize the table at offset 0 with 65 references to $dummy
-  (elem (i32.const 0)
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 8
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 16
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 24
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 32
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 40
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 48
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 56
-    $dummy $dummy $dummy $dummy $dummy $dummy $dummy $dummy ;; 64
-    $dummy ;; 65 (The one that breaks the camel's back)
-  )
-
-  (func $escrow_finish (result i32)
-    i32.const 1
-  )
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/table_uint_max.wat b/src/test/app/wasm_fixtures/wat/table_uint_max.wat
deleted file mode 100644
index 23908611c8..0000000000
--- a/src/test/app/wasm_fixtures/wat/table_uint_max.wat
+++ /dev/null
@@ -1,15 +0,0 @@
-(module
-  ;; Definition: (table   )
-  ;; We use 0xFFFFFFFF (4,294,967,295), which is the unsigned equivalent of -1.
-  ;; This tests if the runtime handles the maximum possible u32 value
-  ;; without integer overflows or attempting a massive allocation.
-  ;;
-  ;; Note that using -1 as the table size cannot be parsed by wasm-tools or wat2wasm
-  (table 0xFFFFFFFF funcref)
-
-  (func $escrow_finish (result i32)
-    ;; If the module loads despite the massive table, return 1.
-    i32.const 1
-  )
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/trap_divide_by_0.wat b/src/test/app/wasm_fixtures/wat/trap_divide_by_0.wat
deleted file mode 100644
index 6f8754ec8f..0000000000
--- a/src/test/app/wasm_fixtures/wat/trap_divide_by_0.wat
+++ /dev/null
@@ -1,15 +0,0 @@
-(module
-  (func $escrow_finish (export "escrow_finish") (result i32)
-    ;; Setup for Requirement 2: Divide an i32 by 0
-    i32.const 42   ;; Push numerator
-    i32.const 0    ;; Push denominator (0)
-    i32.div_s      ;; Perform signed division (42 / 0)
-
-    ;; --- NOTE: Execution usually traps (crashes) at the line above ---
-
-    ;; Logic to satisfy Requirement 1: Return i32 = 1
-    ;; If execution continued, we would drop the division result and return 1
-    drop           ;; Clear the stack
-    i32.const 1    ;; Push the return value
-  )
-)
diff --git a/src/test/app/wasm_fixtures/wat/trap_func_signature_mismatch.wat b/src/test/app/wasm_fixtures/wat/trap_func_signature_mismatch.wat
deleted file mode 100644
index fd20f6176a..0000000000
--- a/src/test/app/wasm_fixtures/wat/trap_func_signature_mismatch.wat
+++ /dev/null
@@ -1,33 +0,0 @@
-(module
-  ;; Define a table with 1 slot
-  (table 1 funcref)
-
-  ;; Define Type A: Takes nothing, returns nothing
-  (type $type_void (func))
-
-  ;; Define Type B: Takes nothing, returns i32
-  (type $type_i32 (func (result i32)))
-
-  ;; Define a function of Type A
-  (func $void_func (type $type_void)
-    nop
-  )
-
-  ;; Put Type A function into Table[0]
-  (elem (i32.const 0) $void_func)
-
-  (func $escrow_finish (result i32)
-    ;; Attempt to call Index 0, but CLAIM we expect Type B (result i32).
-    ;; The function at Index 0 matches Type A.
-    ;; TRAP: "indirect call type mismatch"
-
-    ;; 1. Push the table index (0) onto the stack
-    i32.const 0
-
-    ;; 2. Call indirect using Type B signature.
-    ;;    This pops the index (0) from the stack.
-    call_indirect (type $type_i32)
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/trap_int_overflow.wat b/src/test/app/wasm_fixtures/wat/trap_int_overflow.wat
deleted file mode 100644
index 208e5bd211..0000000000
--- a/src/test/app/wasm_fixtures/wat/trap_int_overflow.wat
+++ /dev/null
@@ -1,18 +0,0 @@
-(module
-  (func $test_int_overflow (result i32)
-    ;; 1. Push INT_MIN (-2147483648)
-    ;; In Hex: 0x80000000
-    i32.const -2147483648
-
-    ;; 2. Push -1
-    i32.const -1
-
-    ;; 3. Signed Division
-    ;; This specific case is the ONLY integer arithmetic operation
-    ;; (besides divide by zero) that traps in the spec.
-    ;; Result would be +2147483648, which is too big for signed i32.
-    i32.div_s
-  )
-
-  (export "escrow_finish" (func $test_int_overflow))
-)
diff --git a/src/test/app/wasm_fixtures/wat/trap_null_call.wat b/src/test/app/wasm_fixtures/wat/trap_null_call.wat
deleted file mode 100644
index 63173303c5..0000000000
--- a/src/test/app/wasm_fixtures/wat/trap_null_call.wat
+++ /dev/null
@@ -1,22 +0,0 @@
-(module
-  ;; Table size is 1, so Index 0 is VALID bounds.
-  ;; However, we do NOT initialize it, so it contains 'ref.null'.
-  (table 1 funcref)
-
-  (type $t (func (result i32)))
-
-  (func $escrow_finish (result i32)
-    ;; Call Index 0.
-    ;; Bounds check passes (0 < 1).
-    ;; Null check fails.
-    ;; TRAP: "uninitialized element" or "undefined element"
-
-    ;; 1. Push the index (0) onto the stack first
-    i32.const 0
-
-    ;; 2. Perform the call. This pops the index.
-    call_indirect (type $t)
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/trap_unreachable.wat b/src/test/app/wasm_fixtures/wat/trap_unreachable.wat
deleted file mode 100644
index 3d8fe89f5b..0000000000
--- a/src/test/app/wasm_fixtures/wat/trap_unreachable.wat
+++ /dev/null
@@ -1,12 +0,0 @@
-(module
-  (func $escrow_finish (result i32)
-    ;; This instruction explicitly causes a trap.
-    ;; It consumes no fuel (beyond the instruction itself) and stops execution.
-    unreachable
-
-    ;; This code is dead and never reached
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/wasi_get_time.wat b/src/test/app/wasm_fixtures/wat/wasi_get_time.wat
deleted file mode 100644
index 6b525067a8..0000000000
--- a/src/test/app/wasm_fixtures/wat/wasi_get_time.wat
+++ /dev/null
@@ -1,38 +0,0 @@
-(module
-  ;; Import clock_time_get from WASI
-  ;; Signature: (param clock_id precision return_ptr) (result errno)
-  (import "wasi_snapshot_preview1" "clock_time_get"
-    (func $clock_time_get (param i32 i64 i32) (result i32))
-  )
-
-  (memory 1)
-  (export "memory" (memory 0))
-
-  (func $escrow_finish (result i32)
-    ;; We will store the timestamp (a 64-bit integer) at address 0.
-    ;; No setup required in memory beforehand!
-
-    ;; Call the function
-    (call $clock_time_get
-      (i32.const 0)       ;; clock_id: 0 = Realtime (Wallclock)
-      (i64.const 1000)    ;; precision: 1000ns (hint to OS)
-      (i32.const 0)       ;; result_ptr: Write the time to address 0
-    )
-
-    ;; The function returns an 'errno' (error code).
-    ;; 0 = Success. Anything else = Error.
-
-    ;; Check if errno (top of stack) is 0
-    i32.eqz
-    if (result i32)
-      ;; Success! The time is now stored in heap[0..8].
-      ;; We return 1 as requested.
-      i32.const 1
-    else
-      ;; Failed (maybe WASI is disabled or clock is missing)
-      i32.const -1
-    end
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/wasi_print.wat b/src/test/app/wasm_fixtures/wat/wasi_print.wat
deleted file mode 100644
index 511c5ba724..0000000000
--- a/src/test/app/wasm_fixtures/wat/wasi_print.wat
+++ /dev/null
@@ -1,59 +0,0 @@
-(module
-  ;; Import WASI fd_write
-  ;; Signature: (fd, iovs_ptr, iovs_len, nwritten_ptr) -> errno
-  (import "wasi_snapshot_preview1" "fd_write"
-    (func $fd_write (param i32 i32 i32 i32) (result i32))
-  )
-
-  (memory 1)
-  (export "memory" (memory 0))
-
-  ;; --- DATA SEGMENTS ---
-
-  ;; 1. The String Data "Hello\n" placed at offset 16
-  ;;    We assume offset 0-16 is reserved for the IOVec struct
-  (data (i32.const 16) "Hello\n")
-
-  ;; 2. The IO Vector (struct iovec) placed at offset 0
-  ;;    Structure: { buf_ptr: u32, buf_len: u32 }
-
-  ;;    Field 1: buf_ptr = 16 (Location of "Hello\n")
-  ;;    Encoded in little-endian: 10 00 00 00
-  (data (i32.const 0) "\10\00\00\00")
-
-  ;;    Field 2: buf_len = 6 (Length of "Hello\n")
-  ;;    Encoded in little-endian: 06 00 00 00
-  (data (i32.const 4) "\06\00\00\00")
-
-  (func $escrow_finish (result i32)
-    (local $nwritten_ptr i32)
-
-    ;; We will ask WASI to write the "number of bytes written" to address 24
-    ;; (safely after our string data)
-    i32.const 24
-    local.set $nwritten_ptr
-
-    ;; Call fd_write
-    (call $fd_write
-      (i32.const 1)       ;; fd: 1 = STDOUT
-      (i32.const 0)       ;; iovs_ptr: Address 0 (where we defined the struct)
-      (i32.const 1)       ;; iovs_len: We are passing 1 vector
-      (local.get $nwritten_ptr) ;; nwritten_ptr: Address 24
-    )
-
-    ;; The function returns an 'errno' (i32).
-    ;; 0 means Success.
-
-    ;; Check if errno == 0
-    i32.eqz
-    if (result i32)
-      ;; Success: Return 1
-      i32.const 1
-    else
-      ;; Failure: Return -1
-      i32.const -1
-    end
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/app/wasm_fixtures/wat/wide_arithmetic.wat b/src/test/app/wasm_fixtures/wat/wide_arithmetic.wat
deleted file mode 100644
index 13b1ab4836..0000000000
--- a/src/test/app/wasm_fixtures/wat/wide_arithmetic.wat
+++ /dev/null
@@ -1,22 +0,0 @@
-(module
-  (func $escrow_finish (result i32)
-    ;; 1. Push operands
-    i64.const 1
-    i64.const 2
-
-    ;; 2. Execute Wide Multiplication
-    ;;    If the feature is DISABLED, the parser/validator will trap here
-    ;;    with "unknown instruction" or "invalid opcode".
-    ;;    Input: [i64, i64] -> Output: [i64, i64]
-    i64.mul_wide_u
-
-    ;; 3. Clean up the stack (drop the two i64 results)
-    drop
-    drop
-
-    ;; 4. Return 1 to signal that validation passed
-    i32.const 1
-  )
-
-  (export "escrow_finish" (func $escrow_finish))
-)
diff --git a/src/test/basics/PerfLog_test.cpp b/src/test/basics/PerfLog_test.cpp
index 24ea971515..f7679dc488 100644
--- a/src/test/basics/PerfLog_test.cpp
+++ b/src/test/basics/PerfLog_test.cpp
@@ -15,14 +15,10 @@
 #include 
 #include 
 
-#include 
-#include 
-#include 
-#include 
-
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -31,6 +27,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -43,7 +40,7 @@ class PerfLog_test : public beast::unit_test::Suite
 {
     enum class WithFile : bool { No = false, Yes = true };
 
-    using path = boost::filesystem::path;
+    using path = std::filesystem::path;
 
     // We're only using Env for its Journal.  That Journal gives better
     // coverage in unit tests.
@@ -66,14 +63,14 @@ class PerfLog_test : public beast::unit_test::Suite
             // The error code is intentionally ignored: if the path doesn't
             // exist (the common case on a clean runner) remove_all returns
             // an error, and that's fine — there's nothing to clean up.
-            using namespace boost::filesystem;
-            boost::system::error_code ec;
+            using namespace std::filesystem;
+            std::error_code ec;
             remove_all(logDir(), ec);
         }
 
         ~Fixture()
         {
-            using namespace boost::filesystem;
+            using namespace std::filesystem;
 
             auto const dir{logDir()};
             auto const file{logFile()};
@@ -96,7 +93,7 @@ class PerfLog_test : public beast::unit_test::Suite
         static path
         logDir()
         {
-            using namespace boost::filesystem;
+            using namespace std::filesystem;
             return temp_directory_path() / "perf_log_test_dir";
         }
 
@@ -129,7 +126,7 @@ class PerfLog_test : public beast::unit_test::Suite
         static void
         wait()
         {
-            using namespace boost::filesystem;
+            using namespace std::filesystem;
 
             auto const path = logFile();
             if (!exists(path))
@@ -201,7 +198,7 @@ public:
     void
     testFileCreation()
     {
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
 
         {
             // Verify a PerfLog creates its file when constructed.
@@ -250,28 +247,30 @@ public:
             // Put a write protected file where PerfLog wants to write its
             // file.  Make sure that PerfLog tries to shutdown the server
             // since it can't open its file.
+            using std::filesystem::perms;
+
             Fixture fixture{env_.app(), j_};
             if (!BEAST_EXPECT(!exists(fixture.logDir())))
                 return;
 
             // Construct and write protect a file to prevent PerfLog
             // from creating its file.
-            boost::system::error_code ec;
-            boost::filesystem::create_directories(fixture.logDir(), ec);
+            std::error_code ec;
+            std::filesystem::create_directories(fixture.logDir(), ec);
             if (!BEAST_EXPECT(!ec))
                 return;
 
-            auto fileWriteable = [](boost::filesystem::path const& p) -> bool {
-                return std::ofstream{p.c_str(), std::ios::out | std::ios::app}.is_open();
+            auto fileWriteable = [](std::filesystem::path const& p) -> bool {
+                return std::ofstream{p, std::ios::out | std::ios::app}.is_open();
             };
 
             if (!BEAST_EXPECT(fileWriteable(fixture.logFile())))
                 return;
 
-            boost::filesystem::permissions(
+            std::filesystem::permissions(
                 fixture.logFile(),
-                perms::remove_perms | perms::owner_write | perms::others_write |
-                    perms::group_write);
+                perms::owner_write | perms::others_write | perms::group_write,
+                std::filesystem::perm_options::remove);
 
             // If the test is running as root, then the write protect may have
             // no effect.  Make sure write protect worked before proceeding.
@@ -295,9 +294,10 @@ public:
             perfLog->stop();
 
             // Fix file permissions so the file can be cleaned up.
-            boost::filesystem::permissions(
+            std::filesystem::permissions(
                 fixture.logFile(),
-                perms::add_perms | perms::owner_write | perms::others_write | perms::group_write);
+                perms::owner_write | perms::others_write | perms::group_write,
+                std::filesystem::perm_options::add);
         }
     }
 
@@ -962,7 +962,7 @@ public:
         // We can't fully test rotate because unit tests must run on Windows,
         // and Windows doesn't (may not?) support rotate.  But at least call
         // the interface and see that it doesn't crash.
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
 
         Fixture fixture{env_.app(), j_};
         BEAST_EXPECT(!exists(fixture.logDir()));
diff --git a/src/test/core/Config_test.cpp b/src/test/core/Config_test.cpp
index ac5471fd3c..5ed5ef4049 100644
--- a/src/test/core/Config_test.cpp
+++ b/src/test/core/Config_test.cpp
@@ -3,16 +3,13 @@
 
 #include 
 
+#include 
 #include 
-#include 
 #include 
 #include 
 #include   // IWYU pragma: keep
 #include 
 
-#include 
-#include   // IWYU pragma: keep
-#include 
 #include 
 
 #include 
@@ -20,6 +17,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -36,7 +35,7 @@ namespace detail {
 std::string
 configContents(std::string const& dbPath, std::string const& validatorsFile)
 {
-    static boost::format kConfigContentsTemplate(R"xrpldConfig(
+    static constexpr char const* kConfigContentsTemplate = R"xrpldConfig(
 [server]
 port_rpc
 port_peer
@@ -83,9 +82,9 @@ cache_mb=256
 file_size_mb=8
 file_size_mult=2
 
-%1%
+{}
 
-%2%
+{}
 
 # This needs to be an absolute directory reference, not a relative one.
 # Modify this value as required.
@@ -106,7 +105,7 @@ r.ripple.com 51235
 # Turn down default logging to save disk space in the long run.
 # Valid values here are trace, debug, info, warning, error, and fatal
 [rpc_startup]
-{ "command": "log_level", "severity": "warning" }
+{{ "command": "log_level", "severity": "warning" }}
 
 # Defaults to 1 ("yes") so that certificates will be validated. To allow the use
 # of self-signed certificates for development or internal use, set to 0 ("no").
@@ -115,12 +114,12 @@ r.ripple.com 51235
 
 [sqdb]
 backend=sqlite
-)xrpldConfig");
+)xrpldConfig";
 
     std::string dbPathSection = dbPath.empty() ? "" : "[database_path]\n" + dbPath;
     std::string valFileSection =
         validatorsFile.empty() ? "" : "[validators_file]\n" + validatorsFile;
-    return boost::str(kConfigContentsTemplate % dbPathSection % valFileSection);
+    return std::format(kConfigContentsTemplate, dbPathSection, valFileSection);
 }
 
 /**
@@ -179,7 +178,7 @@ public:
     [[nodiscard]] bool
     dataDirExists() const
     {
-        return boost::filesystem::is_directory(dataDir_);
+        return std::filesystem::is_directory(dataDir_);
     }
 
     [[nodiscard]] bool
@@ -192,7 +191,7 @@ public:
     {
         try
         {
-            using namespace boost::filesystem;
+            using namespace std::filesystem;
             if (rmDataDir_)
                 rmDir(dataDir_);
         }
@@ -273,7 +272,7 @@ public:
 class Config_test final : public TestSuite
 {
 private:
-    using path = boost::filesystem::path;
+    using path = std::filesystem::path;
 
 public:
     void
@@ -309,7 +308,7 @@ port_wss_admin
     {
         testcase("config_file");
 
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
         auto const cwd = current_path();
 
         // Test both config file names.
@@ -319,7 +318,7 @@ port_wss_admin
         for (auto const& configFile : configFiles)
         {
             // Use a temporary directory for testing.
-            beast::TempDir const td;
+            TempDir const td;
             current_path(td.path());
             path const f = td.file(std::string{configFile});
             std::ofstream o(f.string());
@@ -341,13 +340,13 @@ port_wss_admin
         {
             // Point the current working directory to a temporary directory, so
             // we don't pick up an actual config file from the repository root.
-            beast::TempDir const td;
+            TempDir const td;
             current_path(td.path());
 
             // The XDG config directory is set: the config file must be in a
             // subdirectory named after the system.
             {
-                beast::TempDir const tc;
+                TempDir const tc;
 
                 // Set the HOME and XDG_CONFIG_HOME environment variables. The
                 // HOME variable is not used when XDG_CONFIG_HOME is set, but
@@ -381,7 +380,7 @@ port_wss_admin
             // The XDG config directory is not set: the config file must be in a
             // subdirectory named .config followed by the system name.
             {
-                beast::TempDir const tc;
+                TempDir const tc;
 
                 // Set only the HOME environment variable.
                 char const* h = getenv("HOME");
@@ -425,9 +424,9 @@ port_wss_admin
     {
         testcase("database_path");
 
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
         {
-            boost::format cc("[database_path]\n%1%\n");
+            constexpr char const* cc = "[database_path]\n{}\n";
 
             auto const cwd = current_path();
             path const dataDirRel("test_data_dir");
@@ -435,13 +434,13 @@ port_wss_admin
             {
                 // Dummy test - do we get back what we put in
                 Config c;
-                c.loadFromString(boost::str(cc % dataDirAbs.string()));
+                c.loadFromString(std::format(cc, dataDirAbs.string()));
                 BEAST_EXPECT(c.legacy(Sections::kDatabasePath) == dataDirAbs.string());
             }
             {
                 // Rel paths should convert to abs paths
                 Config c;
-                c.loadFromString(boost::str(cc % dataDirRel.string()));
+                c.loadFromString(std::format(cc, dataDirRel.string()));
                 BEAST_EXPECT(c.legacy(Sections::kDatabasePath) == dataDirAbs.string());
             }
             {
@@ -508,20 +507,20 @@ port_wss_admin
 
         {
             Config c;
-            static boost::format kConfigTemplate(R"xrpldConfig(
+            static constexpr char const* kConfigTemplate = R"xrpldConfig(
 [validation_seed]
-%1%
+{}
 
 [validator_token]
-%2%
-)xrpldConfig");
+{}
+)xrpldConfig";
             std::string error;
             auto const expectedError =
                 "Cannot have both [validation_seed] "
                 "and [validator_token] config sections";
             try
             {
-                c.loadFromString(boost::str(kConfigTemplate % validationSeed % token));
+                c.loadFromString(std::format(kConfigTemplate, validationSeed, token));
             }
             catch (std::runtime_error const& e)
             {
@@ -601,10 +600,10 @@ main
     {
         testcase("validators_file");
 
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
         {
             // load should throw for missing specified validators file
-            boost::format cc("[validators_file]\n%1%\n");
+            constexpr char const* cc = "[validators_file]\n{}\n";
             std::string error;
             std::string const missingPath = "/no/way/this/path/exists";
             auto const expectedError =
@@ -612,7 +611,7 @@ main
             try
             {
                 Config c;
-                c.loadFromString(boost::str(cc % missingPath));
+                c.loadFromString(std::format(cc, missingPath));
             }
             catch (std::runtime_error const& e)
             {
@@ -624,14 +623,14 @@ main
             // load should throw for invalid [validators_file]
             detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.cfg");
             path const invalidFile = current_path() / vtg.subdir();
-            boost::format cc("[validators_file]\n%1%\n");
+            constexpr char const* cc = "[validators_file]\n{}\n";
             std::string error;
             auto const expectedError =
                 "Invalid file specified in [validators_file]: " + invalidFile.string();
             try
             {
                 Config c;
-                c.loadFromString(boost::str(cc % invalidFile.string()));
+                c.loadFromString(std::format(cc, invalidFile.string()));
             }
             catch (std::runtime_error const& e)
             {
@@ -829,8 +828,8 @@ trust-these-validators.gov
             detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.cfg");
             BEAST_EXPECT(vtg.validatorsFileExists());
             Config c;
-            boost::format cc("[validators_file]\n%1%\n");
-            c.loadFromString(boost::str(cc % vtg.validatorsFile()));
+            constexpr char const* cc = "[validators_file]\n{}\n";
+            c.loadFromString(std::format(cc, vtg.validatorsFile()));
             BEAST_EXPECT(c.legacy(Sections::kValidatorsFile) == vtg.validatorsFile());
             BEAST_EXPECT(c.section(Sections::kValidators).values().size() == 8);
             BEAST_EXPECT(c.section(Sections::kValidatorListSites).values().size() == 2);
@@ -909,9 +908,9 @@ trust-these-validators.gov
 
         {
             // load validators from both config and validators file
-            boost::format cc(R"xrpldConfig(
+            constexpr char const* cc = R"xrpldConfig(
 [validators_file]
-%1%
+{}
 
 [validators]
 n949f75evCHwgyP4fPVgaHqNHxUVN15PsJEZ3B3HnXPcPjcZAoy7
@@ -930,11 +929,11 @@ trust-these-validators.gov
 
 [validator_list_keys]
 021A99A537FDEBC34E4FCA03B39BEADD04299BB19E85097EC92B15A3518801E566
-)xrpldConfig");
+)xrpldConfig";
             detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.cfg");
             BEAST_EXPECT(vtg.validatorsFileExists());
             Config c;
-            c.loadFromString(boost::str(cc % vtg.validatorsFile()));
+            c.loadFromString(std::format(cc, vtg.validatorsFile()));
             BEAST_EXPECT(c.legacy(Sections::kValidatorsFile) == vtg.validatorsFile());
             BEAST_EXPECT(c.section(Sections::kValidators).values().size() == 15);
             BEAST_EXPECT(c.section(Sections::kValidatorListSites).values().size() == 4);
@@ -945,13 +944,13 @@ trust-these-validators.gov
         {
             // load should throw if [validator_list_threshold] is present both
             // in xrpld.cfg and validators file
-            boost::format cc(R"xrpldConfig(
+            constexpr char const* cc = R"xrpldConfig(
 [validators_file]
-%1%
+{}
 
 [validator_list_threshold]
 1
-)xrpldConfig");
+)xrpldConfig";
             std::string error;
             detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.cfg");
             BEAST_EXPECT(vtg.validatorsFileExists());
@@ -961,7 +960,7 @@ trust-these-validators.gov
             try
             {
                 Config c;
-                c.loadFromString(boost::str(cc % vtg.validatorsFile()));
+                c.loadFromString(std::format(cc, vtg.validatorsFile()));
                 fail();
             }
             catch (std::runtime_error const& e)
@@ -975,7 +974,7 @@ trust-these-validators.gov
             // [validator_list_keys] are missing from xrpld.cfg and
             // validators file
             Config const c;
-            boost::format cc("[validators_file]\n%1%\n");
+            constexpr char const* cc = "[validators_file]\n{}\n";
             std::string error;
             detail::ValidatorsTxtGuard const vtg(*this, "test_cfg", "validators.cfg");
             BEAST_EXPECT(vtg.validatorsFileExists());
@@ -988,7 +987,7 @@ trust-these-validators.gov
             try
             {
                 Config c2;
-                c2.loadFromString(boost::str(cc % vtg.validatorsFile()));
+                c2.loadFromString(std::format(cc, vtg.validatorsFile()));
             }
             catch (std::runtime_error const& e)
             {
diff --git a/src/test/core/SociDB_test.cpp b/src/test/core/SociDB_test.cpp
index 373ec66cd1..a7bb8e71bc 100644
--- a/src/test/core/SociDB_test.cpp
+++ b/src/test/core/SociDB_test.cpp
@@ -6,9 +6,6 @@
 #include 
 #include 
 
-#include 
-#include 
-#include 
 #include   // IWYU pragma: keep
 
 #include   // IWYU pragma: keep
@@ -20,6 +17,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -32,7 +30,7 @@ class SociDB_test final : public TestSuite
 {
 private:
     static void
-    setupSQLiteConfig(BasicConfig& config, boost::filesystem::path const& dbPath)
+    setupSQLiteConfig(BasicConfig& config, std::filesystem::path const& dbPath)
     {
         config.overwrite(Sections::kSqdb, Keys::kBackend, "sqlite");
         auto value = dbPath.string();
@@ -41,18 +39,18 @@ private:
     }
 
     static void
-    cleanupDatabaseDir(boost::filesystem::path const& dbPath)
+    cleanupDatabaseDir(std::filesystem::path const& dbPath)
     {
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
         if (!exists(dbPath) || !is_directory(dbPath) || !is_empty(dbPath))
             return;
         remove(dbPath);
     }
 
     static void
-    setupDatabaseDir(boost::filesystem::path const& dbPath)
+    setupDatabaseDir(std::filesystem::path const& dbPath)
     {
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
         if (!exists(dbPath))
         {
             create_directory(dbPath);
@@ -65,10 +63,10 @@ private:
             Throw("Cannot create directory: " + dbPath.string());
         }
     }
-    static boost::filesystem::path
+    static std::filesystem::path
     getDatabasePath()
     {
-        return boost::filesystem::current_path() / "socidb_test_databases";
+        return std::filesystem::current_path() / "socidb_test_databases";
     }
 
 public:
@@ -108,7 +106,7 @@ public:
         for (auto const& i : d)
         {
             DBConfig const sc(c, i.first);
-            BEAST_EXPECT(boost::ends_with(sc.connectionString(), i.first + i.second));
+            BEAST_EXPECT(sc.connectionString().ends_with(i.first + i.second));
         }
     }
     void
@@ -158,7 +156,7 @@ public:
             checkValues(s);
         }
         {
-            namespace bfs = boost::filesystem;
+            namespace bfs = std::filesystem;
             // Remove the database
             bfs::path const dbPath(sc.connectionString());
             if (bfs::is_regular_file(dbPath))
@@ -232,7 +230,7 @@ public:
             // boost::tuple. DO NOT USE soci row!
         }
         {
-            namespace bfs = boost::filesystem;
+            namespace bfs = std::filesystem;
             // Remove the database
             bfs::path const dbPath(sc.connectionString());
             if (bfs::is_regular_file(dbPath))
@@ -284,7 +282,7 @@ public:
             s << "SELECT LedgerSeq FROM Ledgers;", soci::into(ledgersLS);
             BEAST_EXPECT(ledgersLS.size() == numRows);
         }
-        namespace bfs = boost::filesystem;
+        namespace bfs = std::filesystem;
         // Remove the database
         bfs::path const dbPath(sc.connectionString());
         if (bfs::is_regular_file(dbPath))
diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h
index 5c8486e6c5..801c3627b8 100644
--- a/src/test/jtx/TestHelpers.h
+++ b/src/test/jtx/TestHelpers.h
@@ -43,6 +43,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -315,19 +316,11 @@ auto const kData = JTxFieldWrapper(sfData);
 
 auto const kAmount = JTxFieldWrapper(sfAmount);
 
-// TODO We only need this long "requires" clause as polyfill, for C++20
-// implementations which are missing  header. Replace with
-// `std::ranges::range`, and accordingly use std::ranges::begin/end
-// when we have moved to better compilers.
-template 
+template 
 auto
 makeVector(Input const& input)
-    requires requires(Input& v) {
-        std::begin(v);
-        std::end(v);
-    }
 {
-    return std::vector(std::begin(input), std::end(input));
+    return std::vector(std::ranges::begin(input), std::ranges::end(input));
 }
 
 // Functions used in debugging
diff --git a/src/test/jtx/TrustedPublisherServer.h b/src/test/jtx/TrustedPublisherServer.h
index f5ee8aac3a..941af374ef 100644
--- a/src/test/jtx/TrustedPublisherServer.h
+++ b/src/test/jtx/TrustedPublisherServer.h
@@ -16,7 +16,6 @@
 #include 
 #include 
 
-#include 
 #include 
 #include 
 #include 
@@ -549,7 +548,7 @@ private:
                 res.keep_alive(req.keep_alive());
                 bool prepare = true;
 
-                if (boost::starts_with(path, "/validators2"))
+                if (path.starts_with("/validators2"))
                 {
                     res.result(http::status::ok);
                     res.insert("Content-Type", "application/json");
@@ -565,7 +564,7 @@ private:
                     {
                         int refresh = 5;
                         static constexpr char const* kRefreshPrefix = "/validators2/refresh/";
-                        if (boost::starts_with(path, kRefreshPrefix))
+                        if (path.starts_with(kRefreshPrefix))
                         {
                             refresh = boost::lexical_cast(
                                 path.substr(strlen(kRefreshPrefix)));
@@ -573,7 +572,7 @@ private:
                         res.body() = getList2_(refresh);
                     }
                 }
-                else if (boost::starts_with(path, "/validators"))
+                else if (path.starts_with("/validators"))
                 {
                     res.result(http::status::ok);
                     res.insert("Content-Type", "application/json");
@@ -589,7 +588,7 @@ private:
                     {
                         int refresh = 5;
                         static constexpr char const* kRefreshPrefix = "/validators/refresh/";
-                        if (boost::starts_with(path, kRefreshPrefix))
+                        if (path.starts_with(kRefreshPrefix))
                         {
                             refresh = boost::lexical_cast(
                                 path.substr(strlen(kRefreshPrefix)));
@@ -597,13 +596,13 @@ private:
                         res.body() = getList_(refresh);
                     }
                 }
-                else if (boost::starts_with(path, "/textfile"))
+                else if (path.starts_with("/textfile"))
                 {
                     prepare = false;
                     res.result(http::status::ok);
                     res.insert("Content-Type", "text/example");
                     // if huge was requested, lie about content length
-                    std::uint64_t const cl = boost::starts_with(path, "/textfile/huge")
+                    std::uint64_t const cl = path.starts_with("/textfile/huge")
                         ? std::numeric_limits::max()
                         : 1024;
                     res.content_length(cl);
@@ -617,41 +616,39 @@ private:
                         }
                     }
                 }
-                else if (boost::starts_with(path, "/sleep/"))
+                else if (path.starts_with("/sleep/"))
                 {
                     auto const sleepSec = boost::lexical_cast(path.substr(7));
                     std::this_thread::sleep_for(std::chrono::seconds(sleepSec));
                 }
-                else if (boost::starts_with(path, "/redirect"))
+                else if (path.starts_with("/redirect"))
                 {
-                    if (boost::ends_with(path, "/301"))
+                    if (path.ends_with("/301"))
                     {
                         res.result(http::status::moved_permanently);
                     }
-                    else if (boost::ends_with(path, "/302"))
+                    else if (path.ends_with("/302"))
                     {
                         res.result(http::status::found);
                     }
-                    else if (boost::ends_with(path, "/307"))
+                    else if (path.ends_with("/307"))
                     {
                         res.result(http::status::temporary_redirect);
                     }
-                    else if (boost::ends_with(path, "/308"))
+                    else if (path.ends_with("/308"))
                     {
                         res.result(http::status::permanent_redirect);
                     }
 
                     std::stringstream location;
-                    if (boost::starts_with(path, "/redirect_to/"))
+                    if (path.starts_with("/redirect_to/"))
                     {
                         location << path.substr(13);
                     }
-                    else if (!boost::starts_with(path, "/redirect_nolo"))
+                    else if (!path.starts_with("/redirect_nolo"))
                     {
                         location << (ssl ? "https://" : "http://") << localEndpoint()
-                                 << (boost::starts_with(path, "/redirect_forever/")
-                                         ? path
-                                         : "/validators");
+                                 << (path.starts_with("/redirect_forever/") ? path : "/validators");
                     }
                     if (!location.str().empty())
                         res.insert("Location", location.str());
diff --git a/src/test/jtx/amount.h b/src/test/jtx/amount.h
index 57a4502db9..94dd8aef9e 100644
--- a/src/test/jtx/amount.h
+++ b/src/test/jtx/amount.h
@@ -162,12 +162,6 @@ operator==(PrettyAmount const& lhs, PrettyAmount const& rhs)
     return lhs.value() == rhs.value();
 }
 
-inline bool
-operator!=(PrettyAmount const& lhs, PrettyAmount const& rhs)
-{
-    return !operator==(lhs, rhs);
-}
-
 std::ostream&
 operator<<(std::ostream& os, PrettyAmount const& amount);
 
diff --git a/src/test/jtx/envconfig.h b/src/test/jtx/envconfig.h
index 1f920fca58..5ad24e25c4 100644
--- a/src/test/jtx/envconfig.h
+++ b/src/test/jtx/envconfig.h
@@ -3,6 +3,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -62,6 +63,19 @@ envconfig(F&& modfunc, Args&&... args)
     return modfunc(envconfig(), std::forward(args)...);
 }
 
+/**
+ * @brief adjust config to enable online_delete
+ *
+ * @param cfg config instance to be modified
+ *
+ * @param deleteInterval how many new ledgers should be available before
+ * rotating. Defaults to 8, because the standalone minimum is 8.
+ *
+ * @return unique_ptr to Config instance
+ */
+std::unique_ptr
+onlineDelete(std::unique_ptr cfg, std::uint32_t deleteInterval = 8);
+
 /**
  * @brief adjust config so no admin ports are enabled
  *
diff --git a/src/test/jtx/impl/envconfig.cpp b/src/test/jtx/impl/envconfig.cpp
index d65e6f89c6..9eab91459a 100644
--- a/src/test/jtx/impl/envconfig.cpp
+++ b/src/test/jtx/impl/envconfig.cpp
@@ -7,8 +7,10 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl::test {
@@ -65,6 +67,15 @@ setupConfigForUnitTests(Config& cfg)
 
 namespace jtx {
 
+std::unique_ptr
+onlineDelete(std::unique_ptr cfg, std::uint32_t deleteInterval)
+{
+    cfg->ledgerHistory = deleteInterval;
+    auto& section = cfg->section(Sections::kNodeDatabase);
+    section.set(Keys::kOnlineDelete, std::to_string(deleteInterval));
+    return cfg;
+}
+
 std::unique_ptr
 noAdmin(std::unique_ptr cfg)
 {
diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp
index c6cd49fa26..0c1ff14eab 100644
--- a/src/test/jtx/impl/mpt.cpp
+++ b/src/test/jtx/impl/mpt.cpp
@@ -17,7 +17,7 @@
 #include 
 #include 
 #include 
-#include 
+#include 
 #include 
 #include 
 #include 
@@ -648,6 +648,38 @@ MPTTester::checkImmutableFlags(std::uint32_t expectedFlags) const
     });
 }
 
+[[nodiscard]] bool
+MPTTester::checkKeyEpochs(
+    std::optional issuerKeyEpoch,
+    std::optional auditorKeyEpoch) const
+{
+    return forObject([&](SLEP const& sle) -> bool {
+        return (*sle)[~sfIssuerKeyEpoch] == issuerKeyEpoch &&
+            (*sle)[~sfAuditorKeyEpoch] == auditorKeyEpoch;
+    });
+}
+
+[[nodiscard]] bool
+MPTTester::checkEncryptionKeys(
+    std::optional const& issuerKeyOwner,
+    std::optional const& auditorKeyOwner) const
+{
+    auto const matches =
+        [this](SLEP const& sle, SF_VL const& field, std::optional const& owner) {
+            if (!owner)
+                return !sle->isFieldPresent(field);
+
+            auto const expected = getPubKey(*owner);
+            return expected && sle->isFieldPresent(field) &&
+                strHex((*sle)[field]) == strHex(*expected);
+        };
+
+    return forObject([&](SLEP const& sle) -> bool {
+        return matches(sle, sfIssuerEncryptionKey, issuerKeyOwner) &&
+            matches(sle, sfAuditorEncryptionKey, auditorKeyOwner);
+    });
+}
+
 void
 MPTTester::pay(
     Account const& src,
diff --git a/src/test/jtx/impl/multisign.cpp b/src/test/jtx/impl/multisign.cpp
index d948042bda..e04e1bb58a 100644
--- a/src/test/jtx/impl/multisign.cpp
+++ b/src/test/jtx/impl/multisign.cpp
@@ -60,10 +60,12 @@ signers(Account const& account, NoneT)
 //------------------------------------------------------------------------------
 
 void
-Msig::operator()(Env& env, JTx& jt) const
+Msig::operator()(Env&, JTx& jt) const
 {
     auto const mySigners = signers;
-    auto callback = [subField = subField, mySigners, &env](Env&, JTx& jtx) {
+    auto callback = [subField = subField, mySigners](Env& env, JTx& jtx) {
+        auto const prefix =
+            signingPrefix(jtx::signatureRole(subField), true, env.current()->rules());
         // Where to put the signature. Supports sfCounterPartySignature and
         // sfSponsorSignature.
         auto& sigObject = subField ? jtx[*subField] : jtx.jv;
@@ -95,7 +97,7 @@ Msig::operator()(Env& env, JTx& jt) const
             jo[jss::Account] = e.acct.human();
             jo[jss::SigningPubKey] = strHex(e.sig.pk().slice());
 
-            Serializer const ss{buildMultiSigningData(*st, e.acct.id())};
+            Serializer const ss{buildMultiSigningData(*st, e.acct.id(), prefix)};
             auto const sig = xrpl::sign(*publicKeyType(e.sig.pk().slice()), e.sig.sk(), ss.slice());
             jo[sfTxnSignature.getJsonName()] = strHex(Slice{sig.data(), sig.size()});
         }
diff --git a/src/test/jtx/impl/sig.cpp b/src/test/jtx/impl/sig.cpp
index e0123073b1..41833c8802 100644
--- a/src/test/jtx/impl/sig.cpp
+++ b/src/test/jtx/impl/sig.cpp
@@ -4,6 +4,8 @@
 #include 
 #include 
 
+#include 
+
 namespace xrpl::test::jtx {
 
 void
@@ -17,11 +19,15 @@ Sig::operator()(Env&, JTx& jt) const
     {
         // VFALCO Inefficient pre-C++14
         auto const account = *account_;
-        auto callback = [subField = subField_, account](Env&, JTx& jtx) {
+        auto callback = [subField = subField_, account](Env& env, JTx& jtx) {
             // Where to put the signature. Supports sfCounterPartySignature and sfSponsorSignature.
             auto& sigObject = subField ? jtx[*subField] : jtx.jv;
 
-            jtx::sign(jtx.jv, account, sigObject);
+            jtx::sign(
+                jtx.jv,
+                account,
+                sigObject,
+                signingPrefix(jtx::signatureRole(subField), false, env.current()->rules()));
         };
         if (subField_ == nullptr)
         {
diff --git a/src/test/jtx/impl/utility.cpp b/src/test/jtx/impl/utility.cpp
index c298cee684..f83cb7772c 100644
--- a/src/test/jtx/impl/utility.cpp
+++ b/src/test/jtx/impl/utility.cpp
@@ -19,9 +19,11 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
+#include 
 #include 
 #include 
 
@@ -36,12 +38,22 @@ parse(json::Value const& jv)
     return std::move(*p.object);
 }
 
+SignatureRole
+signatureRole(SField const* subField)
+{
+    if (subField == nullptr)
+        return SignatureRole::Transaction;
+    if (auto const role = xrpl::signatureRole(*subField))
+        return *role;
+    Throw(subField->getName() + " does not hold a transaction signature.");
+}
+
 void
-sign(json::Value& jv, Account const& account, json::Value& sigObject)
+sign(json::Value& jv, Account const& account, json::Value& sigObject, HashPrefix prefix)
 {
     sigObject[jss::SigningPubKey] = strHex(account.pk().slice());
     Serializer ss;
-    ss.add32(HashPrefix::TxSign);
+    ss.add32(prefix);
     parse(jv).addWithoutSigningFields(ss);
     auto const sig = xrpl::sign(account.pk(), account.sk(), ss.slice());
     sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()});
diff --git a/src/test/jtx/impl/vault.cpp b/src/test/jtx/impl/vault.cpp
index baff576243..5bf8ac9981 100644
--- a/src/test/jtx/impl/vault.cpp
+++ b/src/test/jtx/impl/vault.cpp
@@ -3,17 +3,22 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 
+#include 
+#include 
 #include 
 #include 
+#include 
 
 namespace xrpl::test::jtx {
 
@@ -28,9 +33,38 @@ Vault::create(CreateArgs const& args) const
     jv[jss::Asset] = toJson(args.asset);
     if (args.flags)
         jv[jss::Flags] = *args.flags;
+    if (args.vaultKind)
+        jv[sfVaultKind] = *args.vaultKind;
+    if (args.subscriptionDate)
+        jv[sfSubscriptionDate] = *args.subscriptionDate;
+    if (args.redemptionDate)
+        jv[sfRedemptionDate] = *args.redemptionDate;
+    if (args.leVersion)
+        jv[sfLEVersion] = std::to_underlying(*args.leVersion);
     return {jv, keylet};
 }
 
+std::tuple
+Vault::createClosedEnded(CreateClosedEndedArgs const& args) const
+{
+    auto const sub = env.now() + args.subscriptionOffset;
+    auto const red = sub + args.investmentWindow;
+    auto [jv, keylet] = create(
+        {.owner = args.owner,
+         .asset = args.asset,
+         .flags = args.flags,
+         .vaultKind = std::to_underlying(VaultKind::ClosedEnded),
+         .subscriptionDate = static_cast(sub.time_since_epoch().count()),
+         .redemptionDate = static_cast(red.time_since_epoch().count())});
+    return {jv, keylet, sub};
+}
+
+void
+Vault::closePastSubscription(NetClock::time_point subscriptionDate) const
+{
+    env.close(subscriptionDate + std::chrono::seconds{1});
+}
+
 json::Value
 Vault::set(SetArgs const& args)
 {
diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h
index 35ab7264bd..26329ad78c 100644
--- a/src/test/jtx/mpt.h
+++ b/src/test/jtx/mpt.h
@@ -612,6 +612,21 @@ public:
     [[nodiscard]] bool
     checkImmutableFlags(std::uint32_t expectedFlags) const;
 
+    // Checks both key epochs on the issuance. Pass std::nullopt for an epoch
+    // that is expected to be absent, which means the key is never rotated.
+    [[nodiscard]] bool
+    checkKeyEpochs(
+        std::optional issuerKeyEpoch,
+        std::optional auditorKeyEpoch) const;
+
+    // Checks that the issuance carries the encryption keys of the given
+    // accounts. Pass std::nullopt for a key that is expected to be absent,
+    // which means the key is never registered.
+    [[nodiscard]] bool
+    checkEncryptionKeys(
+        std::optional const& issuerKeyOwner,
+        std::optional const& auditorKeyOwner) const;
+
     [[nodiscard]] Account const&
     issuer() const
     {
diff --git a/src/test/jtx/utility.h b/src/test/jtx/utility.h
index 289afec8b3..5a37abd920 100644
--- a/src/test/jtx/utility.h
+++ b/src/test/jtx/utility.h
@@ -5,7 +5,10 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
+#include 
 
 #include 
 #include 
@@ -33,12 +36,29 @@ struct ParseError : std::logic_error
 STObject
 parse(json::Value const& jv);
 
+/**
+ * The role that signs into an optional signature subfield.
+ *
+ * @param subField The signature field, or nullptr for the transaction's own
+ * signature. Throws if the field does not hold a transaction signature.
+ */
+SignatureRole
+signatureRole(SField const* subField);
+
 /**
  * Sign automatically into a specific Json field of the jv object.
+ *
+ * @param prefix Prefix to insert before the serialized transaction when
+ * hashing. Use signingPrefix to get the prefix that matches the field that
+ * holds sigObject.
  * @note This only works on accounts with multi-signing off.
  */
 void
-sign(json::Value& jv, Account const& account, json::Value& sigObject);
+sign(
+    json::Value& jv,
+    Account const& account,
+    json::Value& sigObject,
+    HashPrefix prefix = HashPrefix::TxSign);
 
 /**
  * Sign automatically.
diff --git a/src/test/jtx/vault.h b/src/test/jtx/vault.h
index e72eae89b7..000e8a20ea 100644
--- a/src/test/jtx/vault.h
+++ b/src/test/jtx/vault.h
@@ -3,10 +3,13 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 
+#include 
 #include 
 #include 
 #include 
@@ -25,6 +28,14 @@ struct Vault
         Asset asset;
         std::optional flags =
             std::nullopt;  // NOLINT(readability-redundant-member-init)
+        std::optional vaultKind =
+            std::nullopt;  // NOLINT(readability-redundant-member-init)
+        std::optional subscriptionDate =
+            std::nullopt;  // NOLINT(readability-redundant-member-init)
+        std::optional redemptionDate =
+            std::nullopt;  // NOLINT(readability-redundant-member-init)
+        std::optional leVersion =
+            std::nullopt;  // NOLINT(readability-redundant-member-init)
     };
 
     /**
@@ -33,6 +44,38 @@ struct Vault
     [[nodiscard]] std::tuple
     create(CreateArgs const& args) const;
 
+    struct CreateClosedEndedArgs
+    {
+        Account owner;
+        Asset asset;
+        std::optional flags =
+            std::nullopt;  // NOLINT(readability-redundant-member-init)
+        NetClock::duration subscriptionOffset = std::chrono::seconds{10};
+        NetClock::duration investmentWindow = std::chrono::seconds{1'000'000};
+    };
+
+    /**
+     * Return a VaultCreate transaction for a closed-ended vault, its
+     * expected keylet, and the vault's SubscriptionDate.
+     *
+     * Under featureLendingProtocolV1_1, LoanBrokerSet::preclaim only
+     * accepts closed-ended vaults, so tests that attach a loan broker
+     * need one. SubscriptionDate is set to now() + subscriptionOffset,
+     * giving callers a window to deposit while still in the Subscription
+     * phase; pass the returned date to closePastSubscription() afterwards
+     * to advance into the Investment phase.
+     */
+    [[nodiscard]] std::tuple
+    createClosedEnded(CreateClosedEndedArgs const& args) const;
+
+    /**
+     * Advance env's clock to just past subscriptionDate, moving a
+     * closed-ended vault from the Subscription phase into the Investment
+     * phase.
+     */
+    void
+    closePastSubscription(NetClock::time_point subscriptionDate) const;
+
     struct SetArgs
     {
         Account owner;
diff --git a/src/test/overlay/ProtocolVersion_test.cpp b/src/test/overlay/ProtocolVersion_test.cpp
index e31a574502..e7b63a34cb 100644
--- a/src/test/overlay/ProtocolVersion_test.cpp
+++ b/src/test/overlay/ProtocolVersion_test.cpp
@@ -33,22 +33,30 @@ public:
     void
     run() override
     {
-        testcase("Convert protocol version to string");
-        BEAST_EXPECT(to_string(makeProtocol(1, 3)) == "XRPL/1.3");
-        BEAST_EXPECT(to_string(makeProtocol(2, 0)) == "XRPL/2.0");
-        BEAST_EXPECT(to_string(makeProtocol(2, 1)) == "XRPL/2.1");
-        BEAST_EXPECT(to_string(makeProtocol(10, 10)) == "XRPL/10.10");
+        {
+            testcase("Convert protocol version to string");
+
+            BEAST_EXPECT(to_string(makeProtocol(0, 0)) == "XRPL/0.0");
+            BEAST_EXPECT(to_string(makeProtocol(0, 1)) == "XRPL/0.1");
+            BEAST_EXPECT(to_string(makeProtocol(1, 3)) == "XRPL/1.3");
+            BEAST_EXPECT(to_string(makeProtocol(2, 0)) == "XRPL/2.0");
+            BEAST_EXPECT(to_string(makeProtocol(2, 1)) == "XRPL/2.1");
+            BEAST_EXPECT(to_string(makeProtocol(10, 10)) == "XRPL/10.10");
+            BEAST_EXPECT(to_string(makeProtocol(65535, 65535)) == "XRPL/65535.65535");
+        }
 
         {
             testcase("Convert strings to protocol versions");
 
-            // Empty string
+            // Invalid versions, either they do not parse as XRPL/N.M or are unsupported.
             check("", "");
+            check("RTXP/1.1,RTXP/1.2,RTXP/1.3", "");
+            check("XRPL/-2.1,XRPL/0.3,XRPL/2,XRPL/2.01,websocket", "");
 
-            check("RTXP/1.1,RTXP/1.2,RTXP/1.3,XRPL/2.1,XRPL/2.0,/XRPL/3.0", "XRPL/2.0,XRPL/2.1");
-            check("RTXP/0.9,RTXP/1.01,XRPL/0.3,XRPL/2.01,websocket", "");
+            // Mixture of valid, duplicate, and invalid versions.
+            check("RTXP/1.3,XRPL/2.1,XRPL/2.0,/XRPL/3.0", "XRPL/2.0,XRPL/2.1");
             check(
-                "XRPL/2.0,XRPL/2.0,XRPL/19.4,XRPL/7.89,XRPL/XRPL/3.0,XRPL/2.01",
+                "XRPL/2.0,XRPL/2.0,XRPL/19.4,XRPL/7.89,XRPL/XRPL/3.0,XRPL/2.01,XRPL/-65535.65535",
                 "XRPL/2.0,XRPL/7.89,XRPL/19.4");
             check(
                 "XRPL/2.0,XRPL/3.0,XRPL/4,XRPL/,XRPL,OPT XRPL/2.2,XRPL/5.67",
@@ -58,15 +66,17 @@ public:
         {
             testcase("Protocol version negotiation");
 
-            BEAST_EXPECT(negotiateProtocolVersion("RTXP/1.2") == std::nullopt);
+            // Only the highest supported protocol version, if any, is returned.
+            BEAST_EXPECT(negotiateProtocolVersion("") == std::nullopt);
+            BEAST_EXPECT(negotiateProtocolVersion("XRPL/0.0") == std::nullopt);
+            BEAST_EXPECT(negotiateProtocolVersion("RTXP/1.2,XRPL/0.1") == std::nullopt);
             BEAST_EXPECT(
-                negotiateProtocolVersion("RTXP/1.2, XRPL/2.0, XRPL/2.1") == makeProtocol(2, 1));
+                negotiateProtocolVersion("XRPL/999.999, XRPL/-2.2,WebSocket/1.0") == std::nullopt);
             BEAST_EXPECT(negotiateProtocolVersion("XRPL/2.2") == makeProtocol(2, 2));
             BEAST_EXPECT(
-                negotiateProtocolVersion("RTXP/1.2, XRPL/2.3, XRPL/2.4, XRPL/999.999") ==
+                negotiateProtocolVersion(
+                    "RTXP/1.2, XRPL/2.1, XRPL/2.2, XRPL/2.3, XRPL/2.4, XRPL/999.999") ==
                 makeProtocol(2, 3));
-            BEAST_EXPECT(negotiateProtocolVersion("XRPL/999.999, WebSocket/1.0") == std::nullopt);
-            BEAST_EXPECT(negotiateProtocolVersion("") == std::nullopt);
         }
     }
 };
diff --git a/src/test/overlay/compression_test.cpp b/src/test/overlay/compression_test.cpp
index a583a3aeab..40dee96c75 100644
--- a/src/test/overlay/compression_test.cpp
+++ b/src/test/overlay/compression_test.cpp
@@ -292,33 +292,6 @@ public:
         return getObject;
     }
 
-    static std::shared_ptr
-    buildValidatorList()
-    {
-        auto list = std::make_shared();
-
-        auto master = randomKeyPair(KeyType::Ed25519);
-        auto signing = randomKeyPair(KeyType::Ed25519);
-        STObject st(sfGeneric);
-        st[sfSequence] = 0;
-        st[sfPublicKey] = std::get<0>(master);
-        st[sfSigningPubKey] = std::get<0>(signing);
-        st[sfDomain] = makeSlice(std::string("example.com"));
-        sign(st, HashPrefix::Manifest, KeyType::Ed25519, std::get<1>(master), sfMasterSignature);
-        sign(st, HashPrefix::Manifest, KeyType::Ed25519, std::get<1>(signing));
-        Serializer s;
-        st.add(s);
-        list->set_manifest(s.data(), s.size());
-        list->set_version(3);
-        STObject const signature(sfSignature);
-        xrpl::sign(st, HashPrefix::Manifest, KeyType::Ed25519, std::get<1>(signing));
-        Serializer s1;
-        st.add(s1);
-        list->set_signature(s1.data(), s1.size());
-        list->set_blob(strHex(s.slice()));
-        return list;
-    }
-
     static std::shared_ptr
     buildValidatorListCollection()
     {
@@ -359,7 +332,6 @@ public:
         protocol::TMGetLedger const getLedger;
         protocol::TMLedgerData const ledgerData;
         protocol::TMGetObjectByHash const getObject;
-        protocol::TMValidatorList const validatorList;
         protocol::TMValidatorListCollection const validatorListCollection;
 
         // 4.5KB
@@ -386,8 +358,6 @@ public:
         doTest(buildLedgerData(500000, *logs), protocol::mtLEDGER_DATA, 100, "TMLedgerData500000");
         // 7.7KB
         doTest(buildGetObjectByHash(), protocol::mtGET_OBJECTS, 4, "TMGetObjectByHash");
-        // 895B
-        doTest(buildValidatorList(), protocol::mtVALIDATOR_LIST, 4, "TMValidatorList");
         doTest(
             buildValidatorListCollection(),
             protocol::mtVALIDATOR_LIST_COLLECTION,
diff --git a/src/test/protocol/STAmount_test.cpp b/src/test/protocol/STAmount_test.cpp
index f6c5a94752..c3a681cf01 100644
--- a/src/test/protocol/STAmount_test.cpp
+++ b/src/test/protocol/STAmount_test.cpp
@@ -1,16 +1,21 @@
 
 #include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -24,6 +29,7 @@
 #include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
@@ -990,6 +996,84 @@ public:
         }
     }
 
+    void
+    testMPTRateRounding()
+    {
+        testcase("MPT transfer rate rounding uses Number arithmetic");
+
+        MPTIssue const asset{makeMptID(1, AccountID(0x4985601))};
+        Rate const transferRate{1'500'000'000};
+        STAmount const largeAmount{asset, UINT64_C(1'230'000'000'000'000'000)};
+        STAmount const scaledAmount{asset, UINT64_C(1'845'000'000'000'000'000)};
+
+        auto rules = [](bool const mptV2) {
+            // Rules keeps a reference to the presets set, so use static
+            // storage here rather than a local temporary.
+            static std::unordered_set> const kNoFeatures;
+            static std::unordered_set> const kMptV2Features{
+                featureMPTokensV2};
+            return Rules{mptV2 ? kMptV2Features : kNoFeatures};
+        };
+
+        auto throwsOverflow = [&](auto&& f, bool expected = true) {
+            bool threw = false;
+            try
+            {
+                f();
+            }
+            catch (std::overflow_error const&)
+            {
+                threw = true;
+            }
+            BEAST_EXPECT(threw == expected);
+        };
+
+        {
+            CurrentTransactionRulesGuard const rg(rules(false));
+
+            throwsOverflow([&] { (void)multiplyRound(largeAmount, transferRate, asset, true); });
+            throwsOverflow([&] { (void)divideRound(scaledAmount, transferRate, asset, true); });
+        }
+
+        {
+            CurrentTransactionRulesGuard const rg(rules(true));
+
+            throwsOverflow(
+                [&] { (void)multiplyRound(largeAmount, transferRate, asset, true); }, false);
+            throwsOverflow(
+                [&] { (void)divideRound(scaledAmount, transferRate, asset, true); }, false);
+        }
+
+        {
+            CurrentTransactionRulesGuard const rg(rules(true));
+            STAmount const one{asset, 1};
+            STAmount const two{asset, 2};
+
+            BEAST_EXPECT(multiplyRound(one, transferRate, asset, true) == two);
+            BEAST_EXPECT(multiplyRound(one, transferRate, asset, false) == one);
+            BEAST_EXPECT(divideRound(two, transferRate, asset, true) == two);
+            BEAST_EXPECT(divideRound(two, transferRate, asset, false) == one);
+
+            BEAST_EXPECT(multiplyRound(largeAmount, transferRate, asset, true) == scaledAmount);
+            BEAST_EXPECT(divideRound(scaledAmount, transferRate, asset, true) == largeAmount);
+        }
+
+        {
+            // mulRound with an integral (XRP) operand whose mantissa is below
+            // kMinValue exercises the legacy value-scaling loop that normalizes
+            // the mantissa before multiply. The MPTokensV2 Number path is
+            // not taken here because the target asset is an IOU.
+            Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
+            STAmount const iouVal{usd, 5};
+            STAmount const xrpVal{XRPAmount{7}};  // integral, mantissa < kMinValue
+
+            auto const up = mulRound(iouVal, xrpVal, usd, /*roundUp*/ true);
+            auto const down = mulRound(iouVal, xrpVal, usd, /*roundUp*/ false);
+            BEAST_EXPECT(down.signum() > 0);
+            BEAST_EXPECT(up >= down);
+        }
+    }
+
     void
     testCanSubtractXRP()
     {
@@ -1267,6 +1351,7 @@ public:
         testCanAddXRP();
         testCanAddIOU();
         testCanAddMPT();
+        testMPTRateRounding();
         testCanSubtractXRP();
         testCanSubtractIOU();
         testCanSubtractMPT();
diff --git a/src/test/protocol/STNumber_test.cpp b/src/test/protocol/STNumber_test.cpp
index 74792e0a70..1e5027df49 100644
--- a/src/test/protocol/STNumber_test.cpp
+++ b/src/test/protocol/STNumber_test.cpp
@@ -12,6 +12,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -176,61 +177,32 @@ struct STNumber_test : public beast::unit_test::Suite
                 numberFromJson(sfNumber, std::to_string(kUMax)) ==
                 STNumber(sfNumber, Number(kUMax, 0)));
 
+            auto const expectJsonThrows = [this](
+                                              json::Value const& num, std::string const& expected) {
+                try
+                {
+                    numberFromJson(sfNumber, num);
+                    fail();
+                }
+                catch (std::exception const& e)
+                {
+                    std::ostringstream out;
+                    out << "Json: " << num.asString() << " got exception: " << e.what()
+                        << ", expected: " << expected;
+                    BEAST_EXPECTS(std::string(e.what()) == expected, out.str());
+                }
+            };
+
+            // Obvious overflows tested here
+            expectJsonThrows("1e2000000", "Number::normalize 2");
+            expectJsonThrows("1e2000000000", "Number::normalize 2");
+
             // Obvious non-numbers tested here
-            try
-            {
-                auto _ = numberFromJson(sfNumber, "");
-                BEAST_EXPECT(false);
-            }
-            catch (std::runtime_error const& e)
-            {
-                std::string const expected = "'' is not a number";
-                BEAST_EXPECT(e.what() == expected);
-            }
-
-            try
-            {
-                auto _ = numberFromJson(sfNumber, "e");
-                BEAST_EXPECT(false);
-            }
-            catch (std::runtime_error const& e)
-            {
-                std::string const expected = "'e' is not a number";
-                BEAST_EXPECT(e.what() == expected);
-            }
-
-            try
-            {
-                auto _ = numberFromJson(sfNumber, "1e");
-                BEAST_EXPECT(false);
-            }
-            catch (std::runtime_error const& e)
-            {
-                std::string const expected = "'1e' is not a number";
-                BEAST_EXPECT(e.what() == expected);
-            }
-
-            try
-            {
-                auto _ = numberFromJson(sfNumber, "e2");
-                BEAST_EXPECT(false);
-            }
-            catch (std::runtime_error const& e)
-            {
-                std::string const expected = "'e2' is not a number";
-                BEAST_EXPECT(e.what() == expected);
-            }
-
-            try
-            {
-                auto _ = numberFromJson(sfNumber, json::Value());
-                BEAST_EXPECT(false);
-            }
-            catch (std::runtime_error const& e)
-            {
-                std::string const expected = "not a number";
-                BEAST_EXPECT(e.what() == expected);
-            }
+            expectJsonThrows("", "'' is not a number");
+            expectJsonThrows("e", "'e' is not a number");
+            expectJsonThrows("1e", "'1e' is not a number");
+            expectJsonThrows("e2", "'e2' is not a number");
+            expectJsonThrows(json::Value(), "not a number");
 
             try
             {
diff --git a/src/test/protocol/STTx_test.cpp b/src/test/protocol/STTx_test.cpp
index 777234be83..f310274e56 100644
--- a/src/test/protocol/STTx_test.cpp
+++ b/src/test/protocol/STTx_test.cpp
@@ -1,9 +1,13 @@
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include   // IWYU pragma: keep
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -11,10 +15,12 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -24,6 +30,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -55,6 +62,279 @@ public:
         testSTTx(KeyType::Ed25519);
         testObjectCtorErrors();
         testBatchInnerCtorErrors();
+        testSigningPrefixes();
+        testRoleSignatureBinding();
+        testRoleMultiSignatureBinding();
+    }
+
+    // Rules with no amendments enabled, and rules with only fixCleanup3_4_0
+    // enabled. Rules keep a reference to the presets, so the presets must
+    // outlive the Rules; both are returned together.
+    struct RulesFixture
+    {
+        std::unordered_set> const noPresets;
+        std::unordered_set> const fixPresets{fixCleanup3_4_0};
+        Rules const legacy{noPresets};
+        Rules const fixed{fixPresets};
+    };
+
+    // A transaction with fixed contents, so its signing data is stable from
+    // run to run.
+    static STTx
+    makeFixedTx()
+    {
+        auto const keypair = generateKeyPair(KeyType::Secp256k1, generateSeed("masterpassphrase"));
+        return STTx(ttACCOUNT_SET, [&keypair](auto& obj) {
+            obj.setAccountID(sfAccount, calcAccountID(keypair.first));
+            obj.setFieldAmount(sfFee, STAmount(10ull));
+            obj.setFieldU32(sfSequence, 1);
+            obj.setFieldVL(sfSigningPubKey, keypair.first.slice());
+        });
+    }
+
+    void
+    testSigningPrefixes()
+    {
+        testcase("signing prefixes");
+
+        // The prefixes are protocol constants. Spell them out so that a typo
+        // in a prefix character fails here, and not in a downstream library.
+        static_assert(safeCast(HashPrefix::TxSign) == 0x53545800);
+        static_assert(safeCast(HashPrefix::TxMultiSign) == 0x534D5400);
+        static_assert(safeCast(HashPrefix::CounterpartyTxSign) == 0x43505400);
+        static_assert(safeCast(HashPrefix::CounterpartyTxMultiSign) == 0x43504D00);
+        static_assert(safeCast(HashPrefix::SponsorTxSign) == 0x53504E00);
+        static_assert(safeCast(HashPrefix::SponsorTxMultiSign) == 0x53504D00);
+
+        RulesFixture const r;
+
+        // Every role gets its own prefix once the fix is enabled.
+        BEAST_EXPECT(
+            signingPrefix(SignatureRole::Transaction, false, r.fixed) == HashPrefix::TxSign);
+        BEAST_EXPECT(
+            signingPrefix(SignatureRole::Transaction, true, r.fixed) == HashPrefix::TxMultiSign);
+        BEAST_EXPECT(
+            signingPrefix(SignatureRole::Counterparty, false, r.fixed) ==
+            HashPrefix::CounterpartyTxSign);
+        BEAST_EXPECT(
+            signingPrefix(SignatureRole::Counterparty, true, r.fixed) ==
+            HashPrefix::CounterpartyTxMultiSign);
+        BEAST_EXPECT(
+            signingPrefix(SignatureRole::Sponsor, false, r.fixed) == HashPrefix::SponsorTxSign);
+        BEAST_EXPECT(
+            signingPrefix(SignatureRole::Sponsor, true, r.fixed) == HashPrefix::SponsorTxMultiSign);
+
+        // Before the fix, every role signs the same bytes.
+        for (auto const role :
+             {SignatureRole::Transaction, SignatureRole::Counterparty, SignatureRole::Sponsor})
+        {
+            BEAST_EXPECT(signingPrefix(role, false, r.legacy) == HashPrefix::TxSign);
+            BEAST_EXPECT(signingPrefix(role, true, r.legacy) == HashPrefix::TxMultiSign);
+        }
+
+        // Each role's field, and each signature field's role, agree.
+        BEAST_EXPECT(signatureField(SignatureRole::Transaction) == nullptr);
+        BEAST_EXPECT(*signatureField(SignatureRole::Counterparty) == sfCounterpartySignature);
+        BEAST_EXPECT(*signatureField(SignatureRole::Sponsor) == sfSponsorSignature);
+        BEAST_EXPECT(signatureRole(sfCounterpartySignature) == SignatureRole::Counterparty);
+        BEAST_EXPECT(signatureRole(sfSponsorSignature) == SignatureRole::Sponsor);
+        BEAST_EXPECT(!signatureRole(sfBook));
+        BEAST_EXPECT(!signatureRole(sfSigner));
+
+        // The bytes signed by an ordinary transaction must not move. Both the
+        // single- and the multi-signing data are pinned, and neither depends
+        // on the amendment.
+        auto const tx = makeFixedTx();
+        for (Rules const& rules : {r.legacy, r.fixed})
+        {
+            Serializer single;
+            single.add32(signingPrefix(SignatureRole::Transaction, false, rules));
+            tx.addWithoutSigningFields(single);
+            // 53545800 STX prefix, 120003 AccountSet, 2400000001 Sequence,
+            // 68400000000000000A Fee, 7321... SigningPubKey, 8114... Account.
+            BEAST_EXPECT(
+                strHex(single.peekData()) ==
+                "53545800"
+                "120003"
+                "2400000001"
+                "68400000000000000A"
+                "73210330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020"
+                "8114B5F762798A53D543A014CAF8B297CFF8F2F937E8");
+            BEAST_EXPECT(
+                to_string(single.getSHA512Half()) ==
+                "AB7473EA8D05527A7465229447B0E9B05365C72B87E921762AD30742B253F3E6");
+
+            auto const signer = calcAccountID(
+                generateKeyPair(KeyType::Secp256k1, generateSeed("multisigner")).first);
+            Serializer const multi = buildMultiSigningData(
+                tx, signer, signingPrefix(SignatureRole::Transaction, true, rules));
+
+            // The multi-signing data is the single-signing data with a
+            // different prefix and the signer's account appended.
+            Serializer expected;
+            expected.add32(HashPrefix::TxMultiSign);
+            tx.addWithoutSigningFields(expected);
+            expected.addBitString(signer);
+            BEAST_EXPECT(strHex(multi.peekData()) == strHex(expected.peekData()));
+        }
+    }
+
+    void
+    testRoleSignatureBinding()
+    {
+        testcase("role signature binding");
+
+        RulesFixture const r;
+
+        auto const keypair = generateKeyPair(KeyType::Secp256k1, generateSeed("masterpassphrase"));
+        auto const account = calcAccountID(keypair.first);
+
+        // A transaction signed by its own account, with that signature copied
+        // into an alternate signature field. sfSponsorSignature is a common
+        // field; sfCounterpartySignature is only on a LoanSet.
+        auto makeCopiedSig = [&keypair, &account](SField const& sigField) {
+            bool const counterparty = sigField == sfCounterpartySignature;
+            STTx tx(counterparty ? ttLOAN_SET : ttACCOUNT_SET, [&](auto& obj) {
+                obj.setAccountID(sfAccount, account);
+                obj.setFieldAmount(sfFee, STAmount(10ull));
+                obj.setFieldU32(sfSequence, 1);
+                obj.setFieldVL(sfSigningPubKey, keypair.first.slice());
+                if (counterparty)
+                {
+                    obj.setFieldH256(sfLoanBrokerID, uint256{1});
+                    obj.setFieldNumber(
+                        sfPrincipalRequested, STNumber{sfPrincipalRequested, Number{1}});
+                }
+                else
+                {
+                    obj.setAccountID(sfSponsor, account);
+                    obj.setFieldU32(sfSponsorFlags, 0);
+                }
+            });
+            tx.sign(keypair.first, keypair.second);
+
+            STObject sigObject(sigField);
+            sigObject.setFieldVL(sfSigningPubKey, keypair.first.slice());
+            sigObject.setFieldVL(sfTxnSignature, tx.getSignature());
+
+            // NOLINTNEXTLINE(cppcoreguidelines-slicing)
+            STObject copy{tx};
+            copy.setFieldObject(sigField, sigObject);
+            return STTx{std::move(copy)};
+        };
+
+        for (SField const& sigField :
+             {std::cref(sfCounterpartySignature), std::cref(sfSponsorSignature)})
+        {
+            auto const tx = makeCopiedSig(sigField);
+
+            // Before the fix, the copied signature verifies in the other role.
+            BEAST_EXPECT(tx.checkSign(r.legacy));
+
+            // With the fix, it does not, and the error names the role that
+            // failed so the two role checks cannot be confused.
+            auto const ret = tx.checkSign(r.fixed);
+            BEAST_EXPECT(!ret);
+            if (!ret)
+            {
+                char const* const rolePrefix =
+                    sigField == sfCounterpartySignature ? "Counterparty: " : "Sponsor: ";
+                BEAST_EXPECT(ret.error().starts_with(rolePrefix));
+                BEAST_EXPECT(matches(ret.error().c_str(), "Invalid signature"));
+            }
+        }
+    }
+
+    // Multi-sign analogue of testRoleSignatureBinding. Both the top-level
+    // Signers array and a role slot's Signers array carry the same signer
+    // entry, signed under HashPrefix::TxMultiSign. Before the fix every role
+    // uses that same prefix, so the copied entry verifies in the role slot;
+    // after the fix the role slot verifies against CounterpartyTxMultiSign
+    // (CPM) or SponsorTxMultiSign (SPM) and the entry no longer matches.
+    void
+    testRoleMultiSignatureBinding()
+    {
+        testcase("role multi-signature binding");
+
+        RulesFixture const r;
+
+        auto const acctKp = generateKeyPair(KeyType::Secp256k1, generateSeed("masterpassphrase"));
+        auto const account = calcAccountID(acctKp.first);
+        // The signer must differ from the top-level account so that the
+        // multiSignHelper "account owner may not multisign for themselves"
+        // check passes for the top-level Signers array.
+        auto const signerKp = generateKeyPair(KeyType::Secp256k1, generateSeed("multisigner"));
+        auto const signerId = calcAccountID(signerKp.first);
+
+        auto makeCopiedMultiSig = [&](SField const& sigField) {
+            bool const counterparty = sigField == sfCounterpartySignature;
+            STTx const tx(counterparty ? ttLOAN_SET : ttACCOUNT_SET, [&](auto& obj) {
+                obj.setAccountID(sfAccount, account);
+                obj.setFieldAmount(sfFee, STAmount(10ull));
+                obj.setFieldU32(sfSequence, 1);
+                // Empty SigningPubKey selects the multi-sign path.
+                obj.setFieldVL(sfSigningPubKey, Slice{});
+                if (counterparty)
+                {
+                    obj.setFieldH256(sfLoanBrokerID, uint256{1});
+                    obj.setFieldNumber(
+                        sfPrincipalRequested, STNumber{sfPrincipalRequested, Number{1}});
+                }
+                else
+                {
+                    obj.setAccountID(sfSponsor, account);
+                    obj.setFieldU32(sfSponsorFlags, 0);
+                }
+            });
+
+            // Sign the top-level tx with TxMultiSign, which is what a
+            // multi-signer of the transaction itself would use.
+            Serializer const ss = buildMultiSigningData(tx, signerId, HashPrefix::TxMultiSign);
+            auto const sig = xrpl::sign(signerKp.first, signerKp.second, ss.slice());
+
+            STObject entry(sfSigner);
+            entry.setAccountID(sfAccount, signerId);
+            entry.setFieldVL(sfSigningPubKey, signerKp.first.slice());
+            entry.setFieldVL(sfTxnSignature, sig);
+            STArray signers(sfSigners, 1);
+            signers.pushBack(entry);
+
+            // Attach the Signers array to the top level so its own signature
+            // check succeeds, then copy the identical array into the role
+            // slot. Both arrays carry TxMultiSign-based signatures.
+            // NOLINTNEXTLINE(cppcoreguidelines-slicing)
+            STObject copy{tx};
+            copy.setFieldArray(sfSigners, signers);
+
+            STObject sigObject(sigField);
+            sigObject.setFieldVL(sfSigningPubKey, Slice{});
+            sigObject.setFieldArray(sfSigners, signers);
+            copy.setFieldObject(sigField, sigObject);
+            return STTx{std::move(copy)};
+        };
+
+        for (SField const& sigField :
+             {std::cref(sfCounterpartySignature), std::cref(sfSponsorSignature)})
+        {
+            auto const tx = makeCopiedMultiSig(sigField);
+
+            // Before the fix, both signature checks use TxMultiSign, so the
+            // copied Signers array verifies in the role slot as well.
+            BEAST_EXPECT(tx.checkSign(r.legacy));
+
+            // With the fix, the role slot uses its own multi-sign prefix
+            // (CPM or SPM) and the copied signature no longer verifies. The
+            // error must name the role that failed.
+            auto const ret = tx.checkSign(r.fixed);
+            BEAST_EXPECT(!ret);
+            if (!ret)
+            {
+                char const* const rolePrefix =
+                    sigField == sfCounterpartySignature ? "Counterparty: " : "Sponsor: ";
+                BEAST_EXPECT(ret.error().starts_with(rolePrefix));
+                BEAST_EXPECT(matches(ret.error().c_str(), "Invalid signature"));
+            }
+        }
     }
 
     void
@@ -1555,7 +1835,7 @@ public:
         auto const id2 = calcAccountID(kp2.first);
 
         // Get the stream of the transaction for use in multi-signing.
-        Serializer const s = buildMultiSigningData(txn, id2);
+        Serializer const s = buildMultiSigningData(txn, id2, HashPrefix::TxMultiSign);
 
         auto const saMultiSignature = sign(kp2.first, kp2.second, s.slice());
 
diff --git a/src/test/rpc/AccountLines_test.cpp b/src/test/rpc/AccountLines_test.cpp
index cb20de9bf5..3de2bdefa3 100644
--- a/src/test/rpc/AccountLines_test.cpp
+++ b/src/test/rpc/AccountLines_test.cpp
@@ -94,6 +94,24 @@ public:
         LedgerHeader const ledger3Info = env.closed()->header();
         BEAST_EXPECT(ledger3Info.seq == 3);
 
+        {
+            // test peer non-string
+            auto testInvalidPeerParam = [&](auto const& param) {
+                json::Value params;
+                params[jss::account] = alice.human();
+                params[jss::peer] = param;
+                auto jrr = env.rpc("json", "account_lines", to_string(params))[jss::result];
+                BEAST_EXPECT(jrr[jss::error] == "invalidParams");
+                BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'peer'.");
+            };
+
+            testInvalidPeerParam(1);
+            testInvalidPeerParam(1.1);
+            testInvalidPeerParam(true);
+            testInvalidPeerParam(json::Value(json::ValueType::Null));
+            testInvalidPeerParam(json::Value(json::ValueType::Object));
+            testInvalidPeerParam(json::Value(json::ValueType::Array));
+        }
         {
             // alice is funded but has no lines.  An empty array is returned.
             json::Value params;
@@ -775,6 +793,35 @@ public:
         LedgerHeader const ledger3Info = env.closed()->header();
         BEAST_EXPECT(ledger3Info.seq == 3);
 
+        {
+            // test peer non-string
+            auto testInvalidPeerParam = [&](auto const& param) {
+                json::Value params;
+                params[jss::account] = alice.human();
+                params[jss::peer] = param;
+
+                json::Value request;
+                request[jss::method] = "account_lines";
+                request[jss::jsonrpc] = "2.0";
+                request[jss::ripplerpc] = "2.0";
+                request[jss::id] = 5;
+                request[jss::params] = params;
+
+                auto const lines = env.rpc("json2", to_string(request));
+                BEAST_EXPECT(lines[jss::error][jss::error] == "invalidParams");
+                BEAST_EXPECT(lines[jss::error][jss::message] == "Invalid field 'peer'.");
+                BEAST_EXPECT(lines.isMember(jss::jsonrpc) && lines[jss::jsonrpc] == "2.0");
+                BEAST_EXPECT(lines.isMember(jss::ripplerpc) && lines[jss::ripplerpc] == "2.0");
+                BEAST_EXPECT(lines.isMember(jss::id) && lines[jss::id] == 5);
+            };
+
+            testInvalidPeerParam(1);
+            testInvalidPeerParam(1.1);
+            testInvalidPeerParam(true);
+            testInvalidPeerParam(json::Value(json::ValueType::Null));
+            testInvalidPeerParam(json::Value(json::ValueType::Object));
+            testInvalidPeerParam(json::Value(json::ValueType::Array));
+        }
         {
             // alice is funded but has no lines.  An empty array is returned.
             json::Value params;
diff --git a/src/test/rpc/BookChanges_test.cpp b/src/test/rpc/BookChanges_test.cpp
index 98a9372982..f0b4a4e187 100644
--- a/src/test/rpc/BookChanges_test.cpp
+++ b/src/test/rpc/BookChanges_test.cpp
@@ -1,3 +1,4 @@
+#include 
 #include 
 #include 
 #include 
@@ -8,13 +9,33 @@
 #include 
 #include 
 
+#include 
+
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
 namespace xrpl::test {
 
 class BookChanges_test : public beast::unit_test::Suite
@@ -115,6 +136,195 @@ public:
         BEAST_EXPECT(jrr[jss::changes][0u][jss::domain].asString() == to_string(domainID));
     }
 
+    void
+    testSkipsOverflowingRate()
+    {
+        testcase("book_changes skips overflowing rate");
+        using namespace jtx;
+
+        Env env(*this);
+        Account const gw{"gw"};
+        Account const iouGw{"iouGw"};
+
+        auto const big = MPT{gw.id(), 1};
+        auto const usd = iouGw["USD"];
+
+        // This metadata represents a partial MPT/IOU offer fill whose deltas
+        // make divide(deltaGets, deltaPays) overflow before MPTokensV2 skips
+        // the unrepresentable book-change rate.
+        STObject finalFields = STObject::makeInnerObject(sfFinalFields);
+        finalFields.setFieldU32(sfSequence, 1);
+        finalFields.setFieldAmount(sfTakerGets, big(1'800'000'000'000'000'000ull));
+        finalFields.setFieldAmount(sfTakerPays, usd(9));
+
+        STObject previousFields = STObject::makeInnerObject(sfPreviousFields);
+        previousFields.setFieldU32(sfSequence, 1);
+        previousFields.setFieldAmount(sfTakerGets, big(3'600'000'000'000'000'000ull));
+        previousFields.setFieldAmount(sfTakerPays, usd(18));
+
+        STObject modifiedOffer{sfModifiedNode};
+        modifiedOffer.setFieldU16(sfLedgerEntryType, ltOFFER);
+        modifiedOffer.setFieldObject(sfFinalFields, finalFields);
+        modifiedOffer.setFieldObject(sfPreviousFields, previousFields);
+
+        STArray affectedNodes{sfAffectedNodes};
+        affectedNodes.pushBack(std::move(modifiedOffer));
+
+        auto metadata = std::make_shared(sfTransactionMetaData);
+        metadata->setFieldArray(sfAffectedNodes, affectedNodes);
+
+        auto tx = std::make_shared(ttOFFER_CREATE, [](STObject&) {});
+
+        auto const test = [&](std::unordered_set> const& features) {
+            auto ledger = std::make_shared(
+                2,
+                NetClock::time_point{},
+                Rules{features},
+                env.current()->fees(),
+                env.app().getNodeFamily());
+
+            auto txSerializer = std::make_shared();
+            tx->add(*txSerializer);
+
+            auto metaSerializer = std::make_shared();
+            metadata->add(*metaSerializer);
+
+            ledger->rawTxInsert(uint256{1}, txSerializer, metaSerializer);
+            ledger->setImmutable();
+            ledger->setValidated();
+
+            try
+            {
+                auto const result =
+                    xrpl::rpc::computeBookChanges(std::static_pointer_cast(ledger));
+                BEAST_EXPECT(result[jss::type] == "bookChanges");
+                BEAST_EXPECT(result[jss::changes].size() == 0);
+            }
+            catch (std::overflow_error const&)
+            {
+                fail("Overflowing book-change rate shouldn't throw");
+            }
+        };
+
+        test(std::unordered_set>{});
+        test(std::unordered_set>{featureMPTokensV2});
+    }
+
+    // Build a ledger whose transactions are OfferCreates carrying the supplied
+    // consumed-offer deltas, then run computeBookChanges over it. Each pair is
+    // (TakerGets, TakerPays) fully consumed off a resting offer.
+    static json::Value
+    bookChangesFor(jtx::Env& env, std::vector> const& crossings)
+    {
+        auto ledger = std::make_shared(
+            2,
+            NetClock::time_point{},
+            Rules{std::unordered_set>{featureMPTokensV2}},
+            env.current()->fees(),
+            env.app().getNodeFamily());
+
+        std::uint32_t seq = 0;
+        for (auto const& [gets, pays] : crossings)
+        {
+            ++seq;
+
+            STObject finalFields = STObject::makeInnerObject(sfFinalFields);
+            finalFields.setFieldU32(sfSequence, seq);
+            finalFields.setFieldAmount(sfTakerGets, STAmount{gets.asset()});
+            finalFields.setFieldAmount(sfTakerPays, STAmount{pays.asset()});
+
+            STObject previousFields = STObject::makeInnerObject(sfPreviousFields);
+            previousFields.setFieldU32(sfSequence, seq);
+            previousFields.setFieldAmount(sfTakerGets, gets);
+            previousFields.setFieldAmount(sfTakerPays, pays);
+
+            STObject modifiedOffer{sfModifiedNode};
+            modifiedOffer.setFieldU16(sfLedgerEntryType, ltOFFER);
+            modifiedOffer.setFieldObject(sfFinalFields, finalFields);
+            modifiedOffer.setFieldObject(sfPreviousFields, previousFields);
+
+            STArray affectedNodes{sfAffectedNodes};
+            affectedNodes.pushBack(std::move(modifiedOffer));
+
+            auto metadata = std::make_shared(sfTransactionMetaData);
+            metadata->setFieldArray(sfAffectedNodes, affectedNodes);
+
+            STTx const tx{ttOFFER_CREATE, [](STObject&) {}};
+
+            auto txSerializer = std::make_shared();
+            tx.add(*txSerializer);
+
+            auto metaSerializer = std::make_shared();
+            metadata->add(*metaSerializer);
+
+            ledger->rawTxInsert(uint256{seq}, txSerializer, metaSerializer);
+        }
+
+        ledger->setImmutable();
+        ledger->setValidated();
+
+        return xrpl::rpc::computeBookChanges(std::static_pointer_cast(ledger));
+    }
+
+    void
+    testSkipsOverflowingVolume()
+    {
+        testcase("book_changes skips overflowing volume");
+        using namespace jtx;
+
+        Env env(*this);
+
+        // Two crossings in one book, accumulated by the `+=` in the tally's
+        // else branch. The rate is 1 either way, so the divide() guard is not
+        // what is under test here.
+        //
+        // MPT: kMaxMpTokenAmount is INT64_MAX, so two halves sum past it. The
+        // add is a raw int64 add, which wraps to a negative amount rather than
+        // throwing, and canonicalize() only bounds the magnitude -- so before
+        // the fix this reported a negative volume.
+        {
+            auto const mptA = MPT{Account{"gw"}.id(), 1};
+            auto const mptB = MPT{Account{"gw"}.id(), 2};
+            auto const half = 5'000'000'000'000'000'000ull;  // 2 * half > INT64_MAX
+
+            auto const result =
+                bookChangesFor(env, {{mptA(half), mptB(half)}, {mptA(half), mptB(half)}});
+
+            BEAST_EXPECT(result[jss::type] == "bookChanges");
+            if (BEAST_EXPECT(result[jss::changes].size() == 1))
+            {
+                auto const& change = result[jss::changes][0u];
+                // The second crossing is dropped, so the first one's volume
+                // stands. Above all it must not be negative.
+                BEAST_EXPECT(change[jss::volume_a].asString() == std::to_string(half));
+                BEAST_EXPECT(change[jss::volume_b].asString() == std::to_string(half));
+            }
+        }
+
+        // IOU: the addition throws std::overflow_error once the summed
+        // exponent passes IOUAmount::kMaxExponent. Before the fix that
+        // escaped computeBookChanges entirely.
+        {
+            Account const gwA{"gwA"};
+            Account const gwB{"gwB"};
+            // Mantissa in range, exponent at the maximum: two of these sum to
+            // one exponent past it.
+            STAmount const bigA{gwA["USD"].issue(), UINT64_C(9'000'000'000'000'000), 80};
+            STAmount const bigB{gwB["EUR"].issue(), UINT64_C(9'000'000'000'000'000), 80};
+
+            try
+            {
+                auto const result = bookChangesFor(env, {{bigA, bigB}, {bigA, bigB}});
+                BEAST_EXPECT(result[jss::type] == "bookChanges");
+                BEAST_EXPECT(result[jss::changes].size() == 1);
+            }
+            catch (std::overflow_error const&)
+            {
+                fail("Overflowing book-change volume shouldn't throw");
+            }
+        }
+    }
+
     void
     run() override
     {
@@ -122,6 +332,8 @@ public:
         testLedgerInputDefaultBehavior();
 
         testDomainOffer();
+        testSkipsOverflowingRate();
+        testSkipsOverflowingVolume();
         // Note: Other aspects of the book_changes rpc are fertile grounds
         // for unit-testing purposes. It can be included in future work
     }
diff --git a/src/test/rpc/GatewayBalances_test.cpp b/src/test/rpc/GatewayBalances_test.cpp
index 106b9b5f1a..91d9126f61 100644
--- a/src/test/rpc/GatewayBalances_test.cpp
+++ b/src/test/rpc/GatewayBalances_test.cpp
@@ -176,6 +176,45 @@ public:
         });
     }
 
+    void
+    testGWBInvalidAccount(FeatureBitset features)
+    {
+        testcase("Gateway Balances with non-string account/ident");
+        using namespace std::chrono_literals;
+        using namespace jtx;
+        Env env(*this, features);
+
+        Account const alice{"alice"};
+        env.fund(XRP(10000), alice);
+        env.close();
+
+        auto wsc = makeWSClient(env.app().config());
+
+        // A non-string "account" must be rejected cleanly with invalidParams
+        // rather than throwing a Json::LogicError that surfaces as internal.
+        json::Value qry;
+        qry[jss::account] = 42;
+        qry[jss::hotwallet] = alice.human();
+
+        forAllApiVersions([&, this](unsigned apiVersion) {
+            qry[jss::api_version] = apiVersion;
+            auto jv = wsc->invoke("gateway_balances", qry);
+            expect(jv[jss::status] == "error");
+            BEAST_EXPECT(jv[jss::result][jss::error] == "invalidParams");
+        });
+
+        // The same applies to a non-string "ident".
+        json::Value qry2;
+        qry2[jss::ident] = 42;
+
+        forAllApiVersions([&, this](unsigned apiVersion) {
+            qry2[jss::api_version] = apiVersion;
+            auto jv = wsc->invoke("gateway_balances", qry2);
+            expect(jv[jss::status] == "error");
+            BEAST_EXPECT(jv[jss::result][jss::error] == "invalidParams");
+        });
+    }
+
     void
     testGWBOverflow()
     {
@@ -280,6 +319,7 @@ public:
         {
             testGWB(feature);
             testGWBApiVersions(feature);
+            testGWBInvalidAccount(feature);
         }
         testGWBWithMPT();
         testGWBOverflow();
diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp
index af93108ff2..e7c5dd4a80 100644
--- a/src/test/rpc/LedgerRPC_test.cpp
+++ b/src/test/rpc/LedgerRPC_test.cpp
@@ -5,6 +5,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -20,6 +21,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
@@ -258,6 +260,102 @@ class LedgerRPC_test : public beast::unit_test::Suite
         BEAST_EXPECT(jrr[jss::ledger][jss::accountState].size() == 3u);
     }
 
+    void
+    testLedgerOwnerFundsMPTOffer()
+    {
+        testcase("Ledger owner_funds with MPT offer");
+        using namespace test::jtx;
+
+        Env env{*this};
+        Account const gw{"gateway"};
+        Account const alice{"alice"};
+        auto const usd = gw["USD"];
+
+        env.fund(XRP(10'000), gw, alice);
+        env.close();
+        env.trust(usd(1'000), alice);
+        env(pay(gw, alice, usd(100)));
+        MPTTester mpt(
+            {.env = env,
+             .issuer = gw,
+             .holders = {alice},
+             .pay = 100,
+             .flags = tfMPTRequireAuth | kMptDexFlags,
+             .authHolder = true,
+             .close = false});
+        MPT const mptAsset = mpt;
+        env.close();
+
+        env(noop(alice));
+        // These offers differ only by TakerGets asset type. Omitting
+        // owner_funds serializes the tx JSON without computing offer balances;
+        // owner_funds=true asks LedgerToJson to compute accountFunds(TakerGets)
+        // for both offers, which is where IOU and MPT used to diverge.
+        env(offer(alice, XRP(10), usd(10)));
+        env(offer(alice, XRP(10), mptAsset(10)));
+        // The MPT offer was created while authorized. Unauthorizing in the
+        // same ledger makes owner_funds depend on AuthHandling::IgnoreAuth.
+        mpt.authorize({.account = gw, .holder = alice, .flags = tfMPTUnauthorize});
+        env(noop(alice));
+        env.close();
+
+        auto const ledgerHash = to_string(env.closed()->header().hash);
+
+        auto const getTransactions = [&](bool includeOwnerFunds) {
+            json::Value params;
+            params[jss::ledger_hash] = ledgerHash;
+            params[jss::transactions] = true;
+            params[jss::expand] = true;
+            // The baseline omits owner_funds, which the RPC treats as false.
+            // Setting it true requests the same ledger, but asks the ledger
+            // serializer to add owner_funds to offer transactions in that
+            // ledger's transaction array.
+            if (includeOwnerFunds)
+                params[jss::owner_funds] = true;
+
+            auto const result = env.rpc("json", "ledger", to_string(params))[jss::result];
+            BEAST_EXPECT(!result.isMember(jss::error));
+            BEAST_EXPECT(result[jss::ledger][jss::transactions].isArray());
+            return result[jss::ledger][jss::transactions];
+        };
+
+        auto const findOffer = [](json::Value const& txs, bool mpt) -> json::Value const* {
+            for (auto i = 0u; i < txs.size(); ++i)
+            {
+                auto const& tx = txs[i].isMember(jss::tx_json) ? txs[i][jss::tx_json] : txs[i];
+                if (tx[jss::TransactionType] == jss::OfferCreate &&
+                    tx[jss::TakerGets].isMember(jss::mpt_issuance_id) == mpt)
+                {
+                    return &txs[i];
+                }
+            }
+            return nullptr;
+        };
+
+        // Baseline: same ledger request without owner_funds fields.
+        auto const baseline = getTransactions(false);
+        BEAST_EXPECT(baseline.size() == 5u);
+        BEAST_EXPECT(findOffer(baseline, false) != nullptr);
+        BEAST_EXPECT(findOffer(baseline, true) != nullptr);
+
+        // Same ledger request with owner_funds added to eligible offer txs.
+        auto const withOwnerFunds = getTransactions(true);
+        // Requesting owner_funds must not change which ledger transactions are
+        // returned, even when one offer's TakerGets is MPT.
+        BEAST_EXPECT(withOwnerFunds.size() == baseline.size());
+
+        // The IOU offer is the control case for expected owner_funds output.
+        auto const* iouOfferTx = findOffer(withOwnerFunds, false);
+        if (BEAST_EXPECT(iouOfferTx != nullptr))
+            BEAST_EXPECT((*iouOfferTx)[jss::owner_funds] == "100");
+
+        // MPT owner_funds should match the IOU behavior, even though Alice is
+        // unauthorized by the ledger snapshot used for serialization.
+        auto const* mptOfferTx = findOffer(withOwnerFunds, true);
+        if (BEAST_EXPECT(mptOfferTx != nullptr))
+            BEAST_EXPECT((*mptOfferTx)[jss::owner_funds] == "100");
+    }
+
     /**
      * @brief ledger RPC requests as a way to drive
      * input options to lookupLedger. The point of this test is
@@ -719,6 +817,7 @@ public:
         testLedgerFull();
         testLedgerFullNonAdmin();
         testLedgerAccounts();
+        testLedgerOwnerFundsMPTOffer();
         testLookupLedger();
         testNoQueue();
         testQueue();
diff --git a/src/test/rpc/NoRippleCheck_test.cpp b/src/test/rpc/NoRippleCheck_test.cpp
index 6e30f944c7..8e719e6407 100644
--- a/src/test/rpc/NoRippleCheck_test.cpp
+++ b/src/test/rpc/NoRippleCheck_test.cpp
@@ -27,8 +27,6 @@
 #include 
 #include 
 
-#include 
-
 #include 
 #include 
 
@@ -203,13 +201,13 @@ class NoRippleCheck_test : public beast::unit_test::Suite
 
             if (user)
             {
-                BEAST_EXPECT(boost::starts_with(pa[0u].asString(), "You appear to have set"));
-                BEAST_EXPECT(boost::starts_with(pa[1u].asString(), "You should probably set"));
+                BEAST_EXPECT(pa[0u].asString().starts_with("You appear to have set"));
+                BEAST_EXPECT(pa[1u].asString().starts_with("You should probably set"));
             }
             else
             {
-                BEAST_EXPECT(boost::starts_with(pa[0u].asString(), "You should immediately set"));
-                BEAST_EXPECT(boost::starts_with(pa[1u].asString(), "You should clear"));
+                BEAST_EXPECT(pa[0u].asString().starts_with("You should immediately set"));
+                BEAST_EXPECT(pa[1u].asString().starts_with("You should clear"));
             }
         }
         else
diff --git a/src/test/rpc/ServerInfo_test.cpp b/src/test/rpc/ServerInfo_test.cpp
index 52a1e6cdb0..100ae0e49b 100644
--- a/src/test/rpc/ServerInfo_test.cpp
+++ b/src/test/rpc/ServerInfo_test.cpp
@@ -9,8 +9,7 @@
 #include 
 #include 
 
-#include 
-
+#include 
 #include 
 
 namespace xrpl::test {
@@ -36,12 +35,13 @@ public:
     makeValidatorConfig()
     {
         auto p = std::make_unique();
-        boost::format toLoad(R"xrpldConfig(
+        auto const toLoad = std::format(
+            R"xrpldConfig(
 [validator_token]
-%1%
+{}
 
 [validators]
-%2%
+{}
 
 [port_grpc]
 ip = 0.0.0.0
@@ -52,9 +52,11 @@ ip = 0.0.0.0
 port = 50052
 protocol = wss2
 admin = 127.0.0.1
-)xrpldConfig");
+)xrpldConfig",
+            validator_data::kToken,
+            validator_data::kPublicKey);
 
-        p->loadFromString(boost::str(toLoad % validator_data::kToken % validator_data::kPublicKey));
+        p->loadFromString(toLoad);
 
         setupConfigForUnitTests(*p);
 
diff --git a/src/test/server/ServerStatus_test.cpp b/src/test/server/ServerStatus_test.cpp
index 5adf6a08f5..f1989ed171 100644
--- a/src/test/server/ServerStatus_test.cpp
+++ b/src/test/server/ServerStatus_test.cpp
@@ -56,8 +56,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
     static auto
     makeConfig(std::string const& proto, bool admin = true, bool credentials = false)
     {
-        auto const sectionName =
-            boost::starts_with(proto, "h") ? Sections::kPortRpc : Sections::kPortWs;
+        auto const sectionName = proto.starts_with("h") ? Sections::kPortRpc : Sections::kPortWs;
         auto p = jtx::envconfig();
 
         p->overwrite(sectionName, Keys::kProtocol, proto);
@@ -71,9 +70,9 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
         }
 
         p->overwrite(
-            boost::starts_with(proto, "h") ? Sections::kPortWs : Sections::kPortRpc,
+            proto.starts_with("h") ? Sections::kPortWs : Sections::kPortRpc,
             Keys::kProtocol,
-            boost::starts_with(proto, "h") ? "ws" : "http");
+            proto.starts_with("h") ? "ws" : "http");
 
         if (proto == "https")
         {
@@ -261,7 +260,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
             }
         }
 
-        if (boost::starts_with(proto, "h"))
+        if (proto.starts_with("h"))
         {
             auto jrc = makeJSONRPCClient(env.app().config());
             jrr = jrc->invoke("ledger_accept", jp);
@@ -289,7 +288,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
         Env env{*this, makeConfig(proto, admin, credentials)};
 
         json::Value jrr;
-        auto const protoWs = boost::starts_with(proto, "w");
+        auto const protoWs = proto.starts_with("w");
 
         // the set of checks we do are different depending
         // on how the admin config options are set
@@ -485,7 +484,7 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
 
         boost::beast::http::response resp;
         boost::system::error_code ec;
-        if (boost::starts_with(clientProtocol, "h"))
+        if (clientProtocol.starts_with("h"))
         {
             doHTTPRequest(env, yield, clientProtocol == "https", resp, ec);
             BEAST_EXPECT(ec);
diff --git a/src/test/unit_test/FileDirGuard.h b/src/test/unit_test/FileDirGuard.h
index b583f821a4..2e6b3fd179 100644
--- a/src/test/unit_test/FileDirGuard.h
+++ b/src/test/unit_test/FileDirGuard.h
@@ -3,9 +3,8 @@
 #include 
 #include 
 
-#include 
-
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -20,7 +19,7 @@ namespace xrpl::detail {
 class DirGuard
 {
 protected:
-    using path = boost::filesystem::path;
+    using path = std::filesystem::path;
 
 private:
     path subDir_;
@@ -47,7 +46,7 @@ public:
     DirGuard(beast::unit_test::Suite& test, path subDir, bool useCounter = true)
         : subDir_(std::move(subDir)), test_(test)
     {
-        using namespace boost::filesystem;
+        using namespace std::filesystem;
 
         static auto kSubDirCounter = 0;
         if (useCounter)
@@ -73,7 +72,7 @@ public:
     {
         try
         {
-            using namespace boost::filesystem;
+            using namespace std::filesystem;
 
             if (rmSubDir_)
                 rmDir(subDir_);
@@ -130,7 +129,7 @@ public:
     {
         try
         {
-            using namespace boost::filesystem;
+            using namespace std::filesystem;
             if (exists(file_))
             {
                 remove(file_);
@@ -160,7 +159,7 @@ public:
     [[nodiscard]] bool
     fileExists() const
     {
-        return boost::filesystem::exists(file_);
+        return std::filesystem::exists(file_);
     }
 };
 
diff --git a/src/test/unit_test/multi_runner.cpp b/src/test/unit_test/multi_runner.cpp
index 71208313a4..918fc7c89f 100644
--- a/src/test/unit_test/multi_runner.cpp
+++ b/src/test/unit_test/multi_runner.cpp
@@ -7,7 +7,6 @@
 #include 
 #include 
 #include 
-#include 
 
 #include 
 #include 
@@ -36,7 +35,7 @@ fmtdur(typename clock_type::duration const& d)
     using namespace std::chrono;
     auto const ms = duration_cast(d);
     if (ms < seconds{1})
-        return boost::lexical_cast(ms.count()) + "ms";
+        return std::to_string(ms.count()) + "ms";
     std::stringstream ss;
     ss << std::fixed << std::setprecision(1) << (ms.count() / 1000.) << "s";
     return ss.str();
diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt
index 5e4cda243a..f8c53e02bd 100644
--- a/src/tests/libxrpl/CMakeLists.txt
+++ b/src/tests/libxrpl/CMakeLists.txt
@@ -5,15 +5,24 @@ include(verify_headers)
 # Test requirements.
 find_package(GTest REQUIRED)
 
-# Single combined gtest binary built from the shared test helpers and all test
-# modules below.
-add_executable(
-    xrpl_tests
-    main.cpp
+add_library(
+    xrpl.testkit.wasm
+    STATIC
     helpers/Account.cpp
     helpers/TestSink.cpp
     helpers/TxTest.cpp
+    tx/wasm/fixtures/NftSetup.cpp
+    tx/wasm/fixtures/WasmLedger.cpp
+    tx/wasm/fixtures/WasmRun.cpp
 )
+target_include_directories(xrpl.testkit.wasm PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+target_link_libraries(
+    xrpl.testkit.wasm
+    PUBLIC xrpl.libxrpl xrpl_wasm_testkit_cxxbridge
+)
+add_dependencies(xrpl.testkit.wasm xrpl_crates)
+
+add_executable(xrpl_tests main.cpp)
 patch_nix_binary(xrpl_tests)
 set_target_properties(
     xrpl_tests
@@ -21,7 +30,10 @@ set_target_properties(
 )
 # Lets test sources include the shared helpers as .
 target_include_directories(xrpl_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
-target_link_libraries(xrpl_tests PRIVATE GTest::gtest GTest::gmock xrpl.libxrpl)
+target_link_libraries(
+    xrpl_tests
+    PRIVATE GTest::gtest GTest::gmock xrpl.libxrpl xrpl.testkit.wasm
+)
 
 # One source subdirectory per module. Network unit tests are currently not
 # supported on Windows.
@@ -52,6 +64,13 @@ foreach(module IN LISTS test_modules)
         "${CMAKE_CURRENT_SOURCE_DIR}/${module}/*.cpp"
         "${CMAKE_CURRENT_SOURCE_DIR}/${module}.cpp"
     )
+    # The framework-free half of tx/wasm/fixtures/ is compiled into xrpl.testkit.wasm,
+    # which this binary links; the rest of that folder belongs here.
+    list(
+        FILTER sources
+        EXCLUDE
+        REGEX "/fixtures/(NftSetup|WasmLedger|WasmRun)\\.cpp$"
+    )
     target_sources(xrpl_tests PRIVATE ${sources})
 
     # Expose the module's private headers under their canonical include path.
diff --git a/src/tests/libxrpl/basics/Buffer.cpp b/src/tests/libxrpl/basics/Buffer.cpp
index 9cdf610282..a3f78e8bcf 100644
--- a/src/tests/libxrpl/basics/Buffer.cpp
+++ b/src/tests/libxrpl/basics/Buffer.cpp
@@ -4,6 +4,7 @@
 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -12,8 +13,18 @@
 
 namespace xrpl::test {
 
+static_assert(std::is_nothrow_move_constructible_v);
+static_assert(std::is_nothrow_move_assignable_v);
+
 struct BufferTest : public ::testing::Test
 {
+    static constexpr auto kRandomData = std::to_array(
+        {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, 0x71, 0x6d, 0x2a,
+         0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c,
+         0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3});
+
+    static constexpr std::size_t kHalf = kRandomData.size() / 2;
+
     static bool
     sane(Buffer const& b)
     {
@@ -22,239 +33,321 @@ struct BufferTest : public ::testing::Test
 
         return b.data() != nullptr;
     }
+
+    /**
+     * Check the state Buffer documents for a moved-from buffer: "the other buffer is reset", i.e.
+     * empty and sane.
+     *
+     * Zeroing the size is not incidental tidiness. Moving the member unique_ptr nulls the data
+     * pointer whether Buffer wants it or not, so a moved-from buffer that kept its old size would
+     * lie about itself everywhere: alloc() would take its `n == size_` early-out and hand back a
+     * null pointer while still reporting the old size, fill() would run std::fill_n over a null
+     * pointer, and the Slice conversion would publish {nullptr, oldSize} to callers. A moved-from
+     * Buffer has to be a usable empty Buffer rather than a landmine, which is why the tests below
+     * assert this state instead of treating a moved-from buffer as untouchable.
+     */
+    static void
+    checkEmptyAfterMove(Buffer const& buf)
+    {
+        EXPECT_TRUE(sane(buf));
+        EXPECT_TRUE(buf.empty());
+    }
+
+    Buffer const emptyBuffer;
+    Buffer const firstHalf{kRandomData.data(), kHalf};
+    Buffer const secondHalf{kRandomData.data() + kHalf, kHalf};
+    Buffer const whole{kRandomData.data(), kRandomData.size()};
 };
 
-TEST_F(BufferTest, buffer)
+TEST_F(BufferTest, default_constructed_is_empty)
 {
-    std::uint8_t const data[] = {0xa8, 0xa1, 0x38, 0x45, 0x23, 0xec, 0xe4, 0x23, 0x71, 0x6d, 0x2a,
-                                 0x18, 0xb4, 0x70, 0xcb, 0xf5, 0xac, 0x2d, 0x89, 0x4d, 0x19, 0x9c,
-                                 0xf0, 0x2c, 0x15, 0xd1, 0xf9, 0x9b, 0x66, 0xd2, 0x30, 0xd3};
+    Buffer const b;
 
-    Buffer const b0;
-    EXPECT_TRUE(sane(b0));
-    EXPECT_TRUE(b0.empty());
+    EXPECT_TRUE(sane(b));
+    EXPECT_TRUE(b.empty());
+    EXPECT_EQ(b.data(), nullptr);
+}
 
-    Buffer b1{0};
-    EXPECT_TRUE(sane(b1));
-    EXPECT_TRUE(b1.empty());
-    std::memcpy(b1.alloc(16), data, 16);
-    EXPECT_TRUE(sane(b1));
-    EXPECT_FALSE(b1.empty());
-    EXPECT_EQ(b1.size(), 16);
+TEST_F(BufferTest, zero_sized_construction_is_empty)
+{
+    Buffer const b{0};
 
-    Buffer b2{b1.size()};
-    EXPECT_TRUE(sane(b2));
-    EXPECT_FALSE(b2.empty());
-    EXPECT_EQ(b2.size(), b1.size());
-    std::memcpy(b2.data(), data + 16, 16);
+    EXPECT_TRUE(sane(b));
+    EXPECT_TRUE(b.empty());
+}
 
-    Buffer b3{data, sizeof(data)};
-    EXPECT_TRUE(sane(b3));
-    EXPECT_FALSE(b3.empty());
-    EXPECT_EQ(b3.size(), sizeof(data));
-    EXPECT_EQ(std::memcmp(b3.data(), data, b3.size()), 0);
+TEST_F(BufferTest, alloc_grows_an_empty_buffer)
+{
+    Buffer b{0};
+    std::memcpy(b.alloc(kHalf), kRandomData.data(), kHalf);
 
-    // Check equality and inequality comparisons.
-    // For code readability, we want to use general
-    // EXPECT_TRUE instead of specific EXPECT_EQ etc.
-    EXPECT_TRUE(b0 == b0);
-    EXPECT_TRUE(b0 != b1);
-    EXPECT_TRUE(b1 == b1);
-    EXPECT_TRUE(b1 != b2);
-    EXPECT_TRUE(b2 != b3);
+    EXPECT_TRUE(sane(b));
+    EXPECT_FALSE(b.empty());
+    EXPECT_EQ(b.size(), kHalf);
+    EXPECT_EQ(b, firstHalf);
+}
 
-    // Check copy constructors and copy assignments:
-    {
-        Buffer x{b0};
-        EXPECT_EQ(x, b0);
-        EXPECT_TRUE(sane(x));
-        Buffer y{b1};
-        EXPECT_EQ(y, b1);
-        EXPECT_TRUE(sane(y));
-        x = b2;
-        EXPECT_EQ(x, b2);
-        EXPECT_TRUE(sane(x));
-        x = y;
-        EXPECT_EQ(x, y);
-        EXPECT_TRUE(sane(x));
-        y = b3;
-        EXPECT_EQ(y, b3);
-        EXPECT_TRUE(sane(y));
-        x = b0;
-        EXPECT_EQ(x, b0);
-        EXPECT_TRUE(sane(x));
+TEST_F(BufferTest, sized_construction_reserves_without_filling)
+{
+    Buffer b{kHalf};
+
+    EXPECT_TRUE(sane(b));
+    EXPECT_FALSE(b.empty());
+    EXPECT_EQ(b.size(), kHalf);
+
+    std::memcpy(b.data(), kRandomData.data() + kHalf, kHalf);
+    EXPECT_EQ(b, secondHalf);
+}
+
+TEST_F(BufferTest, construction_copies_raw_memory)
+{
+    Buffer const b{kRandomData.data(), kRandomData.size()};
+
+    EXPECT_TRUE(sane(b));
+    EXPECT_FALSE(b.empty());
+    EXPECT_EQ(b.size(), kRandomData.size());
+    EXPECT_EQ(std::memcmp(b.data(), kRandomData.data(), b.size()), 0);
+}
+
+TEST_F(BufferTest, equality_compares_contents)
+{
+    // Uses EXPECT_TRUE rather than EXPECT_EQ/EXPECT_NE because the operators are what is under test
+    // here.
+    EXPECT_TRUE(emptyBuffer == emptyBuffer);
+    EXPECT_TRUE(firstHalf == firstHalf);
+
+    EXPECT_TRUE(emptyBuffer != firstHalf);
+    EXPECT_TRUE(firstHalf != secondHalf);
+    EXPECT_TRUE(secondHalf != whole);
+}
+
+TEST_F(BufferTest, copy_construction)
+{
+    Buffer const fromEmpty{emptyBuffer};
+    EXPECT_TRUE(sane(fromEmpty));
+    EXPECT_EQ(fromEmpty, emptyBuffer);
+
+    Buffer const fromNonEmpty{firstHalf};
+    EXPECT_TRUE(sane(fromNonEmpty));
+    EXPECT_EQ(fromNonEmpty, firstHalf);
+}
+
+TEST_F(BufferTest, copy_assignment)
+{
+    Buffer b{emptyBuffer};
+
+    // empty <- non-empty
+    b = secondHalf;
+    EXPECT_TRUE(sane(b));
+    EXPECT_EQ(b, secondHalf);
+
+    // non-empty <- non-empty of a different size
+    b = whole;
+    EXPECT_TRUE(sane(b));
+    EXPECT_EQ(b, whole);
+
+    // non-empty <- empty
+    b = emptyBuffer;
+    EXPECT_TRUE(sane(b));
+    EXPECT_EQ(b, emptyBuffer);
+}
+
+TEST_F(BufferTest, self_assignment_preserves_contents)
+{
 #ifdef __clang__
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wself-assign-overloaded"
 #endif
 
-        x = x;
-        EXPECT_EQ(x, b0);
-        EXPECT_TRUE(sane(x));
-        y = y;
-        EXPECT_EQ(y, b3);
-        EXPECT_TRUE(sane(y));
+    Buffer emptyCopy{emptyBuffer};
+    emptyCopy = emptyCopy;
+    EXPECT_TRUE(sane(emptyCopy));
+    EXPECT_EQ(emptyCopy, emptyBuffer);
+
+    Buffer wholeCopy{whole};
+    wholeCopy = wholeCopy;
+    EXPECT_TRUE(sane(wholeCopy));
+    EXPECT_EQ(wholeCopy, whole);
 
 #ifdef __clang__
 #pragma clang diagnostic pop
 #endif
-    }
+}
 
-    // Check move constructor & move assignments:
+TEST_F(BufferTest, move_construct_from_empty)
+{
+    Buffer source;
+    Buffer const moved{std::move(source)};
+
+    checkEmptyAfterMove(source);  // NOLINT(bugprone-use-after-move)
+    EXPECT_TRUE(sane(moved));
+    EXPECT_TRUE(moved.empty());
+}
+
+TEST_F(BufferTest, move_construct_from_non_empty)
+{
+    Buffer source{firstHalf};
+    Buffer const moved{std::move(source)};
+
+    checkEmptyAfterMove(source);  // NOLINT(bugprone-use-after-move)
+    EXPECT_TRUE(sane(moved));
+    EXPECT_EQ(moved, firstHalf);
+}
+
+TEST_F(BufferTest, move_assign_empty_to_empty)
+{
+    Buffer target;
+    Buffer source;
+
+    target = std::move(source);
+
+    EXPECT_TRUE(sane(target));
+    EXPECT_TRUE(target.empty());
+    checkEmptyAfterMove(source);  // NOLINT(bugprone-use-after-move)
+}
+
+TEST_F(BufferTest, move_assign_non_empty_to_empty)
+{
+    Buffer target;
+    Buffer source{firstHalf};
+
+    target = std::move(source);
+
+    EXPECT_TRUE(sane(target));
+    EXPECT_EQ(target, firstHalf);
+    checkEmptyAfterMove(source);  // NOLINT(bugprone-use-after-move)
+}
+
+TEST_F(BufferTest, move_assign_empty_to_non_empty)
+{
+    Buffer target{firstHalf};
+    Buffer source;
+
+    target = std::move(source);
+
+    EXPECT_TRUE(sane(target));
+    EXPECT_TRUE(target.empty());
+    checkEmptyAfterMove(source);  // NOLINT(bugprone-use-after-move)
+}
+
+TEST_F(BufferTest, move_assign_non_empty_to_non_empty)
+{
+    Buffer target{firstHalf};
+    Buffer sameSize{secondHalf};
+    Buffer largerSize{whole};
+
+    target = std::move(sameSize);
+    EXPECT_TRUE(sane(target));
+    EXPECT_EQ(target, secondHalf);
+    checkEmptyAfterMove(sameSize);  // NOLINT(bugprone-use-after-move)
+
+    target = std::move(largerSize);
+    EXPECT_TRUE(sane(target));
+    EXPECT_EQ(target, whole);
+    checkEmptyAfterMove(largerSize);  // NOLINT(bugprone-use-after-move)
+}
+
+TEST_F(BufferTest, construction_from_slice)
+{
+    Buffer const fromEmpty{static_cast(emptyBuffer)};
+    EXPECT_TRUE(sane(fromEmpty));
+    EXPECT_EQ(fromEmpty, emptyBuffer);
+
+    Buffer const fromNonEmpty{static_cast(whole)};
+    EXPECT_TRUE(sane(fromNonEmpty));
+    EXPECT_EQ(fromNonEmpty, whole);
+}
+
+TEST_F(BufferTest, assignment_from_slice)
+{
+    Buffer b;
+
+    // empty <- empty slice
+    b = static_cast(emptyBuffer);
+    EXPECT_TRUE(sane(b));
+    EXPECT_EQ(b, emptyBuffer);
+
+    // empty <- non-empty slice
+    b = static_cast(firstHalf);
+    EXPECT_TRUE(sane(b));
+    EXPECT_EQ(b, firstHalf);
+
+    // non-empty <- non-empty slice
+    b = static_cast(secondHalf);
+    EXPECT_TRUE(sane(b));
+    EXPECT_EQ(b, secondHalf);
+
+    // non-empty <- empty slice
+    b = static_cast(emptyBuffer);
+    EXPECT_TRUE(sane(b));
+    EXPECT_EQ(b, emptyBuffer);
+}
+
+TEST_F(BufferTest, resize_allocates_and_clear_releases)
+{
+    auto check = [](Buffer const& original, std::size_t size) {
+        SCOPED_TRACE(::testing::Message() << "size: " << size);
+
+        Buffer b{original};
+
+        // Resizing to zero is equivalent to clearing.
+        b(size);
+        EXPECT_TRUE(sane(b));
+        EXPECT_EQ(b.size(), size);
+        EXPECT_EQ(b.data() == nullptr, size == 0);
+
+        b(size + 1);
+        EXPECT_TRUE(sane(b));
+        EXPECT_EQ(b.size(), size + 1);
+        EXPECT_NE(b.data(), nullptr);
+
+        b.clear();
+        EXPECT_TRUE(sane(b));
+        EXPECT_TRUE(b.empty());
+        EXPECT_EQ(b.data(), nullptr);
+
+        // clear() is idempotent.
+        b.clear();
+        EXPECT_TRUE(sane(b));
+        EXPECT_TRUE(b.empty());
+        EXPECT_EQ(b.data(), nullptr);
+    };
+
+    for (auto size = 0uz; size < kHalf; ++size)
     {
-        static_assert(std::is_nothrow_move_constructible_v);
-        static_assert(std::is_nothrow_move_assignable_v);
-
-        {  // Move-construct from empty buf
-            Buffer x;
-            Buffer const y{std::move(x)};
-            EXPECT_TRUE(sane(x));    // NOLINT(bugprone-use-after-move)
-            EXPECT_TRUE(x.empty());  // NOLINT(bugprone-use-after-move)
-            EXPECT_TRUE(sane(y));
-            EXPECT_TRUE(y.empty());
-            EXPECT_EQ(x, y);  // NOLINT(bugprone-use-after-move)
-        }
-
-        {  // Move-construct from non-empty buf
-            Buffer x{b1};
-            Buffer const y{std::move(x)};
-            EXPECT_TRUE(sane(x));    // NOLINT(bugprone-use-after-move)
-            EXPECT_TRUE(x.empty());  // NOLINT(bugprone-use-after-move)
-            EXPECT_TRUE(sane(y));
-            EXPECT_EQ(y, b1);
-        }
-
-        {  // Move assign empty buf to empty buf
-            Buffer x;
-            Buffer y;
-
-            x = std::move(y);
-            EXPECT_TRUE(sane(x));
-            EXPECT_TRUE(x.empty());
-            EXPECT_TRUE(sane(y));    // NOLINT(bugprone-use-after-move)
-            EXPECT_TRUE(y.empty());  // NOLINT(bugprone-use-after-move)
-        }
-
-        {  // Move assign non-empty buf to empty buf
-            Buffer x;
-            Buffer y{b1};
-
-            x = std::move(y);
-            EXPECT_TRUE(sane(x));
-            EXPECT_EQ(x, b1);
-            EXPECT_TRUE(sane(y));    // NOLINT(bugprone-use-after-move)
-            EXPECT_TRUE(y.empty());  // NOLINT(bugprone-use-after-move)
-        }
-
-        {  // Move assign empty buf to non-empty buf
-            Buffer x{b1};
-            Buffer y;
-
-            x = std::move(y);
-            EXPECT_TRUE(sane(x));
-            EXPECT_TRUE(x.empty());
-            EXPECT_TRUE(sane(y));    // NOLINT(bugprone-use-after-move)
-            EXPECT_TRUE(y.empty());  // NOLINT(bugprone-use-after-move)
-        }
-
-        {  // Move assign non-empty buf to non-empty buf
-            Buffer x{b1};
-            Buffer y{b2};
-            Buffer z{b3};
-
-            x = std::move(y);
-            EXPECT_TRUE(sane(x));
-            EXPECT_FALSE(x.empty());
-            EXPECT_TRUE(sane(y));    // NOLINT(bugprone-use-after-move)
-            EXPECT_TRUE(y.empty());  // NOLINT(bugprone-use-after-move)
-
-            x = std::move(z);
-            EXPECT_TRUE(sane(x));
-            EXPECT_FALSE(x.empty());
-            EXPECT_TRUE(sane(z));    // NOLINT(bugprone-use-after-move)
-            EXPECT_TRUE(z.empty());  // NOLINT(bugprone-use-after-move)
-        }
-    }
-
-    {
-        Buffer w{static_cast(b0)};
-        EXPECT_TRUE(sane(w));
-        EXPECT_EQ(w, b0);
-
-        Buffer x{static_cast(b1)};
-        EXPECT_TRUE(sane(x));
-        EXPECT_EQ(x, b1);
-
-        Buffer y{static_cast(b2)};
-        EXPECT_TRUE(sane(y));
-        EXPECT_EQ(y, b2);
-
-        Buffer z{static_cast(b3)};
-        EXPECT_TRUE(sane(z));
-        EXPECT_EQ(z, b3);
-
-        // Assign empty slice to empty buffer
-        w = static_cast(b0);
-        EXPECT_TRUE(sane(w));
-        EXPECT_EQ(w, b0);
-
-        // Assign non-empty slice to empty buffer
-        w = static_cast(b1);
-        EXPECT_TRUE(sane(w));
-        EXPECT_EQ(w, b1);
-
-        // Assign non-empty slice to non-empty buffer
-        x = static_cast(b2);
-        EXPECT_TRUE(sane(x));
-        EXPECT_EQ(x, b2);
-
-        // Assign non-empty slice to non-empty buffer
-        y = static_cast(z);
-        EXPECT_TRUE(sane(y));
-        EXPECT_EQ(y, z);
-
-        // Assign empty slice to non-empty buffer:
-        z = static_cast(b0);
-        EXPECT_TRUE(sane(z));
-        EXPECT_EQ(z, b0);
-    }
-
-    {
-        auto test = [](Buffer const& b, std::size_t i) {
-            Buffer x{b};
-
-            // Try to allocate some number of bytes, possibly
-            // zero (which means clear) and sanity check
-            x(i);
-            EXPECT_TRUE(sane(x));
-            EXPECT_EQ(x.size(), i);
-            EXPECT_EQ((x.data() == nullptr), (i == 0));
-
-            // Try to allocate some more data (always non-zero)
-            x(i + 1);
-            EXPECT_TRUE(sane(x));
-            EXPECT_EQ(x.size(), i + 1);
-            EXPECT_NE(x.data(), nullptr);
-
-            // Try to clear:
-            x.clear();
-            EXPECT_TRUE(sane(x));
-            EXPECT_TRUE(x.empty());
-            EXPECT_EQ(x.data(), nullptr);
-
-            // Try to clear again:
-            x.clear();
-            EXPECT_TRUE(sane(x));
-            EXPECT_TRUE(x.empty());
-            EXPECT_EQ(x.data(), nullptr);
-        };
-
-        for (std::size_t i = 0; i < 16; ++i)
-        {
-            test(b0, i);
-            test(b1, i);
-        }
+        check(emptyBuffer, size);
+        check(firstHalf, size);
     }
 }
 
+TEST_F(BufferTest, fill_sets_every_byte)
+{
+    Buffer b{4};
+    b.fill(0xab);
+
+    EXPECT_EQ(b.size(), 4);
+    for (auto const byte : Slice{b})
+        EXPECT_EQ(byte, 0xab);
+}
+
+TEST_F(BufferTest, fill_overwrites_and_keeps_size)
+{
+    Buffer b{4};
+    b.fill(0xab);
+    b.fill(0x00);
+
+    EXPECT_EQ(b.size(), 4);
+    for (auto const byte : Slice{b})
+        EXPECT_EQ(byte, 0x00);
+}
+
+TEST_F(BufferTest, fill_on_empty_buffer_is_a_noop)
+{
+    Buffer empty;
+    empty.fill(0xff);
+
+    EXPECT_TRUE(empty.empty());
+    EXPECT_EQ(empty.data(), nullptr);
+}
+
 }  // namespace xrpl::test
diff --git a/src/tests/libxrpl/basics/FileUtilities.cpp b/src/tests/libxrpl/basics/FileUtilities.cpp
index cd24abd696..5cf2b72709 100644
--- a/src/tests/libxrpl/basics/FileUtilities.cpp
+++ b/src/tests/libxrpl/basics/FileUtilities.cpp
@@ -2,16 +2,14 @@
 
 #include 
 
-#include 
-#include 
-#include 
-#include 
-
 #include 
 
+#include 
 #include 
+#include 
 #include 
 #include 
+#include 
 
 namespace xrpl {
 
@@ -20,15 +18,14 @@ namespace {
 class TempFile
 {
 public:
-    explicit TempFile(boost::filesystem::path file, std::string const& contents)
-        : dir_(
-              boost::filesystem::temp_directory_path() /
-              boost::filesystem::unique_path("xrpl-file-utilities-%%%%-%%%%-%%%%"))
-        , file_(dir_ / file)
+    explicit TempFile(std::string const& file, std::string const& contents)
+        : file_(
+              uniqueRandomPath(std::filesystem::temp_directory_path(), "xrpl-file-utilities-") /
+              file)
     {
-        boost::filesystem::create_directory(dir_);
+        std::filesystem::create_directory(file_.parent_path());
 
-        std::ofstream output(file_.string());
+        std::ofstream output(file_);
         if (!output)
             throw std::runtime_error("Unable to create temporary test file");
 
@@ -37,33 +34,36 @@ public:
 
     ~TempFile()
     {
-        boost::system::error_code ec;
-        boost::filesystem::remove(file_, ec);
-        boost::filesystem::remove(dir_, ec);
+        // use non-throwing calls in the destructor
+        std::error_code ec;
+        auto const dir = file_.parent_path();
+        std::filesystem::remove_all(dir, ec);
+        if (ec)
+        {
+            std::cerr << "Unable to remove temporary directory '" << dir.string()
+                      << "': " << ec.message() << '\n';
+        }
     }
 
-    [[nodiscard]] boost::filesystem::path const&
+    [[nodiscard]] std::filesystem::path const&
     file() const
     {
         return file_;
     }
 
 private:
-    boost::filesystem::path dir_;
-    boost::filesystem::path file_;
+    std::filesystem::path file_;
 };
 
 }  // namespace
 
 TEST(FileUtilitiesTest, get_file_contents)
 {
-    using namespace boost::system;
-
     constexpr char const* kExpectedContents = "This file is very short. That's all we need.";
 
     TempFile const file("test_file", "This is temporary text that should get overwritten");
 
-    error_code ec;
+    std::error_code ec;
     auto const path = file.file();
 
     writeFileContents(ec, path, kExpectedContents);
@@ -86,7 +86,7 @@ TEST(FileUtilitiesTest, get_file_contents)
     {
         // Test with small max
         auto const bad = getFileContents(ec, path, 16);
-        EXPECT_TRUE(ec && ec.value() == boost::system::errc::file_too_large);
+        EXPECT_TRUE(ec && ec.value() == static_cast(std::errc::file_too_large));
         EXPECT_TRUE(bad.empty());
     }
 }
diff --git a/src/tests/libxrpl/basics/IntrusiveShared.cpp b/src/tests/libxrpl/basics/IntrusiveShared.cpp
index b9f8930b7b..c6c9fcfef0 100644
--- a/src/tests/libxrpl/basics/IntrusiveShared.cpp
+++ b/src/tests/libxrpl/basics/IntrusiveShared.cpp
@@ -92,6 +92,7 @@ public:
     static constexpr std::size_t kMaxStates = 128;
     static std::array, kMaxStates> state;
     static std::atomic nextId;
+
     static TrackedState
     getState(std::size_t id)
     {
@@ -100,13 +101,12 @@ public:
 
         return state[id].load(std::memory_order_acquire);
     }
+
     static void
     resetStates(bool resetCallback)
     {
         for (std::size_t i = 0; i < kMaxStates; ++i)
-        {
             state[i].store(TrackedState::Uninitialized, std::memory_order_release);
-        }
         nextId.store(0, std::memory_order_release);
         if (resetCallback)
             TIBase::tracingCallback = [](TrackedState, std::optional) {};
@@ -120,6 +120,7 @@ public:
         {
             TIBase::resetStates(resetCallback);
         }
+
         ~ResetStatesGuard()
         {
             TIBase::resetStates(resetCallback);
@@ -130,6 +131,7 @@ public:
     {
         state[id].store(TrackedState::Alive, std::memory_order_relaxed);
     }
+
     ~TIBase() override
     {
         using enum TrackedState;
@@ -218,9 +220,7 @@ TEST(IntrusiveSharedTest, basics)
         EXPECT_EQ(TIBase::getState(id), Alive);
         EXPECT_EQ(b->useCount(), 1);
         for (auto i = 0uz; i < 10; ++i)
-        {
             strong.push_back(b);
-        }
         b.reset();
         EXPECT_EQ(TIBase::getState(id), Alive);
         strong.resize(strong.size() - 1);
@@ -244,8 +244,7 @@ TEST(IntrusiveSharedTest, basics)
         EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
         while (!weak.empty())
         {
-            weak.resize(weak.size() - 1);
-            if (!weak.empty())
+            if (weak.resize(weak.size() - 1); !weak.empty())
             {
                 EXPECT_EQ(TIBase::getState(id), PartiallyDeleted);
             }
diff --git a/src/tests/libxrpl/basics/Number.cpp b/src/tests/libxrpl/basics/Number.cpp
index 32f93eb1f7..8e958b40d4 100644
--- a/src/tests/libxrpl/basics/Number.cpp
+++ b/src/tests/libxrpl/basics/Number.cpp
@@ -1,5 +1,6 @@
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -16,6 +17,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -183,6 +185,17 @@ TEST(NumberTest, limits)
         }
         EXPECT_TRUE(caught);
 
+        try
+        {
+            Number{1, 2000000, Number::Normalized{}};
+            ADD_FAILURE();
+        }
+        catch (std::overflow_error const& e)
+        {
+            std::string const expected = "Number::normalize 2";
+            EXPECT_EQ(e.what(), expected) << e.what();
+        }
+
         if (scale == MantissaRange::MantissaScale::Large330)
         {
             // Normalization with the other scales, including the older large mantissa scales, will
@@ -406,6 +419,158 @@ TEST(NumberTest, add)
     }
 }
 
+TEST(NumberTest, add_sub_extreme_exponents)
+{
+    for (auto const mantissaScale : MantissaRange::getAllScales())
+    {
+        NumberMantissaScaleGuard const sg(mantissaScale);
+
+        auto const scale = Number::getMantissaScale();
+
+        EXPECT_EQ(Number::getround(), Number::RoundingMode::ToNearest)
+            << to_string(Number::getround());
+
+        // Special cases: Exponents at each end of the allowable range
+        for (auto const round :
+             {Number::RoundingMode::ToNearest,
+              Number::RoundingMode::TowardsZero,
+              Number::RoundingMode::Downward,
+              Number::RoundingMode::Upward})
+        {
+            NumberRoundModeGuard const rg{round};
+
+            auto const bigMantissa = std::invoke([scale, round] {
+                auto m = Number::maxMantissa();
+                if (scale != MantissaRange::MantissaScale::Small)
+                {
+                    // At the large scales, the maxMantissa is not representable, so we need to
+                    // shrink it down to a representable value.
+                    m /= 10;
+                }
+                if (round == Number::RoundingMode::Upward)
+                {
+                    // Rounding upward will overflow if the mantissa is at maxMantissa. Subtract an
+                    // arbitrary small value to keep the mantissa near the limit, but with a
+                    // little room to grow. 67 has no meaning, except that it's, you know,
+                    // six seven.
+                    m -= 67;
+                }
+                return m;
+            });
+            auto const params = {
+                std::make_pair(Number::minMantissa(), 0),
+                // At the large scales, the maxMantissa is not representable, so we need to shrink
+                // it down to a representable value. Rounding upward will overflow if the mantissa
+                // is right at the all nines value. To keep things a little simpler, do those
+                // modifications unconditionally.
+                std::make_pair(bigMantissa, 1),
+            };
+            for (auto const& [mantissa, exponentOffset] : params)
+            {
+                auto const x = Number{mantissa, Number::kMaxExponent, Number::Normalized{}};
+                auto const y =
+                    Number{mantissa, Number::kMinExponent + exponentOffset, Number::Normalized{}};
+
+                std::ostringstream detail;
+                detail << "Scale: " << to_string(scale) << ", round: " << to_string(round)
+                       << ", x: " << x << ", y: " << y;
+
+                EXPECT_EQ(x.mantissa(), mantissa);
+                EXPECT_EQ(x.exponent(), Number::kMaxExponent);
+                EXPECT_NE(x, beast::kZero);
+                EXPECT_EQ(y.mantissa(), mantissa);
+                EXPECT_EQ(y.exponent(), Number::kMinExponent + exponentOffset);
+                EXPECT_NE(y, beast::kZero);
+
+                {
+                    // x + y
+                    auto const result = x + y;
+
+                    if (round == Number::RoundingMode::Upward)
+                    {
+                        // Rounding upward will take that little x-bit and round result up to the
+                        // next representable value.
+                        EXPECT_NE(result, x);
+                        EXPECT_EQ(result, (Number{x.mantissa() + 1, x.exponent()}));
+                    }
+                    else
+                    {
+                        EXPECT_EQ(result, x);
+                    }
+                }
+                {
+                    // x - y
+                    auto const result = x - y;
+
+                    switch (round)
+                    {
+                        case Number::RoundingMode::TowardsZero:
+                            if (scale < MantissaRange::MantissaScale::Large330)
+                            {
+                                // Rounding TowardsZero was broken before Large330.
+                                EXPECT_EQ(result, x) << detail.str();
+                                break;
+                            }
+                            [[fallthrough]];
+                        case Number::RoundingMode::Downward:
+                            // Rounding downward (or toward zero in Large330) will take that little
+                            // x-bit and round result down to the next representable value.
+                            EXPECT_NE(result, x) << detail.str();
+                            EXPECT_EQ(result, (Number{x.mantissa() - 1, x.exponent()}))
+                                << detail.str();
+                            break;
+                        default:
+                            // Rounding up and toNearest rounds back to the original value
+                            EXPECT_EQ(result, x) << detail.str();
+                    }
+                }
+                {
+                    // y + x
+                    auto const result = y + x;
+
+                    if (round == Number::RoundingMode::Upward)
+                    {
+                        // Rounding upward will take that little x-bit and round result up to the
+                        // next representable value.
+                        EXPECT_NE(result, x);
+                        EXPECT_EQ(result, (Number{x.mantissa() + 1, x.exponent()}));
+                    }
+                    else
+                    {
+                        EXPECT_EQ(result, x);
+                    }
+                }
+                {
+                    // y - x
+                    auto const result = y - x;
+
+                    switch (round)
+                    {
+                        case Number::RoundingMode::TowardsZero:
+                            if (scale < MantissaRange::MantissaScale::Large330)
+                            {
+                                // Rounding TowardsZero was broken before Large330.
+                                EXPECT_EQ(result, -x) << detail.str();
+                                break;
+                            }
+                            [[fallthrough]];
+                        case Number::RoundingMode::Upward:
+                            // Rounding upward (or toward zero in Large330) will take that little
+                            // x-bit and round result up to the next representable negative value.
+                            EXPECT_NE(result, -x) << detail.str();
+                            EXPECT_EQ(result, (Number{-x.mantissa() + 1, x.exponent()}))
+                                << detail.str();
+                            break;
+                        default:
+                            // Rounding up and toNearest rounds back to the original value
+                            EXPECT_EQ(result, -x) << detail.str();
+                    }
+                }
+            }
+        }
+    }
+}
+
 TEST(NumberTest, sub)
 {
     for (auto const mantissaScale : MantissaRange::getAllScales())
diff --git a/src/tests/libxrpl/basics/StringUtilities.cpp b/src/tests/libxrpl/basics/StringUtilities.cpp
index a10711abdb..0180e25db0 100644
--- a/src/tests/libxrpl/basics/StringUtilities.cpp
+++ b/src/tests/libxrpl/basics/StringUtilities.cpp
@@ -290,4 +290,44 @@ TEST_F(StringUtilitiesTest, to_string)
     EXPECT_EQ(result, "hello");
 }
 
+TEST_F(StringUtilitiesTest, trimWhitespace)
+{
+    EXPECT_EQ(trimWhitespace(""), "");
+    EXPECT_EQ(trimWhitespace("   "), "");
+    EXPECT_EQ(trimWhitespace("abc"), "abc");
+    EXPECT_EQ(trimWhitespace("  abc"), "abc");
+    EXPECT_EQ(trimWhitespace("abc  "), "abc");
+    EXPECT_EQ(trimWhitespace(" \t\n\v\f\r abc \t\n\v\f\r "), "abc");
+
+    // Interior whitespace is preserved.
+    EXPECT_EQ(trimWhitespace("  a b\tc  "), "a b\tc");
+}
+
+TEST_F(StringUtilitiesTest, toLower)
+{
+    EXPECT_EQ(toLower(""), "");
+    EXPECT_EQ(toLower("ABC"), "abc");
+    EXPECT_EQ(toLower("AbC123"), "abc123");
+    EXPECT_EQ(toLower("already lower"), "already lower");
+
+    // Only 'A'-'Z' are remapped. Neighbouring punctuation and digits, which a
+    // buggy range check could catch, must survive untouched.
+    EXPECT_EQ(toLower("@[`{_^"), "@[`{_^");
+}
+
+// Both helpers are documented as depending only on their input. Guard that by
+// checking the bytes just outside ASCII, which a locale-aware isspace/tolower
+// could classify differently.
+TEST_F(StringUtilitiesTest, trimAndLowerIgnoreLocale)
+{
+    // 0xA0 is NO-BREAK SPACE in Latin-1 and is whitespace to some locales.
+    std::string const nbsp("\xA0", 1);
+    EXPECT_EQ(trimWhitespace(nbsp), nbsp);
+    EXPECT_EQ(trimWhitespace(" " + nbsp + " "), nbsp);
+
+    // 0xC0 is LATIN CAPITAL LETTER A WITH GRAVE in Latin-1.
+    std::string const agrave("\xC0", 1);
+    EXPECT_EQ(toLower(agrave), agrave);
+}
+
 }  // namespace xrpl
diff --git a/src/tests/libxrpl/basics/base_uint.cpp b/src/tests/libxrpl/basics/base_uint.cpp
index 10795f4563..969705b5b7 100644
--- a/src/tests/libxrpl/basics/base_uint.cpp
+++ b/src/tests/libxrpl/basics/base_uint.cpp
@@ -6,6 +6,7 @@
 
 #include 
 
+#include 
 #include 
 
 #include 
@@ -205,125 +206,119 @@ TEST_F(BaseUintTest, base_uint)
     Blob const raw{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
     EXPECT_EQ(BaseUInt96::kBytes, raw.size());
 
-    BaseUInt96 u = BaseUInt96::fromRaw(raw);
-    uset.insert(u);
-    EXPECT_EQ(raw.size(), u.size());
-    EXPECT_EQ(to_string(u), "0102030405060708090A0B0C");
-    EXPECT_EQ(toShortString(u), "01020304...");
-    EXPECT_EQ(*u.data(), 1);
-    EXPECT_EQ(u.signum(), 1);
-    EXPECT_FALSE(!u);
-    EXPECT_FALSE(u.isZero());
-    EXPECT_TRUE(u.isNonZero());
-    unsigned char t = 0;
-    for (auto& d : u)
-    {
-        EXPECT_EQ(d, ++t);
-    }
+    BaseUInt96 ascending = BaseUInt96::fromRaw(raw);
+    uset.insert(ascending);
+    EXPECT_EQ(raw.size(), ascending.size());
+    EXPECT_EQ(to_string(ascending), "0102030405060708090A0B0C");
+    EXPECT_EQ(toShortString(ascending), "01020304...");
+    EXPECT_EQ(*ascending.data(), 1);
+    EXPECT_EQ(ascending.signum(), 1);
+    EXPECT_FALSE(!ascending);
+    EXPECT_FALSE(ascending.isZero());
+    EXPECT_TRUE(ascending.isNonZero());
+    unsigned char expectedByte = 0;
+    for (auto& byte : ascending)
+        EXPECT_EQ(byte, ++expectedByte);
 
-    // Test hash_append by "hashing" with a no-op hasher (h)
+    // Test hash_append by "hashing" with a no-op hasher (hasher)
     // and then extracting the bytes that were written during hashing
-    // back into another base_uint (w) for comparison with the original
-    Nonhash<96> h{};
-    hash_append(h, u);
-    BaseUInt96 const w =
-        BaseUInt96::fromRaw(std::vector(h.data.begin(), h.data.end()));
-    EXPECT_EQ(w, u);
+    // back into another base_uint (rehashed) for comparison with the original
+    Nonhash<96> hasher{};
+    hash_append(hasher, ascending);
+    BaseUInt96 const rehashed =
+        BaseUInt96::fromRaw(std::vector(hasher.data.begin(), hasher.data.end()));
+    EXPECT_EQ(rehashed, ascending);
 
-    BaseUInt96 v{~u};
-    uset.insert(v);
-    EXPECT_EQ(to_string(v), "FEFDFCFBFAF9F8F7F6F5F4F3");
-    EXPECT_EQ(toShortString(v), "FEFDFCFB...");
-    EXPECT_EQ(*v.data(), 0xfe);
-    EXPECT_EQ(v.signum(), 1);
-    EXPECT_FALSE(!v);
-    EXPECT_FALSE(v.isZero());
-    EXPECT_TRUE(v.isNonZero());
+    BaseUInt96 complement{~ascending};
+    uset.insert(complement);
+    EXPECT_EQ(to_string(complement), "FEFDFCFBFAF9F8F7F6F5F4F3");
+    EXPECT_EQ(toShortString(complement), "FEFDFCFB...");
+    EXPECT_EQ(*complement.data(), 0xfe);
+    EXPECT_EQ(complement.signum(), 1);
+    EXPECT_FALSE(!complement);
+    EXPECT_FALSE(complement.isZero());
+    EXPECT_TRUE(complement.isNonZero());
 
-    t = 0xff;
-    for (auto& d : v)
-    {
-        EXPECT_EQ(d, --t);
-    }
+    expectedByte = 0xff;
+    for (auto& byte : complement)
+        EXPECT_EQ(byte, --expectedByte);
 
-    EXPECT_LT(u, v);
-    EXPECT_GT(v, u);
+    EXPECT_LT(ascending, complement);
+    EXPECT_GT(complement, ascending);
 
-    v = u;
-    EXPECT_EQ(v, u);
+    complement = ascending;
+    EXPECT_EQ(complement, ascending);
 
-    BaseUInt96 z{beast::kZero};
-    uset.insert(z);
-    EXPECT_EQ(to_string(z), "000000000000000000000000");
-    EXPECT_EQ(toShortString(z), "00000000...");
-    EXPECT_EQ(*z.data(), 0);
-    EXPECT_EQ(*z.begin(), 0);
-    EXPECT_EQ(*std::prev(z.end(), 1), 0);
-    EXPECT_EQ(z.signum(), 0);
-    EXPECT_TRUE(!z);
-    EXPECT_TRUE(z.isZero());
-    EXPECT_FALSE(z.isNonZero());
-    for (auto& d : z)
-    {
-        EXPECT_EQ(d, 0);
-    }
+    BaseUInt96 zero{beast::kZero};
+    uset.insert(zero);
+    EXPECT_EQ(to_string(zero), "000000000000000000000000");
+    EXPECT_EQ(toShortString(zero), "00000000...");
+    EXPECT_EQ(*zero.data(), 0);
+    EXPECT_EQ(*zero.begin(), 0);
+    EXPECT_EQ(*std::prev(zero.end(), 1), 0);
+    EXPECT_EQ(zero.signum(), 0);
+    EXPECT_TRUE(!zero);
+    EXPECT_TRUE(zero.isZero());
+    EXPECT_FALSE(zero.isNonZero());
+    for (auto& byte : zero)
+        EXPECT_EQ(byte, 0);
 
     {
         // There are several ways to create a zero. beast::kZero is tested above. Test some
         // others.
-        BaseUInt96 const z1;
-        EXPECT_EQ(z1, z) << to_string(z1);
+        BaseUInt96 const defaultZero;
+        EXPECT_EQ(defaultZero, zero) << to_string(defaultZero);
 
-        BaseUInt96 const z2{};
-        EXPECT_EQ(z2, z) << to_string(z2);
+        BaseUInt96 const bracedZero{};
+        EXPECT_EQ(bracedZero, zero) << to_string(bracedZero);
 
-        BaseUInt96 const z3{0u};
-        EXPECT_EQ(z3, z) << to_string(z3);
+        BaseUInt96 const zeroFromUInt{0u};
+        EXPECT_EQ(zeroFromUInt, zero) << to_string(zeroFromUInt);
     }
 
-    BaseUInt96 n{z};
-    n++;
-    EXPECT_EQ(n, BaseUInt96(1));
-    n--;
-    EXPECT_EQ(n, beast::kZero);
-    EXPECT_EQ(n, z);
-    n--;
-    EXPECT_EQ(to_string(n), "FFFFFFFFFFFFFFFFFFFFFFFF");
-    EXPECT_EQ(toShortString(n), "FFFFFFFF...");
-    n = beast::kZero;
-    EXPECT_EQ(n, z);
+    BaseUInt96 counter{zero};
+    counter++;
+    EXPECT_EQ(counter, BaseUInt96(1));
+    counter--;
+    EXPECT_EQ(counter, beast::kZero);
+    EXPECT_EQ(counter, zero);
+    counter--;
+    EXPECT_EQ(to_string(counter), "FFFFFFFFFFFFFFFFFFFFFFFF");
+    EXPECT_EQ(toShortString(counter), "FFFFFFFF...");
+    counter = beast::kZero;
+    EXPECT_EQ(counter, zero);
 
-    BaseUInt96 zp1{z};
-    zp1++;
-    BaseUInt96 zm1{z};
-    zm1--;
-    BaseUInt96 const x{zm1 ^ zp1};
-    uset.insert(x);
-    EXPECT_EQ(to_string(x), "FFFFFFFFFFFFFFFFFFFFFFFE") << to_string(x);
-    EXPECT_EQ(toShortString(x), "FFFFFFFF...") << toShortString(x);
+    BaseUInt96 zeroPlusOne{zero};
+    zeroPlusOne++;
+    BaseUInt96 zeroMinusOne{zero};
+    zeroMinusOne--;
+    BaseUInt96 const xored{zeroMinusOne ^ zeroPlusOne};
+    uset.insert(xored);
+    EXPECT_EQ(to_string(xored), "FFFFFFFFFFFFFFFFFFFFFFFE") << to_string(xored);
+    EXPECT_EQ(toShortString(xored), "FFFFFFFF...") << toShortString(xored);
 
     EXPECT_EQ(uset.size(), 4);
 
-    BaseUInt96 tmp;
-    EXPECT_TRUE(tmp.parseHex(to_string(u)));
-    EXPECT_EQ(tmp, u);
-    tmp = z;
+    BaseUInt96 parsed;
+    EXPECT_TRUE(parsed.parseHex(to_string(ascending)));
+    EXPECT_EQ(parsed, ascending);
+    parsed = zero;
 
     // fails with extra char
-    EXPECT_FALSE(tmp.parseHex("A" + to_string(u)));
-    tmp = z;
+    EXPECT_FALSE(parsed.parseHex("A" + to_string(ascending)));
+    parsed = zero;
 
     // fails with extra char at end
-    EXPECT_FALSE(tmp.parseHex(to_string(u) + "A"));
+    EXPECT_FALSE(parsed.parseHex(to_string(ascending) + "A"));
 
     // fails with a non-hex character at some point in the string:
-    tmp = z;
+    parsed = zero;
 
     for (std::size_t i = 0; i != 24; ++i)
     {
-        std::string x = to_string(z);
-        x[i] = ('G' + (i % 10));
-        EXPECT_FALSE(tmp.parseHex(x));
+        std::string xored = to_string(zero);
+        xored[i] = ('G' + (i % 10));
+        EXPECT_FALSE(parsed.parseHex(xored));
     }
 
     // Walking 1s:
@@ -332,8 +327,8 @@ TEST_F(BaseUintTest, base_uint)
         std::string s1 = "000000000000000000000000";
         s1[i] = '1';
 
-        EXPECT_TRUE(tmp.parseHex(s1));
-        EXPECT_EQ(to_string(tmp), s1);
+        EXPECT_TRUE(parsed.parseHex(s1));
+        EXPECT_EQ(to_string(parsed), s1);
     }
 
     // Walking 0s:
@@ -342,8 +337,8 @@ TEST_F(BaseUintTest, base_uint)
         std::string s1 = "111111111111111111111111";
         s1[i] = '0';
 
-        EXPECT_TRUE(tmp.parseHex(s1));
-        EXPECT_EQ(to_string(tmp), s1);
+        EXPECT_TRUE(parsed.parseHex(s1));
+        EXPECT_EQ(to_string(parsed), s1);
     }
 
     // Constexpr constructors
@@ -357,39 +352,27 @@ TEST_F(BaseUintTest, base_uint)
         // Using the constexpr constructor in a non-constexpr context
         // with an error in the parsing throws an exception.
         {
-            // Invalid length for string.
-            bool caught = false;
-            try
-            {
-                // Try to prevent constant evaluation.
-                std::vector str(23, '7');
+            // Invalid length for string. The vector keeps this out of a constant
+            // expression, so the constructor throws instead of failing to compile.
+            auto tooShort = [] {
+                std::vector const str(23, '7');
                 std::string_view const sView(str.data(), str.size());
                 [[maybe_unused]] BaseUInt96 const t96(sView);
-            }
-            catch (std::invalid_argument const& e)
-            {
-                EXPECT_EQ(e.what(), std::string("invalid length for hex string"));
-                caught = true;
-            }
-            EXPECT_TRUE(caught);
+            };
+            EXPECT_THAT(
+                tooShort,
+                ::testing::ThrowsMessage("invalid length for hex string"));
         }
         {
             // Invalid character in string.
-            bool caught = false;
-            try
-            {
-                // Try to prevent constant evaluation.
+            auto badCharacter = [] {
                 std::vector str(23, '7');
                 str.push_back('G');
                 std::string_view const sView(str.data(), str.size());
                 [[maybe_unused]] BaseUInt96 const t96(sView);
-            }
-            catch (std::range_error const& e)
-            {
-                EXPECT_EQ(e.what(), std::string("invalid hex character"));
-                caught = true;
-            }
-            EXPECT_TRUE(caught);
+            };
+            EXPECT_THAT(
+                badCharacter, ::testing::ThrowsMessage("invalid hex character"));
         }
 
         // Verify that constexpr base_uints interpret a string the same
@@ -412,11 +395,11 @@ TEST_F(BaseUintTest, base_uint)
             "fFfFfFfFfFfFfFfFfFfFfFfF",
         });
 
-        for (StrBaseUInt const& t : kTestCases)
+        for (StrBaseUInt const& expectedByte : kTestCases)
         {
             BaseUInt96 t96;
-            EXPECT_TRUE(t96.parseHex(t.str));
-            EXPECT_EQ(t96, t.tst);
+            EXPECT_TRUE(t96.parseHex(expectedByte.str));
+            EXPECT_EQ(t96, expectedByte.tst);
         }
     }
 }
diff --git a/src/tests/libxrpl/helpers/TestServiceRegistry.h b/src/tests/libxrpl/helpers/TestServiceRegistry.h
index 5c2875c8fc..2b27d2d4ca 100644
--- a/src/tests/libxrpl/helpers/TestServiceRegistry.h
+++ b/src/tests/libxrpl/helpers/TestServiceRegistry.h
@@ -1,13 +1,22 @@
 #pragma once
 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
 #include 
 #include 
 
@@ -16,11 +25,15 @@
 #include 
 #include 
 
+#include 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
 
 namespace xrpl::test {
 
@@ -41,6 +54,100 @@ public:
     }
 };
 
+/**
+ * Minimal AmendmentTable for tests.
+ */
+class TestAmendmentTable final : public AmendmentTable
+{
+public:
+    [[nodiscard]] uint256
+    find(std::string const& name) const override
+    {
+        return getRegisteredFeature(name).value_or(uint256{});
+    }
+
+    bool
+    veto(uint256 const&) override
+    {
+        throw std::logic_error("TestAmendmentTable::veto not implemented");
+    }
+    bool
+    unVeto(uint256 const&) override
+    {
+        throw std::logic_error("TestAmendmentTable::unVeto not implemented");
+    }
+    bool
+    enable(uint256 const&) override
+    {
+        throw std::logic_error("TestAmendmentTable::enable not implemented");
+    }
+    [[nodiscard]] bool
+    isEnabled(uint256 const&) const override
+    {
+        throw std::logic_error("TestAmendmentTable::isEnabled not implemented");
+    }
+    [[nodiscard]] bool
+    isSupported(uint256 const&) const override
+    {
+        throw std::logic_error("TestAmendmentTable::isSupported not implemented");
+    }
+    [[nodiscard]] bool
+    hasUnsupportedEnabled() const override
+    {
+        throw std::logic_error("TestAmendmentTable::hasUnsupportedEnabled not implemented");
+    }
+    [[nodiscard]] std::optional
+    firstUnsupportedExpected() const override
+    {
+        throw std::logic_error("TestAmendmentTable::firstUnsupportedExpected not implemented");
+    }
+    [[nodiscard]] json::Value
+    getJson(bool) const override
+    {
+        throw std::logic_error("TestAmendmentTable::getJson not implemented");
+    }
+    [[nodiscard]] json::Value
+    getJson(uint256 const&, bool) const override
+    {
+        throw std::logic_error("TestAmendmentTable::getJson(amendment) not implemented");
+    }
+    [[nodiscard]] bool
+    needValidatedLedger(LedgerIndex) const override
+    {
+        throw std::logic_error("TestAmendmentTable::needValidatedLedger not implemented");
+    }
+    void
+    doValidatedLedger(LedgerIndex, std::set const&, majorityAmendments_t const&) override
+    {
+        throw std::logic_error("TestAmendmentTable::doValidatedLedger not implemented");
+    }
+    void
+    trustChanged(hash_set const&) override
+    {
+        throw std::logic_error("TestAmendmentTable::trustChanged not implemented");
+    }
+    std::map
+    doVoting(
+        Rules const&,
+        NetClock::time_point,
+        std::set const&,
+        majorityAmendments_t const&,
+        std::vector> const&) override
+    {
+        throw std::logic_error("TestAmendmentTable::doVoting not implemented");
+    }
+    [[nodiscard]] std::vector
+    doValidation(std::set const&) const override
+    {
+        throw std::logic_error("TestAmendmentTable::doValidation not implemented");
+    }
+    [[nodiscard]] std::vector
+    getDesired() const override
+    {
+        throw std::logic_error("TestAmendmentTable::getDesired not implemented");
+    }
+};
+
 /**
  * Simple NetworkIDService implementation for tests.
  */
@@ -72,6 +179,10 @@ private:
  */
 class TestServiceRegistry : public ServiceRegistry
 {
+public:
+    /**
+     * @brief The fee settings a test environment starts with.
+     */
     static Fees
     defaultFees()
     {
@@ -82,6 +193,7 @@ class TestServiceRegistry : public ServiceRegistry
         return fees;
     }
 
+private:
     TestLogs logs_{beast::Severity::Warning};
     boost::asio::io_context ioContext_;
     TestFamily family_{logs_.journal("TestFamily")};
@@ -103,6 +215,7 @@ class TestServiceRegistry : public ServiceRegistry
         logs_.journal("TaggedCache")};
     PendingSaves pendingSaves_;
     std::optional trapTxID_;
+    TestAmendmentTable amendmentTable_;
 
 public:
     TestServiceRegistry() = default;
@@ -155,7 +268,7 @@ public:
     AmendmentTable&
     getAmendmentTable() override
     {
-        throw std::logic_error("TestServiceRegistry::getAmendmentTable() not implemented");
+        return amendmentTable_;
     }
 
     HashRouter&
@@ -392,6 +505,15 @@ public:
         return fees_;
     }
 
+    /**
+     * @brief Override the fee settings the transactors see.
+     */
+    void
+    setFees(Fees const& fees)
+    {
+        fees_ = fees;
+    }
+
     // Temporary: Get the underlying Application
     Application&
     getApp() override
diff --git a/src/tests/libxrpl/helpers/TxTest.cpp b/src/tests/libxrpl/helpers/TxTest.cpp
index 4b4f407eeb..fb209aede8 100644
--- a/src/tests/libxrpl/helpers/TxTest.cpp
+++ b/src/tests/libxrpl/helpers/TxTest.cpp
@@ -16,6 +16,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -24,6 +26,7 @@
 
 #include 
 #include 
+#include 
 
 #include 
 #include 
@@ -55,11 +58,21 @@ allFeatures()
     return kFeatures;
 }
 
+//------------------------------------------------------------------------------
+// TxTest free helpers
+//------------------------------------------------------------------------------
+
+std::uint32_t
+closeTimeOffset(TxTest const& env, std::uint32_t seconds)
+{
+    return static_cast(env.getCloseTime().time_since_epoch().count()) + seconds;
+}
+
 //------------------------------------------------------------------------------
 // TxTest
 //------------------------------------------------------------------------------
 
-TxTest::TxTest(std::optional features)
+TxTest::TxTest(std::optional features, std::optional feesOverride)
 {
     // Convert FeatureBitset to unordered_set for Rules constructor
     auto const featureBits = features.value_or(allFeatures());
@@ -68,8 +81,9 @@ TxTest::TxTest(std::optional features)
     // Create rules with the specified features
     rules_.emplace(featureSet_);
 
-    // Default fees for testing
-    Fees const fees{XRPAmount{10}, XRPAmount{10000000}, XRPAmount{2000000}};
+    // One fee set for both the view and the registry.
+    Fees const fees = feesOverride.value_or(TestServiceRegistry::defaultFees());
+    registry_.setFees(fees);
 
     // Create a genesis ledger as the base
     closedLedger_ = std::make_shared(
@@ -155,6 +169,18 @@ TxTest::getAccountRoot(AccountID const& id) const
     return ledger_entries::AccountRoot{std::const_pointer_cast(sle)};
 }
 
+std::uint32_t
+TxTest::getOwnerCount(AccountID const& id) const
+{
+    return getAccountRoot(id).getOwnerCount();
+}
+
+XRPAmount
+TxTest::getXrpBalance(AccountID const& id) const
+{
+    return getAccountRoot(id).getBalance().xrp();
+}
+
 OpenView&
 TxTest::getOpenLedger()
 {
@@ -194,6 +220,7 @@ TxTest::close()
     for (auto const& tx : pendingTxs_)
         txSet.insert(tx);
 
+    closedMetadata_.clear();
     {
         OpenView accum(&*newLedger);
         for (auto const& [key, tx] : txSet)
@@ -203,6 +230,11 @@ TxTest::close()
             {
                 throw std::runtime_error("TxTest::close: failed to apply transaction");
             }
+            // `accum` is not an open view, so this is the apply that produces metadata.
+            if (result.metadata.has_value())
+            {
+                closedMetadata_.emplace(tx->getTransactionID(), *std::move(result).metadata);
+            }
         }
         accum.apply(*newLedger);
     }
@@ -218,6 +250,17 @@ TxTest::close()
         std::make_shared(kOpenLedger, closedLedger_.get(), *rules_, closedLedger_);
 }
 
+std::optional
+TxTest::getMetadata(uint256 const& txId) const
+{
+    auto const it = closedMetadata_.find(txId);
+    if (it == std::end(closedMetadata_))
+    {
+        return std::nullopt;
+    }
+    return it->second;
+}
+
 void
 TxTest::advanceTime(NetClock::duration duration)
 {
diff --git a/src/tests/libxrpl/helpers/TxTest.h b/src/tests/libxrpl/helpers/TxTest.h
index 98198e45f8..9a26050eb5 100644
--- a/src/tests/libxrpl/helpers/TxTest.h
+++ b/src/tests/libxrpl/helpers/TxTest.h
@@ -4,12 +4,12 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -30,6 +30,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -163,6 +164,19 @@ struct TxResult
     std::shared_ptr tx;  ///< Pointer to the submitted transaction.
 };
 
+/**
+ * @brief Result of a transaction submission that has been closed into a ledger.
+ *
+ * `TxResult::metadata` is always `std::nullopt`, because metadata is only built for a view
+ * that is not open. This is what `TxTest::submitAndClose` returns instead: the result code
+ * paired with the metadata that closing produced.
+ */
+struct ClosedResult
+{
+    TER ter;                     ///< The transaction engine result code.
+    std::optional meta;  ///< Metadata from the close, absent if none was produced.
+};
+
 /**
  * @brief A lightweight transaction testing harness.
  *
@@ -191,8 +205,16 @@ public:
      *
      * @param features Optional set of features to enable. If not specified,
      *                 uses all testable amendments.
+     * @param fees Optional fee settings. If not specified, uses
+     *             `TestServiceRegistry::defaultFees()`. Applied to **both** the genesis
+     *             ledger and the service registry, because transactors read limits from the
+     *             registry (`ctx.registry.get().getFees()`) while `calculateBaseFee` reads
+     *             `view.fees()` — a test whose fees disagree across the two is testing a
+     *             state no ledger can be in.
      */
-    explicit TxTest(std::optional features = std::nullopt);
+    explicit TxTest(
+        std::optional features = std::nullopt,
+        std::optional fees = std::nullopt);
 
     /**
      * @brief Check if a feature is enabled.
@@ -218,17 +240,22 @@ public:
      * @tparam T A type derived from TransactionBuilderBase.
      * @param builder The transaction builder.
      * @param signer The account to sign with.
+     * @param fee The fee to pay. The 10 drop default is below what some transactions
+     *            require: an `EscrowCreate` carrying `sfBytecode` owes
+     *            `base * 10 + 5 * bytecodeBytes` (`EscrowCreate::calculateBaseFee`), and an
+     *            `EscrowFinish` carrying `sfGas` owes the allowance priced at `gasPrice`.
+     *            Those submissions would fail on the fee rather than on whatever they meant
+     *            to test, so they must pass one explicitly.
      * @return TxResult containing the result code, applied status, and metadata.
      */
     template 
         requires std::
             derived_from, transactions::TransactionBuilderBase>>
         [[nodiscard]] TxResult
-        submit(T&& builder, Account const& signer)
+        submit(T&& builder, Account const& signer, XRPAmount fee = XRPAmount{10})
     {
         auto const& obj = builder.getSTObject();
         auto accountId = obj[sfAccount];
-        // Only set sequence if not using a ticket (ticket sets sequence to 0)
         if (!obj.isFieldPresent(sfTicketSequence))
         {
             builder.setSequence(getAccountRoot(accountId).getSequence());
@@ -237,10 +264,35 @@ public:
         {
             builder.setSequence(0);
         }
-        builder.setFee(XRPAmount(10));
+        builder.setFee(fee);
         return submit(builder.build(signer.pk(), signer.sk()).getSTTx());
     }
 
+    /**
+     * @brief Submit a transaction, then close the ledger and return its metadata.
+     *
+     * Metadata comes into being at `close`, not at `submit` (see `close`), so any assertion
+     * about `sfGasUsed`, `sfVMReturnCode`, or a delivered amount needs this three-step
+     * sequence rather than `submit` alone. Closing also advances time by one close
+     * interval, which matters to a test sensitive to a `FinishAfter` or an expiry.
+     *
+     * @tparam T A type derived from TransactionBuilderBase.
+     * @param builder The transaction builder.
+     * @param signer The account to sign with.
+     * @param fee The fee to pay; see `submit` for when the default is not enough.
+     * @return The result code and the metadata produced by the close.
+     */
+    template 
+        requires std::
+            derived_from, transactions::TransactionBuilderBase>>
+        [[nodiscard]] ClosedResult
+        submitAndClose(T&& builder, Account const& signer, XRPAmount fee = XRPAmount{10})
+    {
+        auto const result = submit(std::forward(builder), signer, fee);
+        close();
+        return ClosedResult{.ter = result.ter, .meta = getMetadata(result.tx->getTransactionID())};
+    }
+
     /**
      * @brief Submit a transaction to the open ledger.
      *
@@ -281,6 +333,28 @@ public:
     [[nodiscard]] ledger_entries::AccountRoot
     getAccountRoot(AccountID const& id) const;
 
+    /**
+     * @brief Get an account's owner count.
+     * @param id The account ID.
+     * @return The number of ledger objects the account owns.
+     * @throws std::runtime_error if the account does not exist.
+     */
+    [[nodiscard]] std::uint32_t
+    getOwnerCount(AccountID const& id) const;
+
+    /**
+     * @brief Get an account's XRP balance.
+     *
+     * The IOU overload of `getBalance` covers trust lines; this covers the account's own
+     * drops, which is what a fee- or reserve-sensitive test needs to assert on.
+     *
+     * @param id The account ID.
+     * @return The balance in drops.
+     * @throws std::runtime_error if the account does not exist.
+     */
+    [[nodiscard]] XRPAmount
+    getXrpBalance(AccountID const& id) const;
+
     /**
      * @brief Get the current open ledger view.
      * @return A mutable reference to the open ledger.
@@ -307,10 +381,26 @@ public:
      *
      * Creates a new closed ledger from the current open ledger.
      * All pending transactions are re-applied in canonical order.
+     *
+     * @note This is where transaction **metadata** comes into being: it is only built for a
+     *       view that is not open (`ApplyStateTable::apply`), so `submit` cannot return any.
+     *       Each closed transaction's metadata is retained for `getMetadata`.
      */
     void
     close();
 
+    /**
+     * @brief Get the metadata of a transaction in the most recently closed ledger.
+     *
+     * Metadata is a property of a *closed* ledger, so the sequence is submit → `close` →
+     * `getMetadata`. Only the latest close is retained.
+     *
+     * @param txId The transaction's ID (`TxResult::tx->getTransactionID()`).
+     * @return The metadata, or `std::nullopt` if that transaction was not in the last close.
+     */
+    [[nodiscard]] std::optional
+    getMetadata(uint256 const& txId) const;
+
     /**
      * @brief Advance time without closing the ledger.
      *
@@ -345,9 +435,14 @@ public:
 
     /**
      * @brief Get the service registry.
+     *
+     * Returns the concrete test type so a test can reach its setters — `setFees` in
+     * particular, for the cases that need a limit to change *after* setup, which the
+     * constructor's `fees` parameter cannot express.
+     *
      * @return A reference to the service registry.
      */
-    ServiceRegistry&
+    TestServiceRegistry&
     getServiceRegistry()
     {
         return registry_;
@@ -365,10 +460,52 @@ private:
      */
     std::vector> pendingTxs_;
 
+    /**
+     * Metadata from the most recent close, keyed by transaction ID. Replaced each close.
+     */
+    std::map closedMetadata_;
+
     /**
      * Current time (can be advanced arbitrarily for testing).
      */
     NetClock::time_point now_;
 };
 
+//------------------------------------------------------------------------------
+// TxTest free helpers
+//------------------------------------------------------------------------------
+
+/**
+ * @brief A ledger-close-time deadline `seconds` in the future.
+ *
+ * Time fields on the wire are `std::uint32_t` seconds since the XRPL epoch, while the
+ * environment reports a `NetClock::time_point`. Every `CancelAfter` / `FinishAfter` needs
+ * the same cast, and getting it wrong yields a deadline in the past — which a transactor
+ * reports as `temBAD_EXPIRATION`, a failure that looks like the case under test.
+ *
+ * @param env The environment whose close time the deadline is relative to.
+ * @param seconds How far past the current close time the deadline should sit.
+ * @return The deadline, as a transaction field expects it.
+ */
+[[nodiscard]] std::uint32_t
+closeTimeOffset(TxTest const& env, std::uint32_t seconds);
+
+/**
+ * @brief Create and fund several accounts with the same balance.
+ *
+ * @code
+ *     createAccounts(env, XRP(5'000), alice, carol);
+ * @endcode
+ *
+ * @param env The environment to create the accounts in.
+ * @param xrp The initial balance for each account.
+ * @param accounts The accounts to create.
+ */
+template ... Accounts>
+void
+createAccounts(TxTest& env, XRPAmount xrp, Accounts const&... accounts)
+{
+    (env.createAccount(accounts, xrp), ...);
+}
+
 }  // namespace xrpl::test
diff --git a/src/tests/libxrpl/nodestore/Backend.cpp b/src/tests/libxrpl/nodestore/Backend.cpp
index eb78851429..3bd36ced8d 100644
--- a/src/tests/libxrpl/nodestore/Backend.cpp
+++ b/src/tests/libxrpl/nodestore/Backend.cpp
@@ -1,8 +1,8 @@
 #include 
 
 #include 
+#include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -84,7 +84,7 @@ protected:
     }
 
     DummyScheduler scheduler_;
-    beast::TempDir const tempDir_;
+    TempDir const tempDir_;
     beast::Journal const journal_{TestSink::instance()};
     Section params_;
     Batch batch_;
diff --git a/src/tests/libxrpl/nodestore/Database.cpp b/src/tests/libxrpl/nodestore/Database.cpp
index 82012ed347..a3f7340f62 100644
--- a/src/tests/libxrpl/nodestore/Database.cpp
+++ b/src/tests/libxrpl/nodestore/Database.cpp
@@ -1,8 +1,8 @@
 #include 
 
 #include 
+#include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -81,7 +81,7 @@ protected:
     }
 
     DummyScheduler scheduler_;
-    beast::TempDir const nodeDb_;
+    TempDir const nodeDb_;
     beast::Journal const journal_{TestSink::instance()};
     Section nodeParams_;
     Batch batch_;
@@ -157,7 +157,7 @@ INSTANTIATE_TEST_SUITE_P(
 TEST(NodeStoreDatabase, memory_earliest_seq)
 {
     DummyScheduler scheduler;
-    beast::TempDir const nodeDb;
+    TempDir const nodeDb;
     Section nodeParams;
     nodeParams.set("type", "memory");
     nodeParams.set("path", nodeDb.path());
@@ -204,7 +204,7 @@ TEST_P(DatabaseImportTest, same_backend)
     DummyScheduler scheduler;
     beast::Journal const journal(TestSink::instance());
 
-    beast::TempDir const srcDir;
+    TempDir const srcDir;
     Section srcParams;
     srcParams.set("type", type);
     srcParams.set("path", srcDir.path());
@@ -222,7 +222,7 @@ TEST_P(DatabaseImportTest, same_backend)
         // re-open source and import into a fresh destination
         auto src = Manager::instance().makeDatabase(megabytes(4), scheduler, 2, srcParams, journal);
 
-        beast::TempDir const destDir;
+        TempDir const destDir;
         Section destParams;
         destParams.set("type", type);
         destParams.set("path", destDir.path());
diff --git a/src/tests/libxrpl/nodestore/NuDBFactory.cpp b/src/tests/libxrpl/nodestore/NuDBFactory.cpp
index c126984630..7240f08256 100644
--- a/src/tests/libxrpl/nodestore/NuDBFactory.cpp
+++ b/src/tests/libxrpl/nodestore/NuDBFactory.cpp
@@ -1,6 +1,6 @@
 #include 
+#include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -58,7 +58,7 @@ runRoundTrip(Section const& params, std::size_t expectedBlocksize)
 
 TEST(NuDBFactory, default_block_size)
 {
-    beast::TempDir const tempDir;
+    TempDir const tempDir;
     auto const params = makeSection(tempDir.path());
     ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 4096));
 }
@@ -69,14 +69,14 @@ TEST(NuDBFactory, valid_block_sizes)
     for (auto const size : kValidSizes)
     {
         SCOPED_TRACE("size=" + std::to_string(size));
-        beast::TempDir const tempDir;
+        TempDir const tempDir;
         auto const params = makeSection(tempDir.path(), std::to_string(size));
         ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, size));
     }
 
     // empty value is ignored by config parser; default (4096) is used
     {
-        beast::TempDir const tempDir;
+        TempDir const tempDir;
         auto const params = makeSection(tempDir.path(), "");
         ASSERT_NO_FATAL_FAILURE(runRoundTrip(params, 4096));
     }
@@ -101,7 +101,7 @@ TEST(NuDBFactory, invalid_block_sizes)
     for (auto const& size : kInvalidSizes)
     {
         SCOPED_TRACE("size='" + size + "'");
-        beast::TempDir const tempDir;
+        TempDir const tempDir;
         auto const params = makeSection(tempDir.path(), size);
         EXPECT_THROW(runRoundTrip(params, 4096), std::exception);
     }
@@ -111,7 +111,7 @@ TEST(NuDBFactory, invalid_block_sizes)
     for (auto const& size : kWhitespaceSizes)
     {
         SCOPED_TRACE("size='" + size + "'");
-        beast::TempDir const tempDir;
+        TempDir const tempDir;
         auto const params = makeSection(tempDir.path(), size);
         EXPECT_THROW(runRoundTrip(params, 4096), std::exception);
     }
@@ -121,7 +121,7 @@ TEST(NuDBFactory, log_messages)
 {
     // valid custom block size emits info log
     {
-        beast::TempDir const tempDir;
+        TempDir const tempDir;
         auto const params = makeSection(tempDir.path(), "8192");
         test::CaptureSink sink(beast::Severity::Info);
         beast::Journal const journal(sink);
@@ -135,7 +135,7 @@ TEST(NuDBFactory, log_messages)
 
     // invalid block size throws with informative message
     {
-        beast::TempDir const tempDir;
+        TempDir const tempDir;
         auto const params = makeSection(tempDir.path(), "5000");
         test::CaptureSink sink(beast::Severity::Warning);
         beast::Journal const journal(sink);
@@ -156,7 +156,7 @@ TEST(NuDBFactory, log_messages)
 
     // non-numeric value throws
     {
-        beast::TempDir const tempDir;
+        TempDir const tempDir;
         auto const params = makeSection(tempDir.path(), "invalid");
         test::CaptureSink sink(beast::Severity::Warning);
         beast::Journal const journal(sink);
@@ -191,7 +191,7 @@ TEST(NuDBFactory, power_of_two_validation)
     for (auto const& [size, shouldWork] : kCASES)
     {
         SCOPED_TRACE("size=" + size + " shouldWork=" + (shouldWork ? "true" : "false"));
-        beast::TempDir const tempDir;
+        TempDir const tempDir;
         auto const params = makeSection(tempDir.path(), size);
         test::CaptureSink sink(beast::Severity::Warning);
         beast::Journal const journal(sink);
@@ -216,7 +216,7 @@ TEST(NuDBFactory, power_of_two_validation)
 
 TEST(NuDBFactory, both_constructor_variants)
 {
-    beast::TempDir const tempDir;
+    TempDir const tempDir;
     auto const params = makeSection(tempDir.path(), "16384");
     DummyScheduler scheduler;
     beast::Journal const journal(TestSink::instance());
@@ -235,7 +235,7 @@ TEST(NuDBFactory, configuration_parsing)
 {
     // basic valid format emits success log
     {
-        beast::TempDir const tempDir;
+        TempDir const tempDir;
         auto const params = makeSection(tempDir.path(), "8192");
         test::CaptureSink sink(beast::Severity::Info);
         beast::Journal const journal(sink);
@@ -250,7 +250,7 @@ TEST(NuDBFactory, configuration_parsing)
     for (auto const& format : kWhitespaceFormats)
     {
         SCOPED_TRACE("format='" + format + "'");
-        beast::TempDir const tempDir;
+        TempDir const tempDir;
         auto const params = makeSection(tempDir.path(), format);
         test::CaptureSink sink(beast::Severity::Debug);
         beast::Journal const journal(sink);
@@ -265,7 +265,7 @@ TEST(NuDBFactory, data_persistence)
     for (auto const& size : kBlockSizes)
     {
         SCOPED_TRACE("size=" + size);
-        beast::TempDir const tempDir;
+        TempDir const tempDir;
         auto const params = makeSection(tempDir.path(), size);
         DummyScheduler scheduler;
         beast::Journal const journal(TestSink::instance());
diff --git a/src/tests/libxrpl/protocol/STXChainBridge.cpp b/src/tests/libxrpl/protocol/STXChainBridge.cpp
new file mode 100644
index 0000000000..f4e6e60cc9
--- /dev/null
+++ b/src/tests/libxrpl/protocol/STXChainBridge.cpp
@@ -0,0 +1,60 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+#include 
+#include 
+
+using namespace xrpl;
+
+namespace {
+
+// Built from raw bytes rather than base58 so the test does not depend on
+// hand-computed checksums.
+AccountID
+account(std::string_view hex)
+{
+    AccountID id;
+    EXPECT_TRUE(id.parseHex(hex));
+    return id;
+}
+
+}  // namespace
+
+// getText() builds its string from eight substitutions of the same type, so a
+// transposed pair would still compile and still type check. Pin the output so
+// the field/value pairing is actually verified.
+TEST(STXChainBridge, getTextPairsEachFieldWithItsValue)
+{
+    auto const lockingDoor = account("0102030405060708090A0B0C0D0E0F1011121314");
+    auto const issuingDoor = account("14131211100F0E0D0C0B0A090807060504030201");
+
+    auto const lockingIssue = xrpIssue();
+    Issue const issuingIssue{toCurrency("USD"), issuingDoor};
+
+    STXChainBridge const bridge{lockingDoor, lockingIssue, issuingDoor, issuingIssue};
+
+    std::string const expected = "{ LockingChainDoor = " + toBase58(lockingDoor) +
+        ", LockingChainIssue = " + lockingIssue.getText() +
+        ", IssuingChainDoor = " + toBase58(issuingDoor) +
+        ", IssuingChainIssue = " + issuingIssue.getText() + " }";
+
+    EXPECT_EQ(bridge.getText(), expected);
+}
+
+TEST(STXChainBridge, getTextOnADefaultBridge)
+{
+    STXChainBridge const bridge;
+    auto const text = bridge.getText();
+
+    // The outer braces are literal, and the four field names appear in
+    // declaration order regardless of the values.
+    EXPECT_TRUE(text.starts_with("{ LockingChainDoor = "));
+    EXPECT_TRUE(text.ends_with(" }"));
+    EXPECT_LT(text.find("LockingChainIssue"), text.find("IssuingChainDoor"));
+    EXPECT_LT(text.find("IssuingChainDoor"), text.find("IssuingChainIssue"));
+}
diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp
index 974d0e81d7..e8c1b645d8 100644
--- a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp
+++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp
@@ -36,6 +36,8 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
     auto const referenceHoldingValue = canonical_UINT256();
     auto const issuerEncryptionKeyValue = canonical_VL();
     auto const auditorEncryptionKeyValue = canonical_VL();
+    auto const issuerKeyEpochValue = canonical_UINT32();
+    auto const auditorKeyEpochValue = canonical_UINT32();
     auto const confidentialOutstandingAmountValue = canonical_UINT64();
 
     MPTokenIssuanceBuilder builder{
@@ -57,6 +59,8 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
     builder.setReferenceHolding(referenceHoldingValue);
     builder.setIssuerEncryptionKey(issuerEncryptionKeyValue);
     builder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
+    builder.setIssuerKeyEpoch(issuerKeyEpochValue);
+    builder.setAuditorKeyEpoch(auditorKeyEpochValue);
     builder.setConfidentialOutstandingAmount(confidentialOutstandingAmountValue);
 
     builder.setLedgerIndex(index);
@@ -184,6 +188,22 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
         EXPECT_TRUE(entry.hasAuditorEncryptionKey());
     }
 
+    {
+        auto const& expected = issuerKeyEpochValue;
+        auto const actualOpt = entry.getIssuerKeyEpoch();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfIssuerKeyEpoch");
+        EXPECT_TRUE(entry.hasIssuerKeyEpoch());
+    }
+
+    {
+        auto const& expected = auditorKeyEpochValue;
+        auto const actualOpt = entry.getAuditorKeyEpoch();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfAuditorKeyEpoch");
+        EXPECT_TRUE(entry.hasAuditorKeyEpoch());
+    }
+
     {
         auto const& expected = confidentialOutstandingAmountValue;
         auto const actualOpt = entry.getConfidentialOutstandingAmount();
@@ -221,6 +241,8 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
     auto const referenceHoldingValue = canonical_UINT256();
     auto const issuerEncryptionKeyValue = canonical_VL();
     auto const auditorEncryptionKeyValue = canonical_VL();
+    auto const issuerKeyEpochValue = canonical_UINT32();
+    auto const auditorKeyEpochValue = canonical_UINT32();
     auto const confidentialOutstandingAmountValue = canonical_UINT64();
 
     auto sle = std::make_shared(MPTokenIssuance::entryType, index);
@@ -241,6 +263,8 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
     sle->at(sfReferenceHolding) = referenceHoldingValue;
     sle->at(sfIssuerEncryptionKey) = issuerEncryptionKeyValue;
     sle->at(sfAuditorEncryptionKey) = auditorEncryptionKeyValue;
+    sle->at(sfIssuerKeyEpoch) = issuerKeyEpochValue;
+    sle->at(sfAuditorKeyEpoch) = auditorKeyEpochValue;
     sle->at(sfConfidentialOutstandingAmount) = confidentialOutstandingAmountValue;
 
     MPTokenIssuanceBuilder builderFromSle{sle};
@@ -442,6 +466,32 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
         expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptionKey");
     }
 
+    {
+        auto const& expected = issuerKeyEpochValue;
+
+        auto const fromSleOpt = entryFromSle.getIssuerKeyEpoch();
+        auto const fromBuilderOpt = entryFromBuilder.getIssuerKeyEpoch();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfIssuerKeyEpoch");
+        expectEqualField(expected, *fromBuilderOpt, "sfIssuerKeyEpoch");
+    }
+
+    {
+        auto const& expected = auditorKeyEpochValue;
+
+        auto const fromSleOpt = entryFromSle.getAuditorKeyEpoch();
+        auto const fromBuilderOpt = entryFromBuilder.getAuditorKeyEpoch();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfAuditorKeyEpoch");
+        expectEqualField(expected, *fromBuilderOpt, "sfAuditorKeyEpoch");
+    }
+
     {
         auto const& expected = confidentialOutstandingAmountValue;
 
@@ -539,6 +589,10 @@ TEST(MPTokenIssuanceTests, OptionalFieldsReturnNullopt)
     EXPECT_FALSE(entry.getIssuerEncryptionKey().has_value());
     EXPECT_FALSE(entry.hasAuditorEncryptionKey());
     EXPECT_FALSE(entry.getAuditorEncryptionKey().has_value());
+    EXPECT_FALSE(entry.hasIssuerKeyEpoch());
+    EXPECT_FALSE(entry.getIssuerKeyEpoch().has_value());
+    EXPECT_FALSE(entry.hasAuditorKeyEpoch());
+    EXPECT_FALSE(entry.getAuditorKeyEpoch().has_value());
     EXPECT_FALSE(entry.hasConfidentialOutstandingAmount());
     EXPECT_FALSE(entry.getConfidentialOutstandingAmount().has_value());
 }
diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp
index f55d01f606..26dde55563 100644
--- a/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp
+++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/VaultTests.cpp
@@ -36,6 +36,9 @@ TEST(VaultTests, BuilderSettersRoundTrip)
     auto const withdrawalPolicyValue = canonical_UINT8();
     auto const scaleValue = canonical_UINT8();
     auto const lEVersionValue = canonical_UINT8();
+    auto const vaultKindValue = canonical_UINT8();
+    auto const subscriptionDateValue = canonical_UINT32();
+    auto const redemptionDateValue = canonical_UINT32();
 
     VaultBuilder builder{
         previousTxnIDValue,
@@ -56,6 +59,9 @@ TEST(VaultTests, BuilderSettersRoundTrip)
     builder.setLossUnrealized(lossUnrealizedValue);
     builder.setScale(scaleValue);
     builder.setLEVersion(lEVersionValue);
+    builder.setVaultKind(vaultKindValue);
+    builder.setSubscriptionDate(subscriptionDateValue);
+    builder.setRedemptionDate(redemptionDateValue);
 
     builder.setLedgerIndex(index);
     builder.setFlags(0x1u);
@@ -176,6 +182,30 @@ TEST(VaultTests, BuilderSettersRoundTrip)
         EXPECT_TRUE(entry.hasLEVersion());
     }
 
+    {
+        auto const& expected = vaultKindValue;
+        auto const actualOpt = entry.getVaultKind();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfVaultKind");
+        EXPECT_TRUE(entry.hasVaultKind());
+    }
+
+    {
+        auto const& expected = subscriptionDateValue;
+        auto const actualOpt = entry.getSubscriptionDate();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfSubscriptionDate");
+        EXPECT_TRUE(entry.hasSubscriptionDate());
+    }
+
+    {
+        auto const& expected = redemptionDateValue;
+        auto const actualOpt = entry.getRedemptionDate();
+        ASSERT_TRUE(actualOpt.has_value());
+        expectEqualField(expected, *actualOpt, "sfRedemptionDate");
+        EXPECT_TRUE(entry.hasRedemptionDate());
+    }
+
     EXPECT_TRUE(entry.hasLedgerIndex());
     auto const ledgerIndex = entry.getLedgerIndex();
     ASSERT_TRUE(ledgerIndex.has_value());
@@ -205,6 +235,9 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
     auto const withdrawalPolicyValue = canonical_UINT8();
     auto const scaleValue = canonical_UINT8();
     auto const lEVersionValue = canonical_UINT8();
+    auto const vaultKindValue = canonical_UINT8();
+    auto const subscriptionDateValue = canonical_UINT32();
+    auto const redemptionDateValue = canonical_UINT32();
 
     auto sle = std::make_shared(Vault::entryType, index);
 
@@ -224,6 +257,9 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
     sle->at(sfWithdrawalPolicy) = withdrawalPolicyValue;
     sle->at(sfScale) = scaleValue;
     sle->at(sfLEVersion) = lEVersionValue;
+    sle->at(sfVaultKind) = vaultKindValue;
+    sle->at(sfSubscriptionDate) = subscriptionDateValue;
+    sle->at(sfRedemptionDate) = redemptionDateValue;
 
     VaultBuilder builderFromSle{sle};
     EXPECT_TRUE(builderFromSle.validate());
@@ -415,6 +451,45 @@ TEST(VaultTests, BuilderFromSleRoundTrip)
         expectEqualField(expected, *fromBuilderOpt, "sfLEVersion");
     }
 
+    {
+        auto const& expected = vaultKindValue;
+
+        auto const fromSleOpt = entryFromSle.getVaultKind();
+        auto const fromBuilderOpt = entryFromBuilder.getVaultKind();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfVaultKind");
+        expectEqualField(expected, *fromBuilderOpt, "sfVaultKind");
+    }
+
+    {
+        auto const& expected = subscriptionDateValue;
+
+        auto const fromSleOpt = entryFromSle.getSubscriptionDate();
+        auto const fromBuilderOpt = entryFromBuilder.getSubscriptionDate();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfSubscriptionDate");
+        expectEqualField(expected, *fromBuilderOpt, "sfSubscriptionDate");
+    }
+
+    {
+        auto const& expected = redemptionDateValue;
+
+        auto const fromSleOpt = entryFromSle.getRedemptionDate();
+        auto const fromBuilderOpt = entryFromBuilder.getRedemptionDate();
+
+        ASSERT_TRUE(fromSleOpt.has_value());
+        ASSERT_TRUE(fromBuilderOpt.has_value());
+
+        expectEqualField(expected, *fromSleOpt, "sfRedemptionDate");
+        expectEqualField(expected, *fromBuilderOpt, "sfRedemptionDate");
+    }
+
     EXPECT_EQ(entryFromSle.getKey(), index);
     EXPECT_EQ(entryFromBuilder.getKey(), index);
 }
@@ -499,5 +574,11 @@ TEST(VaultTests, OptionalFieldsReturnNullopt)
     EXPECT_FALSE(entry.getScale().has_value());
     EXPECT_FALSE(entry.hasLEVersion());
     EXPECT_FALSE(entry.getLEVersion().has_value());
+    EXPECT_FALSE(entry.hasVaultKind());
+    EXPECT_FALSE(entry.getVaultKind().has_value());
+    EXPECT_FALSE(entry.hasSubscriptionDate());
+    EXPECT_FALSE(entry.getSubscriptionDate().has_value());
+    EXPECT_FALSE(entry.hasRedemptionDate());
+    EXPECT_FALSE(entry.getRedemptionDate().has_value());
 }
 }
diff --git a/src/tests/libxrpl/protocol_autogen/transactions/LoanBrokerCoverWithdrawTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/LoanBrokerCoverWithdrawTests.cpp
index 5b0a8c9146..043ab0a252 100644
--- a/src/tests/libxrpl/protocol_autogen/transactions/LoanBrokerCoverWithdrawTests.cpp
+++ b/src/tests/libxrpl/protocol_autogen/transactions/LoanBrokerCoverWithdrawTests.cpp
@@ -33,6 +33,7 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderSettersRoundTrip)
     auto const amountValue = canonical_AMOUNT();
     auto const destinationValue = canonical_ACCOUNT();
     auto const destinationTagValue = canonical_UINT32();
+    auto const credentialIDsValue = canonical_VECTOR256();
 
     LoanBrokerCoverWithdrawBuilder builder{
         accountValue,
@@ -45,6 +46,7 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderSettersRoundTrip)
     // Set optional fields
     builder.setDestination(destinationValue);
     builder.setDestinationTag(destinationTagValue);
+    builder.setCredentialIDs(credentialIDsValue);
 
     auto tx = builder.build(publicKey, secretKey);
 
@@ -90,6 +92,14 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderSettersRoundTrip)
         EXPECT_TRUE(tx.hasDestinationTag());
     }
 
+    {
+        auto const& expected = credentialIDsValue;
+        auto const actualOpt = tx.getCredentialIDs();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present";
+        expectEqualField(expected, *actualOpt, "sfCredentialIDs");
+        EXPECT_TRUE(tx.hasCredentialIDs());
+    }
+
 }
 
 // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
@@ -110,6 +120,7 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderFromStTxRoundTrip)
     auto const amountValue = canonical_AMOUNT();
     auto const destinationValue = canonical_ACCOUNT();
     auto const destinationTagValue = canonical_UINT32();
+    auto const credentialIDsValue = canonical_VECTOR256();
 
     // Build an initial transaction
     LoanBrokerCoverWithdrawBuilder initialBuilder{
@@ -122,6 +133,7 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderFromStTxRoundTrip)
 
     initialBuilder.setDestination(destinationValue);
     initialBuilder.setDestinationTag(destinationTagValue);
+    initialBuilder.setCredentialIDs(credentialIDsValue);
 
     auto initialTx = initialBuilder.build(publicKey, secretKey);
 
@@ -166,6 +178,13 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, BuilderFromStTxRoundTrip)
         expectEqualField(expected, *actualOpt, "sfDestinationTag");
     }
 
+    {
+        auto const& expected = credentialIDsValue;
+        auto const actualOpt = rebuiltTx.getCredentialIDs();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present";
+        expectEqualField(expected, *actualOpt, "sfCredentialIDs");
+    }
+
 }
 
 // 3) Verify wrapper throws when constructed from wrong transaction type.
@@ -229,6 +248,8 @@ TEST(TransactionsLoanBrokerCoverWithdrawTests, OptionalFieldsReturnNullopt)
     EXPECT_FALSE(tx.getDestination().has_value());
     EXPECT_FALSE(tx.hasDestinationTag());
     EXPECT_FALSE(tx.getDestinationTag().has_value());
+    EXPECT_FALSE(tx.hasCredentialIDs());
+    EXPECT_FALSE(tx.getCredentialIDs().has_value());
 }
 
 }
diff --git a/src/tests/libxrpl/protocol_autogen/transactions/VaultCreateTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/VaultCreateTests.cpp
index 9c1e14f6f4..592d40a6f6 100644
--- a/src/tests/libxrpl/protocol_autogen/transactions/VaultCreateTests.cpp
+++ b/src/tests/libxrpl/protocol_autogen/transactions/VaultCreateTests.cpp
@@ -36,6 +36,9 @@ TEST(TransactionsVaultCreateTests, BuilderSettersRoundTrip)
     auto const withdrawalPolicyValue = canonical_UINT8();
     auto const dataValue = canonical_VL();
     auto const scaleValue = canonical_UINT8();
+    auto const vaultKindValue = canonical_UINT8();
+    auto const subscriptionDateValue = canonical_UINT32();
+    auto const redemptionDateValue = canonical_UINT32();
 
     VaultCreateBuilder builder{
         accountValue,
@@ -51,6 +54,9 @@ TEST(TransactionsVaultCreateTests, BuilderSettersRoundTrip)
     builder.setWithdrawalPolicy(withdrawalPolicyValue);
     builder.setData(dataValue);
     builder.setScale(scaleValue);
+    builder.setVaultKind(vaultKindValue);
+    builder.setSubscriptionDate(subscriptionDateValue);
+    builder.setRedemptionDate(redemptionDateValue);
 
     auto tx = builder.build(publicKey, secretKey);
 
@@ -122,6 +128,30 @@ TEST(TransactionsVaultCreateTests, BuilderSettersRoundTrip)
         EXPECT_TRUE(tx.hasScale());
     }
 
+    {
+        auto const& expected = vaultKindValue;
+        auto const actualOpt = tx.getVaultKind();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfVaultKind should be present";
+        expectEqualField(expected, *actualOpt, "sfVaultKind");
+        EXPECT_TRUE(tx.hasVaultKind());
+    }
+
+    {
+        auto const& expected = subscriptionDateValue;
+        auto const actualOpt = tx.getSubscriptionDate();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSubscriptionDate should be present";
+        expectEqualField(expected, *actualOpt, "sfSubscriptionDate");
+        EXPECT_TRUE(tx.hasSubscriptionDate());
+    }
+
+    {
+        auto const& expected = redemptionDateValue;
+        auto const actualOpt = tx.getRedemptionDate();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRedemptionDate should be present";
+        expectEqualField(expected, *actualOpt, "sfRedemptionDate");
+        EXPECT_TRUE(tx.hasRedemptionDate());
+    }
+
 }
 
 // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
@@ -145,6 +175,9 @@ TEST(TransactionsVaultCreateTests, BuilderFromStTxRoundTrip)
     auto const withdrawalPolicyValue = canonical_UINT8();
     auto const dataValue = canonical_VL();
     auto const scaleValue = canonical_UINT8();
+    auto const vaultKindValue = canonical_UINT8();
+    auto const subscriptionDateValue = canonical_UINT32();
+    auto const redemptionDateValue = canonical_UINT32();
 
     // Build an initial transaction
     VaultCreateBuilder initialBuilder{
@@ -160,6 +193,9 @@ TEST(TransactionsVaultCreateTests, BuilderFromStTxRoundTrip)
     initialBuilder.setWithdrawalPolicy(withdrawalPolicyValue);
     initialBuilder.setData(dataValue);
     initialBuilder.setScale(scaleValue);
+    initialBuilder.setVaultKind(vaultKindValue);
+    initialBuilder.setSubscriptionDate(subscriptionDateValue);
+    initialBuilder.setRedemptionDate(redemptionDateValue);
 
     auto initialTx = initialBuilder.build(publicKey, secretKey);
 
@@ -226,6 +262,27 @@ TEST(TransactionsVaultCreateTests, BuilderFromStTxRoundTrip)
         expectEqualField(expected, *actualOpt, "sfScale");
     }
 
+    {
+        auto const& expected = vaultKindValue;
+        auto const actualOpt = rebuiltTx.getVaultKind();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfVaultKind should be present";
+        expectEqualField(expected, *actualOpt, "sfVaultKind");
+    }
+
+    {
+        auto const& expected = subscriptionDateValue;
+        auto const actualOpt = rebuiltTx.getSubscriptionDate();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfSubscriptionDate should be present";
+        expectEqualField(expected, *actualOpt, "sfSubscriptionDate");
+    }
+
+    {
+        auto const& expected = redemptionDateValue;
+        auto const actualOpt = rebuiltTx.getRedemptionDate();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfRedemptionDate should be present";
+        expectEqualField(expected, *actualOpt, "sfRedemptionDate");
+    }
+
 }
 
 // 3) Verify wrapper throws when constructed from wrong transaction type.
@@ -295,6 +352,12 @@ TEST(TransactionsVaultCreateTests, OptionalFieldsReturnNullopt)
     EXPECT_FALSE(tx.getData().has_value());
     EXPECT_FALSE(tx.hasScale());
     EXPECT_FALSE(tx.getScale().has_value());
+    EXPECT_FALSE(tx.hasVaultKind());
+    EXPECT_FALSE(tx.getVaultKind().has_value());
+    EXPECT_FALSE(tx.hasSubscriptionDate());
+    EXPECT_FALSE(tx.getSubscriptionDate().has_value());
+    EXPECT_FALSE(tx.hasRedemptionDate());
+    EXPECT_FALSE(tx.getRedemptionDate().has_value());
 }
 
 }
diff --git a/src/tests/libxrpl/protocol_autogen/transactions/VaultWithdrawTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/VaultWithdrawTests.cpp
index 4067a6551d..518957d47b 100644
--- a/src/tests/libxrpl/protocol_autogen/transactions/VaultWithdrawTests.cpp
+++ b/src/tests/libxrpl/protocol_autogen/transactions/VaultWithdrawTests.cpp
@@ -33,6 +33,7 @@ TEST(TransactionsVaultWithdrawTests, BuilderSettersRoundTrip)
     auto const amountValue = canonical_AMOUNT();
     auto const destinationValue = canonical_ACCOUNT();
     auto const destinationTagValue = canonical_UINT32();
+    auto const credentialIDsValue = canonical_VECTOR256();
 
     VaultWithdrawBuilder builder{
         accountValue,
@@ -45,6 +46,7 @@ TEST(TransactionsVaultWithdrawTests, BuilderSettersRoundTrip)
     // Set optional fields
     builder.setDestination(destinationValue);
     builder.setDestinationTag(destinationTagValue);
+    builder.setCredentialIDs(credentialIDsValue);
 
     auto tx = builder.build(publicKey, secretKey);
 
@@ -90,6 +92,14 @@ TEST(TransactionsVaultWithdrawTests, BuilderSettersRoundTrip)
         EXPECT_TRUE(tx.hasDestinationTag());
     }
 
+    {
+        auto const& expected = credentialIDsValue;
+        auto const actualOpt = tx.getCredentialIDs();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present";
+        expectEqualField(expected, *actualOpt, "sfCredentialIDs");
+        EXPECT_TRUE(tx.hasCredentialIDs());
+    }
+
 }
 
 // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
@@ -110,6 +120,7 @@ TEST(TransactionsVaultWithdrawTests, BuilderFromStTxRoundTrip)
     auto const amountValue = canonical_AMOUNT();
     auto const destinationValue = canonical_ACCOUNT();
     auto const destinationTagValue = canonical_UINT32();
+    auto const credentialIDsValue = canonical_VECTOR256();
 
     // Build an initial transaction
     VaultWithdrawBuilder initialBuilder{
@@ -122,6 +133,7 @@ TEST(TransactionsVaultWithdrawTests, BuilderFromStTxRoundTrip)
 
     initialBuilder.setDestination(destinationValue);
     initialBuilder.setDestinationTag(destinationTagValue);
+    initialBuilder.setCredentialIDs(credentialIDsValue);
 
     auto initialTx = initialBuilder.build(publicKey, secretKey);
 
@@ -166,6 +178,13 @@ TEST(TransactionsVaultWithdrawTests, BuilderFromStTxRoundTrip)
         expectEqualField(expected, *actualOpt, "sfDestinationTag");
     }
 
+    {
+        auto const& expected = credentialIDsValue;
+        auto const actualOpt = rebuiltTx.getCredentialIDs();
+        ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present";
+        expectEqualField(expected, *actualOpt, "sfCredentialIDs");
+    }
+
 }
 
 // 3) Verify wrapper throws when constructed from wrong transaction type.
@@ -229,6 +248,8 @@ TEST(TransactionsVaultWithdrawTests, OptionalFieldsReturnNullopt)
     EXPECT_FALSE(tx.getDestination().has_value());
     EXPECT_FALSE(tx.hasDestinationTag());
     EXPECT_FALSE(tx.getDestinationTag().has_value());
+    EXPECT_FALSE(tx.hasCredentialIDs());
+    EXPECT_FALSE(tx.getCredentialIDs().has_value());
 }
 
 }
diff --git a/src/tests/libxrpl/shamap/SHAMap.cpp b/src/tests/libxrpl/shamap/SHAMap.cpp
index e662e16be4..7f7d6ffba2 100644
--- a/src/tests/libxrpl/shamap/SHAMap.cpp
+++ b/src/tests/libxrpl/shamap/SHAMap.cpp
@@ -3,13 +3,16 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
@@ -113,7 +116,7 @@ protected:
     intToVuc(std::uint8_t v)
     {
         Buffer vuc{32};
-        std::fill_n(vuc.data(), vuc.size(), v);
+        vuc.fill(v);
         return vuc;
     }
 };
@@ -347,4 +350,149 @@ TEST_F(SHAMapPathProof, verify_proof_path)
     EXPECT_FALSE(map.verifyProofPath(rootHash, key, badPath));
 }
 
+// A legitimate proof path for two keys sharing all 63 leading nibbles is 65 elements: inner nodes
+// at depths 0..63 plus the leaf at depth 64. This pins that the 65 bound is real, so the fix for
+// the forged-path case below must not simply tighten the length limit.
+TEST_F(SHAMapPathProof, legitimate_deep_path_is_sixty_five_elements)
+{
+    tests::TestNodeFamily f{j_};
+    SHAMap map{SHAMapType::FREE, f};
+    map.setUnbacked();
+
+    auto const kA = uint256{std::string_view{std::string(63, 'a') + "1"}};
+    auto const kB = uint256{std::string_view{std::string(63, 'a') + "2"}};
+
+    for (auto const& k : {kA, kB})
+    {
+        Buffer vuc{32};
+        std::fill_n(vuc.data(), vuc.size(), std::uint8_t{1});
+        ASSERT_TRUE(map.addItem(SHAMapNodeType::TnAccountState, makeShamapitem(k, std::move(vuc))));
+    }
+    map.invariants();
+
+    auto const pathA = map.getProofPath(kA);
+    ASSERT_TRUE(pathA.has_value());
+    // NOLINTBEGIN(bugprone-unchecked-optional-access) has_value() checked above
+    EXPECT_EQ(pathA->size(), 65u);
+    EXPECT_TRUE(SHAMap::verifyProofPath(map.getHash().asUInt256(), kA, *pathA));
+    // NOLINTEND(bugprone-unchecked-optional-access)
+
+    auto const pathB = map.getProofPath(kB);
+    ASSERT_TRUE(pathB.has_value());
+    // NOLINTBEGIN(bugprone-unchecked-optional-access) has_value() checked above
+    EXPECT_EQ(pathB->size(), 65u);
+    EXPECT_TRUE(SHAMap::verifyProofPath(map.getHash().asUInt256(), kB, *pathB));
+    // NOLINTEND(bugprone-unchecked-optional-access)
+}
+
+// A forged path of 65 hash-chained inner nodes reaches depth kLeafDepth, where only the leaf
+// terminating the path may sit. Such a path must be rejected.
+TEST_F(SHAMapPathProof, all_inner_path_at_leaf_depth_is_rejected)
+{
+    // An arbitrary well-formed key; the test does not care about its specific value.
+    constexpr uint256 kTestKey("b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
+
+    // Build upwards from the deepest node so each parent's selected branch carries its child's hash
+    // and the hash chain validates at every level.
+    std::vector path;
+    SHAMapHash childHash{uint256{1}};
+
+    for (auto depth = SHAMap::kLeafDepth + 1u; depth-- > 0;)
+    {
+        auto const id = SHAMapNodeID::createID(std::min(depth, SHAMap::kLeafDepth - 1u), kTestKey);
+        auto const branch = selectBranch(id, kTestKey);
+
+        Serializer s;
+        for (auto i = 0u; i < SHAMap::kBranchFactor; ++i)
+            s.addBitString(i == branch ? childHash.asUInt256() : uint256{});
+        s.add8(kWireTypeInner);
+        path.push_back(s.getData());
+
+        auto node = SHAMapTreeNode::makeFromWire(makeSlice(path.back()));
+        ASSERT_TRUE(node);
+        node->updateHash();
+        childHash = node->getHash();
+    }
+
+    ASSERT_EQ(path.size(), 65u);
+    EXPECT_FALSE(SHAMap::verifyProofPath(childHash.asUInt256(), kTestKey, path));
+}
+
+/**
+ * Wrap a leaf blob in a forged root inner node whose branch for `key` carries that leaf's hash.
+ *
+ * The resulting two-element path hash-chains for `key` no matter which leaf sits at the bottom,
+ * which is exactly the substitution a peer could attempt.
+ *
+ * @param leafBlob the wire form of the leaf to place at the bottom of the path.
+ * @param key the key the forged path claims to prove.
+ * @return the path (deepest element first) and the forged root hash, or an empty path if the leaf
+ *         blob does not parse.
+ */
+static std::pair, uint256>
+forgeRootOverLeaf(Blob const& leafBlob, uint256 const& key)
+{
+    auto leaf = SHAMapTreeNode::makeFromWire(makeSlice(leafBlob));
+    if (!leaf || !leaf->isLeaf())
+        return {};
+    leaf->updateHash();
+
+    auto const branch = selectBranch(SHAMapNodeID::createID(0, key), key);
+    Serializer s;
+    for (auto i = 0u; i < SHAMap::kBranchFactor; ++i)
+        s.addBitString(i == branch ? leaf->getHash().asUInt256() : uint256{});
+    s.add8(kWireTypeInner);
+
+    auto root = SHAMapTreeNode::makeFromWire(makeSlice(s.peekData()));
+    if (!root)
+        return {};
+    root->updateHash();
+
+    return {std::vector{leafBlob, s.getData()}, root->getHash().asUInt256()};
+}
+
+// The hash chain above a leaf proves nothing about which key that leaf holds, so a peer can graft a
+// genuine leaf from elsewhere in the map onto a path forged for another key. Comparing the terminal
+// leaf's own key against the key being proved is what rejects it.
+TEST_F(SHAMapPathProof, substituted_leaf_for_other_key_is_rejected)
+{
+    tests::TestNodeFamily f{j_};
+    SHAMap map{SHAMapType::FREE, f};
+    map.setUnbacked();
+
+    // Two arbitrary keys differing in their first nibble, so each leaf hangs off the root directly.
+    constexpr uint256 kKey("1c8cec8e5e9b0e5e0e0f5b3e2c9f7a1d6b4e8c2a0d7f3b9e5c1a8d4f2b6e0c93");
+    constexpr uint256 kOtherKey("e3f1a7d5b9c2e8f406a1d3b5c7e9f2a4d6b8c0e2f4a6d8b0c2e4f6a8d0b2c4e6");
+
+    for (auto const& k : {kKey, kOtherKey})
+    {
+        ASSERT_TRUE(map.addItem(
+            SHAMapNodeType::TnAccountState, makeShamapitem(k, Slice{k.data(), k.size()})));
+    }
+    map.invariants();
+
+    auto const ownPath = map.getProofPath(kKey);
+    auto const otherPath = map.getProofPath(kOtherKey);
+    ASSERT_TRUE(ownPath.has_value());
+    ASSERT_TRUE(otherPath.has_value());
+
+    // NOLINTBEGIN(bugprone-unchecked-optional-access) has_value() checked above
+    // The genuine leaf blobs, deepest element first.
+    auto const& ownLeaf = ownPath->front();
+    auto const& otherLeaf = otherPath->front();
+    // NOLINTEND(bugprone-unchecked-optional-access)
+
+    // Control: the forged root is accepted when the leaf below it really is kKey's leaf, so the
+    // rejection below can only come from the leaf key comparison.
+    auto const [goodPath, goodRoot] = forgeRootOverLeaf(ownLeaf, kKey);
+    ASSERT_EQ(goodPath.size(), 2u);
+    EXPECT_TRUE(SHAMap::verifyProofPath(goodRoot, kKey, goodPath));
+
+    // Same forged root, but kOtherKey's leaf substituted at the bottom: the hash chain still
+    // validates, yet the path does not prove anything about kKey.
+    auto const [badPath, badRoot] = forgeRootOverLeaf(otherLeaf, kKey);
+    ASSERT_EQ(badPath.size(), 2u);
+    EXPECT_FALSE(SHAMap::verifyProofPath(badRoot, kKey, badPath));
+}
+
 }  // namespace xrpl::tests
diff --git a/src/tests/libxrpl/shamap/SHAMapNodeID.cpp b/src/tests/libxrpl/shamap/SHAMapNodeID.cpp
new file mode 100644
index 0000000000..95b7497c9c
--- /dev/null
+++ b/src/tests/libxrpl/shamap/SHAMapNodeID.cpp
@@ -0,0 +1,193 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+#include 
+
+namespace xrpl::tests {
+
+// An arbitrary 32-byte key reused across tests below that don't care about its specific value,
+// only that it is a well-formed key.
+constexpr uint256 kTestKey("b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
+
+TEST(SHAMapNodeIDTest, root_is_prefix_of_every_key)
+{
+    SHAMapNodeID const root;
+    EXPECT_EQ(root.getDepth(), 0u);
+    EXPECT_TRUE(root.isPrefixOf(uint256{}));
+    EXPECT_TRUE(root.isPrefixOf(kTestKey));
+}
+
+TEST(SHAMapNodeIDTest, child_id_is_prefix_of_keys_in_that_branch)
+{
+    // Walking the branches spelled by the key's own nibbles must keep every
+    // intermediate ID a prefix of that key.
+    SHAMapNodeID id;
+    for (auto depth = 0u; depth < SHAMap::kLeafDepth; ++depth)
+    {
+        id = id.getChildNodeID(selectBranch(id, kTestKey));
+        EXPECT_EQ(id.getDepth(), depth + 1);
+        EXPECT_TRUE(id.isPrefixOf(kTestKey)) << "depth " << id.getDepth();
+    }
+}
+
+TEST(SHAMapNodeIDTest, wrong_branch_is_not_prefix_of_key)
+{
+    SHAMapNodeID const root;
+    auto const correct = selectBranch(root, kTestKey);
+    ASSERT_EQ(correct, 0xbu);
+
+    // An ID built from the wrong branch still has a valid depth and a self-consistent mask, so
+    // isPrefixOf(kTestKey) below is what actually distinguishes the correct branch from the rest.
+    for (auto branch = 0u; branch < SHAMap::kBranchFactor; ++branch)
+    {
+        auto const child = root.getChildNodeID(branch);
+        EXPECT_EQ(child.getDepth(), 1u);
+        EXPECT_EQ(child.isPrefixOf(kTestKey), branch == correct) << "branch " << branch;
+    }
+}
+
+TEST(SHAMapNodeIDTest, prefix_check_is_depth_sensitive)
+{
+    // kTestKey and kOther agree on the first two nibbles ("b9") and then diverge.
+    constexpr uint256 kOther("b99891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
+
+    auto id = SHAMapNodeID{}.getChildNodeID(selectBranch(SHAMapNodeID{}, kTestKey));
+    EXPECT_TRUE(id.isPrefixOf(kTestKey));
+    EXPECT_TRUE(id.isPrefixOf(kOther)) << "shared first nibble";
+
+    id = id.getChildNodeID(selectBranch(id, kTestKey));
+    EXPECT_TRUE(id.isPrefixOf(kTestKey));
+    EXPECT_TRUE(id.isPrefixOf(kOther)) << "shared second nibble";
+
+    // Third nibble differs, so the deeper ID no longer covers kOther.
+    id = id.getChildNodeID(selectBranch(id, kTestKey));
+    EXPECT_TRUE(id.isPrefixOf(kTestKey));
+    EXPECT_FALSE(id.isPrefixOf(kOther));
+}
+
+TEST(SHAMapNodeIDTest, leaf_id_from_key_is_prefix_of_that_key)
+{
+    SHAMapNodeID const leaf{SHAMap::kLeafDepth, kTestKey};
+    EXPECT_TRUE(leaf.isPrefixOf(kTestKey));
+
+    // At full depth the prefix is the whole key, so nothing else matches.
+    constexpr uint256 kOther("b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca9");
+    EXPECT_FALSE(leaf.isPrefixOf(kOther));
+}
+
+TEST(SHAMapNodeIDTest, create_id_masks_key_to_depth)
+{
+    for (auto depth = 0u; depth <= SHAMap::kLeafDepth; ++depth)
+    {
+        auto const id = SHAMapNodeID::createID(depth, kTestKey);
+        EXPECT_EQ(id.getDepth(), depth);
+        EXPECT_TRUE(id.isPrefixOf(kTestKey)) << "depth " << depth;
+    }
+}
+
+// The guards below must hold with XRPL_ASSERT compiled out (NDEBUG), so each one
+// has to be a real runtime check rather than an assert.
+
+TEST(SHAMapNodeIDTest, child_of_leaf_depth_id_throws)
+{
+    auto const leafDepthID = SHAMapNodeID::createID(SHAMap::kLeafDepth, kTestKey);
+    ASSERT_EQ(leafDepthID.getDepth(), SHAMap::kLeafDepth);
+    EXPECT_THROW((void)leafDepthID.getChildNodeID(0), std::logic_error);
+}
+
+TEST(SHAMapNodeIDDeathTest, out_of_range_depth_is_clamped)
+{
+    // A depth past kLeafDepth has no mask in depthMask's 65-entry table, so both the constructor
+    // and createID clamp it. createID needs its own clamp: it picks the mask while evaluating the
+    // constructor's argument, so the constructor's clamp cannot cover that read.
+    //
+    // Both clamps are marked UNREACHABLE, which is an assert and therefore fatal wherever asserts
+    // are live. Only a build with them compiled out (or routed to Antithesis's non-fatal handler)
+    // reaches the clamp itself, so that is the only configuration that can assert on the result.
+#if defined(NDEBUG) || defined(ENABLE_VOIDSTAR)
+    for (auto const depth : {SHAMap::kLeafDepth + 1u, 100u, 255u, 256u, 320u})
+    {
+        auto const id = SHAMapNodeID::createID(depth, kTestKey);
+
+        // Clamped to a real depth, not the depth asked for, and not a byte-narrowed version of it:
+        // 256 would otherwise become 0 and name the root, 320 would become 64.
+        EXPECT_EQ(id.getDepth(), SHAMap::kLeafDepth) << "depth " << depth;
+
+        // id_ and depth_ still agree, so the object is usable rather than merely non-crashing.
+        EXPECT_TRUE(id.isPrefixOf(kTestKey)) << "depth " << depth;
+        EXPECT_EQ(id, SHAMapNodeID::createID(SHAMap::kLeafDepth, kTestKey)) << "depth " << depth;
+
+        // The clamp holds through the wire format too, which encodes the depth in one byte.
+        auto const roundTripped = deserializeSHAMapNodeID(id.getRawString());
+        ASSERT_TRUE(roundTripped.has_value()) << "depth " << depth;
+        EXPECT_EQ(roundTripped->getDepth(), SHAMap::kLeafDepth) << "depth " << depth;
+    }
+
+    // The constructor clamps on its own, for the paths that do not go through createID.
+    SHAMapNodeID const direct{SHAMap::kLeafDepth + 1u, uint256{}};
+    EXPECT_EQ(direct.getDepth(), SHAMap::kLeafDepth);
+#else
+    EXPECT_DEATH(
+        (void)SHAMapNodeID::createID(SHAMap::kLeafDepth + 1u, kTestKey), "depth within tree");
+#endif
+}
+
+TEST(SHAMapNodeIDDeathTest, select_branch_clamps_leaf_depth)
+{
+    // selectBranch's own precondition is depth < kLeafDepth: a depth-64 ID has no nibble left
+    // to select. That makes it unlike the guards above, which have a throw/return reachable
+    // even with XRPL_ASSERT compiled out; selectBranch has no such path, so the two build
+    // configurations have to be tested differently.
+    //
+    // Under ENABLE_VOIDSTAR, XRPL_ASSERT routes to Antithesis's assert_impl, which only records
+    // the hit and returns rather than aborting, even though NDEBUG is undefined there (voidstar
+    // requires a Debug build). So the assert is live in name but never fatal, the same as the
+    // NDEBUG case below.
+    auto const leafDepthID = SHAMapNodeID::createID(SHAMap::kLeafDepth, kTestKey);
+
+#if defined(NDEBUG) || defined(ENABLE_VOIDSTAR)
+    // With the assert compiled out or routed to a non-fatal handler, the clamp is what stands
+    // between this call and reading past the end of the 32-byte key. Clamping means it reads the
+    // same byte, and returns the same branch, as the deepest ID that still has one: depth 63.
+    auto const deepestWithBranchID = SHAMapNodeID::createID(SHAMap::kLeafDepth - 1u, kTestKey);
+    auto const branch = selectBranch(leafDepthID, kTestKey);
+    EXPECT_LT(branch, SHAMap::kBranchFactor);
+    EXPECT_EQ(branch, selectBranch(deepestWithBranchID, kTestKey));
+#else
+    // In a debug build the assert is live and must reject this call outright, in a forked
+    // process so a failure here cannot take down the rest of the suite.
+    EXPECT_DEATH((void)selectBranch(leafDepthID, kTestKey), "depth below leaf depth");
+#endif
+}
+
+TEST(SHAMapNodeIDTest, deserialize_rejects_out_of_range_depth)
+{
+    // getRawString() only serializes a depth already accepted by the constructor's own
+    // assertion, so an out-of-range depth here is built by hand instead.
+    auto serializeWithRawDepth = [](unsigned int depth) {
+        Serializer s;
+        s.addBitString(uint256{});
+        s.add8(static_cast(depth));
+        return s.getString();
+    };
+
+    for (auto const depth : {65u, 100u, 255u})
+    {
+        EXPECT_FALSE(deserializeSHAMapNodeID(serializeWithRawDepth(depth)).has_value())
+            << "depth " << depth;
+    }
+
+    // A depth-64 ID is legal, since leaves live there, but it has no children.
+    auto const id =
+        deserializeSHAMapNodeID(SHAMapNodeID{SHAMap::kLeafDepth, uint256{}}.getRawString());
+    ASSERT_TRUE(id.has_value());
+    // NOLINTNEXTLINE(bugprone-unchecked-optional-access) has_value checked above
+    EXPECT_THROW((void)id->getChildNodeID(0), std::logic_error);
+}
+
+}  // namespace xrpl::tests
diff --git a/src/tests/libxrpl/tx/wasm/Preflight.cpp b/src/tests/libxrpl/tx/wasm/Preflight.cpp
new file mode 100644
index 0000000000..095ef4ced8
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/Preflight.cpp
@@ -0,0 +1,248 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+namespace {
+
+// A contract the engine can run: it compiles, imports only a declared host function, and
+// exports the entry point as `() -> i32`.
+constexpr std::string_view kRunnableWat = R"wat(
+(module
+  (import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (call $ldgr_index (i32.const 0) (i32.const 4))))
+)wat";
+
+}  // namespace
+
+// `preflightEscrowWasm` takes no host, so this fixture holds none - which is the point of
+// the signature, and what deriving from `MockVmTest` would hide. Only a journal, to read the
+// refusal out of.
+struct PreflightTest : testing::Test
+{
+    CaptureSink sink{beast::Severity::Warning};
+
+    NotTEC
+    preflight(std::string_view wat, std::string_view funcName = escrowFunctionName)
+    {
+        return preflightEscrowWasm(assembleWat(wat), beast::Journal{sink}, funcName);
+    }
+
+    NotTEC
+    preflightBytes(Bytes const& wasm, std::string_view funcName = escrowFunctionName)
+    {
+        return preflightEscrowWasm(wasm, beast::Journal{sink}, funcName);
+    }
+
+    [[nodiscard]] std::string
+    logged() const
+    {
+        return sink.messages();
+    }
+};
+
+TEST_F(PreflightTest, RunnableContractPasses)
+{
+    EXPECT_EQ(preflight(kRunnableWat), tesSUCCESS);
+    EXPECT_TRUE(logged().empty()) << logged();
+}
+
+TEST_F(PreflightTest, GarbageIsRefused)
+{
+    EXPECT_EQ(preflightBytes(Bytes{}), temINVALID_BYTECODE);
+    EXPECT_EQ(preflightBytes(Bytes{0x00, 0x61, 0x73, 0x6d}), temINVALID_BYTECODE);
+}
+
+// The engine takes wasm binaries, and text is not one. The suite writes its modules as text
+// and assembles them, so this feeds the engine the very text the other tests assemble: a
+// transaction's validity must not depend on whether an assembler was linked in.
+TEST_F(PreflightTest, TextFormatModuleIsRefused)
+{
+    Bytes const text{kRunnableWat.begin(), kRunnableWat.end()};
+
+    EXPECT_EQ(preflightBytes(text), temINVALID_BYTECODE);
+    EXPECT_EQ(preflight(kRunnableWat), tesSUCCESS) << "the same module, assembled first";
+}
+
+TEST_F(PreflightTest, ImportOfAnUnknownHostFunctionIsRefused)
+{
+    constexpr std::string_view wat = R"wat(
+    (module
+      (import "host_lib" "no_such_function" (func $f (param i32) (result i32)))
+      (memory (export "memory") 1)
+      (func (export "escrow_finish") (result i32) (call $f (i32.const 0))))
+    )wat";
+
+    EXPECT_EQ(preflight(wat), temINVALID_BYTECODE);
+    EXPECT_THAT(logged(), testing::HasSubstr("no host function 'no_such_function'"));
+}
+
+// Host functions are registered under one module name. `env` is what plain clang emits, so a
+// contract built without the SDK's import attributes lands here.
+TEST_F(PreflightTest, ImportFromAnotherModuleIsRefused)
+{
+    constexpr std::string_view wat = R"wat(
+    (module
+      (import "env" "ldgr_index" (func $f (param i32 i32) (result i32)))
+      (memory (export "memory") 1)
+      (func (export "escrow_finish") (result i32) (i32.const 0)))
+    )wat";
+
+    EXPECT_EQ(preflight(wat), temINVALID_BYTECODE);
+    EXPECT_THAT(logged(), testing::HasSubstr("is not from 'host_lib'"));
+}
+
+// A contract asking for more linear memory than the engine grants can never run, so it is
+// refused before it can be escrowed. The cap itself is granted.
+TEST_F(PreflightTest, MemoryPastTheCapIsRefused)
+{
+    constexpr std::string_view tooMuch = R"wat(
+    (module
+      (memory (export "memory") 129)
+      (func (export "escrow_finish") (result i32) (i32.const 0)))
+    )wat";
+
+    EXPECT_EQ(preflight(tooMuch), temINVALID_BYTECODE);
+    EXPECT_THAT(logged(), testing::HasSubstr("memory: initial memory of 129 pages"));
+
+    constexpr std::string_view atTheCap = R"wat(
+    (module
+      (memory (export "memory") 128)
+      (func (export "escrow_finish") (result i32) (i32.const 0)))
+    )wat";
+
+    EXPECT_EQ(preflight(atTheCap), tesSUCCESS);
+}
+
+// A table is allocated in full at instantiation, before any gas is charged, so an oversized
+// one is refused before it can be escrowed. Screening sees only an *exported* table; the
+// store's limiter is what refuses the table a contract keeps to itself.
+TEST_F(PreflightTest, TablePastTheCapIsRefused)
+{
+    constexpr std::string_view tooMuch = R"wat(
+    (module
+      (memory (export "memory") 1)
+      (table (export "t") 1025 funcref)
+      (func (export "escrow_finish") (result i32) (i32.const 0)))
+    )wat";
+
+    EXPECT_EQ(preflight(tooMuch), temINVALID_BYTECODE);
+    EXPECT_THAT(logged(), testing::HasSubstr("table: initial table of 1025 elements"));
+
+    constexpr std::string_view atTheCap = R"wat(
+    (module
+      (memory (export "memory") 1)
+      (table (export "t") 1024 funcref)
+      (func (export "escrow_finish") (result i32) (i32.const 0)))
+    )wat";
+
+    EXPECT_EQ(preflight(atTheCap), tesSUCCESS);
+}
+
+TEST_F(PreflightTest, MissingEntryPointIsRefused)
+{
+    constexpr std::string_view wat = R"wat(
+    (module
+      (memory (export "memory") 1)
+      (func (export "other") (result i32) (i32.const 0)))
+    )wat";
+
+    EXPECT_EQ(preflight(wat), temINVALID_BYTECODE);
+    EXPECT_THAT(logged(), testing::HasSubstr("no entry point 'escrow_finish'"));
+}
+
+TEST_F(PreflightTest, EntryPointOfTheWrongTypeIsRefused)
+{
+    constexpr std::string_view wat = R"wat(
+    (module
+      (memory (export "memory") 1)
+      (func (export "escrow_finish") (result i64) (i64.const 0)))
+    )wat";
+
+    EXPECT_EQ(preflight(wat), temINVALID_BYTECODE);
+    EXPECT_THAT(logged(), testing::HasSubstr("has the wrong signature"));
+}
+
+// Screening is for the entry point the caller names, as a run is: a contract screened for one
+// export says nothing about another.
+TEST_F(PreflightTest, EntryPointIsTheNameTheCallerGives)
+{
+    constexpr std::string_view wat = R"wat(
+    (module
+      (memory (export "memory") 1)
+      (func (export "other") (result i32) (i32.const 0)))
+    )wat";
+
+    EXPECT_EQ(preflight(wat, "other"), tesSUCCESS);
+    EXPECT_EQ(preflight(wat), temINVALID_BYTECODE);
+}
+
+// Every refusal is logged with the engine's own description and the TER: without it a node
+// operator has a `temINVALID_BYTECODE` and no way to tell a contract author which of the three
+// stages refused the module.
+TEST_F(PreflightTest, RefusalNamesTheReasonAndTheTer)
+{
+    EXPECT_EQ(preflightBytes(Bytes{0x00, 0x61, 0x73, 0x6d}), temINVALID_BYTECODE);
+
+    EXPECT_THAT(logged(), testing::HasSubstr("compile: "));
+    EXPECT_THAT(logged(), testing::HasSubstr(transToken(temINVALID_BYTECODE)));
+}
+
+// A module that passes screening still has to pass the run's own stages, and one that fails
+// screening would have failed the run. Same modules through both entry points, so the two do
+// not have to be trusted to agree.
+TEST_F(PreflightTest, ScreeningAgreesWithARun)
+{
+    struct Case
+    {
+        std::string_view label;
+        std::string_view wat;
+        bool passes;
+    };
+
+    // clang-format off
+    constexpr Case cases[]{
+        {.label = "a runnable contract", .wat = kRunnableWat, .passes = true},
+        {.label = "an unknown host function",
+         .wat = R"wat((module (import "host_lib" "nope" (func $f (result i32)))
+                        (memory (export "memory") 1)
+                        (func (export "escrow_finish") (result i32) (call $f))))wat",
+         .passes = false},
+        {.label = "no entry point",
+         .wat = R"wat((module (memory (export "memory") 1)
+                        (func (export "other") (result i32) (i32.const 0))))wat",
+         .passes = false},
+    };
+    // clang-format on
+
+    for (auto const& [label, wat, passes] : cases)
+    {
+        auto const screened = preflight(wat);
+        EXPECT_EQ(isTesSuccess(screened), passes) << label;
+
+        // The run's own verdict on the same bytes. A refused module must not reach the
+        // contract's first instruction; an accepted one must get past the entry-point
+        // lookup, whatever it then does.
+        testing::StrictMock host{beast::Journal{sink}};
+        EXPECT_CALL(host, checkSelf()).WillRepeatedly(testing::Return(true));
+        EXPECT_CALL(host, getLedgerSqn()).WillRepeatedly(testing::Return(7u));
+
+        auto const ran = runEscrowWasm(assembleWat(wat), host, 100'000);
+        EXPECT_EQ(ran.has_value(), passes) << label;
+    }
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/README.md b/src/tests/libxrpl/tx/wasm/README.md
new file mode 100644
index 0000000000..46ef6379ff
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/README.md
@@ -0,0 +1,92 @@
+# WASM host-function tests — layering
+
+These tests are deliberately **layered**: each isolates one thing, so a failure points at one
+place instead of "somewhere in the stack." If a folder looks thin, the breadth it seems to be
+missing lives in a sibling layer.
+
+## The layers
+
+| Layer                                 | Location                                            | host | VM  | ledger | Answers                                                                                  |
+| ------------------------------------- | --------------------------------------------------- | ---- | --- | ------ | ---------------------------------------------------------------------------------------- |
+| Engine / gas / limits / ABI           | `crates/xrpl-wasm-vm`, `crates/xrpl-host-functions` | mock | ✓   | ✗      | gas, transfer budget, memory/field limits, preflight, VM limits, generated ABI           |
+| `host_context/` (`HostContextTest`)   | `.../host_context`                                  | mock | ✗   | ✗      | the `HostContext` marshalling shim alone (byte order, buffer sizing, `SField` xlat)      |
+| `host_calls/` (`HostCallTest`)        | `.../host_calls`                                    | mock | ✓   | ✗      | per-function **wire contract** — what the host was asked, what came back                 |
+| `host_functions/` (`RealHostFixture`) | `.../host_functions`                                | real | ✗   | real   | each function's **actual answer** vs. a real `TxTest` ledger                             |
+| `e2e/` (`RealVmTest`)                 | `.../e2e`                                           | real | ✓   | real   | **full-stack integration** — VM + `HostContext` + real impl + real ledger                |
+| `transactor/` (`TxTest`)              | `.../transactor`                                    | real | ✓   | real   | the **transactor** around a contract — fees, reserves, limits, what each failure reports |
+
+Run the C++ side with:
+
+```bash
+./build/xrpl_tests --gtest_filter='*Impl.*:*Call.*:*E2e.*:WasmVMTest.*:WasmVMDeathTest.*:PreflightTest.*:BytecodeSize.*:BytecodePreflight.*:FinishFailures.*:BytecodeRun.*:GasFees.*:DataOnReject.*'
+```
+
+(744 tests, 142 suites.) The engine-level coverage is Rust: `cd crates && cargo test`.
+
+## `fixtures/` — split by whether it needs a test framework
+
+|                                                |                                                                                                                                                                                                                                                                                    |
+| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **No GTest** — the `xrpl.testkit.wasm` library | `WasmLedger` (real genesis ledger + the real host over it), `WasmRun` (WAT assembler), `NftSetup`, `FloatConstants`                                                                                                                                                                |
+| **GTest** → `xrpl_tests`                       | `RealHostFixture` (`: testing::Test, WasmLedger` + `expectValue`/`expectError`/`expectKeyletMatches`), `FloatFixture`, `NFTFixture`, `MockHostFunctions`, `WasmFixture`, `RealVmTest`, `HostContextFixture`, `EscrowWasm` (transactor contracts + fee arithmetic), `ModuleBuilder` |
+
+The split exists because a benchmark wants a ledger and a host, not GTest's lifecycle:
+`xrpl.bench.wasm` links no GTest and no GMock at all.
+
+Setup steps in `WasmLedger` and `NftSetup` **throw** (`fixtureFailed`) rather than using `EXPECT_`.
+An `EXPECT_` outside a running test is recorded and discarded, so a benchmark whose escrow was
+never created would still run its host call, take the not-found path, and report a cheap,
+plausible, completely wrong price. **If you add a setup step that can fail, throw.**
+
+## Gas calibration
+
+The benchmarks pricing these host functions live in `src/benchmarks/libxrpl/wasm/`, mirroring this
+tree one file per function, with their own README.
+
+## What `e2e/` covers — the rule
+
+**`e2e/` covers every marshalling shape and cross-call convention exactly once. It does not cover
+every function.** That is a completeness claim on the axis e2e uniquely tests, not a sample.
+
+`host_calls` pins what the bridge _asks_ with a canned answer; `host_functions` pins what the real
+impl _answers_. The type system guarantees they agree on signatures, but nothing guarantees they
+agree on **conventions** — units, endianness, buffer layout — because in neither test does a real
+guest write bytes a real host reads. That is the `seq`-as-little-endian-region bug: every internal
+test passed, and it was caught by cross-checking the guest SDK.
+
+Convention mismatch is a property of a call's **shape**, not of the function — all 19 keylets share
+one shape, so a 19th keylet e2e proves nothing the 1st did. The inventory is meant to be exhaustive:
+
+| Shape / convention                        | Covered by                 | Why it is its own row                                    |
+| ----------------------------------------- | -------------------------- | -------------------------------------------------------- |
+| no-input scalar getter                    | `LedgerSqnE2e`             | header read; the minimal call                            |
+| field code in, bytes out (ledger object)  | `CurrentLedgerObjFieldE2e` | `SField` translation over a real object                  |
+| field code in, bytes out (transaction)    | `TxFieldE2e`               | a different source than a ledger object                  |
+| region in, bytes out + `u32` region       | `CacheLedgerObjE2e`        | the 4-byte little-endian region convention               |
+| slot in, bytes out — **cross-call state** | `CacheLedgerObjE2e`        | the slot table is the only host state outliving one call |
+| locator (path of i32 steps)               | `TxNestedFieldE2e`         | a wire format the guest writes and the host walks        |
+| **two** output regions                    | `FloatToMantExpE2e`        | two bounds checks, two writes, an ordering between them  |
+| write / mutation                          | `SetDataE2e`               | the one thing a contract changes                         |
+| **error** path from a real impl           | `HostErrorE2e`             | a soft code from a real failure, not a staged one        |
+| realistic multi-call contract             | `HostFunctionTourE2e`      | the old `all_host_functions` tour shape, as one test     |
+
+Adding a function needs no new e2e case unless it introduces a shape not in that table. Per-function
+breadth lives in `host_functions/` and `host_calls/`, one case each.
+
+## Out of scope
+
+**The guest SDK** (`xrpl-std` / `xrpl-escrow`, external `xrpl-wasm-stdlib` repo) is the SDK repo's
+own suite. These tests hand-write the ABI in WAT (raw imports, literal field codes, hand-built byte
+layouts), deliberately bypassing all SDK code. Agreement is verified _transitively_: the SDK repo
+tests the SDK against the ABI spec, this repo tests the host against the same spec. That would not
+catch a drift where both diverge on an ambiguous point; closing it needs a **cross-repo integration
+test** (compiled guests against a real host) in CI, where the Rust→wasm toolchain exists.
+
+## Adding to `transactor/`
+
+Things to know:
+
+- **Fees live in two places and must agree.** A transactor reads its limits from the service
+  registry (`ctx.registry.get().getFees()`), while `calculateBaseFee` reads `view.fees()`. Pass a
+  `Fees` to `TxTest`'s constructor to set both; reach for `getServiceRegistry().setFees` only when
+  a limit has to change _after_ setup.
diff --git a/src/tests/libxrpl/tx/wasm/WasmVM.cpp b/src/tests/libxrpl/tx/wasm/WasmVM.cpp
new file mode 100644
index 0000000000..59d1c9a2f2
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/WasmVM.cpp
@@ -0,0 +1,324 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+namespace {
+
+// One module with an export per way a run can end. Kept together because these are properties
+// of the engine rather than of any host function: the only import is there so the
+// out-of-gas and no-memory cases have a host call to fail in.
+constexpr std::string_view kEngineWat = R"wat(
+(module
+  (import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
+  (memory (export "memory") 1)
+
+  (func (export "escrow_finish") (result i32) (i32.const 5))
+
+  (func (export "calls_the_host") (result i32)
+    (call $ldgr_index (i32.const 0) (i32.const 4)))
+
+  (func (export "traps") (result i32) unreachable)
+
+  (func (export "never_returns") (result i32) (loop (br 0)) (i32.const 0))
+
+  (func (export "wrong_signature") (param i32) (result i32) (local.get 0))
+
+  (global (export "not_a_function") i32 (i32.const 0)))
+)wat";
+
+// The same host call with no memory exported, so the engine has nothing to resolve a byte
+// region against.
+constexpr std::string_view kNoMemoryWat = R"wat(
+(module
+  (import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
+  (func (export "escrow_finish") (result i32)
+    (call $ldgr_index (i32.const 0) (i32.const 4))))
+)wat";
+
+}  // namespace
+
+class WasmVMTest : public MockVmTest
+{
+};
+
+TEST_F(WasmVMTest, ContractReturnValueReachesCaller)
+{
+    auto const outcome = run(kEngineWat);
+
+    ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
+    EXPECT_EQ(outcome->result, 5);
+    EXPECT_GT(outcome->cost, 0) << "running any instruction costs gas";
+    EXPECT_LT(outcome->cost, kAmpleGas);
+}
+
+TEST_F(WasmVMTest, GuestTrapIsChargedAsContractFault)
+{
+    auto const outcome = run(kEngineWat, kAmpleGas, "traps");
+
+    ASSERT_FALSE(outcome.has_value());
+    EXPECT_EQ(outcome.error().ter, tecFAILED_PROCESSING);
+    ASSERT_TRUE(outcome.error().cost.has_value());
+    EXPECT_GT(*outcome.error().cost, 0);  // NOLINT(bugprone-unchecked-optional-access)
+}
+
+TEST_F(WasmVMTest, NonTerminatingContractSpendsWholeBudget)
+{
+    auto const outcome = run(kEngineWat, kAmpleGas, "never_returns");
+
+    ASSERT_FALSE(outcome.has_value());
+    EXPECT_EQ(outcome.error().ter, tecOUT_OF_GAS);
+    ASSERT_TRUE(outcome.error().cost.has_value());
+
+    // The cost break down is as follows:
+    // 1. There is a function entry charge (finish function) which seems to be 63 units of fuel.
+    // 2. Each iteration costs 2 units of fuel.
+    // For a GAS amount of 100,000, we will be limited to burning an odd number of fuel.
+    // So the way the test is written, the most fuel that will be used is 99,999 units.
+    EXPECT_EQ(*outcome.error().cost, kAmpleGas - 1);  // NOLINT(bugprone-unchecked-optional-access)
+}
+
+// A budget too small to reach the first host charge is still out of gas, whatever the engine
+// can account for by then.
+TEST_F(WasmVMTest, BudgetTooSmallToRunIsOutOfGas)
+{
+    auto const outcome = run(kEngineWat, 1, "calls_the_host");
+
+    ASSERT_FALSE(outcome.has_value());
+    EXPECT_EQ(outcome.error().ter, tecOUT_OF_GAS);
+    EXPECT_TRUE(outcome.error().cost.has_value());
+}
+
+// A host call needs a memory to resolve its byte regions against, and the export is not
+// optional for a contract that makes one.
+TEST_F(WasmVMTest, HostCallWithNoExportedMemoryFails)
+{
+    auto const outcome = run(kNoMemoryWat);
+
+    ASSERT_FALSE(outcome.has_value());
+    EXPECT_EQ(outcome.error().ter, tecFAILED_PROCESSING);
+    EXPECT_TRUE(outcome.error().cost.has_value());
+}
+
+// A module that will not instantiate is the contract's fault and is charged, not the node's.
+// Screening does not see every way this happens - a linear memory the module keeps to itself
+// is absent from its exports - so such a module can pass preflight and still be refused here.
+TEST_F(WasmVMTest, ModuleThatWillNotInstantiateIsChargedToTheContract)
+{
+    // 129 pages, not exported, so nothing outside the module declares it.
+    static constexpr std::string_view wat = R"wat(
+    (module
+      (memory 129)
+      (func (export "escrow_finish") (result i32) (i32.const 0)))
+    )wat";
+
+    EXPECT_EQ(preflightEscrowWasm(assembleWat(wat), beast::Journal{sink}), tesSUCCESS)
+        << "screening cannot see an unexported memory";
+
+    auto const outcome = run(wat);
+
+    ASSERT_FALSE(outcome.has_value());
+    EXPECT_EQ(outcome.error().ter, tecFAILED_PROCESSING);
+    EXPECT_TRUE(outcome.error().cost.has_value());
+}
+
+// Preflight is meant to refuse these with `temINVALID_BYTECODE`; reaching apply means the screening
+// did not happen, which is the node's fault and not the transaction's.
+TEST_F(WasmVMTest, UnrunnableModuleIsNodeSideFault)
+{
+    struct Case
+    {
+        char const* what;
+        Bytes code;
+        std::string_view entryPoint;
+    };
+    std::array const cases = {
+        Case{
+            .what = "not wasm at all", .code = Bytes{0, 1, 2, 3}, .entryPoint = escrowFunctionName},
+        Case{.what = "empty", .code = Bytes{}, .entryPoint = escrowFunctionName},
+        Case{
+            .what = "no such export", .code = assemble(kEngineWat), .entryPoint = "no_such_export"},
+        Case{
+            .what = "export is not a function",
+            .code = assemble(kEngineWat),
+            .entryPoint = "not_a_function"},
+        Case{
+            .what = "export takes a parameter",
+            .code = assemble(kEngineWat),
+            .entryPoint = "wrong_signature"},
+    };
+
+    for (auto const& c : cases)
+    {
+        auto const outcome = runBytes(c.code, kAmpleGas, c.entryPoint);
+
+        ASSERT_FALSE(outcome.has_value()) << c.what;
+        EXPECT_EQ(outcome.error().ter, tecINTERNAL) << c.what;
+        EXPECT_FALSE(outcome.error().cost.has_value()) << c.what;
+    }
+}
+
+// wasmi's `wat` feature would make `Module::new` accept text as readily as binary, which would
+// put an assembler on the consensus path and make a module's validity a build flag. The
+// engine turns that feature off; this is the guest-side proof, using the very text the rest
+// of this file assembles.
+TEST_F(WasmVMTest, TextFormatModuleIsRejected)
+{
+    Bytes const text{kEngineWat.begin(), kEngineWat.end()};
+
+    auto const outcome = runBytes(text);
+
+    ASSERT_FALSE(outcome.has_value());
+    EXPECT_EQ(outcome.error().ter, tecINTERNAL);
+}
+
+// A soft host error is the contract's to interpret, so its code has to cross the boundary
+// unchanged: the engine must not renumber it, clamp it, or turn it into a failure of its own.
+//
+// Over the whole of `HostFunctionError` rather than a sample, because `HostFunctionError` and
+// the Rust ABI's `HostError` are two hand-maintained lists of the same wire numbers: -1
+// through -20 have to mean the same thing on both sides, and this is the test that notices if
+// either side renumbers.
+//
+// The two exclusions are the codes the Rust engine converts into a fault, which stops the run
+// instead of reaching the guest: -1 `Unimplemented` and -14 `NoMemExported`. Both say the call
+// was not served at all.
+TEST_F(WasmVMTest, SoftHostErrorCodesCrossUnchanged)
+{
+    static constexpr HostFunctionError kSoftErrors[] = {
+        HostFunctionError::FieldNotFound,
+        HostFunctionError::BufferTooSmall,
+        HostFunctionError::NoArray,
+        HostFunctionError::NotLeafField,
+        HostFunctionError::LocatorMalformed,
+        HostFunctionError::SlotOutRange,
+        HostFunctionError::SlotsFull,
+        HostFunctionError::EmptySlot,
+        HostFunctionError::LedgerObjNotFound,
+        HostFunctionError::OutOfTransferLimit,
+        HostFunctionError::DataFieldTooLarge,
+        HostFunctionError::PointerOutOfBounds,
+        HostFunctionError::InvalidParams,
+        HostFunctionError::InvalidAccount,
+        HostFunctionError::InvalidField,
+        HostFunctionError::IndexOutOfBounds,
+        HostFunctionError::FloatInputMalformed,
+        HostFunctionError::FloatComputationError,
+    };
+
+    auto refused = HostFunctionError::FieldNotFound;
+    EXPECT_CALL(host, getLedgerSqn())
+        .WillRepeatedly([&refused]() -> std::expected {
+            return std::unexpected(refused);
+        });
+
+    for (auto const error : kSoftErrors)
+    {
+        refused = error;
+
+        auto const outcome = run(kEngineWat, kAmpleGas, "calls_the_host");
+
+        ASSERT_TRUE(outcome.has_value()) << hfErrorToInt(error) << " stopped the run";
+        EXPECT_EQ(outcome->result, hfErrorToInt(error));
+    }
+}
+
+// The counterpart: a fatal code stops the run rather than reaching the contract, so a host
+// that cannot serve a call cannot be second-guessed by the contract.
+TEST_F(WasmVMTest, FatalHostErrorStopsRun)
+{
+    auto refused = HostFunctionError::Unimplemented;
+    EXPECT_CALL(host, getLedgerSqn())
+        .WillRepeatedly([&refused]() -> std::expected {
+            return std::unexpected(refused);
+        });
+
+    for (auto const error :
+         {HostFunctionError::InternalFatal,
+          HostFunctionError::Unimplemented,
+          HostFunctionError::NoMemExported})
+    {
+        refused = error;
+
+        auto const outcome = run(kEngineWat, kAmpleGas, "calls_the_host");
+
+        ASSERT_FALSE(outcome.has_value()) << hfErrorToInt(error) << " reached the contract";
+    }
+}
+
+// The point of the bridge's C++ half: an exception must not reach the Rust frames that called
+// the host, and must not take the node with it.
+TEST_F(WasmVMTest, ThrowingHostFunctionBecomesInternal)
+{
+    EXPECT_CALL(host, getLedgerSqn())
+        .WillOnce([]() -> std::expected {
+            Throw("the ledger came apart");
+        });
+
+    auto const outcome = run(kEngineWat, kAmpleGas, "calls_the_host");
+
+    ASSERT_FALSE(outcome.has_value());
+    EXPECT_EQ(outcome.error().ter, tecINTERNAL);
+    EXPECT_FALSE(outcome.error().cost.has_value()) << "a node-side fault charges nothing";
+    // Caught is not swallowed: the condition has to be recorded, and the line has to name the
+    // call it came out of.
+    EXPECT_THAT(logged(), testing::HasSubstr("the ledger came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getLedgerSqn"));
+}
+
+struct WasmVMDeathTest : WasmVMTest
+{
+};
+
+// No gas is not a small budget, it is a malformed transaction — refused before the engine is
+// asked to run anything.
+TEST_F(WasmVMDeathTest, NoGasIsRefusedAsMalformedRatherThanRun)
+{
+    for (auto const gas : {std::int64_t{0}, std::int64_t{-1}})
+    {
+        EXPECT_DEBUG_DEATH(
+            {
+                auto const outcome = run(kEngineWat, gas);
+
+                ASSERT_FALSE(outcome.has_value()) << "gas: " << gas;
+                EXPECT_EQ(outcome.error().ter, temBAD_AMOUNT) << "gas: " << gas;
+                EXPECT_FALSE(outcome.error().cost.has_value()) << "gas: " << gas;
+            },
+            "gas limit is positive");
+    }
+}
+
+// The host caches the current ledger object, the slot table and the contract's data for the
+// length of one run, so a reused one would answer a later contract out of an earlier
+// contract's state.
+TEST_F(WasmVMDeathTest, DirtyHostIsRefusedBeforeContractRuns)
+{
+    EXPECT_DEBUG_DEATH(
+        {
+            EXPECT_CALL(host, checkSelf()).WillOnce(testing::Return(false));
+            auto const outcome = run(kEngineWat);
+
+            ASSERT_FALSE(outcome.has_value());
+            EXPECT_EQ(outcome.error().ter, tecINTERNAL);
+            EXPECT_FALSE(outcome.error().cost.has_value());
+            EXPECT_THAT(logged(), testing::HasSubstr("not clean"));
+        },
+        "host functions not clean before the run");
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/e2e/CacheLedgerObj.cpp b/src/tests/libxrpl/tx/wasm/e2e/CacheLedgerObj.cpp
new file mode 100644
index 0000000000..80635fee76
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/e2e/CacheLedgerObj.cpp
@@ -0,0 +1,63 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The keylet -> cache -> read round trip: the only place a contract's host calls depend on
+// each other. Every other e2e case here is one call in isolation. This one is three, and each
+// consumes what the last produced: `accountroot_id` computes a key into guest memory, `cache_le`
+// hands those same bytes back to the host and answers with a slot number, and `le_field`
+// uses that slot to read the object. The slot table is the one piece of host state that
+// outlives a single call, so this is the only test at any layer that can catch the two ends
+// of that state disagreeing — `host_calls` mocks the host, so its slot numbers are whatever
+// the mock was told to return, and `host_functions` calls the impl directly, so its slots
+// never cross the guest boundary at all.
+struct CacheLedgerObjE2e : RealVmTest
+{
+};
+
+TEST_F(CacheLedgerObjE2e, ContractComputesAKeyCachesTheObjectAndReadsItsField)
+{
+    auto const owner = fund("owner");
+    auto const wat = std::format(
+        R"wat(
+(module
+  (import "host_lib" "accountroot_id" (func $accountroot_id (param i32 i32 i32 i32) (result i32)))
+  (import "host_lib" "cache_le" (func $cache_le (param i32 i32 i32) (result i32)))
+  (import "host_lib" "le_field" (func $le_field (param i32 i32 i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (data (i32.const 0) "{}")
+  (func (export "escrow_finish") (result i32)
+    (local $slot i32)
+    (local $r i32)
+    ;; The account's AccountRoot keylet, computed by the host into offset 64.
+    (local.set $r (call $accountroot_id (i32.const 0) (i32.const 20) (i32.const 64) (i32.const 32)))
+    (if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
+    ;; Those same 32 bytes handed straight back: cache the object they name.
+    (local.set $slot (call $cache_le (i32.const 64) (i32.const 32) (i32.const 0)))
+    (if (i32.lt_s (local.get $slot) (i32.const 0)) (then (return (local.get $slot))))
+    ;; And read a field of it through the slot the host just assigned.
+    (call $le_field (local.get $slot) (i32.const {}) (i32.const 128) (i32.const 32))))
+)wat",
+        watEscaped(RealHostFixture::toBytes(owner.id())),
+        sfAccount.getCode());
+
+    auto const outcome = run(wat);
+    ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
+    // 20 bytes: the `sfAccount` the contract read back is the account it started from, so
+    // the key it computed found the right object.
+    EXPECT_EQ(
+        outcome->result, static_cast(RealHostFixture::toBytes(owner.id()).size()));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/e2e/CurrentLedgerObjField.cpp b/src/tests/libxrpl/tx/wasm/e2e/CurrentLedgerObjField.cpp
new file mode 100644
index 0000000000..839aaedd1d
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/e2e/CurrentLedgerObjField.cpp
@@ -0,0 +1,66 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// A contract reads a field of its current ledger object (a real escrow) end to end: the real
+// VM runs the guest, `HostContext` marshals the field code into an `SField`, the real impl
+// reads the real ledger, and the byte count comes back to the guest. `host_calls/` proves the
+// marshalling with a mock and `host_functions/` proves the impl's answer without a VM; this
+// proves the two agree over a real ledger.
+struct CurrentLedgerObjFieldE2e : RealVmTest
+{
+    // Create a real escrow owned by `owner` and return its keylet — the object the contract
+    // runs against.
+    Keylet
+    makeEscrow(Account const& owner, Account const& dest)
+    {
+        ledger.createAccount(owner, XRP(1000));
+        ledger.createAccount(dest, XRP(1000));
+        auto const ownerSeq = ledger.getAccountRoot(owner.id()).getSequence();
+        auto const r = ledger.submit(
+            transactions::EscrowCreateBuilder{owner.id(), dest.id(), XRP(100)}.setFinishAfter(
+                900'000'000),
+            owner);
+        EXPECT_EQ(r.ter, tesSUCCESS) << transToken(r.ter);
+        ledger.close();
+        return keylet::escrow(owner.id(), SeqProxy::rawSequence(ownerSeq));
+    }
+};
+
+TEST_F(CurrentLedgerObjFieldE2e, ContractReadsAFieldOfItsRealEscrow)
+{
+    auto const owner = Account{"owner"};
+    auto const escrow = makeEscrow(owner, Account{"dest"});
+
+    // Ask the current object for `sfAccount` and return the byte count the host wrote — 20 for
+    // an account id — proving the read reached the real ledger and came back through the VM.
+    auto const wat = std::format(
+        R"wat(
+(module
+  (import "host_lib" "home_le_field" (func $home_le_field (param i32 i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (call $home_le_field (i32.const {}) (i32.const 0) (i32.const 32))))
+)wat",
+        sfAccount.getCode());
+
+    auto const outcome = run(wat, escrow);
+    ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
+    EXPECT_EQ(outcome->result, static_cast(toBytes(owner.id()).size()));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/e2e/FloatToMantExp.cpp b/src/tests/libxrpl/tx/wasm/e2e/FloatToMantExp.cpp
new file mode 100644
index 0000000000..a70fdb07ce
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/e2e/FloatToMantExp.cpp
@@ -0,0 +1,66 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The only host function that writes to *two* output regions, and so the only place the
+// "one call, one answer" assumption in every other marshalling path is not what happens.
+// `float_to_mant_exp` splits a float into an eight-byte mantissa and a four-byte exponent,
+// each into its own guest buffer, and answers with a status rather than a byte count. Two
+// regions means two independent bounds checks, two writes, and an ordering between them —
+// none of which the single-output shapes exercise. `host_calls` pins that wiring against a
+// mock; this proves the real impl drives it the same way, with the guest reading both
+// halves back out of its own memory.
+struct FloatToMantExpE2e : RealVmTest
+{
+};
+
+TEST_F(FloatToMantExpE2e, ContractReadsBothHalvesOfASplitFloat)
+{
+    // Pi's canonical encoding in, mantissa to offset 64, exponent to offset 128. The
+    // contract returns the low half of the mantissa so the assertion checks that real bytes
+    // landed in the guest's buffer, not merely that the call reported success.
+    auto const wat = std::format(
+        R"wat(
+(module
+  (import "host_lib" "float_to_mant_exp" (func $split (param i32 i32 i32 i32 i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (data (i32.const 0) "{}")
+  (func (export "escrow_finish") (result i32)
+    (local $r i32)
+    (local.set $r (call $split
+      (i32.const 0) (i32.const 12)
+      (i32.const 64) (i32.const 8)
+      (i32.const 128) (i32.const 4)))
+    (if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
+    (i32.load (i32.const 64))))
+)wat",
+        watEscaped(FloatTest::kPi));
+
+    auto const outcome = run(wat);
+    ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
+
+    // The expected value is derived from the input rather than written out as a literal,
+    // because the derivation is the interesting part: a float stores its mantissa in the
+    // first eight bytes **big-endian**, while `float_to_mant_exp` writes it to the guest
+    // **little-endian**. So the guest's `i32.load` at the start of the mantissa buffer sees
+    // the *low* 32 bits of a number whose bytes arrived in the opposite order. Getting that
+    // flip wrong is exactly the convention mismatch this layer exists to catch, and a
+    // hard-coded constant would hide it.
+    auto mantissa = std::int64_t{0};
+    for (auto i = 0U; i < 8; ++i)
+    {
+        mantissa = (mantissa << 8) | FloatTest::kPi[i];
+    }
+    EXPECT_EQ(outcome->result, static_cast(mantissa & 0xFFFFFFFF));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/e2e/HostError.cpp b/src/tests/libxrpl/tx/wasm/e2e/HostError.cpp
new file mode 100644
index 0000000000..3b7203b415
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/e2e/HostError.cpp
@@ -0,0 +1,52 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The error channel, driven by a real failure rather than a mock's canned one.
+// Every other e2e case here proves a success path. But a contract spends most of its life
+// reacting to codes, and the path a *real* error takes is different from the one a mock
+// error takes: the impl returns a `HostFunctionError`, `HostContext` turns it into a wire
+// code, and the engine hands that back to the guest as a negative i32 without disturbing the
+// run. `host_calls` proves the middle step against a mock that was *told* to fail; nothing
+// until now has proved that a real impl's real failure comes out the far end intact.
+struct HostErrorE2e : RealVmTest
+{
+};
+
+TEST_F(HostErrorE2e, ARealHostErrorReachesTheGuestAsItsWireCode)
+{
+    // The contract runs against an account root, then asks it for `sfMemoData` — a field
+    // that object does not carry. The impl genuinely fails to find it, so the code the
+    // guest reads was produced by the real lookup rather than staged.
+    auto const owner = fund("owner");
+
+    auto const wat = std::format(
+        R"wat(
+(module
+  (import "host_lib" "home_le_field" (func $home_le_field (param i32 i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (call $home_le_field (i32.const {}) (i32.const 0) (i32.const 32))))
+)wat",
+        sfMemoData.getCode());
+
+    auto const outcome = run(wat, keylet::account(owner.id()));
+
+    // The run itself succeeds: a soft host error is an answer to the contract, not a fault
+    // in it. Reporting it as a failed run would be the interesting bug here.
+    ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
+    EXPECT_EQ(outcome->result, static_cast(HostFunctionError::FieldNotFound));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/e2e/HostFunctionTour.cpp b/src/tests/libxrpl/tx/wasm/e2e/HostFunctionTour.cpp
new file mode 100644
index 0000000000..ed3570aab5
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/e2e/HostFunctionTour.cpp
@@ -0,0 +1,49 @@
+#include 
+
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+// A single contract that tours several host functions end to end — a ledger-header read, the
+// base fee, a hash, a keylet, and a data write — returning 1 only if every call succeeds.
+struct HostFunctionTourE2e : RealVmTest
+{
+};
+
+TEST_F(HostFunctionTourE2e, AContractTouringManyHostFunctionsSucceeds)
+{
+    // Each call must return >= 0 (a byte count, i.e. success); the guest returns the first
+    // negative error code, or 1 if the whole tour succeeds. Output regions are disjoint so no
+    // call clobbers another, and buffers are generous so exact value sizes don't matter.
+    static constexpr auto kWat = std::string_view{R"wat(
+(module
+  (import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
+  (import "host_lib" "base_fee" (func $base_fee (param i32 i32) (result i32)))
+  (import "host_lib" "sha512_half" (func $sha512_half (param i32 i32 i32 i32) (result i32)))
+  (import "host_lib" "accountroot_id" (func $accountroot_id (param i32 i32 i32 i32) (result i32)))
+  (import "host_lib" "set_data" (func $set_data (param i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (local $r i32)
+    (local.set $r (call $ldgr_index (i32.const 0) (i32.const 32)))
+    (if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
+    (local.set $r (call $base_fee (i32.const 32) (i32.const 32)))
+    (if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
+    (local.set $r (call $sha512_half (i32.const 0) (i32.const 4) (i32.const 64) (i32.const 32)))
+    (if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
+    (local.set $r (call $accountroot_id (i32.const 0) (i32.const 20) (i32.const 128) (i32.const 32)))
+    (if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
+    (local.set $r (call $set_data (i32.const 0) (i32.const 8)))
+    (if (i32.lt_s (local.get $r) (i32.const 0)) (then (return (local.get $r))))
+    (i32.const 1)))
+)wat"};
+
+    auto const outcome = run(kWat);
+    ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
+    EXPECT_EQ(outcome->result, 1) << "every host call in the tour should have succeeded";
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/e2e/LedgerSqn.cpp b/src/tests/libxrpl/tx/wasm/e2e/LedgerSqn.cpp
new file mode 100644
index 0000000000..824cd19f45
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/e2e/LedgerSqn.cpp
@@ -0,0 +1,33 @@
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The real ledger's sequence.
+struct LedgerSqnE2e : RealVmTest
+{
+};
+
+TEST_F(LedgerSqnE2e, ContractReadsTheRealLedgerSequence)
+{
+    // Ask the host for the ledger sequence into offset 0, then return the i32 stored there.
+    static constexpr auto kWat = std::string_view{R"wat(
+(module
+  (import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (drop (call $ldgr_index (i32.const 0) (i32.const 4)))
+    (i32.load (i32.const 0))))
+)wat"};
+
+    auto const outcome = run(kWat);
+    ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
+    EXPECT_EQ(outcome->result, static_cast(ledger.getOpenLedger().header().seq));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/e2e/SetData.cpp b/src/tests/libxrpl/tx/wasm/e2e/SetData.cpp
new file mode 100644
index 0000000000..130c812dc8
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/e2e/SetData.cpp
@@ -0,0 +1,31 @@
+#include 
+
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+// A contract writes its data field end to end.
+struct SetDataE2e : RealVmTest
+{
+};
+
+TEST_F(SetDataE2e, ContractWritesItsData)
+{
+    // `set_data` over 8 bytes of (zero-initialized) memory returns the byte count it stored.
+    static constexpr auto kWat = std::string_view{R"wat(
+(module
+  (import "host_lib" "set_data" (func $set_data (param i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (call $set_data (i32.const 0) (i32.const 8))))
+)wat"};
+
+    auto const outcome = run(kWat);
+    ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
+    EXPECT_EQ(outcome->result, 8);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/e2e/TxField.cpp b/src/tests/libxrpl/tx/wasm/e2e/TxField.cpp
new file mode 100644
index 0000000000..54c66c1e9e
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/e2e/TxField.cpp
@@ -0,0 +1,47 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// A contract reads a field of its transaction end to end.
+struct TxFieldE2e : RealVmTest
+{
+};
+
+TEST_F(TxFieldE2e, ContractReadsAFieldOfItsTransaction)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    static constexpr auto kScale = std::uint8_t{8};
+    auto const tx = mptIssuanceCreateTx(owner, kScale);
+
+    // Ask the tx for `sfAssetScale` (a single byte) and return the i32 the guest loads — the
+    // scale, zero-extended — so the assertion checks the value flowed through, not just a count.
+    auto const wat = std::format(
+        R"wat(
+(module
+  (import "host_lib" "tx_field" (func $tx_field (param i32 i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (drop (call $tx_field (i32.const {}) (i32.const 0) (i32.const 4)))
+    (i32.load (i32.const 0))))
+)wat",
+        sfAssetScale.getCode());
+
+    auto const outcome = run(wat, keylet::account(owner.id()), tx.type, tx.build);
+    ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
+    EXPECT_EQ(outcome->result, kScale);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/e2e/TxNestedField.cpp b/src/tests/libxrpl/tx/wasm/e2e/TxNestedField.cpp
new file mode 100644
index 0000000000..224e3bb525
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/e2e/TxNestedField.cpp
@@ -0,0 +1,64 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The locator convention, end to end.
+struct TxNestedFieldE2e : RealVmTest
+{
+    // An EscrowFinish carrying a memo, so the locator has a real leaf to reach.
+    TxAssembler
+    withMemo(Account const& acct)
+    {
+        auto assembler = escrowFinishTx(ledger, acct);
+        assembler.build = [inner = std::move(assembler.build)](STObject& obj) {
+            inner(obj);
+            auto memos = STArray{};
+            auto memo = STObject::makeInnerObject(sfMemo);
+            memo.setFieldVL(sfMemoData, Slice{"hello", 5});
+            memos.push_back(std::move(memo));
+            obj.setFieldArray(sfMemos, memos);
+        };
+        return assembler;
+    }
+};
+
+TEST_F(TxNestedFieldE2e, ContractWalksALocatorToANestedTransactionField)
+{
+    auto const owner = fund("owner");
+    auto assembler = withMemo(owner);
+
+    auto const wat = std::format(
+        R"wat(
+(module
+  (import "host_lib" "tx_inner" (func $tx_inner (param i32 i32 i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (i32.store (i32.const 0) (i32.const {}))
+    (i32.store (i32.const 4) (i32.const 0))
+    (i32.store (i32.const 8) (i32.const {}))
+    (call $tx_inner (i32.const 0) (i32.const 12) (i32.const 64) (i32.const 32))))
+)wat",
+        sfMemos.getCode(),
+        sfMemoData.getCode());
+
+    auto const outcome = run(wat, keylet::account(owner.id()), assembler.type, assembler.build);
+    ASSERT_TRUE(outcome.has_value()) << transToken(outcome.error().ter);
+    // Five bytes: "hello", the memo's data, reached through the locator.
+    EXPECT_EQ(outcome->result, 5);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/EscrowWasm.cpp b/src/tests/libxrpl/tx/wasm/fixtures/EscrowWasm.cpp
new file mode 100644
index 0000000000..0157e13998
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/EscrowWasm.cpp
@@ -0,0 +1,49 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+std::string
+gatedOnLedgerSqn(std::uint32_t threshold)
+{
+    return std::format(
+        R"wat(
+(module
+  (import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (drop (call $ldgr_index (i32.const 0) (i32.const 4)))
+    (if (result i32) (i32.ge_u (i32.load (i32.const 0)) (i32.const {}))
+      (then (i32.const 5))
+      (else (i32.const 0)))))
+)wat",
+        threshold);
+}
+
+XRPAmount
+escrowCreateFee(TxTest const& env, Bytes const& bytecode)
+{
+    return (env.getOpenLedger().fees().base * 10) +
+        XRPAmount{static_cast(bytecode.size()) * 5};
+}
+
+XRPAmount
+escrowFinishFee(TxTest const& env, std::uint32_t allowance)
+{
+    auto const& fees = env.getOpenLedger().fees();
+    // Integer division rounds down, so the transactor adds one drop; match it exactly or
+    // the submission fails on the fee rather than on what it meant to test.
+    auto const gasFee = ((std::uint64_t{allowance} * fees.gasPrice) / microDropsPerDrop) + 1;
+    return fees.base + XRPAmount{static_cast(gasFee)};
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/EscrowWasm.h b/src/tests/libxrpl/tx/wasm/fixtures/EscrowWasm.h
new file mode 100644
index 0000000000..2d67a80f36
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/EscrowWasm.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include 
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// Contracts and fee arithmetic shared by the transactor-level escrow tests.
+
+// Reads the ledger sequence and returns 5. A minimal *working* contract: it makes a real
+// host call, so it exercises more than validation, but what it returns is uninteresting.
+inline constexpr auto kReadsLedgerSqn = std::string_view{R"wat(
+(module
+  (import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (drop (call $ldgr_index (i32.const 0) (i32.const 4)))
+    (i32.const 5)))
+)wat"};
+
+// Traps. A fault rather than a rejection: no return code, and nothing it wrote survives.
+inline constexpr auto kTraps = std::string_view{R"wat(
+(module
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (unreachable)))
+)wat"};
+
+// Loops forever, so the only way it stops is by exhausting its gas allowance.
+inline constexpr auto kLoopsForever = std::string_view{R"wat(
+(module
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (loop $forever (br $forever))
+    (i32.const 1)))
+)wat"};
+
+// Imports a host function that does not exist, so screening refuses it. Well-formed wasm —
+// the refusal is about the import list, not the bytes.
+inline constexpr auto kImportsUnknownHostFunction = std::string_view{R"wat(
+(module
+  (import "host_lib" "bad" (func $bad (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (call $bad)))
+)wat"};
+
+// A contract that approves only once the ledger has reached `threshold`: returns 5 at or
+// past it, 0 (a rejection) before.
+//
+// The escrow's release condition is thus a real predicate over ledger state that changes
+// from false to true while the escrow sits there — the shape the whole feature exists for,
+// and the one a fixed contract cannot express. Built at runtime because the threshold has
+// to be chosen relative to the environment's current sequence.
+std::string
+gatedOnLedgerSqn(std::uint32_t threshold);
+
+// What an `EscrowCreate` carrying this bytecode must pay: ten base fees plus five drops a
+// byte (`EscrowCreate::calculateBaseFee`).
+XRPAmount
+escrowCreateFee(TxTest const& env, Bytes const& bytecode);
+
+// What an `EscrowFinish` carrying this gas allowance must pay: the base fee plus the
+// allowance priced at `gasPrice`, rounded up (`EscrowFinish::calculateBaseFee`).
+XRPAmount
+escrowFinishFee(TxTest const& env, std::uint32_t allowance);
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/FloatConstants.h b/src/tests/libxrpl/tx/wasm/fixtures/FloatConstants.h
new file mode 100644
index 0000000000..fe7be63c8d
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/FloatConstants.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+// Canonical float encodings, with no ledger and no test framework behind them — just the byte
+// patterns the float host functions take and return. Benchmarks include this directly;
+// `FloatFixture.h` mixes it into the GTest fixture the `host_functions/` tests use.
+
+namespace xrpl::test {
+
+struct FloatConstants
+{
+    static constexpr std::int64_t kMin64 = std::numeric_limits::min();
+    static constexpr std::int64_t kMax64 = std::numeric_limits::max();
+    static constexpr std::int32_t kNormalExp = 18;
+    static constexpr std::string_view const kInvalidData = "invalid_data";
+
+    // clang-format off
+    static inline Bytes const kIntMin      = {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};  // -2^63 (rounds to -(2^63-1))
+    static inline Bytes const kIntZero     = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00};  // 0
+    static inline Bytes const kIntMax      = {0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00};  // 2^63-1
+    static inline Bytes const kUintMax     = {0x19, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9A, 0x00, 0x00, 0x00, 0x01};  // 2^64-1
+    static inline Bytes const kMaxExp      = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00};  // 1e(kMaxExponent + kNormalExp)
+    static inline Bytes const kPreMaxExp   = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF};  // 1e(kMaxExponent + kNormalExp - 1)
+    static inline Bytes const kMinusMaxExp = {0xF2, 0x1F, 0x49, 0x4C, 0x58, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00};  // -1e(kMaxExponent + kNormalExp)
+    static inline Bytes const kMinExp      = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00};  // 1e(kMinExponent - kNormalExp)
+    static inline Bytes const kMax         = {0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x80, 0x00};  // kMaxRep e(kMaxExponent - kNormalExp)
+    static inline Bytes const kMaxIOU      = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x63, 0xFF, 0x9C, 0x00, 0x00, 0x00, 0x4E};  // 9999999999999999e(96)
+    static inline Bytes const kMinIOU      = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x9D};  // 1e(-81)
+    static inline Bytes const kOne         = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE};  // 1
+    static inline Bytes const kMinusOne    = {0xF2, 0x1F, 0x49, 0x4C, 0x58, 0x9C, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE};  // -1
+    static inline Bytes const kOneMore     = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x03, 0xE8, 0xFF, 0xFF, 0xFF, 0xEE};  // 1.000000000000001
+    static inline Bytes const kTwo         = {0x1B, 0xC1, 0x6D, 0x67, 0x4E, 0xC8, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE};  // 2
+    static inline Bytes const kTen         = {0x0D, 0xE0, 0xB6, 0xB3, 0xA7, 0x64, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEF};  // 10
+    static inline Bytes const kPi          = {0x2B, 0x99, 0x2D, 0xDF, 0xA2, 0x32, 0x48, 0xE8, 0xFF, 0xFF, 0xFF, 0xEE};  // 3.141592653589793
+    static inline Bytes const kInvalidZero = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00};  // non-canonical zero
+    static inline Bytes const kMinusThree  = {0xD6, 0x5D, 0xDB, 0xE5, 0x09, 0xD4, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xEE};  // -3
+    // clang-format on
+
+    static Slice
+    slice(Bytes const& b)
+    {
+        return Slice{b.data(), b.size()};
+    }
+};
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/FloatFixture.h b/src/tests/libxrpl/tx/wasm/fixtures/FloatFixture.h
new file mode 100644
index 0000000000..926a6f5943
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/FloatFixture.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include 
+#include 
+
+// The float constants with a real ledger and GTest attached, for the `host_functions/Float*`
+// tests. The constants alone are in FloatConstants.h, which links no test framework.
+
+namespace xrpl::test {
+
+struct FloatTest : RealHostFixture, FloatConstants
+{
+};
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/HostContextFixture.cpp b/src/tests/libxrpl/tx/wasm/fixtures/HostContextFixture.cpp
new file mode 100644
index 0000000000..fd0fd5d4bc
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/HostContextFixture.cpp
@@ -0,0 +1,69 @@
+#include 
+
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+rust::Slice
+HostContextTest::bytesOf(Bytes const& bytes)
+{
+    return rust::Slice{bytes.data(), bytes.size()};
+}
+
+Bytes
+HostContextTest::bytesOfSteps(std::vector const& steps)
+{
+    Bytes bytes;
+    bytes.reserve(steps.size() * sizeof(std::int32_t));
+    for (auto const step : steps)
+    {
+        auto const wire = bytesOfScalar(step);
+        bytes.insert(bytes.end(), wire.begin(), wire.end());
+    }
+    return bytes;
+}
+
+HostContextTest::OutRegion::OutRegion(std::size_t capacity) : bytes(capacity, kSentinel)
+{
+}
+
+rust::Slice
+HostContextTest::OutRegion::slice()
+{
+    return rust::Slice{bytes.data(), bytes.size()};
+}
+
+bool
+HostContextTest::OutRegion::wasWritten() const
+{
+    return std::ranges::any_of(bytes, [](std::uint8_t b) { return b != kSentinel; });
+}
+
+bool
+HostContextTest::OutRegion::holds(rust::Slice expected) const
+{
+    if (expected.size() > bytes.size())
+    {
+        return false;
+    }
+
+    auto want = std::vector(bytes.size(), kSentinel);
+    std::ranges::copy(expected, want.begin());
+    return bytes == want;
+}
+
+std::string
+HostContextTest::logged() const
+{
+    return sink.messages();
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/HostContextFixture.h b/src/tests/libxrpl/tx/wasm/fixtures/HostContextFixture.h
new file mode 100644
index 0000000000..3789b3b7fa
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/HostContextFixture.h
@@ -0,0 +1,104 @@
+#pragma once
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// Base for the tests that construct `HostContext` directly, rather than reaching it through
+// an assembled module.
+struct HostContextTest : testing::Test
+{
+    static rust::Slice
+    bytesOf(Bytes const& bytes);
+
+    // A scalar's wire form: its bytes little-endian, the way a wasm guest lays them out in
+    // memory.
+    //
+    // Spelled out with shifts rather than a `memcpy` of the value, which would mirror what
+    // `answerScalar` does and so assert nothing about the byte order. That is the whole reason
+    // this exists, so keep it a shift.
+    template 
+    static Bytes
+    bytesOfScalar(T value)
+    {
+        static_assert(std::is_integral_v, "Only integral types");
+
+        auto const bits = static_cast>(value);
+        Bytes bytes(sizeof(bits));
+        for (std::size_t i = 0; i < sizeof(bits); ++i)
+        {
+            bytes[i] = static_cast(bits >> (i * 8));
+        }
+        return bytes;
+    }
+
+    // A locator's wire form: each step as four little-endian bytes.
+    static Bytes
+    bytesOfSteps(std::vector const& steps);
+
+    // Filled with a sentinel rather than left at zero: an answer can itself be all zero, so
+    // only a byte no answer produces tells "wrote nothing" apart from "wrote zeros".
+    struct OutRegion
+    {
+        static constexpr std::uint8_t kSentinel = 0xcd;
+
+        std::vector bytes;
+
+        explicit OutRegion(std::size_t capacity);
+
+        rust::Slice
+        slice();
+
+        [[nodiscard]] bool
+        wasWritten() const;
+
+        // Means "this value and nothing past it".
+        [[nodiscard]] bool
+        holds(rust::Slice expected) const;
+    };
+
+    CaptureSink sink{beast::Severity::Warning};
+    testing::StrictMock host{beast::Journal{sink}};
+    HostContext hostContext{host};
+
+    [[nodiscard]] std::string
+    logged() const;
+};
+
+// `FieldLocator` has no `operator==` and is move-only, so an `EXPECT_CALL` needs a matcher
+// rather than `testing::Ref`/`testing::Eq`. `invokeWithLocator` builds it as a local that is
+// gone once the call returns, so the check has to happen inside the matcher.
+//
+// `MATCHER_P` emits a function of this name, and gmock matchers are CamelCase by convention.
+// NOLINTNEXTLINE(readability-identifier-naming)
+MATCHER_P(LocatorEquals, steps, "")
+{
+    if (arg.size() != static_cast(steps.size()))
+    {
+        return false;
+    }
+    for (std::uint32_t i = 0; i < arg.size(); ++i)
+    {
+        if (arg[i] != steps[i])
+        {
+            return false;
+        }
+    }
+    return true;
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/MockHostFunctions.h b/src/tests/libxrpl/tx/wasm/fixtures/MockHostFunctions.h
new file mode 100644
index 0000000000..024713d1f7
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/MockHostFunctions.h
@@ -0,0 +1,437 @@
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// A mock of the host the wasm engine calls back into.
+//
+// One `MOCK_METHOD` per `HostFunctions` entry, in that header's order, each signature taken
+// verbatim from it. The extra parentheses around a return type are what keeps the comma in
+// `std::expected` from splitting the macro's arguments.
+//
+// No `ON_CALL` defaults, deliberately: this is always used through `StrictMock`, which fails
+// a call to a method carrying no `EXPECT_CALL`.
+struct MockHostFunctions : HostFunctions
+{
+    explicit MockHostFunctions(beast::Journal journal) : HostFunctions(journal)
+    {
+    }
+
+    MOCK_METHOD(bool, checkSelf, (), (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getLedgerSqn,
+        (),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getParentLedgerTime,
+        (),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getParentLedgerHash,
+        (),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getBaseFee,
+        (),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        isAmendmentEnabled,
+        (uint256 const& amendmentId),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        isAmendmentEnabled,
+        (std::string_view const& amendmentName),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        cacheLedgerObj,
+        (uint256 const& objId, std::int32_t cacheIdx),
+        (override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getTxField,
+        (SField const& fname),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getCurrentLedgerObjField,
+        (SField const& fname),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getLedgerObjField,
+        (std::int32_t cacheIdx, SField const& fname),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getTxNestedField,
+        (FieldLocator const& locator),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getCurrentLedgerObjNestedField,
+        (FieldLocator const& locator),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getLedgerObjNestedField,
+        (std::int32_t cacheIdx, FieldLocator const& locator),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getTxArrayLen,
+        (SField const& fname),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getCurrentLedgerObjArrayLen,
+        (SField const& fname),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getLedgerObjArrayLen,
+        (std::int32_t cacheIdx, SField const& fname),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getTxNestedArrayLen,
+        (FieldLocator const& locator),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getCurrentLedgerObjNestedArrayLen,
+        (FieldLocator const& locator),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getLedgerObjNestedArrayLen,
+        (std::int32_t cacheIdx, FieldLocator const& locator),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        updateData,
+        (Slice const& data),
+        (override));
+
+    MOCK_METHOD(
+        (std::expected),
+        checkSignature,
+        (Slice const& message, Slice const& signature, Slice const& pubkey),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        computeSha512HalfHash,
+        (Slice const& data),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        accountKeylet,
+        (AccountID const& account),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        ammKeylet,
+        (Asset const& issue1, Asset const& issue2),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        checkKeylet,
+        (AccountID const& account, std::uint32_t seq),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        credentialKeylet,
+        (AccountID const& subject, AccountID const& issuer, Slice const& credentialType),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        didKeylet,
+        (AccountID const& account),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        delegateKeylet,
+        (AccountID const& account, AccountID const& authorize),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        depositPreauthKeylet,
+        (AccountID const& account, AccountID const& authorize),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        escrowKeylet,
+        (AccountID const& account, std::uint32_t seq),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        trustLineKeylet,
+        (AccountID const& account1, AccountID const& account2, Currency const& currency),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        mptokenIssuanceKeylet,
+        (AccountID const& issuer, std::uint32_t seq),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        mptokenKeylet,
+        (MPTID const& mptid, AccountID const& holder),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        nftokenOfferKeylet,
+        (AccountID const& account, std::uint32_t seq),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        offerKeylet,
+        (AccountID const& account, std::uint32_t seq),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        oracleKeylet,
+        (AccountID const& account, std::uint32_t docId),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        paychannelKeylet,
+        (AccountID const& account, AccountID const& destination, std::uint32_t seq),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        permissionedDomainKeylet,
+        (AccountID const& account, std::uint32_t seq),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        signerListKeylet,
+        (AccountID const& account),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        ticketKeylet,
+        (AccountID const& account, std::uint32_t seq),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        vaultKeylet,
+        (AccountID const& account, std::uint32_t seq),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        sponsorshipKeylet,
+        (AccountID const& sponsor, AccountID const& sponsee),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        loanBrokerKeylet,
+        (AccountID const& owner, std::uint32_t seq),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        loanKeylet,
+        (uint256 const& loanBrokerID, std::uint32_t loanSeq),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getNFT,
+        (AccountID const& account, uint256 const& nftId),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getNFTIssuer,
+        (uint256 const& nftId),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getNFTTaxon,
+        (uint256 const& nftId),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getNFTFlags,
+        (uint256 const& nftId),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getNFTTransferFee,
+        (uint256 const& nftId),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        getNFTSequence,
+        (uint256 const& nftId),
+        (const, override));
+
+    // Takes the rendered text, not the guest's buffer: rendering is `HostContext`'s, so what
+    // a test asserts here is the log line a node would write.
+    MOCK_METHOD(
+        void,
+        trace,
+        (std::string_view const& msg, std::string_view const& data),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatFromInt,
+        (std::int64_t x, std::int32_t mode),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatFromUint,
+        (std::uint64_t x, std::int32_t mode),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatFromSTAmount,
+        (STAmount const& x, std::int32_t mode),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatFromSTNumber,
+        (STNumber const& x, std::int32_t mode),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatToInt,
+        (Slice const& x, std::int32_t mode),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatToMantExp,
+        (Slice const& x),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatFromMantExp,
+        (std::int64_t mantissa, std::int32_t exponent, std::int32_t mode),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatCompare,
+        (Slice const& x, Slice const& y),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatAdd,
+        (Slice const& x, Slice const& y, std::int32_t mode),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatSubtract,
+        (Slice const& x, Slice const& y, std::int32_t mode),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatMultiply,
+        (Slice const& x, Slice const& y, std::int32_t mode),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatDivide,
+        (Slice const& x, Slice const& y, std::int32_t mode),
+        (const, override));
+
+    MOCK_METHOD(
+        (std::expected),
+        floatPower,
+        (Slice const& x, std::int32_t n, std::int32_t mode),
+        (const, override));
+};
+
+// Matches a `Slice` (or anything with `data()`/`size()`) against the bytes of a string, so
+// an expectation can say *what* the guest asked the host to work on.
+//
+// `MATCHER_P` emits a function of this name, and gmock matchers are CamelCase by convention.
+// NOLINTNEXTLINE(readability-identifier-naming)
+MATCHER_P(BytesAre, expected, "")
+{
+    return std::string_view{reinterpret_cast(arg.data()), arg.size()} ==
+        std::string_view{expected};
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/ModuleBuilder.cpp b/src/tests/libxrpl/tx/wasm/fixtures/ModuleBuilder.cpp
new file mode 100644
index 0000000000..47c3d96d9b
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/ModuleBuilder.cpp
@@ -0,0 +1,177 @@
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+namespace {
+
+// Section ids, from the binary format's fixed table.
+constexpr std::uint8_t kSectionType = 0x01;
+constexpr std::uint8_t kSectionFunction = 0x03;
+constexpr std::uint8_t kSectionMemory = 0x05;
+constexpr std::uint8_t kSectionExport = 0x07;
+constexpr std::uint8_t kSectionCode = 0x0A;
+constexpr std::uint8_t kSectionData = 0x0B;
+
+constexpr std::uint8_t kOpcodeNop = 0x01;
+constexpr std::uint8_t kOpcodeEnd = 0x0B;
+constexpr std::uint8_t kOpcodeI32Const = 0x41;
+
+constexpr std::uint8_t kTypeI32 = 0x7F;
+constexpr std::uint8_t kTypeFunc = 0x60;
+
+constexpr std::uint32_t kPageBytes = 65'536;
+
+// Anything that isn't obviously zero-filled is 0xEE, so a dump of a failing module shows at
+// a glance which bytes are padding.
+constexpr std::uint8_t kDataFillByte = 0xEE;
+
+void
+appendU32Leb(Bytes& out, std::uint32_t value)
+{
+    do
+    {
+        auto byte = static_cast(value & 0x7F);
+        value >>= 7;
+        if (value != 0U)
+        {
+            byte |= 0x80;
+        }
+        out.push_back(byte);
+    } while (value != 0U);
+}
+
+void
+appendSection(Bytes& out, std::uint8_t section, Bytes const& payload)
+{
+    out.push_back(section);
+    appendU32Leb(out, static_cast(payload.size()));
+    out.insert(std::end(out), std::begin(payload), std::end(payload));
+}
+
+// A function body: no locals, `code`, `end` — prefixed by its own byte length.
+void
+appendBody(Bytes& out, Bytes const& code)
+{
+    auto body = Bytes{0x00};  // local declaration count
+    body.insert(std::end(body), std::begin(code), std::end(code));
+    body.push_back(kOpcodeEnd);
+
+    appendU32Leb(out, static_cast(body.size()));
+    out.insert(std::end(out), std::begin(body), std::end(body));
+}
+
+Bytes
+header()
+{
+    return Bytes{0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00};  // "\0asm", version 1
+}
+
+// Two types: `() -> ()` for filler functions, `() -> i32` for the entry point.
+constexpr std::uint8_t kTypeVoid = 0;
+constexpr std::uint8_t kTypeReturnsI32 = 1;
+
+void
+appendTypeSection(Bytes& out)
+{
+    auto payload = Bytes{0x02};  // two types
+    payload.insert(std::end(payload), {kTypeFunc, 0x00, 0x00});
+    payload.insert(std::end(payload), {kTypeFunc, 0x00, 0x01, kTypeI32});
+    appendSection(out, kSectionType, payload);
+}
+
+// `fillerCount` functions of type `() -> ()`, then the entry point of type `() -> i32`.
+void
+appendFunctionSection(Bytes& out, std::uint32_t fillerCount)
+{
+    auto payload = Bytes{};
+    appendU32Leb(payload, fillerCount + 1);
+    payload.insert(std::end(payload), fillerCount, kTypeVoid);
+    payload.push_back(kTypeReturnsI32);
+    appendSection(out, kSectionFunction, payload);
+}
+
+// Export the entry point, which is the last function declared.
+void
+appendExportSection(Bytes& out, std::uint32_t fillerCount, bool exportMemory)
+{
+    auto payload = Bytes{};
+    appendU32Leb(payload, exportMemory ? 2 : 1);
+
+    if (exportMemory)
+    {
+        static constexpr auto kMemory = std::string_view{"memory"};
+        appendU32Leb(payload, static_cast(kMemory.size()));
+        payload.insert(std::end(payload), std::begin(kMemory), std::end(kMemory));
+        payload.push_back(0x02);  // export kind: memory
+        payload.push_back(0x00);  // memory index
+    }
+
+    appendU32Leb(payload, static_cast(escrowFunctionName.size()));
+    payload.insert(std::end(payload), std::begin(escrowFunctionName), std::end(escrowFunctionName));
+    payload.push_back(0x00);  // export kind: function
+    appendU32Leb(payload, fillerCount);
+
+    appendSection(out, kSectionExport, payload);
+}
+
+// `i32.const 1` — a completed run that the transactor reads as success.
+Bytes
+entryPointCode()
+{
+    return Bytes{kOpcodeI32Const, 0x01};
+}
+
+}  // namespace
+
+Bytes
+codeHeavyModule(std::uint32_t instructionCount)
+{
+    // One filler function holding every `nop`, plus the entry point.
+    constexpr std::uint32_t kFillerCount = 1;
+
+    auto out = header();
+    appendTypeSection(out);
+    appendFunctionSection(out, kFillerCount);
+    appendExportSection(out, kFillerCount, /*exportMemory*/ false);
+
+    auto codePayload = Bytes{};
+    appendU32Leb(codePayload, kFillerCount + 1);
+    appendBody(codePayload, Bytes(instructionCount, kOpcodeNop));
+    appendBody(codePayload, entryPointCode());
+
+    appendSection(out, kSectionCode, codePayload);
+    return out;
+}
+
+Bytes
+dataHeavyModule(std::uint32_t dataBytes)
+{
+    auto out = header();
+    appendTypeSection(out);
+    appendFunctionSection(out, /*fillerCount*/ 0);
+
+    auto memoryPayload = Bytes{0x01, 0x00};  // one memory, minimum-only limits
+    appendU32Leb(memoryPayload, (dataBytes + kPageBytes - 1) / kPageBytes);
+    appendSection(out, kSectionMemory, memoryPayload);
+
+    appendExportSection(out, /*fillerCount*/ 0, /*exportMemory*/ true);
+
+    auto codePayload = Bytes{0x01};  // one function body
+    appendBody(codePayload, entryPointCode());
+    appendSection(out, kSectionCode, codePayload);
+
+    auto dataPayload = Bytes{0x01, 0x00};  // one segment, memory 0
+    dataPayload.insert(std::end(dataPayload), {kOpcodeI32Const, 0x00, kOpcodeEnd});  // offset 0
+    appendU32Leb(dataPayload, dataBytes);
+    dataPayload.insert(std::end(dataPayload), dataBytes, kDataFillByte);
+    appendSection(out, kSectionData, dataPayload);
+
+    return out;
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/ModuleBuilder.h b/src/tests/libxrpl/tx/wasm/fixtures/ModuleBuilder.h
new file mode 100644
index 0000000000..be4624b45e
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/ModuleBuilder.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+// Modules built to a *byte size* rather than to a behaviour.
+//
+// Everything else in this tree writes WAT, which is the right default: it says what the
+// contract does. These two say only how big it is, for the transactor's `bytecodeSizeLimit`
+// screening, where the boundary cases sit five bytes apart (99'950 accepted, 99'955
+// refused). Assembling text cannot hit a byte count on the nose, and the WAT for a
+// hundred thousand `nop`s would be a ~500 KB string, so these emit the binary directly.
+//
+// Both produce a module that *passes preflight* when it is under the size limit: a real
+// `escrow_finish` exported with type `() -> i32`. That is not incidental — screening
+// checks the entry point's signature (`PreflightTest.EntryPointOfTheWrongTypeIsRefused`),
+// so a module that got it wrong would be refused for that reason at every size and the
+// sweep would measure nothing.
+
+// A module of `instructionCount` `nop`s in a single function, doing nothing.
+//
+// All of them in one function, deliberately: there appears to be no per-function size limit
+// below the module limit, so one function may occupy the whole module. `wasmparser` defines
+// `MAX_WASM_FUNCTION_SIZE` = 128 KiB, which looks like such a limit, but a single body of a
+// million instructions preflights clean — pinned by
+// `BytecodeSize.ASingleFunctionBodyIsNotSeparatelyCapped`. Splitting the `nop`s across
+// functions would imply a constraint that is not there, and would make the byte count the
+// boundary tests depend on harder to predict.
+//
+// The returned module is a few dozen bytes larger than `instructionCount` (the sections
+// around the code). Callers that care about an exact total should measure `.size()` rather
+// than assume it.
+Bytes
+codeHeavyModule(std::uint32_t instructionCount);
+
+// A module carrying `dataBytes` bytes in a data segment, with memory declared to fit.
+//
+// The size lands in a data section rather than a code section, so the two builders
+// together separate "large because there is a lot to translate" from "large because there
+// is a lot to copy".
+Bytes
+dataHeavyModule(std::uint32_t dataBytes);
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/NFTFixture.h b/src/tests/libxrpl/tx/wasm/fixtures/NFTFixture.h
new file mode 100644
index 0000000000..9a163b2ca0
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/NFTFixture.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+// The NFToken helpers with a real ledger and GTest attached, for the `host_functions/NFT*` tests.
+// The ledger-only versions are in NftSetup.h, which links no test framework.
+
+namespace xrpl::test {
+
+struct NFTTest : RealHostFixture, NftIds
+{
+    uint256
+    mintNFT(Account const& issuer, std::optional uri = std::nullopt)
+    {
+        return mintNft(*this, issuer, uri);
+    }
+};
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/NftSetup.cpp b/src/tests/libxrpl/tx/wasm/fixtures/NftSetup.cpp
new file mode 100644
index 0000000000..bdaf29c8f9
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/NftSetup.cpp
@@ -0,0 +1,64 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include   // IWYU pragma: keep
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+uint256
+NftIds::makeNftId(AccountID const& issuer)
+{
+    return NFTokenMint::createNFTokenID(kFlags, kFee, issuer, nft::toTaxon(kTaxon), kSequence);
+}
+
+uint256
+mintNft(WasmLedger& fixture, Account const& issuer, std::optional uri)
+{
+    auto& ledger = fixture.ledger;
+    auto builder = transactions::NFTokenMintBuilder{issuer.id(), 0u};
+    if (uri)
+        builder.setURI(Slice{uri->data(), uri->size()});
+    auto const r = ledger.submit(builder, issuer);
+    if (r.ter != tesSUCCESS)
+    {
+        fixtureFailed(std::string{"minting the NFToken: "} + transToken(r.ter));
+    }
+    ledger.close();
+
+    // The single minted token lives in the owner's first NFTokenPage.
+    auto const& view = ledger.getOpenLedger();
+    auto const first = keylet::nftokenPageMin(issuer.id()).key;
+    auto const last = keylet::nftokenPageMax(issuer.id()).key;
+    auto const pageKey = view.succ(first, last.next());
+    if (!pageKey.has_value())
+    {
+        fixtureFailed("finding the minted token's NFTokenPage");
+    }
+    auto const page = view.read(Keylet{ltNFTOKEN_PAGE, *pageKey});
+    if (page == nullptr)
+    {
+        fixtureFailed("reading the minted token's NFTokenPage");
+    }
+    auto const& tokens = page->getFieldArray(sfNFTokens);
+    if (tokens.empty())
+    {
+        fixtureFailed("the NFTokenPage holds no tokens");
+    }
+    return tokens[0].getFieldH256(sfNFTokenID);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/NftSetup.h b/src/tests/libxrpl/tx/wasm/fixtures/NftSetup.h
new file mode 100644
index 0000000000..93e3f798f1
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/NftSetup.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+// NFToken setup, built on `WasmLedger` rather than on a GTest fixture so a benchmark can mint a
+// token without linking a test framework. Tests reach the same helpers through
+// `RealHostFixture`, which derives from `WasmLedger`.
+
+namespace xrpl::test {
+
+// The fields baked into `makeNftId`, so a caller can assert an extractor returned the right one.
+struct NftIds
+{
+    static constexpr std::uint16_t kFlags = nft::kFlagTransferable | nft::kFlagBurnable;
+    static constexpr std::uint16_t kFee = 314;
+    static constexpr std::uint32_t kTaxon = 12345;
+    static constexpr std::uint32_t kSequence = 7;
+
+    // A well-formed id carrying the constants above. Computed, not minted: the id-extractor host
+    // functions read the id itself and never touch the ledger.
+    static uint256
+    makeNftId(AccountID const& issuer);
+};
+
+// Mint a real NFToken owned by `issuer` (taxon 0) and return its id, read back from the owner's
+// NFTokenPage. `TxTest` applies to the open ledger, which produces no metadata, so the id comes
+// from ledger state rather than from the mint's metadata.
+//
+// Throws via `fixtureFailed` if the mint or the page lookup fails; see WasmLedger.h for why that
+// is a throw and not an `EXPECT_`.
+uint256
+mintNft(
+    WasmLedger& fixture,
+    Account const& issuer,
+    std::optional uri = std::nullopt);
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/RealHostFixture.cpp b/src/tests/libxrpl/tx/wasm/fixtures/RealHostFixture.cpp
new file mode 100644
index 0000000000..6380f301f6
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/RealHostFixture.cpp
@@ -0,0 +1,16 @@
+#include 
+
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+void
+expectKeyletMatches(std::expected const& result, Keylet const& expected)
+{
+    expectValue(result, RealHostFixture::toBytes(expected.key));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/RealHostFixture.h b/src/tests/libxrpl/tx/wasm/fixtures/RealHostFixture.h
new file mode 100644
index 0000000000..f9bdb6b049
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/RealHostFixture.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+
+// The GTest layer over `WasmLedger`: the assertion helpers, and the fixture base the
+// `host_functions/` tests derive from.
+//
+// Everything that touches a ledger or builds a host lives in `WasmLedger.h`, which knows nothing
+// about GTest so the benchmarks can share it. Only what genuinely needs the framework is here.
+
+namespace xrpl::test {
+
+template 
+void
+expectValue(
+    std::expected const& result,
+    U const& expected,
+    std::source_location loc = std::source_location::current())
+{
+    auto trace = testing::ScopedTrace{loc.file_name(), static_cast(loc.line()), ""};
+    ASSERT_TRUE(result.has_value())
+        << "expected a value, got error " << static_cast(result.error());
+    EXPECT_EQ(*result, expected);
+}
+
+template 
+void
+expectError(
+    std::expected const& result,
+    HostFunctionError expected,
+    std::source_location loc = std::source_location::current())
+{
+    auto trace = testing::ScopedTrace{loc.file_name(), static_cast(loc.line()), ""};
+    ASSERT_FALSE(result.has_value()) << "expected error, got a value";
+    EXPECT_EQ(result.error(), expected);
+}
+
+void
+expectKeyletMatches(std::expected const& result, Keylet const& expected);
+
+// A `WasmLedger` with GTest's lifecycle attached. Tests derive from this; benchmarks use
+// `WasmLedger` directly.
+struct RealHostFixture : testing::Test, WasmLedger
+{
+};
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/RealVmTest.h b/src/tests/libxrpl/tx/wasm/fixtures/RealVmTest.h
new file mode 100644
index 0000000000..a2bed7bc4d
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/RealVmTest.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// End-to-end: a WAT contract run through the REAL VM against the REAL host
+// over a REAL `TxTest` ledger.
+struct RealVmTest : RealHostFixture
+{
+    // Assemble `wat` and run its `entryPoint` through the real VM against a real host built
+    // over the current open ledger. `leKey`/`txType`/`assembler` configure the ledger object
+    // the contract runs against and the transaction it reads.
+    std::expected
+    run(
+        std::string_view wat,
+        Keylet const& leKey = keylet::account(AccountID{}),
+        TxType txType = ttESCROW_FINISH,
+        std::function assembler = [](STObject&) {},
+        std::int64_t gas = kAmpleGas,
+        std::string_view entryPoint = escrowFunctionName)
+    {
+        auto host = makeHost(leKey, txType, std::move(assembler));
+        return runWat(*host, wat, gas, entryPoint);
+    }
+};
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/WasmFixture.h b/src/tests/libxrpl/tx/wasm/fixtures/WasmFixture.h
new file mode 100644
index 0000000000..28099f0a97
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/WasmFixture.h
@@ -0,0 +1,110 @@
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// Base for every wasm test that runs a contract against a MOCKED host whose log is captured.
+// Its real-host counterpart is `RealVmTest`; both run a WAT guest through the real VM and
+// forward to the shared `runWat` harness (`WasmRun.h`), differing only in the host.
+//
+// Modules are written as WebAssembly text and assembled by `assembleWat`. The assembler is in
+// a test-only crate: the engine itself refuses text
+// (`the_vm_refuses_a_text_format_module`), because a text assembler on the consensus path
+// would make a transaction's validity a build flag.
+struct MockVmTest : testing::Test
+{
+    // Keeps what a run logged. The host's default journal is a null sink, which would let a
+    // swallowed condition pass a test that only checks the TER.
+    CaptureSink sink{beast::Severity::Warning};
+
+    // Strict: a host call no test asked for is a failure, not a warning. These modules import
+    // exactly what they mean to exercise, so an unplanned call means the engine reached for
+    // something on its own — which is the kind of surprise a test suite exists to catch.
+    testing::StrictMock host{beast::Journal{sink}};
+
+    MockVmTest()
+    {
+        // `runEscrowWasm` asks every run whether the host is clean, so under a strict mock
+        // every test would have to say so. Declared once here, and any number of times
+        // (including none, for the runs refused before the engine is reached). A test that
+        // cares says otherwise and its own expectation wins.
+        EXPECT_CALL(host, checkSelf()).WillRepeatedly(testing::Return(true));
+    }
+
+    static Bytes
+    assemble(std::string_view wat)
+    {
+        return assembleWat(wat);
+    }
+
+    std::expected
+    run(std::string_view wat,
+        std::int64_t gas = kAmpleGas,
+        std::string_view entryPoint = escrowFunctionName)
+    {
+        return runWat(host, wat, gas, entryPoint);
+    }
+
+    std::expected
+    runBytes(
+        Bytes const& wasm,
+        std::int64_t gas = kAmpleGas,
+        std::string_view entryPoint = escrowFunctionName)
+    {
+        return runEscrowWasm(wasm, host, gas, entryPoint);
+    }
+
+    [[nodiscard]] std::string
+    logged() const
+    {
+        return sink.messages();
+    }
+};
+
+// Base for the per-host-function fixtures. Each derives, supplies the module that exercises
+// its own import, and runs it through `callHost()` — so a test says only what the host was
+// asked and what came back.
+struct HostCallTest : MockVmTest
+{
+    // The module under test. One import, one `escrow_finish` that calls it.
+    [[nodiscard]] virtual std::string
+    wat() const = 0;
+
+    std::expected
+    callHost(std::string_view entryPoint = escrowFunctionName)
+    {
+        return run(wat(), kAmpleGas, entryPoint);
+    }
+
+    // The contract's return value, which for these modules is what the host answered — or
+    // its negative error code. Fails the test if the run did not complete.
+    std::int32_t
+    hostAnswer(std::string_view entryPoint = escrowFunctionName)
+    {
+        auto const outcome = callHost(entryPoint);
+        if (!outcome)
+        {
+            ADD_FAILURE() << "the run did not complete: " << transToken(outcome.error().ter)
+                          << "; logged: " << logged();
+            return 0;
+        }
+        return outcome->result;
+    }
+};
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/WasmLedger.cpp b/src/tests/libxrpl/tx/wasm/fixtures/WasmLedger.cpp
new file mode 100644
index 0000000000..887bf4fbab
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/WasmLedger.cpp
@@ -0,0 +1,303 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include   // IWYU pragma: keep
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+void
+fixtureFailed(std::string_view what)
+{
+    throw std::runtime_error("test fixture setup failed: " + std::string{what});
+}
+
+Bytes
+WasmLedger::toBytes(std::uint8_t value)
+{
+    return {value};
+}
+
+Bytes
+WasmLedger::toBytes(std::uint16_t value)
+{
+    return {static_cast(value), static_cast(value >> 8)};
+}
+
+Bytes
+WasmLedger::toBytes(std::uint32_t value)
+{
+    return {
+        static_cast(value),
+        static_cast(value >> 8),
+        static_cast(value >> 16),
+        static_cast(value >> 24)};
+}
+
+Bytes
+WasmLedger::toBytes(uint256 const& value)
+{
+    return Bytes{std::begin(value), std::end(value)};
+}
+
+Bytes
+WasmLedger::toBytes(std::string_view value)
+{
+    return Bytes{std::begin(value), std::end(value)};
+}
+
+Bytes
+WasmLedger::toBytes(std::span value)
+{
+    return Bytes{std::begin(value), std::end(value)};
+}
+
+Bytes
+WasmLedger::toBytes(AccountID const& account)
+{
+    return Bytes{std::begin(account), std::end(account)};
+}
+
+Bytes
+WasmLedger::toBytes(Issue const& issue)
+{
+    auto s = Serializer{};
+    s.addBitString(issue.currency);
+    if (!isXRP(issue.currency))
+        s.addBitString(issue.account);
+    return s.getData();
+}
+
+Bytes
+WasmLedger::toBytes(Asset const& asset)
+{
+    if (asset.holds())
+        return toBytes(asset.get());
+
+    auto const& mptIssue = asset.get();
+    auto const& mptID = mptIssue.getMptID();
+    return Bytes{mptID.cbegin(), mptID.cend()};
+}
+
+Bytes
+WasmLedger::toBytes(STAmount const& amount)
+{
+    auto msg = Serializer{};
+    amount.add(msg);
+    return msg.getData();
+}
+
+Bytes
+WasmLedger::toBytes(STNumber const& number)
+{
+    auto msg = Serializer{};
+    number.add(msg);
+    return msg.getData();
+}
+
+SignedMessage
+signMessage(std::string_view message, KeyType keyType)
+{
+    auto const [pk, sk] = randomKeyPair(keyType);
+    auto const msg = Bytes{std::begin(message), std::end(message)};
+    auto const sig = sign(pk, sk, Slice{msg.data(), msg.size()});
+    return {
+        .message = msg,
+        .signature = Bytes{sig.data(), sig.data() + sig.size()},
+        .publicKey = Bytes{pk.data(), pk.data() + pk.size()}};
+}
+
+uint256
+credentialId(std::string_view hex)
+{
+    auto id = uint256{};
+    if (!id.parseHex(std::string{hex}))
+    {
+        fixtureFailed("parsing the credential id hex");
+    }
+    return id;
+}
+
+STObject
+makeMemo(Bytes const& data)
+{
+    auto memo = STObject::makeInnerObject(sfMemo);
+    memo.setFieldVL(sfMemoData, data);
+    return memo;
+}
+
+TxAssembler
+bareTx(TxType type)
+{
+    return {.type = type, .build = [](STObject&) {}};
+}
+
+TxAssembler
+escrowFinishTx(TxTest& ledger, Account const& acct)
+{
+    return {.type = ttESCROW_FINISH, .build = [&ledger, acct](STObject& obj) {
+                auto credId = uint256{};
+                if (!credId.parseHex(
+                        "0011223344556677889900112233445566778899001122334455667788990011"))
+                {
+                    fixtureFailed("parsing the credential id hex");
+                }
+
+                obj.setAccountID(sfAccount, acct.id());
+                obj.setAccountID(sfOwner, acct.id());
+                obj.setFieldU32(sfOfferSequence, ledger.getAccountRoot(acct.id()).getSequence());
+                obj.setFieldArray(sfMemos, STArray{});
+                auto credIds = STVector256{};
+                credIds.pushBack(credId);
+                obj.setFieldV256(sfCredentialIDs, credIds);
+            }};
+}
+
+TxAssembler
+ammDepositTx(Account const& acct, Asset const& asset1, Asset const& asset2)
+{
+    return {.type = ttAMM_DEPOSIT, .build = [acct, asset1, asset2](STObject& obj) {
+                obj.setAccountID(sfAccount, acct.id());
+                obj.setFieldIssue(sfAsset, STIssue{sfAsset, asset1});
+                obj.setFieldIssue(sfAsset2, STIssue{sfAsset2, asset2});
+            }};
+}
+
+TxAssembler
+mptIssuanceCreateTx(Account const& acct, std::uint8_t scale)
+{
+    return {.type = ttMPTOKEN_ISSUANCE_CREATE, .build = [acct, scale](STObject& obj) {
+                obj.setAccountID(sfAccount, acct.id());
+                obj.setFieldU8(sfAssetScale, scale);
+            }};
+}
+
+WasmHost::WasmHost(
+    std::shared_ptr tx,
+    std::unique_ptr context,
+    std::unique_ptr host)
+    : tx_{std::move(tx)}, context_{std::move(context)}, host_{std::move(host)}
+{
+}
+
+WasmHostFunctionsImpl*
+WasmHost::operator->() const
+{
+    return host_.get();
+}
+
+WasmHostFunctionsImpl&
+WasmHost::operator*() const
+{
+    return *host_;
+}
+
+Account
+WasmLedger::fund(char const* name, XRPAmount amount)
+{
+    auto const account = Account{name};
+    ledger.createAccount(account, amount);
+    return account;
+}
+
+WasmHost
+WasmLedger::makeHost(
+    beast::Journal journal,
+    Keylet const& leKey,
+    TxType txType,
+    std::function assembler)
+{
+    auto tx = std::make_shared(
+        txType, [assembler = std::move(assembler)](STObject& obj) { assembler(obj); });
+    auto context = std::make_unique(
+        ledger.getServiceRegistry(),
+        ledger.getOpenLedger(),
+        *tx,
+        tesSUCCESS,
+        ledger.getOpenLedger().fees().base,
+        TapNone,
+        journal);
+    auto host = std::make_unique(*context, leKey);
+    return WasmHost{std::move(tx), std::move(context), std::move(host)};
+}
+
+WasmHost
+WasmLedger::makeHost(Keylet const& leKey, TxType txType, std::function assembler)
+{
+    return makeHost(
+        beast::Journal{beast::Journal::getNullSink()}, leKey, txType, std::move(assembler));
+}
+
+WasmHost
+WasmLedger::makeTracingHost(
+    Keylet const& leKey,
+    TxType txType,
+    std::function assembler)
+{
+    return makeHost(beast::Journal{traceSink_}, leKey, txType, std::move(assembler));
+}
+
+std::string
+WasmLedger::logged() const
+{
+    return traceSink_.messages();
+}
+
+void
+WasmLedger::makeSignerList(
+    Account const& owner,
+    std::uint32_t quorum,
+    std::vector> const& signers)
+{
+    auto entries = STArray{};
+    for (auto const& [signer, weight] : signers)
+    {
+        auto entry = STObject::makeInnerObject(sfSignerEntry);
+        entry.setAccountID(sfAccount, signer.id());
+        entry.setFieldU16(sfSignerWeight, weight);
+        entries.push_back(std::move(entry));
+    }
+    auto const r = ledger.submit(
+        transactions::SignerListSetBuilder{owner.id(), quorum}.setSignerEntries(entries), owner);
+    if (r.ter != tesSUCCESS)
+    {
+        fixtureFailed(std::string{"submitting the signer list: "} + transToken(r.ter));
+    }
+    ledger.close();
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/WasmLedger.h b/src/tests/libxrpl/tx/wasm/fixtures/WasmLedger.h
new file mode 100644
index 0000000000..abe6aa8e26
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/WasmLedger.h
@@ -0,0 +1,173 @@
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+#include   // keylet::account
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+// A real genesis ledger and the real host built over it, with **no test framework**.
+//
+// This is the piece both `xrpl_tests` and `xrpl.bench.wasm` need, and the reason it is its own
+// type: a benchmark wants a ledger and a host, not GTest's lifecycle. `RealHostFixture` adds the
+// framework on top (`: testing::Test, WasmLedger`) plus the assertion helpers; a benchmark uses
+// `WasmLedger` directly and links no GTest at all.
+//
+// Setup steps here **throw** rather than `EXPECT_`. That is the point of the separation, not a
+// detail: an `EXPECT_` outside a running test is recorded and discarded, so a benchmark whose
+// escrow was never created would still run its host call, take the not-found path, and report a
+// cheap, plausible, completely wrong price. Throwing turns that into a stopped run.
+
+namespace xrpl::test {
+
+// Fail a setup step loudly. See the note above on why this is not an `EXPECT_`.
+[[noreturn]] void
+fixtureFailed(std::string_view what);
+
+struct SignedMessage
+{
+    Bytes message;
+    Bytes signature;
+    Bytes publicKey;
+};
+
+SignedMessage
+signMessage(std::string_view message, KeyType keyType = KeyType::Secp256k1);
+
+uint256
+credentialId(
+    std::string_view hex = "0011223344556677889900112233445566778899001122334455667788990011");
+
+STObject
+makeMemo(Bytes const& data);
+
+struct TxAssembler
+{
+    TxType type;
+    std::function build;
+};
+
+TxAssembler
+bareTx(TxType type = ttESCROW_FINISH);
+TxAssembler
+escrowFinishTx(TxTest& ledger, Account const& acct);
+TxAssembler
+ammDepositTx(Account const& acct, Asset const& asset1, Asset const& asset2);
+TxAssembler
+mptIssuanceCreateTx(Account const& acct, std::uint8_t scale);
+
+class WasmHost
+{
+public:
+    WasmHost(
+        std::shared_ptr tx,
+        std::unique_ptr context,
+        std::unique_ptr host);
+
+    WasmHostFunctionsImpl*
+    operator->() const;
+    WasmHostFunctionsImpl&
+    operator*() const;
+
+private:
+    std::shared_ptr tx_;
+    std::unique_ptr context_;
+    std::unique_ptr host_;
+};
+
+class WasmLedger
+{
+public:
+    TxTest ledger;
+
+    Account
+    fund(char const* name, XRPAmount amount = XRP(1000));
+
+    WasmHost
+    makeHost(
+        beast::Journal journal,
+        Keylet const& leKey = keylet::account(AccountID{}),
+        TxType txType = ttESCROW_FINISH,
+        std::function assembler = [](STObject&) {});
+
+    // The common case: a host that discards its log output.
+    WasmHost
+    makeHost(
+        Keylet const& leKey = keylet::account(AccountID{}),
+        TxType txType = ttESCROW_FINISH,
+        std::function assembler = [](STObject&) {});
+
+    // A host whose `trace` output is captured, so a test can read it back with `logged()`.
+    // The sink is a fixture member, so it outlives the host and accumulates across a test.
+    WasmHost
+    makeTracingHost(
+        Keylet const& leKey = keylet::account(AccountID{}),
+        TxType txType = ttESCROW_FINISH,
+        std::function assembler = [](STObject&) {});
+
+    // Everything `trace` has written to the tracing host so far.
+    [[nodiscard]] std::string
+    logged() const;
+
+    // Submit a real SignerListSet so `keylet::signerList(owner)` exists — the object the
+    // signer-list nested-field / array-length getters read. `signers` pairs each signer
+    // account with its weight.
+    void
+    makeSignerList(
+        Account const& owner,
+        std::uint32_t quorum,
+        std::vector> const& signers);
+
+    static Bytes
+    toBytes(std::uint8_t value);
+    static Bytes
+    toBytes(std::uint16_t value);
+    static Bytes
+    toBytes(std::uint32_t value);
+    static Bytes
+    toBytes(uint256 const& value);
+    static Bytes
+    toBytes(std::string_view value);
+    static Bytes
+    toBytes(std::span value);
+    static Bytes
+    toBytes(AccountID const& account);
+    static Bytes
+    toBytes(Issue const& issue);
+    static Bytes
+    toBytes(Asset const& asset);
+    static Bytes
+    toBytes(STAmount const& amount);
+    static Bytes
+    toBytes(STNumber const& number);
+
+private:
+    CaptureSink traceSink_{beast::Severity::Trace};
+};
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/WasmRun.cpp b/src/tests/libxrpl/tx/wasm/fixtures/WasmRun.cpp
new file mode 100644
index 0000000000..278bd8fabe
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/WasmRun.cpp
@@ -0,0 +1,52 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+Bytes
+assembleWat(std::string_view wat)
+{
+    auto const wasm = rs::wasm_testkit::compile_wat(rust::Str{wat.data(), wat.size()});
+    return Bytes{wasm.begin(), wasm.end()};
+}
+
+std::string
+watEscaped(std::span bytes)
+{
+    static constexpr char kHex[] = "0123456789abcdef";
+    auto out = std::string{};
+    out.reserve(bytes.size() * 3);
+    for (auto const byte : bytes)
+    {
+        out += '\\';
+        out += kHex[byte >> 4];
+        out += kHex[byte & 0x0F];
+    }
+    return out;
+}
+
+std::string
+watEscaped(Bytes const& bytes)
+{
+    return watEscaped(std::span{bytes.data(), bytes.size()});
+}
+
+std::expected
+runWat(HostFunctions& host, std::string_view wat, std::int64_t gas, std::string_view entryPoint)
+{
+    return runEscrowWasm(assembleWat(wat), host, gas, entryPoint);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/fixtures/WasmRun.h b/src/tests/libxrpl/tx/wasm/fixtures/WasmRun.h
new file mode 100644
index 0000000000..a67d784bb6
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/fixtures/WasmRun.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// Enough gas for a small module to run to completion; a test about budgets passes its own.
+inline constexpr std::int64_t kAmpleGas = 100'000;
+
+// Assemble WebAssembly text to bytes via the test-only `wasm_testkit` crate. The engine
+// itself refuses text (a text assembler on the consensus path would make a transaction's
+// validity a build flag), so this is where a WAT string becomes something runnable. Throws
+// `rust::Error` on a typo, which gtest reports against the test that holds it.
+Bytes
+assembleWat(std::string_view wat);
+
+// `bytes` as the escape sequence a WAT string literal wants (`\aa\bb...`), for seeding a
+// contract's memory through a `(data ...)` segment.
+//
+// Guest memory starts zeroed, and zeros are not a usable input to most host functions: an
+// all-zero account id is `InvalidAccount`, an all-zero float is non-canonical. A contract
+// that needs real bytes to work on gets them here, once at instantiation, rather than
+// building them out of `i32.store` instructions.
+std::string
+watEscaped(std::span bytes);
+
+std::string
+watEscaped(Bytes const& bytes);
+
+// Assemble and run `wat`'s `entryPoint` through the real VM, servicing host calls through
+// `host` — a mock (`MockVmTest`) or the real impl over a ledger (`RealVmTest`). The one
+// host-agnostic harness both fixtures inject their host into.
+std::expected
+runWat(
+    HostFunctions& host,
+    std::string_view wat,
+    std::int64_t gas = kAmpleGas,
+    std::string_view entryPoint = escrowFunctionName);
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_calls/CurrentLedgerObjField.cpp b/src/tests/libxrpl/tx/wasm/host_calls/CurrentLedgerObjField.cpp
new file mode 100644
index 0000000000..e6030d7886
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_calls/CurrentLedgerObjField.cpp
@@ -0,0 +1,74 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+using testing::Return;
+
+// home_le_field — a scalar field code in, bytes out.
+struct CurrentLedgerObjFieldCall : HostCallTest
+{
+    // The field code the guest asks for. A real one, so the shim's `SField` lookup has
+    // something to find.
+    std::int32_t fieldCode = sfBalance.getCode();
+
+    [[nodiscard]] std::string
+    wat() const override
+    {
+        return std::string{R"wat(
+(module
+  (import "host_lib" "home_le_field" (func $home_le_field (param i32 i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (func (export "escrow_finish") (result i32)
+    (call $home_le_field (i32.const )wat"} +
+            std::to_string(fieldCode) + R"wat() (i32.const 0) (i32.const 32))))
+)wat";
+    }
+};
+
+// The shim turns the guest's `i32` into the `SField` the C++ interface takes; asserting on
+// the argument is what pins that translation rather than assuming it.
+TEST_F(CurrentLedgerObjFieldCall, FieldCodeBecomesSFieldHostIsAskedFor)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
+        .WillOnce(Return(Bytes{1, 2, 3}));
+
+    EXPECT_EQ(hostAnswer(), 3) << "the length the host reported";
+}
+
+TEST_F(CurrentLedgerObjFieldCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
+{
+    fieldCode = 0x7fff'0000;  // a type nothing is registered under
+    EXPECT_CALL(host, getCurrentLedgerObjField).Times(0);
+
+    EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidField));
+}
+
+TEST_F(CurrentLedgerObjFieldCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjField)
+        .WillOnce(Return(std::unexpected(HostFunctionError::FieldNotFound)));
+
+    EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::FieldNotFound));
+}
+
+// The field cap bounds the status, not just the bytes: a host reporting a length past
+// `kMaxWasmDataLength` is too large whatever the guest's buffer was.
+TEST_F(CurrentLedgerObjFieldCall, FieldPastProtocolCapIsTooLarge)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjField)
+        .WillOnce(Return(Bytes(kMaxWasmDataLength + 1, 0xab)));
+
+    EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::DataFieldTooLarge));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_calls/LedgerSqn.cpp b/src/tests/libxrpl/tx/wasm/host_calls/LedgerSqn.cpp
new file mode 100644
index 0000000000..32f383f5c6
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_calls/LedgerSqn.cpp
@@ -0,0 +1,69 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+using testing::Return;
+
+// ldgr_index — no input, one scalar output.
+struct LedgerSqnCall : HostCallTest
+{
+    [[nodiscard]] std::string
+    wat() const override
+    {
+        return std::string{R"wat(
+(module
+  (import "host_lib" "ldgr_index" (func $ldgr_index (param i32 i32) (result i32)))
+  (memory (export "memory") 1)
+
+  ;; Four bytes is what the value needs. Returns what the host wrote, or its error code.
+  (func (export "escrow_finish") (result i32)
+    (local $n i32)
+    (local.set $n (call $ldgr_index (i32.const 0) (i32.const 4)))
+    (select (local.get $n) (i32.load (i32.const 0)) (i32.lt_s (local.get $n) (i32.const 0))))
+
+  ;; Two bytes is not enough for the value. Returns the host's code when memory is still
+  ;; zero, or 1 if anything was written into it - so a refused write is visibly a refusal
+  ;; and not a truncation.
+  (func (export "into_two_bytes") (result i32)
+    (local $n i32)
+    (local.set $n (call $ldgr_index (i32.const 0) (i32.const 2)))
+    (select (local.get $n) (i32.const 1) (i32.eqz (i32.load (i32.const 0))))))
+)wat"};
+    }
+};
+
+TEST_F(LedgerSqnCall, SequenceReachesGuestAsFourLittleEndianBytes)
+{
+    EXPECT_CALL(host, getLedgerSqn()).WillOnce(Return(0x01020304u));
+
+    // Read back with `i32.load`, which is little-endian by the wasm spec — so the value
+    // arriving intact is the byte order being right.
+    EXPECT_EQ(hostAnswer(), 0x01020304);
+}
+
+TEST_F(LedgerSqnCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getLedgerSqn())
+        .WillOnce(Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+}
+
+// The engine decides the fit, not the host: the host is never told the guest's capacity, it
+// reports the value's true length and the engine turns a length past the buffer into
+// `BufferTooSmall` — with nothing written.
+TEST_F(LedgerSqnCall, BufferTooSmallIsRefusedWholeNotTruncated)
+{
+    EXPECT_CALL(host, getLedgerSqn()).WillOnce(Return(0x01020304u));
+
+    EXPECT_EQ(hostAnswer("into_two_bytes"), hfErrorToInt(HostFunctionError::BufferTooSmall));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_calls/Sha512Half.cpp b/src/tests/libxrpl/tx/wasm/host_calls/Sha512Half.cpp
new file mode 100644
index 0000000000..9aef5d5966
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_calls/Sha512Half.cpp
@@ -0,0 +1,78 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+using testing::Return;
+
+// sha512_half — bytes in and bytes out, the shape that needs the engine's output buffer.
+struct Sha512HalfCall : HostCallTest
+{
+    [[nodiscard]] std::string
+    wat() const override
+    {
+        return std::string{R"wat(
+(module
+  (import "host_lib" "sha512_half" (func $sha512_half (param i32 i32 i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (data (i32.const 64) "abc")
+
+  ;; Hashes the three bytes at 64 into the 32 at 0, then returns the first four bytes of the
+  ;; digest so the answer is shown to have arrived, not just been counted.
+  (func (export "escrow_finish") (result i32)
+    (local $n i32)
+    (local.set $n (call $sha512_half (i32.const 64) (i32.const 3) (i32.const 0) (i32.const 32)))
+    (select (local.get $n) (i32.load (i32.const 0)) (i32.lt_s (local.get $n) (i32.const 0))))
+
+  ;; Reports the length the host gave, for the cases where the digest itself is not the point.
+  (func (export "digest_length") (result i32)
+    (call $sha512_half (i32.const 64) (i32.const 3) (i32.const 0) (i32.const 32))))
+)wat"};
+    }
+
+    // A digest whose first four bytes are distinctive, so the load below cannot pass by
+    // accident.
+    static Hash
+    digest()
+    {
+        Hash value;
+        value.begin()[0] = 0x0d;
+        value.begin()[1] = 0x0c;
+        value.begin()[2] = 0x0b;
+        value.begin()[3] = 0x0a;
+        return value;
+    }
+};
+
+// Both directions in one call: the guest's bytes reach the host borrowed from its memory, and
+// the answer comes back into the same memory through the engine's buffer.
+TEST_F(Sha512HalfCall, GuestBytesReachHostAndDigestComesBack)
+{
+    EXPECT_CALL(host, computeSha512HalfHash(BytesAre("abc"))).WillOnce(Return(digest()));
+
+    EXPECT_EQ(hostAnswer(), 0x0a0b0c0d) << "the digest's first four bytes, little-endian";
+}
+
+TEST_F(Sha512HalfCall, DigestIsThirtyTwoBytes)
+{
+    EXPECT_CALL(host, computeSha512HalfHash).WillOnce(Return(digest()));
+
+    EXPECT_EQ(hostAnswer("digest_length"), 32);
+}
+
+TEST_F(Sha512HalfCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, computeSha512HalfHash)
+        .WillOnce(Return(std::unexpected(HostFunctionError::InvalidParams)));
+
+    EXPECT_EQ(hostAnswer(), hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp b/src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp
new file mode 100644
index 0000000000..fd7b871749
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_calls/Trace.cpp
@@ -0,0 +1,220 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+// For `TraceDataType`: declared in the cxx bridge, defined in the header it generates.
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+namespace {
+
+// Bytes as a WAT data segment's contents. Hex-escaped throughout, so a buffer needs no
+// thought about which of its bytes the text format would otherwise read.
+std::string
+watBytes(Bytes const& bytes)
+{
+    std::string escaped;
+    escaped.reserve(bytes.size() * 4);
+    for (auto const byte : bytes)
+        escaped += std::format("\\{:02x}", byte);
+    return escaped;
+}
+
+Bytes
+serialized(STAmount const& amount)
+{
+    Serializer s;
+    amount.add(s);
+    return s.getData();
+}
+
+}  // namespace
+
+// trace — a message, a data type, and a buffer holding what that type says. One import
+// covers every rendering, so what a test varies is the type rather than the function.
+//
+// The buffer arrives as bytes and leaves as text: `HostContext` renders it, and the host is
+// handed the finished line. So a test says which renderer the type selected.
+struct TraceCall : HostCallTest
+{
+    static constexpr std::int32_t kDataAt = 64;
+
+    // What the guest passes. `typeCode` rather than a `TraceDataType` so a test can send a
+    // code that names no type, which is the guest's to get wrong.
+    std::int32_t typeCode{static_cast(TraceDataType::AsText)};
+    Bytes data;
+
+    void
+    traces(TraceDataType type, Bytes bytes)
+    {
+        typeCode = static_cast(type);
+        data = std::move(bytes);
+    }
+
+    void
+    traces(TraceDataType type, std::string_view text)
+    {
+        traces(type, Bytes{text.begin(), text.end()});
+    }
+
+    [[nodiscard]] std::string
+    wat() const override
+    {
+        // {0} data offset, {1} the data itself, {2} the type under test, {3} its length,
+        // {4} a type the constant modules can name, {5} the data cap.
+        return std::format(
+            R"wat(
+(module
+  (import "host_lib" "trace" (func $trace (param i32 i32 i32 i32 i32)))
+  (memory (export "memory") 1)
+  (data (i32.const 0) "note")
+  (data (i32.const {0}) "{1}")
+
+  (func (export "escrow_finish") (result i32)
+    (call $trace (i32.const 0) (i32.const 4) (i32.const {2}) (i32.const {0}) (i32.const {3}))
+    (i32.const 1))
+
+  (func (export "unnamed_type") (result i32)
+    (call $trace (i32.const 0) (i32.const 4) (i32.const 0) (i32.const {0}) (i32.const 0))
+    (i32.const 1))
+
+  (func (export "past_memory") (result i32)
+    (call $trace (i32.const 0) (i32.const 4) (i32.const {4}) (i32.const 65536) (i32.const 1))
+    (i32.const 1))
+
+  (func (export "too_long") (result i32)
+    (call $trace (i32.const 0) (i32.const 4) (i32.const {4}) (i32.const {0}) (i32.const {5}))
+    (i32.const 1)))
+)wat",
+            kDataAt,
+            watBytes(data),
+            typeCode,
+            data.size(),
+            static_cast(TraceDataType::AsHex),
+            kMaxWasmDataLength);
+    }
+
+    // The line the host was handed, for a run that is expected to reach it.
+    void
+    expectTraced(std::string_view text)
+    {
+        EXPECT_CALL(host, trace(std::string_view("note"), text));
+
+        EXPECT_EQ(hostAnswer(), 1) << "the contract runs on past its trace";
+    }
+};
+
+// The eight-byte types are the pair worth naming: the same bytes, and the type is the whole
+// difference between the two readings.
+TEST_F(TraceCall, Int64ReadsTheBufferSigned)
+{
+    traces(TraceDataType::Int64, Bytes(8, 0xff));
+
+    expectTraced("-1");
+}
+
+TEST_F(TraceCall, Uint64ReadsTheSameBufferUnsigned)
+{
+    traces(TraceDataType::Uint64, Bytes(8, 0xff));
+
+    expectTraced("18446744073709551615");
+}
+
+TEST_F(TraceCall, AsTextTakesTheBufferVerbatim)
+{
+    traces(TraceDataType::AsText, "hello");
+
+    expectTraced("hello");
+}
+
+TEST_F(TraceCall, AsHexEncodesTheBuffer)
+{
+    traces(TraceDataType::AsHex, Bytes{0x07, 0x08, 0xff});
+
+    expectTraced("0708FF");
+}
+
+// The zero account, so the expectation is the well-known base58 rather than a rendering of
+// whatever the renderer happened to do.
+TEST_F(TraceCall, AccountIsBase58)
+{
+    traces(TraceDataType::Account, Bytes(AccountID::size(), 0));
+
+    expectTraced("rrrrrrrrrrrrrrrrrrrrrhoLvTp");
+}
+
+TEST_F(TraceCall, AmountCarriesItsAssetIntoTheText)
+{
+    traces(TraceDataType::Amount, serialized(STAmount{XRPAmount{1000}}));
+
+    expectTraced("1000/XRP");
+}
+
+TEST_F(TraceCall, XfloatIsDecodedToItsValue)
+{
+    auto const encoded = wasm_float::floatFromIntImpl(
+        42, static_cast(Number::RoundingMode::ToNearest));
+    ASSERT_TRUE(encoded.has_value());
+    traces(TraceDataType::Xfloat, *encoded);
+
+    expectTraced("42");
+}
+
+// The width is part of the type, and a buffer that is not it holds no value to print. The
+// contract is not told: a trace answers nothing at all.
+TEST_F(TraceCall, ABufferOfTheWrongWidthIsDropped)
+{
+    traces(TraceDataType::Int64, Bytes(4, 0xff));
+
+    EXPECT_CALL(host, trace).Times(0);
+    EXPECT_EQ(hostAnswer(), 1);
+}
+
+// `STAmount`'s deserializer rejects this by throwing, which must not escape into the run.
+TEST_F(TraceCall, AMalformedAmountIsDroppedRatherThanThrown)
+{
+    traces(TraceDataType::Amount, Bytes(3, 0xff));
+
+    EXPECT_CALL(host, trace).Times(0);
+    EXPECT_EQ(hostAnswer(), 1);
+}
+
+// Zero is the code a guest sends by omission, which is why no type carries it.
+TEST_F(TraceCall, ACodeThatNamesNoTypeIsDropped)
+{
+    EXPECT_CALL(host, trace).Times(0);
+
+    EXPECT_EQ(hostAnswer("unnamed_type"), 1);
+}
+
+// The memory policy every input region is held to, on the one call that cannot report it.
+TEST_F(TraceCall, ARegionPastMemoryIsDropped)
+{
+    EXPECT_CALL(host, trace).Times(0);
+
+    EXPECT_EQ(hostAnswer("past_memory"), 1);
+}
+
+TEST_F(TraceCall, AMessageAndBufferPastTheDataCapAreDropped)
+{
+    EXPECT_CALL(host, trace).Times(0);
+
+    EXPECT_EQ(hostAnswer("too_long"), 1);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/AccountKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/AccountKeylet.cpp
new file mode 100644
index 0000000000..f53d625216
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/AccountKeylet.cpp
@@ -0,0 +1,126 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct AccountKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a,
+                             0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+};
+
+TEST_F(AccountKeyletCall, AccountIsForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, accountKeylet(account)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.accountKeylet(bytesOf(accountBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(AccountKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, accountKeylet(account))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.accountKeylet(bytesOf(accountBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(AccountKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const shortAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, accountKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.accountKeylet(bytesOf(shortAccount), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(AccountKeyletCall, LongAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const longAccount(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, accountKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.accountKeylet(bytesOf(longAccount), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(AccountKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, accountKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.accountKeylet(bytesOf(Bytes{}), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(AccountKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, accountKeylet(account))
+        .WillOnce(testing::Throw(std::runtime_error{"account keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.accountKeylet(bytesOf(accountBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("account keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("accountKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(AccountKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, accountKeylet(account)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.accountKeylet(bytesOf(accountBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(AccountKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, accountKeylet(account)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.accountKeylet(bytesOf(accountBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(AccountKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, accountKeylet(account)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.accountKeylet(bytesOf(accountBytes), out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/AmmKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/AmmKeylet.cpp
new file mode 100644
index 0000000000..5ef7108fda
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/AmmKeylet.cpp
@@ -0,0 +1,156 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+namespace {
+
+Bytes
+concatBytes(Bytes const& first, Bytes const& second)
+{
+    Bytes bytes = first;
+    bytes.insert(bytes.end(), second.begin(), second.end());
+    return bytes;
+}
+
+}  // namespace
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not
+// here. This is `parseAsset`'s only coverage, so every branch of its length-based dispatch
+// is pinned below.
+struct AmmKeyletCall : HostContextTest
+{
+    Bytes const mptWire = Bytes(24, 0x7a);
+    Bytes const xrpWire = Bytes(20, 0x00);
+    Bytes const currencyWire = Bytes(20, 0x42);
+    Bytes const accountWire = Bytes(20, 0x99);
+    Bytes const issueWire = concatBytes(currencyWire, accountWire);
+
+    Asset const mptAsset{MPTID::fromVoid(mptWire.data())};
+    Asset const xrpAsset{xrpIssue()};
+    Asset const issueAsset{
+        Issue{Currency::fromVoid(currencyWire.data()), AccountID::fromVoid(accountWire.data())}};
+
+    Bytes const keylet = Bytes(32, 0xab);
+};
+
+TEST_F(AmmKeyletCall, MptAndIssueAssetsForwardedAndKeyletWritten)
+{
+    EXPECT_CALL(host, ammKeylet(testing::Eq(mptAsset), testing::Eq(issueAsset)))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ammKeylet(bytesOf(mptWire), bytesOf(issueWire), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(AmmKeyletCall, BareXrpCurrencyBytesBecomeNativeAssetHostIsAskedFor)
+{
+    EXPECT_CALL(host, ammKeylet(testing::Eq(xrpAsset), testing::Eq(mptAsset)))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ammKeylet(bytesOf(xrpWire), bytesOf(mptWire), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(AmmKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, ammKeylet(testing::Eq(mptAsset), testing::Eq(issueAsset)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ammKeylet(bytesOf(mptWire), bytesOf(issueWire), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(AmmKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, ammKeylet(testing::Eq(mptAsset), testing::Eq(issueAsset)))
+        .WillOnce(testing::Throw(std::runtime_error{"amm keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ammKeylet(bytesOf(mptWire), bytesOf(issueWire), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("amm keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("ammKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(AmmKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    EXPECT_CALL(host, ammKeylet(testing::Eq(mptAsset), testing::Eq(issueAsset)))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.ammKeylet(bytesOf(mptWire), bytesOf(issueWire), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(AmmKeyletCall, BareNonXrpCurrencyIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, ammKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ammKeylet(bytesOf(currencyWire), bytesOf(mptWire), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(AmmKeyletCall, IssueWithNativeCurrencyIsRefusedWithoutAskingHost)
+{
+    Bytes const nativeIssueWire = concatBytes(xrpWire, accountWire);
+    EXPECT_CALL(host, ammKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ammKeylet(bytesOf(nativeIssueWire), bytesOf(mptWire), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(AmmKeyletCall, EmptyAssetIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, ammKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ammKeylet(bytesOf(Bytes{}), bytesOf(mptWire), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// asset1 is parsed before asset2, but `parseAsset` answers the same `InvalidParams` for every
+// malformed shape, so which one was rejected is not observable here. The two are malformed for
+// different reasons so the case is at least not a duplicate of the single-asset ones above.
+TEST_F(AmmKeyletCall, BothAssetsMalformedIsRefusedWithoutAskingHost)
+{
+    Bytes const wrongLength{1, 2, 3, 4, 5};
+    EXPECT_CALL(host, ammKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ammKeylet(bytesOf(wrongLength), bytesOf(currencyWire), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/BaseFee.cpp b/src/tests/libxrpl/tx/wasm/host_context/BaseFee.cpp
new file mode 100644
index 0000000000..ee8557ce3a
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/BaseFee.cpp
@@ -0,0 +1,109 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// No D or F axis: `getBaseFee` takes no argument, so there is nothing to decode wrong and
+// nothing whose forwarded identity to check.
+struct BaseFeeCall : HostContextTest
+{
+    static constexpr std::uint32_t kBaseFee = 0x12345678;
+    Bytes const expectedBytes = bytesOfScalar(kBaseFee);
+};
+
+TEST_F(BaseFeeCall, HostValueIsWrittenAsLittleEndianBytes)
+{
+    EXPECT_CALL(host, getBaseFee()).WillOnce(testing::Return(kBaseFee));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getBaseFee(out.slice()), 4);
+    EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
+}
+
+TEST_F(BaseFeeCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getBaseFee())
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::Unimplemented)));
+
+    OutRegion out{4};
+    EXPECT_EQ(hostContext.getBaseFee(out.slice()), hfErrorToInt(HostFunctionError::Unimplemented));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(BaseFeeCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getBaseFee())
+        .WillOnce(testing::Throw(std::runtime_error{"base fee came apart"}));
+
+    OutRegion out{4};
+    EXPECT_EQ(hostContext.getBaseFee(out.slice()), hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("base fee came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getBaseFee"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(BaseFeeCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    EXPECT_CALL(host, getBaseFee()).WillOnce(testing::Return(kBaseFee));
+
+    OutRegion out{3};
+    EXPECT_EQ(hostContext.getBaseFee(out.slice()), 4);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(BaseFeeCall, OutRegionOfExactSizeIsWritten)
+{
+    EXPECT_CALL(host, getBaseFee()).WillOnce(testing::Return(kBaseFee));
+
+    OutRegion out{4};
+    EXPECT_EQ(hostContext.getBaseFee(out.slice()), 4);
+    EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
+}
+
+// Cross-cutting: every `HostFunctionError` code crosses `hfErrorToInt` unchanged at this layer.
+// Unlike the engine-side `WasmVMTest.SoftHostErrorCodesCrossUnchanged`, nothing is excluded
+// here - `HostContext` does not distinguish a soft code from a fatal one, so `Unimplemented`
+// and `NoMemExported` cross the same as any other. `InternalFatal` sits outside the -1..-20
+// run other codes occupy (it is `INT32_MIN`), and crosses the same whether the host returns it
+// directly or `guarded` supplies it for a throw.
+TEST_F(BaseFeeCall, EveryHostFunctionErrorCodeCrossesHfErrorToIntUnchanged)
+{
+    static constexpr HostFunctionError kAllErrors[] = {
+        HostFunctionError::Unimplemented,       HostFunctionError::FieldNotFound,
+        HostFunctionError::BufferTooSmall,      HostFunctionError::NoArray,
+        HostFunctionError::NotLeafField,        HostFunctionError::LocatorMalformed,
+        HostFunctionError::SlotOutRange,        HostFunctionError::SlotsFull,
+        HostFunctionError::EmptySlot,           HostFunctionError::LedgerObjNotFound,
+        HostFunctionError::OutOfTransferLimit,  HostFunctionError::DataFieldTooLarge,
+        HostFunctionError::PointerOutOfBounds,  HostFunctionError::NoMemExported,
+        HostFunctionError::InvalidParams,       HostFunctionError::InvalidAccount,
+        HostFunctionError::InvalidField,        HostFunctionError::IndexOutOfBounds,
+        HostFunctionError::FloatInputMalformed, HostFunctionError::FloatComputationError,
+        HostFunctionError::InternalFatal,
+    };
+
+    auto refused = HostFunctionError::Unimplemented;
+    EXPECT_CALL(host, getBaseFee())
+        .WillRepeatedly([&refused]() -> std::expected {
+            return std::unexpected(refused);
+        });
+
+    for (auto const error : kAllErrors)
+    {
+        refused = error;
+
+        OutRegion out{4};
+        EXPECT_EQ(hostContext.getBaseFee(out.slice()), hfErrorToInt(error));
+        EXPECT_FALSE(out.wasWritten());
+    }
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/CacheLedgerObj.cpp b/src/tests/libxrpl/tx/wasm/host_context/CacheLedgerObj.cpp
new file mode 100644
index 0000000000..a9376fa890
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/CacheLedgerObj.cpp
@@ -0,0 +1,81 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+// `cacheLedgerObj` mutates the host's slot table, so it is non-`const`; it answers the slot
+// used directly, with no out region.
+struct CacheLedgerObjCall : HostContextTest
+{
+    Bytes const objIdBytes = Bytes(uint256::size(), 0x33);
+    uint256 const objId = uint256::fromVoid(objIdBytes.data());
+};
+
+TEST_F(CacheLedgerObjCall, ObjIdAndCacheIdxForwardedSlotIsReturned)
+{
+    EXPECT_CALL(host, cacheLedgerObj(testing::Eq(objId), 5)).WillOnce(testing::Return(7));
+
+    EXPECT_EQ(hostContext.cacheLedgerObj(bytesOf(objIdBytes), 5), 7);
+}
+
+TEST_F(CacheLedgerObjCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, cacheLedgerObj(testing::Eq(objId), 5))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::SlotsFull)));
+
+    EXPECT_EQ(
+        hostContext.cacheLedgerObj(bytesOf(objIdBytes), 5),
+        hfErrorToInt(HostFunctionError::SlotsFull));
+}
+
+TEST_F(CacheLedgerObjCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, cacheLedgerObj(testing::Eq(objId), 5))
+        .WillOnce(testing::Throw(std::runtime_error{"cache slot came apart"}));
+
+    EXPECT_EQ(
+        hostContext.cacheLedgerObj(bytesOf(objIdBytes), 5),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("cache slot came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("cacheLedgerObj"));
+}
+
+TEST_F(CacheLedgerObjCall, MalformedObjIdIsRefusedWithoutAskingHost)
+{
+    Bytes const malformed(uint256::size() - 1, 0x33);
+    EXPECT_CALL(host, cacheLedgerObj).Times(0);
+
+    EXPECT_EQ(
+        hostContext.cacheLedgerObj(bytesOf(malformed), 5),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// 0 selects a free slot at the host - a meaningful argument here, not an absent one - and must
+// still cross unchanged.
+TEST_F(CacheLedgerObjCall, ZeroCacheIdxIsForwardedVerbatim)
+{
+    EXPECT_CALL(host, cacheLedgerObj(testing::Eq(objId), 0)).WillOnce(testing::Return(0));
+
+    EXPECT_EQ(hostContext.cacheLedgerObj(bytesOf(objIdBytes), 0), 0);
+}
+
+// Unlike `seq` elsewhere in this file's shape family, `cacheIdx` is not reinterpreted as
+// unsigned: a negative value reaches the host as itself.
+TEST_F(CacheLedgerObjCall, NegativeCacheIdxIsForwardedVerbatim)
+{
+    EXPECT_CALL(host, cacheLedgerObj(testing::Eq(objId), -1))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::SlotOutRange)));
+
+    EXPECT_EQ(
+        hostContext.cacheLedgerObj(bytesOf(objIdBytes), -1),
+        hfErrorToInt(HostFunctionError::SlotOutRange));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/CheckKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/CheckKeylet.cpp
new file mode 100644
index 0000000000..96c11e2a34
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/CheckKeylet.cpp
@@ -0,0 +1,127 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct CheckKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
+                             0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+    std::uint32_t const seq = 54321;
+};
+
+TEST_F(CheckKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, checkKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(CheckKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, checkKeylet(account, seq))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(CheckKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const shortAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, checkKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.checkKeylet(bytesOf(shortAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(CheckKeyletCall, LongAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const longAccount(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, checkKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.checkKeylet(bytesOf(longAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(CheckKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, checkKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.checkKeylet(bytesOf(Bytes{}), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(CheckKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, checkKeylet(account, seq))
+        .WillOnce(testing::Throw(std::runtime_error{"check keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("check keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("checkKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(CheckKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, checkKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(CheckKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, checkKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(CheckKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, checkKeylet(account, seq)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.checkKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/CheckSignature.cpp b/src/tests/libxrpl/tx/wasm/host_context/CheckSignature.cpp
new file mode 100644
index 0000000000..751e4f17aa
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/CheckSignature.cpp
@@ -0,0 +1,63 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+// `checkSignature` validates nothing: message, signature and pubkey reach the host exactly as
+// given, with no length check on any of them - deliberately, not by oversight.
+struct CheckSignatureCall : HostContextTest
+{
+    Bytes const message{'m', 's', 'g'};
+    Bytes const signature{'s', 'i', 'g'};
+    Bytes const pubkey{'k', 'e', 'y'};
+};
+
+TEST_F(CheckSignatureCall, MessageSignatureAndPubkeyForwardedVerbatim)
+{
+    EXPECT_CALL(host, checkSignature(BytesAre("msg"), BytesAre("sig"), BytesAre("key")))
+        .WillOnce(testing::Return(1));
+
+    EXPECT_EQ(hostContext.checkSignature(bytesOf(message), bytesOf(signature), bytesOf(pubkey)), 1);
+}
+
+// The absence of any length check is a decision, not an oversight: empty slices are not a
+// malformed shape here, they reach the host like any other.
+TEST_F(CheckSignatureCall, EmptySlicesReachHostUnvalidated)
+{
+    auto const isEmpty = testing::Property(&Slice::empty, true);
+    EXPECT_CALL(host, checkSignature(isEmpty, isEmpty, isEmpty)).WillOnce(testing::Return(0));
+
+    EXPECT_EQ(hostContext.checkSignature(bytesOf(Bytes{}), bytesOf(Bytes{}), bytesOf(Bytes{})), 0);
+}
+
+TEST_F(CheckSignatureCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, checkSignature(BytesAre("msg"), BytesAre("sig"), BytesAre("key")))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::InvalidParams)));
+
+    EXPECT_EQ(
+        hostContext.checkSignature(bytesOf(message), bytesOf(signature), bytesOf(pubkey)),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(CheckSignatureCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, checkSignature(BytesAre("msg"), BytesAre("sig"), BytesAre("key")))
+        .WillOnce(testing::Throw(std::runtime_error{"signature check came apart"}));
+
+    EXPECT_EQ(
+        hostContext.checkSignature(bytesOf(message), bytesOf(signature), bytesOf(pubkey)),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("signature check came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("checkSignature"));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/CredentialKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/CredentialKeylet.cpp
new file mode 100644
index 0000000000..462babad6c
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/CredentialKeylet.cpp
@@ -0,0 +1,179 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+//
+// `subject` and `issuer` are distinct byte patterns: a happy path built from two copies of the
+// same account would still pass if the two were swapped.
+struct CredentialKeyletCall : HostContextTest
+{
+    Bytes const subjectBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                             0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    Bytes const issuerBytes{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
+                            0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54};
+    Bytes const credentialTypeBytes{0x74, 0x65, 0x72, 0x6d, 0x73};
+    AccountID const subject = AccountID::fromVoid(subjectBytes.data());
+    AccountID const issuer = AccountID::fromVoid(issuerBytes.data());
+    Slice const credentialType{credentialTypeBytes.data(), credentialTypeBytes.size()};
+};
+
+// `credentialType` crosses unvalidated: whatever bytes the guest gives reach the host as-is.
+TEST_F(CredentialKeyletCall, SubjectAndIssuerAreForwardedCredentialTypeUnvalidatedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, credentialKeylet(subject, issuer, credentialType))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.credentialKeylet(
+            bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(credentialTypeBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+// The deliberate edge of "unvalidated": an empty `credentialType` is not a length the ABI
+// rejects, so it reaches the host as an empty `Slice` and the call still succeeds.
+TEST_F(CredentialKeyletCall, EmptyCredentialTypeIsForwardedUnvalidatedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, credentialKeylet(subject, issuer, Slice{})).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.credentialKeylet(
+            bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(Bytes{}), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(CredentialKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, credentialKeylet(subject, issuer, credentialType))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.credentialKeylet(
+            bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(credentialTypeBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(CredentialKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, credentialKeylet(subject, issuer, credentialType))
+        .WillOnce(testing::Throw(std::runtime_error{"credential keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.credentialKeylet(
+            bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(credentialTypeBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("credential keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("credentialKeylet"));
+}
+
+TEST_F(CredentialKeyletCall, MalformedSubjectIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedSubject(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, credentialKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.credentialKeylet(
+            bytesOf(malformedSubject),
+            bytesOf(issuerBytes),
+            bytesOf(credentialTypeBytes),
+            out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(CredentialKeyletCall, MalformedIssuerIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedIssuer(AccountID::size() + 1, 0x41);
+    EXPECT_CALL(host, credentialKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.credentialKeylet(
+            bytesOf(subjectBytes),
+            bytesOf(malformedIssuer),
+            bytesOf(credentialTypeBytes),
+            out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// Both ids fail one combined length check, so a call malformed in both places answers the
+// same `InvalidParams` as either alone; what's observable is that the host is never asked.
+TEST_F(CredentialKeyletCall, BothAccountsMalformedIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedSubject(AccountID::size() - 1, 0x01);
+    Bytes const malformedIssuer(AccountID::size() - 1, 0x41);
+    EXPECT_CALL(host, credentialKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.credentialKeylet(
+            bytesOf(malformedSubject),
+            bytesOf(malformedIssuer),
+            bytesOf(credentialTypeBytes),
+            out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(CredentialKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, credentialKeylet(subject, issuer, credentialType))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.credentialKeylet(
+            bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(credentialTypeBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(CredentialKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, credentialKeylet(subject, issuer, credentialType))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.credentialKeylet(
+            bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(credentialTypeBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(CredentialKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, credentialKeylet(subject, issuer, credentialType))
+        .WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.credentialKeylet(
+            bytesOf(subjectBytes), bytesOf(issuerBytes), bytesOf(credentialTypeBytes), out.slice()),
+        0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjArrayLen.cpp
new file mode 100644
index 0000000000..8c3aab2d8b
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjArrayLen.cpp
@@ -0,0 +1,63 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// `getCurrentLedgerObjArrayLen` answers its count directly rather than through an out region:
+// no axis E, no `OutRegion`, and the happy path asserts the returned count.
+struct CurrentLedgerObjArrayLenCall : HostContextTest
+{
+    std::int32_t fieldCode = sfBalance.getCode();
+};
+
+TEST_F(CurrentLedgerObjArrayLenCall, FieldCodeBecomesSFieldHostIsAskedFor)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjArrayLen(testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(5));
+
+    EXPECT_EQ(hostContext.getCurrentLedgerObjArrayLen(fieldCode), 5);
+}
+
+// `NoArray` is what a field that is not an array actually answers, so it stands in for axis B
+// here rather than an arbitrary code.
+TEST_F(CurrentLedgerObjArrayLenCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjArrayLen(testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::NoArray)));
+
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjArrayLen(fieldCode),
+        hfErrorToInt(HostFunctionError::NoArray));
+}
+
+TEST_F(CurrentLedgerObjArrayLenCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjArrayLen(testing::Ref(sfBalance)))
+        .WillOnce(testing::Throw(std::runtime_error{"current ledger obj array len came apart"}));
+
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjArrayLen(fieldCode),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("current ledger obj array len came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getCurrentLedgerObjArrayLen"));
+}
+
+TEST_F(CurrentLedgerObjArrayLenCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
+{
+    fieldCode = 0x7fff'0000;  // a code nothing is registered under
+    EXPECT_CALL(host, getCurrentLedgerObjArrayLen).Times(0);
+
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjArrayLen(fieldCode),
+        hfErrorToInt(HostFunctionError::InvalidField));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjField.cpp b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjField.cpp
new file mode 100644
index 0000000000..43f139bfc4
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjField.cpp
@@ -0,0 +1,112 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
+// side, not here. The cross-cutting cases over this shape - a non-`std::exception` throw, and
+// a length past `kMaxWasmDataLength` - already live in `TxField.cpp`.
+//
+// Named `CurrentLedgerObjFieldDirectCall`, not `CurrentLedgerObjFieldCall`:
+// `host_calls/CurrentLedgerObjField.cpp` already owns that name in the same gtest binary.
+struct CurrentLedgerObjFieldDirectCall : HostContextTest
+{
+    std::int32_t fieldCode = sfBalance.getCode();
+};
+
+TEST_F(CurrentLedgerObjFieldDirectCall, FieldCodeBecomesSFieldHostIsAskedFor)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjField(fieldCode, out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(CurrentLedgerObjFieldDirectCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FieldNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjField(fieldCode, out.slice()),
+        hfErrorToInt(HostFunctionError::FieldNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(CurrentLedgerObjFieldDirectCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
+{
+    fieldCode = 0x7fff'0000;  // a code nothing is registered under
+    EXPECT_CALL(host, getCurrentLedgerObjField).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjField(fieldCode, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidField));
+}
+
+TEST_F(CurrentLedgerObjFieldDirectCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
+        .WillOnce(testing::Throw(std::runtime_error{"current ledger obj field came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjField(fieldCode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("current ledger obj field came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getCurrentLedgerObjField"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(CurrentLedgerObjFieldDirectCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{value.size() - 1};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjField(fieldCode, out.slice()),
+        static_cast(value.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(CurrentLedgerObjFieldDirectCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{value.size()};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjField(fieldCode, out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(CurrentLedgerObjFieldDirectCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjField(testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getCurrentLedgerObjField(fieldCode, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedArrayLen.cpp
new file mode 100644
index 0000000000..5b68a4c510
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedArrayLen.cpp
@@ -0,0 +1,78 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
+// side, not here.
+//
+// No out region and no axis E: `getCurrentLedgerObjNestedArrayLen` answers the array's
+// element count directly rather than through a written buffer.
+struct CurrentLedgerObjNestedArrayLenCall : HostContextTest
+{
+    std::vector const steps{5, -12, 130};
+    Bytes const locatorBytes = bytesOfSteps(steps);
+};
+
+TEST_F(CurrentLedgerObjNestedArrayLenCall, LocatorBytesBecomeFieldLocatorHostReturnsCount)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjNestedArrayLen(LocatorEquals(steps)))
+        .WillOnce(testing::Return(7));
+
+    EXPECT_EQ(hostContext.getCurrentLedgerObjNestedArrayLen(bytesOf(locatorBytes)), 7);
+}
+
+// `NoArray` - the field the locator resolves to is not an array - is the error this shape
+// most plausibly returns, so it stands in for axis B.
+TEST_F(CurrentLedgerObjNestedArrayLenCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjNestedArrayLen(LocatorEquals(steps)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::NoArray)));
+
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjNestedArrayLen(bytesOf(locatorBytes)),
+        hfErrorToInt(HostFunctionError::NoArray));
+}
+
+TEST_F(CurrentLedgerObjNestedArrayLenCall, EmptyLocatorIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjNestedArrayLen).Times(0);
+
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjNestedArrayLen(bytesOf(Bytes{})),
+        hfErrorToInt(HostFunctionError::LocatorMalformed));
+}
+
+// Distinct from an empty locator: `invokeWithLocator` checks the two conditions separately.
+TEST_F(CurrentLedgerObjNestedArrayLenCall, MisalignedLocatorLengthIsRefusedWithoutAskingHost)
+{
+    Bytes const oddLength{1, 2, 3};
+    EXPECT_CALL(host, getCurrentLedgerObjNestedArrayLen).Times(0);
+
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjNestedArrayLen(bytesOf(oddLength)),
+        hfErrorToInt(HostFunctionError::LocatorMalformed));
+}
+
+TEST_F(CurrentLedgerObjNestedArrayLenCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjNestedArrayLen(LocatorEquals(steps)))
+        .WillOnce(
+            testing::Throw(std::runtime_error{"current ledger obj nested array len came apart"}));
+
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjNestedArrayLen(bytesOf(locatorBytes)),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("current ledger obj nested array len came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getCurrentLedgerObjNestedArrayLen"));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedField.cpp b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedField.cpp
new file mode 100644
index 0000000000..7279438209
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/CurrentLedgerObjNestedField.cpp
@@ -0,0 +1,120 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
+// side, not here.
+struct CurrentLedgerObjNestedFieldCall : HostContextTest
+{
+    std::vector const steps{5, -12, 130};
+    Bytes const locatorBytes = bytesOfSteps(steps);
+};
+
+TEST_F(CurrentLedgerObjNestedFieldCall, LocatorBytesBecomeFieldLocatorHostIsAskedFor)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getCurrentLedgerObjNestedField(LocatorEquals(steps)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjNestedField(bytesOf(locatorBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(CurrentLedgerObjNestedFieldCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjNestedField(LocatorEquals(steps)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::NotLeafField)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjNestedField(bytesOf(locatorBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::NotLeafField));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(CurrentLedgerObjNestedFieldCall, EmptyLocatorIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjNestedField).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjNestedField(bytesOf(Bytes{}), out.slice()),
+        hfErrorToInt(HostFunctionError::LocatorMalformed));
+}
+
+// Distinct from an empty locator: `invokeWithLocator` checks the two conditions separately.
+TEST_F(CurrentLedgerObjNestedFieldCall, MisalignedLocatorLengthIsRefusedWithoutAskingHost)
+{
+    Bytes const oddLength{1, 2, 3};
+    EXPECT_CALL(host, getCurrentLedgerObjNestedField).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjNestedField(bytesOf(oddLength), out.slice()),
+        hfErrorToInt(HostFunctionError::LocatorMalformed));
+}
+
+TEST_F(CurrentLedgerObjNestedFieldCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjNestedField(LocatorEquals(steps)))
+        .WillOnce(testing::Throw(std::runtime_error{"current ledger obj nested field came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjNestedField(bytesOf(locatorBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("current ledger obj nested field came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getCurrentLedgerObjNestedField"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(CurrentLedgerObjNestedFieldCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getCurrentLedgerObjNestedField(LocatorEquals(steps)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{value.size() - 1};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjNestedField(bytesOf(locatorBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(CurrentLedgerObjNestedFieldCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getCurrentLedgerObjNestedField(LocatorEquals(steps)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{value.size()};
+    EXPECT_EQ(
+        hostContext.getCurrentLedgerObjNestedField(bytesOf(locatorBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(CurrentLedgerObjNestedFieldCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, getCurrentLedgerObjNestedField(LocatorEquals(steps)))
+        .WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getCurrentLedgerObjNestedField(bytesOf(locatorBytes), out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/DelegateKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/DelegateKeylet.cpp
new file mode 100644
index 0000000000..d1ef893d00
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/DelegateKeylet.cpp
@@ -0,0 +1,138 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+//
+// `account` and `authorize` are distinct byte patterns: a happy path built from two copies of
+// the same account would still pass if the two were swapped.
+struct DelegateKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                             0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    Bytes const authorizeBytes{0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
+                               0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+    AccountID const authorize = AccountID::fromVoid(authorizeBytes.data());
+};
+
+TEST_F(DelegateKeyletCall, AccountAndAuthorizeAreForwardedInOrderKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, delegateKeylet(account, authorize)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(DelegateKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, delegateKeylet(account, authorize))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(DelegateKeyletCall, MalformedAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, delegateKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.delegateKeylet(bytesOf(malformedAccount), bytesOf(authorizeBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(DelegateKeyletCall, MalformedAuthorizeIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedAuthorize(AccountID::size() + 1, 0xe1);
+    EXPECT_CALL(host, delegateKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(malformedAuthorize), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// Both ids fail one combined length check, so a call malformed in both places answers the
+// same `InvalidParams` as either alone; what's observable is that the host is never asked.
+TEST_F(DelegateKeyletCall, BothAccountsMalformedIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedAccount(AccountID::size() - 1, 0x01);
+    Bytes const malformedAuthorize(AccountID::size() - 1, 0xe1);
+    EXPECT_CALL(host, delegateKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.delegateKeylet(
+            bytesOf(malformedAccount), bytesOf(malformedAuthorize), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(DelegateKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, delegateKeylet(account, authorize))
+        .WillOnce(testing::Throw(std::runtime_error{"delegate keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("delegate keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("delegateKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(DelegateKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, delegateKeylet(account, authorize)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(DelegateKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, delegateKeylet(account, authorize)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(DelegateKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, delegateKeylet(account, authorize)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.delegateKeylet(bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/DepositPreauthKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/DepositPreauthKeylet.cpp
new file mode 100644
index 0000000000..81e524baf2
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/DepositPreauthKeylet.cpp
@@ -0,0 +1,147 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+//
+// `account` and `authorize` are distinct byte patterns: a happy path built from two copies of
+// the same account would still pass if the two were swapped.
+struct DepositPreauthKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                             0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    Bytes const authorizeBytes{0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a,
+                               0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+    AccountID const authorize = AccountID::fromVoid(authorizeBytes.data());
+};
+
+TEST_F(DepositPreauthKeyletCall, AccountAndAuthorizeAreForwardedInOrderKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, depositPreauthKeylet(account, authorize)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.depositPreauthKeylet(
+            bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(DepositPreauthKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, depositPreauthKeylet(account, authorize))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.depositPreauthKeylet(
+            bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(DepositPreauthKeyletCall, MalformedAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, depositPreauthKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.depositPreauthKeylet(
+            bytesOf(malformedAccount), bytesOf(authorizeBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(DepositPreauthKeyletCall, MalformedAuthorizeIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedAuthorize(AccountID::size() + 1, 0x91);
+    EXPECT_CALL(host, depositPreauthKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.depositPreauthKeylet(
+            bytesOf(accountBytes), bytesOf(malformedAuthorize), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// Both ids fail one combined length check, so a call malformed in both places answers the
+// same `InvalidParams` as either alone; what's observable is that the host is never asked.
+TEST_F(DepositPreauthKeyletCall, BothAccountsMalformedIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedAccount(AccountID::size() - 1, 0x01);
+    Bytes const malformedAuthorize(AccountID::size() - 1, 0x91);
+    EXPECT_CALL(host, depositPreauthKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.depositPreauthKeylet(
+            bytesOf(malformedAccount), bytesOf(malformedAuthorize), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(DepositPreauthKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, depositPreauthKeylet(account, authorize))
+        .WillOnce(testing::Throw(std::runtime_error{"deposit preauth keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.depositPreauthKeylet(
+            bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("deposit preauth keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("depositPreauthKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(DepositPreauthKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, depositPreauthKeylet(account, authorize)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.depositPreauthKeylet(
+            bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(DepositPreauthKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, depositPreauthKeylet(account, authorize)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.depositPreauthKeylet(
+            bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(DepositPreauthKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, depositPreauthKeylet(account, authorize)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.depositPreauthKeylet(
+            bytesOf(accountBytes), bytesOf(authorizeBytes), out.slice()),
+        0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/DidKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/DidKeylet.cpp
new file mode 100644
index 0000000000..220cce677f
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/DidKeylet.cpp
@@ -0,0 +1,126 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct DidKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,
+                             0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+};
+
+TEST_F(DidKeyletCall, AccountIsForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, didKeylet(account)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.didKeylet(bytesOf(accountBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(DidKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, didKeylet(account))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.didKeylet(bytesOf(accountBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(DidKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const shortAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, didKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.didKeylet(bytesOf(shortAccount), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(DidKeyletCall, LongAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const longAccount(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, didKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.didKeylet(bytesOf(longAccount), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(DidKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, didKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.didKeylet(bytesOf(Bytes{}), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(DidKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, didKeylet(account))
+        .WillOnce(testing::Throw(std::runtime_error{"did keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.didKeylet(bytesOf(accountBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("did keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("didKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(DidKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, didKeylet(account)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.didKeylet(bytesOf(accountBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(DidKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, didKeylet(account)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.didKeylet(bytesOf(accountBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(DidKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, didKeylet(account)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.didKeylet(bytesOf(accountBytes), out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/EscrowKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/EscrowKeylet.cpp
new file mode 100644
index 0000000000..7517a2a955
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/EscrowKeylet.cpp
@@ -0,0 +1,131 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+//
+// The first file over the account-in, keylet-out shape `invokeWithAccount` gives eleven other
+// methods, so `account` is a distinctive 20 bytes rather than all-zero: a forwarding mistake
+// (a swapped byte, a truncated copy) would still pass against an all-zero id.
+struct EscrowKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                             0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+    std::uint32_t const seq = 12345;
+};
+
+TEST_F(EscrowKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, escrowKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(EscrowKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, escrowKeylet(account, seq))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(EscrowKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const shortAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, escrowKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.escrowKeylet(bytesOf(shortAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(EscrowKeyletCall, LongAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const longAccount(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, escrowKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.escrowKeylet(bytesOf(longAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(EscrowKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, escrowKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.escrowKeylet(bytesOf(Bytes{}), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(EscrowKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, escrowKeylet(account, seq))
+        .WillOnce(testing::Throw(std::runtime_error{"escrow keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("escrow keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("escrowKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(EscrowKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, escrowKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(EscrowKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, escrowKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(EscrowKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, escrowKeylet(account, seq)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.escrowKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatAdd.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatAdd.cpp
new file mode 100644
index 0000000000..5408e80738
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatAdd.cpp
@@ -0,0 +1,89 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
+// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D
+// axis. `x` and `y` carry different content, so a call that swapped them would fail to match.
+struct FloatAddCall : HostContextTest
+{
+    Bytes const x{'a', 'd', 'd', '-', 'x'};
+    Bytes const y{'a', 'd', 'd', '-', 'y', 'y'};
+    std::int32_t const mode = 7;
+};
+
+TEST_F(FloatAddCall, OperandsAndModeAreForwardedResultIsWritten)
+{
+    Bytes const result{9, 8, 7};
+    EXPECT_CALL(host, floatAdd(BytesAre("add-x"), BytesAre("add-yy"), mode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatAdd(bytesOf(x), bytesOf(y), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_TRUE(out.holds(bytesOf(result)));
+}
+
+TEST_F(FloatAddCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatAdd(BytesAre("add-x"), BytesAre("add-yy"), mode))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatAdd(bytesOf(x), bytesOf(y), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::FloatComputationError));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(FloatAddCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatAdd(BytesAre("add-x"), BytesAre("add-yy"), mode))
+        .WillOnce(testing::Throw(std::runtime_error{"float add came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatAdd(bytesOf(x), bytesOf(y), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("float add came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatAdd"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(FloatAddCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const result{9, 8, 7};
+    EXPECT_CALL(host, floatAdd(BytesAre("add-x"), BytesAre("add-yy"), mode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{result.size() - 1};
+    EXPECT_EQ(
+        hostContext.floatAdd(bytesOf(x), bytesOf(y), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+// No length rule exists at this layer: a differently sized operand still reaches the host
+// rather than being refused.
+TEST_F(FloatAddCall, OddSizedOperandReachesHostUnchanged)
+{
+    Bytes const shortX{0x2a};
+    EXPECT_CALL(host, floatAdd(testing::_, BytesAre("add-yy"), mode))
+        .WillOnce(testing::Return(Bytes{1}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.floatAdd(bytesOf(shortX), bytesOf(y), mode, out.slice()), 1);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatCompare.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatCompare.cpp
new file mode 100644
index 0000000000..381dc58e6f
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatCompare.cpp
@@ -0,0 +1,64 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
+// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D axis.
+// `x` and `y` carry different content, so a call that swapped them would fail to match.
+// `floatCompare` answers its comparison directly rather than through `answer`, so there is no
+// out region and no axis E.
+struct FloatCompareCall : HostContextTest
+{
+    Bytes const x{'c', 'm', 'p', '-', 'x'};
+    Bytes const y{'c', 'm', 'p', '-', 'y', 'y'};
+};
+
+TEST_F(FloatCompareCall, XAndYAreForwardedResultReturnedDirectly)
+{
+    EXPECT_CALL(host, floatCompare(BytesAre("cmp-x"), BytesAre("cmp-yy")))
+        .WillOnce(testing::Return(1));
+
+    EXPECT_EQ(hostContext.floatCompare(bytesOf(x), bytesOf(y)), 1);
+}
+
+TEST_F(FloatCompareCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatCompare(BytesAre("cmp-x"), BytesAre("cmp-yy")))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
+
+    EXPECT_EQ(
+        hostContext.floatCompare(bytesOf(x), bytesOf(y)),
+        hfErrorToInt(HostFunctionError::FloatComputationError));
+}
+
+TEST_F(FloatCompareCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatCompare(BytesAre("cmp-x"), BytesAre("cmp-yy")))
+        .WillOnce(testing::Throw(std::runtime_error{"float compare came apart"}));
+
+    EXPECT_EQ(
+        hostContext.floatCompare(bytesOf(x), bytesOf(y)),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("float compare came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatCompare"));
+}
+
+// No length rule exists at this layer: a differently sized operand still reaches the host
+// rather than being refused.
+TEST_F(FloatCompareCall, OddSizedOperandReachesHostUnchanged)
+{
+    Bytes const oddX{0x2a};
+    EXPECT_CALL(host, floatCompare(testing::_, BytesAre("cmp-yy"))).WillOnce(testing::Return(0));
+
+    EXPECT_EQ(hostContext.floatCompare(bytesOf(oddX), bytesOf(y)), 0);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatDivide.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatDivide.cpp
new file mode 100644
index 0000000000..b224e353e8
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatDivide.cpp
@@ -0,0 +1,89 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
+// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D
+// axis. `x` and `y` carry different content, so a call that swapped them would fail to match.
+struct FloatDivideCall : HostContextTest
+{
+    Bytes const x{'d', 'i', 'v', '-', 'x'};
+    Bytes const y{'d', 'i', 'v', '-', 'y', 'y'};
+    std::int32_t const mode = 42;
+};
+
+TEST_F(FloatDivideCall, OperandsAndModeAreForwardedResultIsWritten)
+{
+    Bytes const result{9, 8, 7};
+    EXPECT_CALL(host, floatDivide(BytesAre("div-x"), BytesAre("div-yy"), mode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatDivide(bytesOf(x), bytesOf(y), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_TRUE(out.holds(bytesOf(result)));
+}
+
+TEST_F(FloatDivideCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatDivide(BytesAre("div-x"), BytesAre("div-yy"), mode))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatDivide(bytesOf(x), bytesOf(y), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::FloatComputationError));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(FloatDivideCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatDivide(BytesAre("div-x"), BytesAre("div-yy"), mode))
+        .WillOnce(testing::Throw(std::runtime_error{"float divide came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatDivide(bytesOf(x), bytesOf(y), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("float divide came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatDivide"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(FloatDivideCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const result{9, 8, 7};
+    EXPECT_CALL(host, floatDivide(BytesAre("div-x"), BytesAre("div-yy"), mode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{result.size() - 1};
+    EXPECT_EQ(
+        hostContext.floatDivide(bytesOf(x), bytesOf(y), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+// No length rule exists at this layer: a differently sized operand still reaches the host
+// rather than being refused.
+TEST_F(FloatDivideCall, OddSizedOperandReachesHostUnchanged)
+{
+    Bytes const shortX{0x2a};
+    EXPECT_CALL(host, floatDivide(testing::_, BytesAre("div-yy"), mode))
+        .WillOnce(testing::Return(Bytes{1}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.floatDivide(bytesOf(shortX), bytesOf(y), mode, out.slice()), 1);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatFromInt.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatFromInt.cpp
new file mode 100644
index 0000000000..51736589a8
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatFromInt.cpp
@@ -0,0 +1,84 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// `x` arrives as a wasm scalar, not as bytes to decode, so there is nothing here to get wrong
+// about its shape - no D axis.
+struct FloatFromIntCall : HostContextTest
+{
+    std::int64_t const x = 123456789;
+    std::int32_t const mode = 1;
+};
+
+TEST_F(FloatFromIntCall, ValueAndModeAreForwardedResultIsWritten)
+{
+    Bytes const result{1, 2, 3};
+    EXPECT_CALL(host, floatFromInt(x, mode)).WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromInt(x, mode, out.slice()), static_cast(result.size()));
+    EXPECT_TRUE(out.holds(bytesOf(result)));
+}
+
+// `mode` is forwarded verbatim: this layer validates nothing about it, so a nonsense value
+// still reaches the host unchanged.
+TEST_F(FloatFromIntCall, ModeIsForwardedVerbatim)
+{
+    std::int32_t const nonsenseMode = -12345;
+    Bytes const result{1};
+    EXPECT_CALL(host, floatFromInt(x, nonsenseMode)).WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromInt(x, nonsenseMode, out.slice()),
+        static_cast(result.size()));
+}
+
+TEST_F(FloatFromIntCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatFromInt(x, mode))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromInt(x, mode, out.slice()),
+        hfErrorToInt(HostFunctionError::FloatComputationError));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(FloatFromIntCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatFromInt(x, mode))
+        .WillOnce(testing::Throw(std::runtime_error{"float from int came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromInt(x, mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("float from int came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatFromInt"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(FloatFromIntCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const result{1, 2, 3};
+    EXPECT_CALL(host, floatFromInt(x, mode)).WillOnce(testing::Return(result));
+
+    OutRegion out{result.size() - 1};
+    EXPECT_EQ(
+        hostContext.floatFromInt(x, mode, out.slice()), static_cast(result.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatFromMantExp.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatFromMantExp.cpp
new file mode 100644
index 0000000000..f825f43f49
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatFromMantExp.cpp
@@ -0,0 +1,73 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// `mantissa`, `exponent` and `mode` all arrive as wasm scalars, not as bytes to decode, so
+// there is nothing here to get wrong about their shape - no D axis.
+struct FloatFromMantExpCall : HostContextTest
+{
+    std::int64_t const mantissa = 123456789;
+    std::int32_t const exponent = -5;
+    std::int32_t const mode = 1;
+};
+
+TEST_F(FloatFromMantExpCall, MantissaExponentAndModeAreForwardedResultIsWritten)
+{
+    Bytes const result{1, 2, 3};
+    EXPECT_CALL(host, floatFromMantExp(mantissa, exponent, mode)).WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromMantExp(mantissa, exponent, mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_TRUE(out.holds(bytesOf(result)));
+}
+
+TEST_F(FloatFromMantExpCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatFromMantExp(mantissa, exponent, mode))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromMantExp(mantissa, exponent, mode, out.slice()),
+        hfErrorToInt(HostFunctionError::FloatComputationError));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(FloatFromMantExpCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatFromMantExp(mantissa, exponent, mode))
+        .WillOnce(testing::Throw(std::runtime_error{"float from mant exp came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromMantExp(mantissa, exponent, mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("float from mant exp came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatFromMantExp"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(FloatFromMantExpCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const result{1, 2, 3};
+    EXPECT_CALL(host, floatFromMantExp(mantissa, exponent, mode)).WillOnce(testing::Return(result));
+
+    OutRegion out{result.size() - 1};
+    EXPECT_EQ(
+        hostContext.floatFromMantExp(mantissa, exponent, mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatFromSTAmount.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatFromSTAmount.cpp
new file mode 100644
index 0000000000..ce2b9f070e
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatFromSTAmount.cpp
@@ -0,0 +1,102 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+namespace {
+
+Bytes
+serialized(STAmount const& amount)
+{
+    Serializer s;
+    amount.add(s);
+    return s.getData();
+}
+
+}  // namespace
+
+// The only file exercising `parseST`. A malformed buffer throws inside `STAmount`'s
+// deserializing constructor; `parseST` catches that itself, so the host is never asked - unlike
+// a `guarded`-caught throw from the host's own body.
+struct FloatFromSTAmountCall : HostContextTest
+{
+    STAmount const amount{XRPAmount{1000}};
+    Bytes const wireBytes = serialized(amount);
+    std::int32_t const mode = 1;
+};
+
+TEST_F(FloatFromSTAmountCall, SerializedAmountDecodesToValueHostIsAskedFor)
+{
+    Bytes const result{1, 2, 3};
+    EXPECT_CALL(host, floatFromSTAmount(testing::Eq(amount), mode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromSTAmount(bytesOf(wireBytes), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_TRUE(out.holds(bytesOf(result)));
+}
+
+TEST_F(FloatFromSTAmountCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatFromSTAmount(testing::Eq(amount), mode))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromSTAmount(bytesOf(wireBytes), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::FloatComputationError));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(FloatFromSTAmountCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatFromSTAmount(testing::Eq(amount), mode))
+        .WillOnce(testing::Throw(std::runtime_error{"float from st amount came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromSTAmount(bytesOf(wireBytes), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("float from st amount came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatFromSTAmount"));
+}
+
+// `parseST` catches its own failure: a malformed buffer never reaches the host at all.
+TEST_F(FloatFromSTAmountCall, MalformedBytesAreRefusedWithoutAskingHost)
+{
+    Bytes const malformedBytes{0xff, 0xff, 0xff};
+    EXPECT_CALL(host, floatFromSTAmount).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromSTAmount(bytesOf(malformedBytes), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(FloatFromSTAmountCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const result{1, 2, 3};
+    EXPECT_CALL(host, floatFromSTAmount(testing::Eq(amount), mode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{result.size() - 1};
+    EXPECT_EQ(
+        hostContext.floatFromSTAmount(bytesOf(wireBytes), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatFromSTNumber.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatFromSTNumber.cpp
new file mode 100644
index 0000000000..58497bd914
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatFromSTNumber.cpp
@@ -0,0 +1,110 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+namespace {
+
+// The wire form `STNumber(SerialIter&, SField const&)` expects: an eight-byte mantissa
+// followed by a four-byte exponent. Built directly rather than through `STNumber::add`, which
+// asserts its field is bound to `STI_NUMBER` - an assertion `sfGeneric` does not satisfy.
+Bytes
+serialized(std::int64_t mantissa, std::int32_t exponent)
+{
+    Serializer s;
+    s.add64(mantissa);
+    s.add32(exponent);
+    return s.getData();
+}
+
+}  // namespace
+
+// The only file exercising `parseST`. A malformed buffer throws inside `STNumber`'s
+// deserializing constructor; `parseST` catches that itself, so the host is never asked - unlike
+// a `guarded`-caught throw from the host's own body.
+struct FloatFromSTNumberCall : HostContextTest
+{
+    std::int64_t const mantissa = 123456789;
+    std::int32_t const exponent = -5;
+    STNumber const number{sfGeneric, Number{mantissa, exponent}};
+    Bytes const wireBytes = serialized(mantissa, exponent);
+    std::int32_t const mode = 1;
+};
+
+TEST_F(FloatFromSTNumberCall, SerializedNumberDecodesToValueHostIsAskedFor)
+{
+    Bytes const result{1, 2, 3};
+    EXPECT_CALL(host, floatFromSTNumber(testing::Eq(number), mode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromSTNumber(bytesOf(wireBytes), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_TRUE(out.holds(bytesOf(result)));
+}
+
+TEST_F(FloatFromSTNumberCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatFromSTNumber(testing::Eq(number), mode))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromSTNumber(bytesOf(wireBytes), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::FloatComputationError));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(FloatFromSTNumberCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatFromSTNumber(testing::Eq(number), mode))
+        .WillOnce(testing::Throw(std::runtime_error{"float from st number came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromSTNumber(bytesOf(wireBytes), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("float from st number came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatFromSTNumber"));
+}
+
+// `parseST` catches its own failure: a malformed buffer never reaches the host at all.
+TEST_F(FloatFromSTNumberCall, MalformedBytesAreRefusedWithoutAskingHost)
+{
+    Bytes const malformedBytes{0xff, 0xff, 0xff};
+    EXPECT_CALL(host, floatFromSTNumber).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromSTNumber(bytesOf(malformedBytes), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(FloatFromSTNumberCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const result{1, 2, 3};
+    EXPECT_CALL(host, floatFromSTNumber(testing::Eq(number), mode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{result.size() - 1};
+    EXPECT_EQ(
+        hostContext.floatFromSTNumber(bytesOf(wireBytes), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatFromUint.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatFromUint.cpp
new file mode 100644
index 0000000000..2c8c101863
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatFromUint.cpp
@@ -0,0 +1,130 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The only file exercising `parseUint64`: exactly eight bytes, little-endian.
+struct FloatFromUintCall : HostContextTest
+{
+    // Every byte distinct, so a byte-order mistake in `parseUint64` would decode to a
+    // different value rather than the same one by coincidence.
+    std::uint64_t const value = 0x0102'0304'0506'0708ULL;
+    Bytes const wireBytes = bytesOfScalar(value);
+    std::int32_t const mode = 1;
+};
+
+TEST_F(FloatFromUintCall, LittleEndianWireBytesDecodeToValueHostIsAskedFor)
+{
+    Bytes const result{1, 2, 3};
+    EXPECT_CALL(host, floatFromUint(value, mode)).WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromUint(bytesOf(wireBytes), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_TRUE(out.holds(bytesOf(result)));
+}
+
+TEST_F(FloatFromUintCall, ModeIsForwardedVerbatim)
+{
+    std::int32_t const nonsenseMode = -12345;
+    Bytes const result{1};
+    EXPECT_CALL(host, floatFromUint(value, nonsenseMode)).WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromUint(bytesOf(wireBytes), nonsenseMode, out.slice()),
+        static_cast(result.size()));
+}
+
+TEST_F(FloatFromUintCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatFromUint(value, mode))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatInputMalformed)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromUint(bytesOf(wireBytes), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::FloatInputMalformed));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(FloatFromUintCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatFromUint(value, mode))
+        .WillOnce(testing::Throw(std::runtime_error{"uint came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromUint(bytesOf(wireBytes), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("uint came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatFromUint"));
+}
+
+TEST_F(FloatFromUintCall, SevenByteRegionIsRefusedWithoutAskingHost)
+{
+    Bytes const shortBytes(7, 0);
+    EXPECT_CALL(host, floatFromUint).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromUint(bytesOf(shortBytes), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(FloatFromUintCall, NineByteRegionIsRefusedWithoutAskingHost)
+{
+    Bytes const longBytes(9, 0);
+    EXPECT_CALL(host, floatFromUint).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromUint(bytesOf(longBytes), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(FloatFromUintCall, EmptyRegionIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, floatFromUint).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatFromUint(bytesOf(Bytes{}), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(FloatFromUintCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const result{1, 2, 3};
+    EXPECT_CALL(host, floatFromUint(value, mode)).WillOnce(testing::Return(result));
+
+    OutRegion out{result.size() - 1};
+    EXPECT_EQ(
+        hostContext.floatFromUint(bytesOf(wireBytes), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(FloatFromUintCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const result{1, 2, 3};
+    EXPECT_CALL(host, floatFromUint(value, mode)).WillOnce(testing::Return(result));
+
+    OutRegion out{result.size()};
+    EXPECT_EQ(
+        hostContext.floatFromUint(bytesOf(wireBytes), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_TRUE(out.holds(bytesOf(result)));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatMultiply.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatMultiply.cpp
new file mode 100644
index 0000000000..74f20430e4
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatMultiply.cpp
@@ -0,0 +1,89 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
+// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D
+// axis. `x` and `y` carry different content, so a call that swapped them would fail to match.
+struct FloatMultiplyCall : HostContextTest
+{
+    Bytes const x{'m', 'u', 'l', '-', 'x'};
+    Bytes const y{'m', 'u', 'l', '-', 'y', 'y'};
+    std::int32_t const mode = 21;
+};
+
+TEST_F(FloatMultiplyCall, OperandsAndModeAreForwardedResultIsWritten)
+{
+    Bytes const result{9, 8, 7};
+    EXPECT_CALL(host, floatMultiply(BytesAre("mul-x"), BytesAre("mul-yy"), mode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatMultiply(bytesOf(x), bytesOf(y), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_TRUE(out.holds(bytesOf(result)));
+}
+
+TEST_F(FloatMultiplyCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatMultiply(BytesAre("mul-x"), BytesAre("mul-yy"), mode))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatMultiply(bytesOf(x), bytesOf(y), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::FloatComputationError));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(FloatMultiplyCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatMultiply(BytesAre("mul-x"), BytesAre("mul-yy"), mode))
+        .WillOnce(testing::Throw(std::runtime_error{"float multiply came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatMultiply(bytesOf(x), bytesOf(y), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("float multiply came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatMultiply"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(FloatMultiplyCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const result{9, 8, 7};
+    EXPECT_CALL(host, floatMultiply(BytesAre("mul-x"), BytesAre("mul-yy"), mode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{result.size() - 1};
+    EXPECT_EQ(
+        hostContext.floatMultiply(bytesOf(x), bytesOf(y), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+// No length rule exists at this layer: a differently sized operand still reaches the host
+// rather than being refused.
+TEST_F(FloatMultiplyCall, OddSizedOperandReachesHostUnchanged)
+{
+    Bytes const shortX{0x2a};
+    EXPECT_CALL(host, floatMultiply(testing::_, BytesAre("mul-yy"), mode))
+        .WillOnce(testing::Return(Bytes{1}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.floatMultiply(bytesOf(shortX), bytesOf(y), mode, out.slice()), 1);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatPower.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatPower.cpp
new file mode 100644
index 0000000000..3ec0c3b8be
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatPower.cpp
@@ -0,0 +1,103 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
+// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D
+// axis. `n` and `mode` carry different values, so a call that swapped them would fail to
+// match.
+struct FloatPowerCall : HostContextTest
+{
+    Bytes const x{'p', 'o', 'w', '-', 'x'};
+    std::int32_t const n = 4;
+    std::int32_t const mode = 22;
+};
+
+TEST_F(FloatPowerCall, OperandNAndModeAreForwardedResultIsWritten)
+{
+    Bytes const result{4, 5, 6};
+    EXPECT_CALL(host, floatPower(BytesAre("pow-x"), n, mode)).WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatPower(bytesOf(x), n, mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_TRUE(out.holds(bytesOf(result)));
+}
+
+TEST_F(FloatPowerCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatPower(BytesAre("pow-x"), n, mode))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatPower(bytesOf(x), n, mode, out.slice()),
+        hfErrorToInt(HostFunctionError::FloatComputationError));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(FloatPowerCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatPower(BytesAre("pow-x"), n, mode))
+        .WillOnce(testing::Throw(std::runtime_error{"float power came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatPower(bytesOf(x), n, mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("float power came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatPower"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(FloatPowerCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const result{4, 5, 6};
+    EXPECT_CALL(host, floatPower(BytesAre("pow-x"), n, mode)).WillOnce(testing::Return(result));
+
+    OutRegion out{result.size() - 1};
+    EXPECT_EQ(
+        hostContext.floatPower(bytesOf(x), n, mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+// No length rule exists at this layer: a differently sized operand still reaches the host
+// rather than being refused.
+TEST_F(FloatPowerCall, OddSizedOperandReachesHostUnchanged)
+{
+    Bytes const shortX{0x2a};
+    EXPECT_CALL(host, floatPower(testing::_, n, mode)).WillOnce(testing::Return(Bytes{1}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.floatPower(bytesOf(shortX), n, mode, out.slice()), 1);
+}
+
+// `mode` and `n` validate nothing at this layer and cross verbatim, including values with no
+// real meaning. Worth pinning once across the float family rather than in every file.
+TEST_F(FloatPowerCall, ModeAndNAreForwardedVerbatim)
+{
+    std::int32_t const nonsenseN = -999;
+    std::int32_t const nonsenseMode = 424242;
+    Bytes const result{1};
+    EXPECT_CALL(host, floatPower(BytesAre("pow-x"), nonsenseN, nonsenseMode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatPower(bytesOf(x), nonsenseN, nonsenseMode, out.slice()),
+        static_cast(result.size()));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatSubtract.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatSubtract.cpp
new file mode 100644
index 0000000000..1821acc392
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatSubtract.cpp
@@ -0,0 +1,89 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// Every input slice passes straight through to the host, unlike `invokeWithAccount`'s
+// twenty-byte check or `parseUint64`'s eight: nothing here is validated, so there is no D
+// axis. `x` and `y` carry different content, so a call that swapped them would fail to match.
+struct FloatSubtractCall : HostContextTest
+{
+    Bytes const x{'s', 'u', 'b', '-', 'x'};
+    Bytes const y{'s', 'u', 'b', '-', 'y', 'y'};
+    std::int32_t const mode = 13;
+};
+
+TEST_F(FloatSubtractCall, OperandsAndModeAreForwardedResultIsWritten)
+{
+    Bytes const result{9, 8, 7};
+    EXPECT_CALL(host, floatSubtract(BytesAre("sub-x"), BytesAre("sub-yy"), mode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatSubtract(bytesOf(x), bytesOf(y), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_TRUE(out.holds(bytesOf(result)));
+}
+
+TEST_F(FloatSubtractCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatSubtract(BytesAre("sub-x"), BytesAre("sub-yy"), mode))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatSubtract(bytesOf(x), bytesOf(y), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::FloatComputationError));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(FloatSubtractCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatSubtract(BytesAre("sub-x"), BytesAre("sub-yy"), mode))
+        .WillOnce(testing::Throw(std::runtime_error{"float subtract came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatSubtract(bytesOf(x), bytesOf(y), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("float subtract came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatSubtract"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(FloatSubtractCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const result{9, 8, 7};
+    EXPECT_CALL(host, floatSubtract(BytesAre("sub-x"), BytesAre("sub-yy"), mode))
+        .WillOnce(testing::Return(result));
+
+    OutRegion out{result.size() - 1};
+    EXPECT_EQ(
+        hostContext.floatSubtract(bytesOf(x), bytesOf(y), mode, out.slice()),
+        static_cast(result.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+// No length rule exists at this layer: a differently sized operand still reaches the host
+// rather than being refused.
+TEST_F(FloatSubtractCall, OddSizedOperandReachesHostUnchanged)
+{
+    Bytes const shortX{0x2a};
+    EXPECT_CALL(host, floatSubtract(testing::_, BytesAre("sub-yy"), mode))
+        .WillOnce(testing::Return(Bytes{1}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.floatSubtract(bytesOf(shortX), bytesOf(y), mode, out.slice()), 1);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatToInt.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatToInt.cpp
new file mode 100644
index 0000000000..7f9f3f0ae2
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatToInt.cpp
@@ -0,0 +1,80 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The input slice passes straight through to the host, unlike `invokeWithAccount`'s twenty-byte
+// check or `parseUint64`'s eight: nothing here is validated, so there is no D axis.
+struct FloatToIntCall : HostContextTest
+{
+    Bytes const x{'t', 'o', 'i', 'n', 't'};
+    std::int32_t const mode = 3;
+};
+
+TEST_F(FloatToIntCall, OperandAndModeAreForwardedResultWrittenAsLittleEndianBytes)
+{
+    std::int64_t const value = -123456789;
+    EXPECT_CALL(host, floatToInt(BytesAre("toint"), mode)).WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.floatToInt(bytesOf(x), mode, out.slice()), 8);
+    EXPECT_TRUE(out.holds(bytesOf(bytesOfScalar(value))));
+}
+
+TEST_F(FloatToIntCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatToInt(BytesAre("toint"), mode))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatToInt(bytesOf(x), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::FloatComputationError));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(FloatToIntCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatToInt(BytesAre("toint"), mode))
+        .WillOnce(testing::Throw(std::runtime_error{"float to int came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.floatToInt(bytesOf(x), mode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("float to int came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatToInt"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(FloatToIntCall, SevenByteOutRegionWritesNothingAndReturnsTrueLength)
+{
+    std::int64_t const value = 42;
+    EXPECT_CALL(host, floatToInt(BytesAre("toint"), mode)).WillOnce(testing::Return(value));
+
+    OutRegion out{7};
+    EXPECT_EQ(hostContext.floatToInt(bytesOf(x), mode, out.slice()), 8);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+// No length rule exists at this layer: a differently sized operand still reaches the host
+// rather than being refused.
+TEST_F(FloatToIntCall, OddSizedOperandReachesHostUnchanged)
+{
+    Bytes const oddX{0x2a};
+    EXPECT_CALL(host, floatToInt(testing::_, mode)).WillOnce(testing::Return(std::int64_t{7}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.floatToInt(bytesOf(oddX), mode, out.slice()), 8);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/FloatToMantExp.cpp b/src/tests/libxrpl/tx/wasm/host_context/FloatToMantExp.cpp
new file mode 100644
index 0000000000..fb6a82cdc4
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/FloatToMantExp.cpp
@@ -0,0 +1,101 @@
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The input slice passes straight through to the host, unlike `invokeWithAccount`'s twenty-byte
+// check or `parseUint64`'s eight: nothing here is validated, so there is no D axis. The two out
+// regions are each checked and written independently; the return is their summed true length.
+struct FloatToMantExpCall : HostContextTest
+{
+    Bytes const x{'m', 'a', 'n', 't', 'e', 'x', 'p'};
+    std::int64_t const mantissa = 0x0102'0304'0506'0708LL;
+    std::int32_t const exponent = -5;
+    FloatPair const pair{mantissa, exponent};
+};
+
+TEST_F(FloatToMantExpCall, OperandIsForwardedMantissaAndExponentWrittenAsLittleEndianBytes)
+{
+    EXPECT_CALL(host, floatToMantExp(BytesAre("mantexp"))).WillOnce(testing::Return(pair));
+
+    OutRegion mantissaOut{8};
+    OutRegion exponentOut{4};
+    EXPECT_EQ(hostContext.floatToMantExp(bytesOf(x), mantissaOut.slice(), exponentOut.slice()), 12);
+    EXPECT_TRUE(mantissaOut.holds(bytesOf(bytesOfScalar(mantissa))));
+    EXPECT_TRUE(exponentOut.holds(bytesOf(bytesOfScalar(exponent))));
+}
+
+TEST_F(FloatToMantExpCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, floatToMantExp(BytesAre("mantexp")))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FloatComputationError)));
+
+    OutRegion mantissaOut{8};
+    OutRegion exponentOut{4};
+    EXPECT_EQ(
+        hostContext.floatToMantExp(bytesOf(x), mantissaOut.slice(), exponentOut.slice()),
+        hfErrorToInt(HostFunctionError::FloatComputationError));
+    EXPECT_FALSE(mantissaOut.wasWritten());
+    EXPECT_FALSE(exponentOut.wasWritten());
+}
+
+TEST_F(FloatToMantExpCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, floatToMantExp(BytesAre("mantexp")))
+        .WillOnce(testing::Throw(std::runtime_error{"float to mant exp came apart"}));
+
+    OutRegion mantissaOut{8};
+    OutRegion exponentOut{4};
+    EXPECT_EQ(
+        hostContext.floatToMantExp(bytesOf(x), mantissaOut.slice(), exponentOut.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("float to mant exp came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("floatToMantExp"));
+}
+
+// Each region is checked independently: a short mantissa region does not stop the exponent
+// from being written, and the sum still counts the mantissa's true length.
+TEST_F(FloatToMantExpCall, ShortMantissaRegionWritesNothingThereSumStillCountsIt)
+{
+    EXPECT_CALL(host, floatToMantExp(BytesAre("mantexp"))).WillOnce(testing::Return(pair));
+
+    OutRegion mantissaOut{7};
+    OutRegion exponentOut{4};
+    EXPECT_EQ(hostContext.floatToMantExp(bytesOf(x), mantissaOut.slice(), exponentOut.slice()), 12);
+    EXPECT_FALSE(mantissaOut.wasWritten());
+    EXPECT_TRUE(exponentOut.holds(bytesOf(bytesOfScalar(exponent))));
+}
+
+TEST_F(FloatToMantExpCall, ShortExponentRegionWritesNothingThereSumStillCountsIt)
+{
+    EXPECT_CALL(host, floatToMantExp(BytesAre("mantexp"))).WillOnce(testing::Return(pair));
+
+    OutRegion mantissaOut{8};
+    OutRegion exponentOut{3};
+    EXPECT_EQ(hostContext.floatToMantExp(bytesOf(x), mantissaOut.slice(), exponentOut.slice()), 12);
+    EXPECT_TRUE(mantissaOut.holds(bytesOf(bytesOfScalar(mantissa))));
+    EXPECT_FALSE(exponentOut.wasWritten());
+}
+
+// No length rule exists at this layer: a differently sized operand still reaches the host
+// rather than being refused.
+TEST_F(FloatToMantExpCall, OddSizedOperandReachesHostUnchanged)
+{
+    Bytes const oddX{0x2a};
+    EXPECT_CALL(host, floatToMantExp(testing::_)).WillOnce(testing::Return(pair));
+
+    OutRegion mantissaOut{8};
+    OutRegion exponentOut{4};
+    EXPECT_EQ(
+        hostContext.floatToMantExp(bytesOf(oddX), mantissaOut.slice(), exponentOut.slice()), 12);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/IsAmendmentEnabled.cpp b/src/tests/libxrpl/tx/wasm/host_context/IsAmendmentEnabled.cpp
new file mode 100644
index 0000000000..f8fe9c7010
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/IsAmendmentEnabled.cpp
@@ -0,0 +1,108 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The whole point of this file: a 32-byte input tries as an amendment id first, and falls back
+// to a name lookup - on those same bytes - only if that id lookup does not answer enabled.
+struct IsAmendmentEnabledCall : HostContextTest
+{
+    Bytes const idBytes = Bytes(uint256::size(), 0x11);
+    uint256 const id = uint256::fromVoid(idBytes.data());
+};
+
+TEST_F(IsAmendmentEnabledCall, ThirtyTwoByteEnabledIdAnswersOneWithoutNameLookup)
+{
+    EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(testing::Eq(id))))
+        .WillOnce(testing::Return(1));
+    EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(testing::_)))
+        .Times(0);
+
+    EXPECT_EQ(hostContext.isAmendmentEnabled(bytesOf(idBytes)), 1);
+}
+
+// The same 32 bytes, read first as an id and, once that is not an enabled one, as a name.
+TEST_F(IsAmendmentEnabledCall, ThirtyTwoByteDisabledIdFallsThroughToNameLookupWithSameBytes)
+{
+    std::string_view const nameFromBytes{
+        reinterpret_cast(idBytes.data()), idBytes.size()};
+    EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(testing::Eq(id))))
+        .WillOnce(testing::Return(0));
+    EXPECT_CALL(
+        host,
+        isAmendmentEnabled(testing::Matcher(testing::Eq(nameFromBytes))))
+        .WillOnce(testing::Return(1));
+
+    EXPECT_EQ(hostContext.isAmendmentEnabled(bytesOf(idBytes)), 1);
+}
+
+// An id lookup that errors is treated the same as one that says no: both fall through to the
+// name lookup rather than surfacing the error.
+TEST_F(IsAmendmentEnabledCall, ThirtyTwoByteIdLookupErrorFallsThroughToNameLookup)
+{
+    EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(testing::Eq(id))))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::Unimplemented)));
+    EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(testing::_)))
+        .WillOnce(testing::Return(1));
+
+    EXPECT_EQ(hostContext.isAmendmentEnabled(bytesOf(idBytes)), 1);
+}
+
+// Over 64 bytes cannot be a 32-byte id nor a name short enough to matter, so it is refused
+// before either overload runs.
+TEST_F(IsAmendmentEnabledCall, InputOverSixtyFourBytesIsRefusedWithoutAskingHost)
+{
+    Bytes const tooLong(65, 0x22);
+    EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(testing::_))).Times(0);
+    EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(testing::_)))
+        .Times(0);
+
+    EXPECT_EQ(
+        hostContext.isAmendmentEnabled(bytesOf(tooLong)),
+        hfErrorToInt(HostFunctionError::DataFieldTooLarge));
+}
+
+TEST_F(IsAmendmentEnabledCall, HostErrorBecomesContractReturnValue)
+{
+    Bytes const name{'F', 'e', 'a', 't', 'u', 'r', 'e'};
+    EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(testing::_)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FieldNotFound)));
+
+    EXPECT_EQ(
+        hostContext.isAmendmentEnabled(bytesOf(name)),
+        hfErrorToInt(HostFunctionError::FieldNotFound));
+}
+
+TEST_F(IsAmendmentEnabledCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    Bytes const name{'F', 'e', 'a', 't', 'u', 'r', 'e'};
+    EXPECT_CALL(host, isAmendmentEnabled(testing::Matcher(testing::_)))
+        .WillOnce(testing::Throw(std::runtime_error{"amendment lookup came apart"}));
+
+    EXPECT_EQ(
+        hostContext.isAmendmentEnabled(bytesOf(name)),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("amendment lookup came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("isAmendmentEnabled"));
+}
+
+TEST_F(IsAmendmentEnabledCall, NameBytesForwardedVerbatimToNameLookup)
+{
+    std::string_view const name{"MyAmendment"};
+    Bytes const nameBytes{name.begin(), name.end()};
+    EXPECT_CALL(
+        host, isAmendmentEnabled(testing::Matcher(testing::Eq(name))))
+        .WillOnce(testing::Return(1));
+
+    EXPECT_EQ(hostContext.isAmendmentEnabled(bytesOf(nameBytes)), 1);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/LedgerObjArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjArrayLen.cpp
new file mode 100644
index 0000000000..e7c9c62b0e
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjArrayLen.cpp
@@ -0,0 +1,84 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// `getLedgerObjArrayLen` answers its count directly rather than through an out region: no axis
+// E, no `OutRegion`, and the happy path asserts the returned count.
+struct LedgerObjArrayLenCall : HostContextTest
+{
+    std::int32_t fieldCode = sfBalance.getCode();
+    std::int32_t cacheIdx = 7;
+};
+
+TEST_F(LedgerObjArrayLenCall, FieldCodeBecomesSFieldHostIsAskedFor)
+{
+    EXPECT_CALL(host, getLedgerObjArrayLen(cacheIdx, testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(5));
+
+    EXPECT_EQ(hostContext.getLedgerObjArrayLen(cacheIdx, fieldCode), 5);
+}
+
+// `NoArray` is what a field that is not an array actually answers, so it stands in for axis B
+// here rather than an arbitrary code.
+TEST_F(LedgerObjArrayLenCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getLedgerObjArrayLen(cacheIdx, testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::NoArray)));
+
+    EXPECT_EQ(
+        hostContext.getLedgerObjArrayLen(cacheIdx, fieldCode),
+        hfErrorToInt(HostFunctionError::NoArray));
+}
+
+TEST_F(LedgerObjArrayLenCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getLedgerObjArrayLen(cacheIdx, testing::Ref(sfBalance)))
+        .WillOnce(testing::Throw(std::runtime_error{"ledger obj array len came apart"}));
+
+    EXPECT_EQ(
+        hostContext.getLedgerObjArrayLen(cacheIdx, fieldCode),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("ledger obj array len came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getLedgerObjArrayLen"));
+}
+
+TEST_F(LedgerObjArrayLenCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
+{
+    fieldCode = 0x7fff'0000;  // a code nothing is registered under
+    EXPECT_CALL(host, getLedgerObjArrayLen).Times(0);
+
+    EXPECT_EQ(
+        hostContext.getLedgerObjArrayLen(cacheIdx, fieldCode),
+        hfErrorToInt(HostFunctionError::InvalidField));
+}
+
+// `cacheIdx` is forwarded verbatim, including the two values a guest is likeliest to send: 0
+// (pick a free slot) and a negative one.
+TEST_F(LedgerObjArrayLenCall, CacheIdxOfZeroIsForwardedVerbatim)
+{
+    cacheIdx = 0;
+    EXPECT_CALL(host, getLedgerObjArrayLen(0, testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(5));
+
+    EXPECT_EQ(hostContext.getLedgerObjArrayLen(cacheIdx, fieldCode), 5);
+}
+
+TEST_F(LedgerObjArrayLenCall, NegativeCacheIdxIsForwardedVerbatim)
+{
+    cacheIdx = -7;
+    EXPECT_CALL(host, getLedgerObjArrayLen(-7, testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(5));
+
+    EXPECT_EQ(hostContext.getLedgerObjArrayLen(cacheIdx, fieldCode), 5);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/LedgerObjField.cpp b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjField.cpp
new file mode 100644
index 0000000000..bd33245258
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjField.cpp
@@ -0,0 +1,137 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
+// side, not here. The cross-cutting cases over this shape already live in `TxField.cpp`.
+struct LedgerObjFieldCall : HostContextTest
+{
+    std::int32_t fieldCode = sfBalance.getCode();
+    std::int32_t cacheIdx = 7;
+};
+
+TEST_F(LedgerObjFieldCall, FieldCodeBecomesSFieldHostIsAskedFor)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getLedgerObjField(cacheIdx, testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(LedgerObjFieldCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getLedgerObjField(cacheIdx, testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FieldNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
+        hfErrorToInt(HostFunctionError::FieldNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(LedgerObjFieldCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
+{
+    fieldCode = 0x7fff'0000;  // a code nothing is registered under
+    EXPECT_CALL(host, getLedgerObjField).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidField));
+}
+
+TEST_F(LedgerObjFieldCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getLedgerObjField(cacheIdx, testing::Ref(sfBalance)))
+        .WillOnce(testing::Throw(std::runtime_error{"ledger obj field came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("ledger obj field came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getLedgerObjField"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(LedgerObjFieldCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getLedgerObjField(cacheIdx, testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{value.size() - 1};
+    EXPECT_EQ(
+        hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
+        static_cast(value.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(LedgerObjFieldCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getLedgerObjField(cacheIdx, testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{value.size()};
+    EXPECT_EQ(
+        hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(LedgerObjFieldCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, getLedgerObjField(cacheIdx, testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+// `cacheIdx` is forwarded verbatim, including the two values a guest is likeliest to send: 0
+// (pick a free slot) and a negative one.
+TEST_F(LedgerObjFieldCall, CacheIdxOfZeroIsForwardedVerbatim)
+{
+    cacheIdx = 0;
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getLedgerObjField(0, testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
+        static_cast(value.size()));
+}
+
+TEST_F(LedgerObjFieldCall, NegativeCacheIdxIsForwardedVerbatim)
+{
+    cacheIdx = -7;
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getLedgerObjField(-7, testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjField(cacheIdx, fieldCode, out.slice()),
+        static_cast(value.size()));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedArrayLen.cpp
new file mode 100644
index 0000000000..c08a3f3aa5
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedArrayLen.cpp
@@ -0,0 +1,97 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
+// side, not here.
+//
+// No out region and no axis E: `getLedgerObjNestedArrayLen` answers the array's element
+// count directly rather than through a written buffer.
+struct LedgerObjNestedArrayLenCall : HostContextTest
+{
+    std::int32_t const cacheIdx = 7;
+    std::vector const steps{5, -12, 130};
+    Bytes const locatorBytes = bytesOfSteps(steps);
+};
+
+TEST_F(LedgerObjNestedArrayLenCall, LocatorBytesBecomeFieldLocatorHostReturnsCount)
+{
+    EXPECT_CALL(host, getLedgerObjNestedArrayLen(cacheIdx, LocatorEquals(steps)))
+        .WillOnce(testing::Return(7));
+
+    EXPECT_EQ(hostContext.getLedgerObjNestedArrayLen(cacheIdx, bytesOf(locatorBytes)), 7);
+}
+
+// `NoArray` - the field the locator resolves to is not an array - is the error this shape
+// most plausibly returns, so it stands in for axis B.
+TEST_F(LedgerObjNestedArrayLenCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getLedgerObjNestedArrayLen(cacheIdx, LocatorEquals(steps)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::NoArray)));
+
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedArrayLen(cacheIdx, bytesOf(locatorBytes)),
+        hfErrorToInt(HostFunctionError::NoArray));
+}
+
+TEST_F(LedgerObjNestedArrayLenCall, EmptyLocatorIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, getLedgerObjNestedArrayLen).Times(0);
+
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedArrayLen(cacheIdx, bytesOf(Bytes{})),
+        hfErrorToInt(HostFunctionError::LocatorMalformed));
+}
+
+// Distinct from an empty locator: `invokeWithLocator` checks the two conditions separately.
+TEST_F(LedgerObjNestedArrayLenCall, MisalignedLocatorLengthIsRefusedWithoutAskingHost)
+{
+    Bytes const oddLength{1, 2, 3};
+    EXPECT_CALL(host, getLedgerObjNestedArrayLen).Times(0);
+
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedArrayLen(cacheIdx, bytesOf(oddLength)),
+        hfErrorToInt(HostFunctionError::LocatorMalformed));
+}
+
+TEST_F(LedgerObjNestedArrayLenCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getLedgerObjNestedArrayLen(cacheIdx, LocatorEquals(steps)))
+        .WillOnce(testing::Throw(std::runtime_error{"ledger obj nested array len came apart"}));
+
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedArrayLen(cacheIdx, bytesOf(locatorBytes)),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("ledger obj nested array len came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getLedgerObjNestedArrayLen"));
+}
+
+// `cacheIdx` is signed the whole way to the host, so 0 and a negative slot both cross
+// unchanged.
+TEST_F(LedgerObjNestedArrayLenCall, ZeroCacheIdxArrivesAtHostUnchanged)
+{
+    EXPECT_CALL(host, getLedgerObjNestedArrayLen(0, LocatorEquals(steps)))
+        .WillOnce(testing::Return(7));
+
+    EXPECT_EQ(hostContext.getLedgerObjNestedArrayLen(0, bytesOf(locatorBytes)), 7);
+}
+
+TEST_F(LedgerObjNestedArrayLenCall, NegativeCacheIdxArrivesAtHostUnchanged)
+{
+    std::int32_t const negativeCacheIdx = -3;
+    EXPECT_CALL(host, getLedgerObjNestedArrayLen(negativeCacheIdx, LocatorEquals(steps)))
+        .WillOnce(testing::Return(7));
+
+    EXPECT_EQ(hostContext.getLedgerObjNestedArrayLen(negativeCacheIdx, bytesOf(locatorBytes)), 7);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedField.cpp b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedField.cpp
new file mode 100644
index 0000000000..be4e620cb6
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/LedgerObjNestedField.cpp
@@ -0,0 +1,148 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
+// side, not here.
+struct LedgerObjNestedFieldCall : HostContextTest
+{
+    std::int32_t const cacheIdx = 7;
+    std::vector const steps{5, -12, 130};
+    Bytes const locatorBytes = bytesOfSteps(steps);
+};
+
+TEST_F(LedgerObjNestedFieldCall, LocatorBytesBecomeFieldLocatorHostIsAskedFor)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getLedgerObjNestedField(cacheIdx, LocatorEquals(steps)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(locatorBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(LedgerObjNestedFieldCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getLedgerObjNestedField(cacheIdx, LocatorEquals(steps)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::NotLeafField)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(locatorBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::NotLeafField));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(LedgerObjNestedFieldCall, EmptyLocatorIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, getLedgerObjNestedField).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(Bytes{}), out.slice()),
+        hfErrorToInt(HostFunctionError::LocatorMalformed));
+}
+
+// Distinct from an empty locator: `invokeWithLocator` checks the two conditions separately.
+TEST_F(LedgerObjNestedFieldCall, MisalignedLocatorLengthIsRefusedWithoutAskingHost)
+{
+    Bytes const oddLength{1, 2, 3};
+    EXPECT_CALL(host, getLedgerObjNestedField).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(oddLength), out.slice()),
+        hfErrorToInt(HostFunctionError::LocatorMalformed));
+}
+
+TEST_F(LedgerObjNestedFieldCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getLedgerObjNestedField(cacheIdx, LocatorEquals(steps)))
+        .WillOnce(testing::Throw(std::runtime_error{"ledger obj nested field came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(locatorBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("ledger obj nested field came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getLedgerObjNestedField"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(LedgerObjNestedFieldCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getLedgerObjNestedField(cacheIdx, LocatorEquals(steps)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{value.size() - 1};
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(locatorBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(LedgerObjNestedFieldCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getLedgerObjNestedField(cacheIdx, LocatorEquals(steps)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{value.size()};
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(locatorBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(LedgerObjNestedFieldCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, getLedgerObjNestedField(cacheIdx, LocatorEquals(steps)))
+        .WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getLedgerObjNestedField(cacheIdx, bytesOf(locatorBytes), out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+// `cacheIdx` is signed the whole way to the host, so 0 and a negative slot both cross
+// unchanged.
+TEST_F(LedgerObjNestedFieldCall, ZeroCacheIdxArrivesAtHostUnchanged)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getLedgerObjNestedField(0, LocatorEquals(steps)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedField(0, bytesOf(locatorBytes), out.slice()),
+        static_cast(value.size()));
+}
+
+TEST_F(LedgerObjNestedFieldCall, NegativeCacheIdxArrivesAtHostUnchanged)
+{
+    std::int32_t const negativeCacheIdx = -3;
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getLedgerObjNestedField(negativeCacheIdx, LocatorEquals(steps)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getLedgerObjNestedField(negativeCacheIdx, bytesOf(locatorBytes), out.slice()),
+        static_cast(value.size()));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/LedgerSqn.cpp b/src/tests/libxrpl/tx/wasm/host_context/LedgerSqn.cpp
new file mode 100644
index 0000000000..4471a0e2b0
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/LedgerSqn.cpp
@@ -0,0 +1,76 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// No D or F axis: `getLedgerSqn` takes no argument, so there is nothing to decode wrong and
+// nothing whose forwarded identity to check.
+//
+// Named `LedgerSqnDirectCall`, not `LedgerSqnCall`: `host_calls/LedgerSqn.cpp` already owns
+// that name in the same gtest binary.
+struct LedgerSqnDirectCall : HostContextTest
+{
+    static constexpr std::uint32_t kLedgerSqn = 0x12345678;
+    Bytes const expectedBytes = bytesOfScalar(kLedgerSqn);
+};
+
+TEST_F(LedgerSqnDirectCall, HostValueIsWrittenAsLittleEndianBytes)
+{
+    EXPECT_CALL(host, getLedgerSqn()).WillOnce(testing::Return(kLedgerSqn));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getLedgerSqn(out.slice()), 4);
+    EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
+}
+
+TEST_F(LedgerSqnDirectCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getLedgerSqn())
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::Unimplemented)));
+
+    OutRegion out{4};
+    EXPECT_EQ(
+        hostContext.getLedgerSqn(out.slice()), hfErrorToInt(HostFunctionError::Unimplemented));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(LedgerSqnDirectCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getLedgerSqn())
+        .WillOnce(testing::Throw(std::runtime_error{"ledger sqn came apart"}));
+
+    OutRegion out{4};
+    EXPECT_EQ(
+        hostContext.getLedgerSqn(out.slice()), hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("ledger sqn came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getLedgerSqn"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(LedgerSqnDirectCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    EXPECT_CALL(host, getLedgerSqn()).WillOnce(testing::Return(kLedgerSqn));
+
+    OutRegion out{3};
+    EXPECT_EQ(hostContext.getLedgerSqn(out.slice()), 4);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(LedgerSqnDirectCall, OutRegionOfExactSizeIsWritten)
+{
+    EXPECT_CALL(host, getLedgerSqn()).WillOnce(testing::Return(kLedgerSqn));
+
+    OutRegion out{4};
+    EXPECT_EQ(hostContext.getLedgerSqn(out.slice()), 4);
+    EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/LoanBrokerKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/LoanBrokerKeylet.cpp
new file mode 100644
index 0000000000..0cc62d2b7e
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/LoanBrokerKeylet.cpp
@@ -0,0 +1,127 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct LoanBrokerKeyletCall : HostContextTest
+{
+    Bytes const ownerBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                           0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    AccountID const owner = AccountID::fromVoid(ownerBytes.data());
+    std::uint32_t const seq = 12345;
+};
+
+TEST_F(LoanBrokerKeyletCall, OwnerAndSeqAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, loanBrokerKeylet(owner, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.loanBrokerKeylet(bytesOf(ownerBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(LoanBrokerKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, loanBrokerKeylet(owner, seq))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.loanBrokerKeylet(bytesOf(ownerBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(LoanBrokerKeyletCall, ShortOwnerIsRefusedWithoutAskingHost)
+{
+    Bytes const shortOwner(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, loanBrokerKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.loanBrokerKeylet(bytesOf(shortOwner), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(LoanBrokerKeyletCall, LongOwnerIsRefusedWithoutAskingHost)
+{
+    Bytes const longOwner(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, loanBrokerKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.loanBrokerKeylet(bytesOf(longOwner), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(LoanBrokerKeyletCall, EmptyOwnerIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, loanBrokerKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.loanBrokerKeylet(bytesOf(Bytes{}), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(LoanBrokerKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, loanBrokerKeylet(owner, seq))
+        .WillOnce(testing::Throw(std::runtime_error{"loan broker keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.loanBrokerKeylet(bytesOf(ownerBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("loan broker keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("loanBrokerKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(LoanBrokerKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, loanBrokerKeylet(owner, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.loanBrokerKeylet(bytesOf(ownerBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(LoanBrokerKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, loanBrokerKeylet(owner, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.loanBrokerKeylet(bytesOf(ownerBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(LoanBrokerKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, loanBrokerKeylet(owner, seq)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.loanBrokerKeylet(bytesOf(ownerBytes), seq, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/LoanKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/LoanKeylet.cpp
new file mode 100644
index 0000000000..aae0dbee21
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/LoanKeylet.cpp
@@ -0,0 +1,111 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct LoanKeyletCall : HostContextTest
+{
+    Bytes const loanBrokerIdBytes{0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b,
+                                  0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
+                                  0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70};
+    uint256 const loanBrokerId = uint256::fromVoid(loanBrokerIdBytes.data());
+    std::uint32_t const loanSeq = 12345;
+};
+
+TEST_F(LoanKeyletCall, LoanBrokerIdAndSeqAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, loanKeylet(testing::Eq(loanBrokerId), loanSeq))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.loanKeylet(bytesOf(loanBrokerIdBytes), loanSeq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(LoanKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, loanKeylet(testing::Eq(loanBrokerId), loanSeq))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.loanKeylet(bytesOf(loanBrokerIdBytes), loanSeq, out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(LoanKeyletCall, MalformedLoanBrokerIdIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedLoanBrokerId(uint256::size() - 1, 0x51);
+    EXPECT_CALL(host, loanKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.loanKeylet(bytesOf(malformedLoanBrokerId), loanSeq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(LoanKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, loanKeylet(testing::Eq(loanBrokerId), loanSeq))
+        .WillOnce(testing::Throw(std::runtime_error{"loan keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.loanKeylet(bytesOf(loanBrokerIdBytes), loanSeq, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("loan keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("loanKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(LoanKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, loanKeylet(testing::Eq(loanBrokerId), loanSeq))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.loanKeylet(bytesOf(loanBrokerIdBytes), loanSeq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(LoanKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, loanKeylet(testing::Eq(loanBrokerId), loanSeq))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.loanKeylet(bytesOf(loanBrokerIdBytes), loanSeq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(LoanKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, loanKeylet(testing::Eq(loanBrokerId), loanSeq))
+        .WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.loanKeylet(bytesOf(loanBrokerIdBytes), loanSeq, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/MptokenIssuanceKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/MptokenIssuanceKeylet.cpp
new file mode 100644
index 0000000000..17b10e6d9e
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/MptokenIssuanceKeylet.cpp
@@ -0,0 +1,127 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct MptokenIssuanceKeyletCall : HostContextTest
+{
+    Bytes const issuerBytes{0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a,
+                            0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64};
+    AccountID const issuer = AccountID::fromVoid(issuerBytes.data());
+    std::uint32_t const seq = 98765;
+};
+
+TEST_F(MptokenIssuanceKeyletCall, IssuerAndSeqAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(MptokenIssuanceKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, seq))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(MptokenIssuanceKeyletCall, ShortIssuerIsRefusedWithoutAskingHost)
+{
+    Bytes const shortIssuer(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, mptokenIssuanceKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.mptokenIssuanceKeylet(bytesOf(shortIssuer), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(MptokenIssuanceKeyletCall, LongIssuerIsRefusedWithoutAskingHost)
+{
+    Bytes const longIssuer(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, mptokenIssuanceKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.mptokenIssuanceKeylet(bytesOf(longIssuer), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(MptokenIssuanceKeyletCall, EmptyIssuerIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, mptokenIssuanceKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.mptokenIssuanceKeylet(bytesOf(Bytes{}), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(MptokenIssuanceKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, seq))
+        .WillOnce(testing::Throw(std::runtime_error{"mptoken issuance keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("mptoken issuance keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("mptokenIssuanceKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(MptokenIssuanceKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(MptokenIssuanceKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(MptokenIssuanceKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, mptokenIssuanceKeylet(issuer, seq)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.mptokenIssuanceKeylet(bytesOf(issuerBytes), seq, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/MptokenKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/MptokenKeylet.cpp
new file mode 100644
index 0000000000..c26f0cd5cf
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/MptokenKeylet.cpp
@@ -0,0 +1,119 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+// `mptid` and `holder` are checked together in one condition rather than through
+// `invokeWithAccount`, so which one fired is not observable when both are malformed.
+struct MptokenKeyletCall : HostContextTest
+{
+    Bytes const mptidBytes = Bytes(24, 0x7a);
+    Bytes const holderBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                            0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+
+    MPTID const mptid = MPTID::fromVoid(mptidBytes.data());
+    AccountID const holder = AccountID::fromVoid(holderBytes.data());
+
+    Bytes const keylet = Bytes(32, 0xab);
+};
+
+TEST_F(MptokenKeyletCall, MptidAndHolderForwardedAndKeyletWritten)
+{
+    EXPECT_CALL(host, mptokenKeylet(testing::Eq(mptid), testing::Eq(holder)))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.mptokenKeylet(bytesOf(mptidBytes), bytesOf(holderBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(MptokenKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, mptokenKeylet(testing::Eq(mptid), testing::Eq(holder)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.mptokenKeylet(bytesOf(mptidBytes), bytesOf(holderBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(MptokenKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, mptokenKeylet(testing::Eq(mptid), testing::Eq(holder)))
+        .WillOnce(testing::Throw(std::runtime_error{"mptoken keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.mptokenKeylet(bytesOf(mptidBytes), bytesOf(holderBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("mptoken keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("mptokenKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(MptokenKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    EXPECT_CALL(host, mptokenKeylet(testing::Eq(mptid), testing::Eq(holder)))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.mptokenKeylet(bytesOf(mptidBytes), bytesOf(holderBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(MptokenKeyletCall, MalformedMptidIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedMptid(MPTID::size() - 1, 0x7a);
+    EXPECT_CALL(host, mptokenKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.mptokenKeylet(bytesOf(malformedMptid), bytesOf(holderBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// Distinct from a malformed mptid: the mptid is well-formed here, so this exercises the
+// holder's own check rather than the mptid's.
+TEST_F(MptokenKeyletCall, MalformedHolderIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedHolder(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, mptokenKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.mptokenKeylet(bytesOf(mptidBytes), bytesOf(malformedHolder), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// Both lengths are checked in one condition and both answer the same `InvalidParams`, so
+// which one fired is not observable here. What is: neither argument reaches the host.
+TEST_F(MptokenKeyletCall, BothArgumentsMalformedIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedMptid(MPTID::size() - 1, 0x7a);
+    Bytes const malformedHolder(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, mptokenKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.mptokenKeylet(bytesOf(malformedMptid), bytesOf(malformedHolder), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/NFT.cpp b/src/tests/libxrpl/tx/wasm/host_context/NFT.cpp
new file mode 100644
index 0000000000..67ee63ab7b
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/NFT.cpp
@@ -0,0 +1,142 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
+// side, not here.
+struct NFTCall : HostContextTest
+{
+    Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                             0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    Bytes const nftIdBytes{0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
+                           0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36,
+                           0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+    uint256 const nftId = uint256::fromVoid(nftIdBytes.data());
+};
+
+TEST_F(NFTCall, AccountAndNftIdBecomeTypedArgumentsHostIsAskedFor)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getNFT(testing::Eq(account), testing::Eq(nftId)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getNFT(bytesOf(accountBytes), bytesOf(nftIdBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(NFTCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getNFT(testing::Eq(account), testing::Eq(nftId)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getNFT(bytesOf(accountBytes), bytesOf(nftIdBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(NFTCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getNFT(testing::Eq(account), testing::Eq(nftId)))
+        .WillOnce(testing::Throw(std::runtime_error{"nft came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getNFT(bytesOf(accountBytes), bytesOf(nftIdBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("nft came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getNFT"));
+}
+
+TEST_F(NFTCall, MalformedAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedAccount(AccountID::size() - 1, 0xff);
+    EXPECT_CALL(host, getNFT).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getNFT(bytesOf(malformedAccount), bytesOf(nftIdBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// Distinct from a malformed account: the account is well-formed here, so this exercises the
+// nft id's own check rather than the account's.
+TEST_F(NFTCall, MalformedNftIdIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedNftId(uint256::size() - 1, 0xff);
+    EXPECT_CALL(host, getNFT).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getNFT(bytesOf(accountBytes), bytesOf(malformedNftId), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// The account's length is checked before the nft id's, but both checks answer `InvalidParams`,
+// so which one fired is not observable here. What is: neither argument reaches the host.
+TEST_F(NFTCall, BothArgumentsMalformedIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedAccount(AccountID::size() - 1, 0xff);
+    Bytes const malformedNftId(uint256::size() - 1, 0xff);
+    EXPECT_CALL(host, getNFT).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getNFT(bytesOf(malformedAccount), bytesOf(malformedNftId), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(NFTCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getNFT(testing::Eq(account), testing::Eq(nftId)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{value.size() - 1};
+    EXPECT_EQ(
+        hostContext.getNFT(bytesOf(accountBytes), bytesOf(nftIdBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(NFTCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getNFT(testing::Eq(account), testing::Eq(nftId)))
+        .WillOnce(testing::Return(value));
+
+    OutRegion out{value.size()};
+    EXPECT_EQ(
+        hostContext.getNFT(bytesOf(accountBytes), bytesOf(nftIdBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(NFTCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, getNFT(testing::Eq(account), testing::Eq(nftId)))
+        .WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getNFT(bytesOf(accountBytes), bytesOf(nftIdBytes), out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/NFTFlags.cpp b/src/tests/libxrpl/tx/wasm/host_context/NFTFlags.cpp
new file mode 100644
index 0000000000..7c785de074
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/NFTFlags.cpp
@@ -0,0 +1,81 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+// `getNFTFlags` answers its value directly rather than through `answer`, so there is no out
+// region and no axis E.
+struct NFTFlagsCall : HostContextTest
+{
+    Bytes const nftIdBytes{0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb,
+                           0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6,
+                           0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf, 0xd0};
+    uint256 const nftId = uint256::fromVoid(nftIdBytes.data());
+};
+
+TEST_F(NFTFlagsCall, NftIdBytesBecomeTypedArgumentHostIsAskedFor)
+{
+    static constexpr std::int32_t kFlags = 0x0b;
+    EXPECT_CALL(host, getNFTFlags(testing::Eq(nftId))).WillOnce(testing::Return(kFlags));
+
+    EXPECT_EQ(hostContext.getNFTFlags(bytesOf(nftIdBytes)), kFlags);
+}
+
+TEST_F(NFTFlagsCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getNFTFlags(testing::Eq(nftId)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    EXPECT_EQ(
+        hostContext.getNFTFlags(bytesOf(nftIdBytes)),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+}
+
+TEST_F(NFTFlagsCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getNFTFlags(testing::Eq(nftId)))
+        .WillOnce(testing::Throw(std::runtime_error{"nft flags came apart"}));
+
+    EXPECT_EQ(
+        hostContext.getNFTFlags(bytesOf(nftIdBytes)),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("nft flags came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getNFTFlags"));
+}
+
+TEST_F(NFTFlagsCall, MalformedNftIdIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedNftId(uint256::size() - 1, 0xff);
+    EXPECT_CALL(host, getNFTFlags).Times(0);
+
+    EXPECT_EQ(
+        hostContext.getNFTFlags(bytesOf(malformedNftId)),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// `getNFTFlags` answers its value directly rather than through `answer`, so a legitimate
+// flags word with the high bit set is bit-for-bit the same value as
+// `HostFunctionError::InternalFatal` (`INT32_MIN`) - the code `guarded` supplies for a thrown
+// exception. The ABI at this layer has no way to tell the two apart; this is a property of
+// the shape, not a bug to fix.
+TEST_F(NFTFlagsCall, HighBitFlagsAreIndistinguishableFromInternalFatal)
+{
+    EXPECT_CALL(host, getNFTFlags(testing::Eq(nftId)))
+        .WillOnce(testing::Return(std::numeric_limits::min()));
+
+    EXPECT_EQ(
+        hostContext.getNFTFlags(bytesOf(nftIdBytes)),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/NFTIssuer.cpp b/src/tests/libxrpl/tx/wasm/host_context/NFTIssuer.cpp
new file mode 100644
index 0000000000..cf0c222cf2
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/NFTIssuer.cpp
@@ -0,0 +1,106 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct NFTIssuerCall : HostContextTest
+{
+    Bytes const nftIdBytes{0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b,
+                           0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
+                           0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70};
+    uint256 const nftId = uint256::fromVoid(nftIdBytes.data());
+};
+
+TEST_F(NFTIssuerCall, NftIdBytesBecomeTypedArgumentHostIsAskedFor)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getNFTIssuer(testing::Eq(nftId))).WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getNFTIssuer(bytesOf(nftIdBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(NFTIssuerCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getNFTIssuer(testing::Eq(nftId)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getNFTIssuer(bytesOf(nftIdBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(NFTIssuerCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getNFTIssuer(testing::Eq(nftId)))
+        .WillOnce(testing::Throw(std::runtime_error{"nft issuer came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getNFTIssuer(bytesOf(nftIdBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("nft issuer came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getNFTIssuer"));
+}
+
+TEST_F(NFTIssuerCall, MalformedNftIdIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedNftId(uint256::size() - 1, 0xff);
+    EXPECT_CALL(host, getNFTIssuer).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getNFTIssuer(bytesOf(malformedNftId), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(NFTIssuerCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getNFTIssuer(testing::Eq(nftId))).WillOnce(testing::Return(value));
+
+    OutRegion out{value.size() - 1};
+    EXPECT_EQ(
+        hostContext.getNFTIssuer(bytesOf(nftIdBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(NFTIssuerCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getNFTIssuer(testing::Eq(nftId))).WillOnce(testing::Return(value));
+
+    OutRegion out{value.size()};
+    EXPECT_EQ(
+        hostContext.getNFTIssuer(bytesOf(nftIdBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(NFTIssuerCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, getNFTIssuer(testing::Eq(nftId))).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getNFTIssuer(bytesOf(nftIdBytes), out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/NFTSequence.cpp b/src/tests/libxrpl/tx/wasm/host_context/NFTSequence.cpp
new file mode 100644
index 0000000000..d3fcc9be87
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/NFTSequence.cpp
@@ -0,0 +1,90 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct NFTSequenceCall : HostContextTest
+{
+    Bytes const nftIdBytes{0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b,
+                           0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,
+                           0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0};
+    uint256 const nftId = uint256::fromVoid(nftIdBytes.data());
+    static constexpr std::uint32_t kSequence = 0x89abcdef;
+    Bytes const expectedBytes = bytesOfScalar(kSequence);
+};
+
+TEST_F(NFTSequenceCall, NftIdBytesBecomeTypedArgumentHostIsAskedFor)
+{
+    EXPECT_CALL(host, getNFTSequence(testing::Eq(nftId))).WillOnce(testing::Return(kSequence));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getNFTSequence(bytesOf(nftIdBytes), out.slice()), 4);
+    EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
+}
+
+TEST_F(NFTSequenceCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getNFTSequence(testing::Eq(nftId)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{4};
+    EXPECT_EQ(
+        hostContext.getNFTSequence(bytesOf(nftIdBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(NFTSequenceCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getNFTSequence(testing::Eq(nftId)))
+        .WillOnce(testing::Throw(std::runtime_error{"nft sequence came apart"}));
+
+    OutRegion out{4};
+    EXPECT_EQ(
+        hostContext.getNFTSequence(bytesOf(nftIdBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("nft sequence came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getNFTSequence"));
+}
+
+TEST_F(NFTSequenceCall, MalformedNftIdIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedNftId(uint256::size() - 1, 0xff);
+    EXPECT_CALL(host, getNFTSequence).Times(0);
+
+    OutRegion out{4};
+    EXPECT_EQ(
+        hostContext.getNFTSequence(bytesOf(malformedNftId), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(NFTSequenceCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    EXPECT_CALL(host, getNFTSequence(testing::Eq(nftId))).WillOnce(testing::Return(kSequence));
+
+    OutRegion out{3};
+    EXPECT_EQ(hostContext.getNFTSequence(bytesOf(nftIdBytes), out.slice()), 4);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(NFTSequenceCall, OutRegionOfExactSizeIsWritten)
+{
+    EXPECT_CALL(host, getNFTSequence(testing::Eq(nftId))).WillOnce(testing::Return(kSequence));
+
+    OutRegion out{4};
+    EXPECT_EQ(hostContext.getNFTSequence(bytesOf(nftIdBytes), out.slice()), 4);
+    EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/NFTTaxon.cpp b/src/tests/libxrpl/tx/wasm/host_context/NFTTaxon.cpp
new file mode 100644
index 0000000000..1e2909845e
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/NFTTaxon.cpp
@@ -0,0 +1,90 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct NFTTaxonCall : HostContextTest
+{
+    Bytes const nftIdBytes{0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b,
+                           0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86,
+                           0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90};
+    uint256 const nftId = uint256::fromVoid(nftIdBytes.data());
+    static constexpr std::uint32_t kTaxon = 0x12345678;
+    Bytes const expectedBytes = bytesOfScalar(kTaxon);
+};
+
+TEST_F(NFTTaxonCall, NftIdBytesBecomeTypedArgumentHostIsAskedFor)
+{
+    EXPECT_CALL(host, getNFTTaxon(testing::Eq(nftId))).WillOnce(testing::Return(kTaxon));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getNFTTaxon(bytesOf(nftIdBytes), out.slice()), 4);
+    EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
+}
+
+TEST_F(NFTTaxonCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getNFTTaxon(testing::Eq(nftId)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{4};
+    EXPECT_EQ(
+        hostContext.getNFTTaxon(bytesOf(nftIdBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(NFTTaxonCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getNFTTaxon(testing::Eq(nftId)))
+        .WillOnce(testing::Throw(std::runtime_error{"nft taxon came apart"}));
+
+    OutRegion out{4};
+    EXPECT_EQ(
+        hostContext.getNFTTaxon(bytesOf(nftIdBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("nft taxon came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getNFTTaxon"));
+}
+
+TEST_F(NFTTaxonCall, MalformedNftIdIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedNftId(uint256::size() - 1, 0xff);
+    EXPECT_CALL(host, getNFTTaxon).Times(0);
+
+    OutRegion out{4};
+    EXPECT_EQ(
+        hostContext.getNFTTaxon(bytesOf(malformedNftId), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(NFTTaxonCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    EXPECT_CALL(host, getNFTTaxon(testing::Eq(nftId))).WillOnce(testing::Return(kTaxon));
+
+    OutRegion out{3};
+    EXPECT_EQ(hostContext.getNFTTaxon(bytesOf(nftIdBytes), out.slice()), 4);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(NFTTaxonCall, OutRegionOfExactSizeIsWritten)
+{
+    EXPECT_CALL(host, getNFTTaxon(testing::Eq(nftId))).WillOnce(testing::Return(kTaxon));
+
+    OutRegion out{4};
+    EXPECT_EQ(hostContext.getNFTTaxon(bytesOf(nftIdBytes), out.slice()), 4);
+    EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/NFTTransferFee.cpp b/src/tests/libxrpl/tx/wasm/host_context/NFTTransferFee.cpp
new file mode 100644
index 0000000000..2ccd30f517
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/NFTTransferFee.cpp
@@ -0,0 +1,66 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+// `getNFTTransferFee` answers its value directly rather than through `answer`, so there is no
+// out region and no axis E.
+struct NFTTransferFeeCall : HostContextTest
+{
+    Bytes const nftIdBytes{0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xdb,
+                           0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6,
+                           0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0};
+    uint256 const nftId = uint256::fromVoid(nftIdBytes.data());
+};
+
+TEST_F(NFTTransferFeeCall, NftIdBytesBecomeTypedArgumentHostIsAskedFor)
+{
+    static constexpr std::int32_t kTransferFee = 314;
+    EXPECT_CALL(host, getNFTTransferFee(testing::Eq(nftId)))
+        .WillOnce(testing::Return(kTransferFee));
+
+    EXPECT_EQ(hostContext.getNFTTransferFee(bytesOf(nftIdBytes)), kTransferFee);
+}
+
+TEST_F(NFTTransferFeeCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getNFTTransferFee(testing::Eq(nftId)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    EXPECT_EQ(
+        hostContext.getNFTTransferFee(bytesOf(nftIdBytes)),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+}
+
+TEST_F(NFTTransferFeeCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getNFTTransferFee(testing::Eq(nftId)))
+        .WillOnce(testing::Throw(std::runtime_error{"nft transfer fee came apart"}));
+
+    EXPECT_EQ(
+        hostContext.getNFTTransferFee(bytesOf(nftIdBytes)),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("nft transfer fee came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getNFTTransferFee"));
+}
+
+TEST_F(NFTTransferFeeCall, MalformedNftIdIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedNftId(uint256::size() - 1, 0xff);
+    EXPECT_CALL(host, getNFTTransferFee).Times(0);
+
+    EXPECT_EQ(
+        hostContext.getNFTTransferFee(bytesOf(malformedNftId)),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/NftokenOfferKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/NftokenOfferKeylet.cpp
new file mode 100644
index 0000000000..616bb0688b
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/NftokenOfferKeylet.cpp
@@ -0,0 +1,127 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct NftokenOfferKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,
+                             0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+    std::uint32_t const seq = 13579;
+};
+
+TEST_F(NftokenOfferKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, nftokenOfferKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(NftokenOfferKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, nftokenOfferKeylet(account, seq))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(NftokenOfferKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const shortAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, nftokenOfferKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.nftokenOfferKeylet(bytesOf(shortAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(NftokenOfferKeyletCall, LongAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const longAccount(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, nftokenOfferKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.nftokenOfferKeylet(bytesOf(longAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(NftokenOfferKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, nftokenOfferKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.nftokenOfferKeylet(bytesOf(Bytes{}), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(NftokenOfferKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, nftokenOfferKeylet(account, seq))
+        .WillOnce(testing::Throw(std::runtime_error{"nftoken offer keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("nftoken offer keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("nftokenOfferKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(NftokenOfferKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, nftokenOfferKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(NftokenOfferKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, nftokenOfferKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(NftokenOfferKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, nftokenOfferKeylet(account, seq)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.nftokenOfferKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/OfferKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/OfferKeylet.cpp
new file mode 100644
index 0000000000..243e376e82
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/OfferKeylet.cpp
@@ -0,0 +1,127 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct OfferKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a,
+                             0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+    std::uint32_t const seq = 24680;
+};
+
+TEST_F(OfferKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, offerKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(OfferKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, offerKeylet(account, seq))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(OfferKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const shortAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, offerKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.offerKeylet(bytesOf(shortAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(OfferKeyletCall, LongAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const longAccount(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, offerKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.offerKeylet(bytesOf(longAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(OfferKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, offerKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.offerKeylet(bytesOf(Bytes{}), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(OfferKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, offerKeylet(account, seq))
+        .WillOnce(testing::Throw(std::runtime_error{"offer keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("offer keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("offerKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(OfferKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, offerKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(OfferKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, offerKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(OfferKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, offerKeylet(account, seq)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.offerKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/OracleKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/OracleKeylet.cpp
new file mode 100644
index 0000000000..0e56c69151
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/OracleKeylet.cpp
@@ -0,0 +1,127 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct OracleKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                             0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+    std::uint32_t const docId = 12345;
+};
+
+TEST_F(OracleKeyletCall, AccountAndDocIdAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, oracleKeylet(account, docId)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(OracleKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, oracleKeylet(account, docId))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(OracleKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const shortAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, oracleKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.oracleKeylet(bytesOf(shortAccount), docId, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(OracleKeyletCall, LongAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const longAccount(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, oracleKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.oracleKeylet(bytesOf(longAccount), docId, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(OracleKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, oracleKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.oracleKeylet(bytesOf(Bytes{}), docId, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(OracleKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, oracleKeylet(account, docId))
+        .WillOnce(testing::Throw(std::runtime_error{"oracle keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("oracle keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("oracleKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(OracleKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, oracleKeylet(account, docId)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(OracleKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, oracleKeylet(account, docId)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(OracleKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, oracleKeylet(account, docId)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.oracleKeylet(bytesOf(accountBytes), docId, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/ParentLedgerHash.cpp b/src/tests/libxrpl/tx/wasm/host_context/ParentLedgerHash.cpp
new file mode 100644
index 0000000000..9d11271ae6
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/ParentLedgerHash.cpp
@@ -0,0 +1,86 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// No D or F axis: `getParentLedgerHash` takes no argument, so there is nothing to decode wrong
+// and nothing whose forwarded identity to check.
+//
+// Unlike `getLedgerSqn`/`getParentLedgerTime`, the result is a `Hash` (a `uint256`) written
+// whole through `answer` (`invoke`), not a scalar through `answerScalar` - so it is
+// asserted as bytes, the way `TxField.cpp` asserts its `Bytes` result, rather than as a
+// little-endian scalar.
+struct ParentLedgerHashCall : HostContextTest
+{
+    Bytes const hashBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
+                          0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
+                          0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20};
+    Hash const hash = uint256::fromVoid(hashBytes.data());
+};
+
+TEST_F(ParentLedgerHashCall, HostValueIsWrittenAsBytes)
+{
+    EXPECT_CALL(host, getParentLedgerHash()).WillOnce(testing::Return(hash));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getParentLedgerHash(out.slice()), static_cast(hashBytes.size()));
+    EXPECT_TRUE(out.holds(bytesOf(hashBytes)));
+}
+
+TEST_F(ParentLedgerHashCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getParentLedgerHash())
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getParentLedgerHash(out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(ParentLedgerHashCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getParentLedgerHash())
+        .WillOnce(testing::Throw(std::runtime_error{"parent ledger hash came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getParentLedgerHash(out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("parent ledger hash came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getParentLedgerHash"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(ParentLedgerHashCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    EXPECT_CALL(host, getParentLedgerHash()).WillOnce(testing::Return(hash));
+
+    OutRegion out{hashBytes.size() - 1};
+    EXPECT_EQ(
+        hostContext.getParentLedgerHash(out.slice()), static_cast(hashBytes.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(ParentLedgerHashCall, OutRegionOfExactSizeIsWritten)
+{
+    EXPECT_CALL(host, getParentLedgerHash()).WillOnce(testing::Return(hash));
+
+    OutRegion out{hashBytes.size()};
+    EXPECT_EQ(
+        hostContext.getParentLedgerHash(out.slice()), static_cast(hashBytes.size()));
+    EXPECT_TRUE(out.holds(bytesOf(hashBytes)));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/ParentLedgerTime.cpp b/src/tests/libxrpl/tx/wasm/host_context/ParentLedgerTime.cpp
new file mode 100644
index 0000000000..c31d20a690
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/ParentLedgerTime.cpp
@@ -0,0 +1,75 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// No D or F axis: `getParentLedgerTime` takes no argument, so there is nothing to decode wrong
+// and nothing whose forwarded identity to check.
+struct ParentLedgerTimeCall : HostContextTest
+{
+    static constexpr std::uint32_t kParentLedgerTime = 0x12345678;
+    Bytes const expectedBytes = bytesOfScalar(kParentLedgerTime);
+};
+
+TEST_F(ParentLedgerTimeCall, HostValueIsWrittenAsLittleEndianBytes)
+{
+    EXPECT_CALL(host, getParentLedgerTime()).WillOnce(testing::Return(kParentLedgerTime));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getParentLedgerTime(out.slice()), 4);
+    EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
+}
+
+TEST_F(ParentLedgerTimeCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getParentLedgerTime())
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::Unimplemented)));
+
+    OutRegion out{4};
+    EXPECT_EQ(
+        hostContext.getParentLedgerTime(out.slice()),
+        hfErrorToInt(HostFunctionError::Unimplemented));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(ParentLedgerTimeCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getParentLedgerTime())
+        .WillOnce(testing::Throw(std::runtime_error{"parent ledger time came apart"}));
+
+    OutRegion out{4};
+    EXPECT_EQ(
+        hostContext.getParentLedgerTime(out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("parent ledger time came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getParentLedgerTime"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(ParentLedgerTimeCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    EXPECT_CALL(host, getParentLedgerTime()).WillOnce(testing::Return(kParentLedgerTime));
+
+    OutRegion out{3};
+    EXPECT_EQ(hostContext.getParentLedgerTime(out.slice()), 4);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(ParentLedgerTimeCall, OutRegionOfExactSizeIsWritten)
+{
+    EXPECT_CALL(host, getParentLedgerTime()).WillOnce(testing::Return(kParentLedgerTime));
+
+    OutRegion out{4};
+    EXPECT_EQ(hostContext.getParentLedgerTime(out.slice()), 4);
+    EXPECT_TRUE(out.holds(bytesOf(expectedBytes)));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/PaychannelKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/PaychannelKeylet.cpp
new file mode 100644
index 0000000000..c8305f928e
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/PaychannelKeylet.cpp
@@ -0,0 +1,152 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+//
+// `account` and `destination` are distinct byte patterns: a happy path built from two copies of
+// the same account would still pass if the two were swapped.
+struct PaychannelKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                             0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    Bytes const destinationBytes{0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a,
+                                 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+    AccountID const destination = AccountID::fromVoid(destinationBytes.data());
+    std::uint32_t const seq = 54321;
+};
+
+TEST_F(PaychannelKeyletCall, AccountsAndSeqAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, paychannelKeylet(account, destination, seq))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.paychannelKeylet(
+            bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(PaychannelKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, paychannelKeylet(account, destination, seq))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.paychannelKeylet(
+            bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(PaychannelKeyletCall, MalformedAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, paychannelKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.paychannelKeylet(
+            bytesOf(malformedAccount), bytesOf(destinationBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(PaychannelKeyletCall, MalformedDestinationIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedDestination(AccountID::size() + 1, 0x71);
+    EXPECT_CALL(host, paychannelKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.paychannelKeylet(
+            bytesOf(accountBytes), bytesOf(malformedDestination), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// Both ids fail one combined length check, so a call malformed in both places answers the
+// same `InvalidParams` as either alone; what's observable is that the host is never asked.
+TEST_F(PaychannelKeyletCall, BothAccountsMalformedIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedAccount(AccountID::size() - 1, 0x01);
+    Bytes const malformedDestination(AccountID::size() - 1, 0x71);
+    EXPECT_CALL(host, paychannelKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.paychannelKeylet(
+            bytesOf(malformedAccount), bytesOf(malformedDestination), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(PaychannelKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, paychannelKeylet(account, destination, seq))
+        .WillOnce(testing::Throw(std::runtime_error{"paychannel keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.paychannelKeylet(
+            bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("paychannel keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("paychannelKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(PaychannelKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, paychannelKeylet(account, destination, seq))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.paychannelKeylet(
+            bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(PaychannelKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, paychannelKeylet(account, destination, seq))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.paychannelKeylet(
+            bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(PaychannelKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, paychannelKeylet(account, destination, seq))
+        .WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.paychannelKeylet(
+            bytesOf(accountBytes), bytesOf(destinationBytes), seq, out.slice()),
+        0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/PermissionedDomainKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/PermissionedDomainKeylet.cpp
new file mode 100644
index 0000000000..7e8148913e
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/PermissionedDomainKeylet.cpp
@@ -0,0 +1,127 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct PermissionedDomainKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                             0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+    std::uint32_t const seq = 12345;
+};
+
+TEST_F(PermissionedDomainKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, permissionedDomainKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(PermissionedDomainKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, permissionedDomainKeylet(account, seq))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(PermissionedDomainKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const shortAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, permissionedDomainKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.permissionedDomainKeylet(bytesOf(shortAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(PermissionedDomainKeyletCall, LongAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const longAccount(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, permissionedDomainKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.permissionedDomainKeylet(bytesOf(longAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(PermissionedDomainKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, permissionedDomainKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.permissionedDomainKeylet(bytesOf(Bytes{}), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(PermissionedDomainKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, permissionedDomainKeylet(account, seq))
+        .WillOnce(testing::Throw(std::runtime_error{"permissioned domain keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("permissioned domain keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("permissionedDomainKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(PermissionedDomainKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, permissionedDomainKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(PermissionedDomainKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, permissionedDomainKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(PermissionedDomainKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, permissionedDomainKeylet(account, seq)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.permissionedDomainKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/Sha512Half.cpp b/src/tests/libxrpl/tx/wasm/host_context/Sha512Half.cpp
new file mode 100644
index 0000000000..a7bd781335
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/Sha512Half.cpp
@@ -0,0 +1,81 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+// `host_calls/Sha512Half.cpp` runs the digest through the engine; what is left at this layer is
+// its own contract - the out-region rule, `guarded`, and an empty input.
+struct Sha512HalfDirectCall : HostContextTest
+{
+    Bytes const data{'a', 'b', 'c'};
+    Bytes const digestBytes = Bytes(32, 0x0a);
+    Hash const digest = uint256::fromVoid(digestBytes.data());
+};
+
+TEST_F(Sha512HalfDirectCall, DataForwardedAndDigestWritten)
+{
+    EXPECT_CALL(host, computeSha512HalfHash(BytesAre("abc"))).WillOnce(testing::Return(digest));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.sha512Half(bytesOf(data), out.slice()), 32);
+    EXPECT_TRUE(out.holds(bytesOf(digestBytes)));
+}
+
+TEST_F(Sha512HalfDirectCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, computeSha512HalfHash)
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::InvalidParams)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.sha512Half(bytesOf(data), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(Sha512HalfDirectCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, computeSha512HalfHash)
+        .WillOnce(testing::Throw(std::runtime_error{"sha512 half came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.sha512Half(bytesOf(data), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("sha512 half came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("sha512Half"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(Sha512HalfDirectCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    EXPECT_CALL(host, computeSha512HalfHash(BytesAre("abc"))).WillOnce(testing::Return(digest));
+
+    OutRegion out{31};
+    EXPECT_EQ(hostContext.sha512Half(bytesOf(data), out.slice()), 32);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+// Nothing in the hash requires a non-empty input, so an empty slice is hashed like any other,
+// not refused.
+TEST_F(Sha512HalfDirectCall, EmptyInputIsHashedLikeAnyOther)
+{
+    EXPECT_CALL(host, computeSha512HalfHash(testing::Property(&Slice::empty, true)))
+        .WillOnce(testing::Return(digest));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.sha512Half(bytesOf(Bytes{}), out.slice()), 32);
+    EXPECT_TRUE(out.holds(bytesOf(digestBytes)));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/SignerListKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/SignerListKeylet.cpp
new file mode 100644
index 0000000000..fd48a2d18b
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/SignerListKeylet.cpp
@@ -0,0 +1,126 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct SignerListKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                             0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+};
+
+TEST_F(SignerListKeyletCall, AccountIsForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, signerListKeylet(account)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.signerListKeylet(bytesOf(accountBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(SignerListKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, signerListKeylet(account))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.signerListKeylet(bytesOf(accountBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(SignerListKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const shortAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, signerListKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.signerListKeylet(bytesOf(shortAccount), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(SignerListKeyletCall, LongAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const longAccount(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, signerListKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.signerListKeylet(bytesOf(longAccount), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(SignerListKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, signerListKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.signerListKeylet(bytesOf(Bytes{}), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(SignerListKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, signerListKeylet(account))
+        .WillOnce(testing::Throw(std::runtime_error{"signer list keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.signerListKeylet(bytesOf(accountBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("signer list keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("signerListKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(SignerListKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, signerListKeylet(account)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.signerListKeylet(bytesOf(accountBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(SignerListKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, signerListKeylet(account)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.signerListKeylet(bytesOf(accountBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(SignerListKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, signerListKeylet(account)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.signerListKeylet(bytesOf(accountBytes), out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/SponsorshipKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/SponsorshipKeylet.cpp
new file mode 100644
index 0000000000..5eaf0fc0db
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/SponsorshipKeylet.cpp
@@ -0,0 +1,141 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+//
+// `sponsor` and `sponsee` are distinct byte patterns: a happy path built from two copies of the
+// same account would still pass if the two were swapped.
+struct SponsorshipKeyletCall : HostContextTest
+{
+    Bytes const sponsorBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                             0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    Bytes const sponseeBytes{0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,
+                             0xeb, 0xec, 0xed, 0xee, 0xef, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4};
+    AccountID const sponsor = AccountID::fromVoid(sponsorBytes.data());
+    AccountID const sponsee = AccountID::fromVoid(sponseeBytes.data());
+};
+
+TEST_F(SponsorshipKeyletCall, SponsorAndSponseeAreForwardedInOrderKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, sponsorshipKeylet(sponsor, sponsee)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.sponsorshipKeylet(bytesOf(sponsorBytes), bytesOf(sponseeBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(SponsorshipKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, sponsorshipKeylet(sponsor, sponsee))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.sponsorshipKeylet(bytesOf(sponsorBytes), bytesOf(sponseeBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(SponsorshipKeyletCall, MalformedSponsorIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedSponsor(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, sponsorshipKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.sponsorshipKeylet(
+            bytesOf(malformedSponsor), bytesOf(sponseeBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(SponsorshipKeyletCall, MalformedSponseeIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedSponsee(AccountID::size() + 1, 0xe1);
+    EXPECT_CALL(host, sponsorshipKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.sponsorshipKeylet(
+            bytesOf(sponsorBytes), bytesOf(malformedSponsee), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// Both ids fail one combined length check, so a call malformed in both places answers the same
+// `InvalidParams` as either alone; what's observable is that the host is never asked.
+TEST_F(SponsorshipKeyletCall, BothAccountsMalformedIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedSponsor(AccountID::size() - 1, 0x01);
+    Bytes const malformedSponsee(AccountID::size() - 1, 0xe1);
+    EXPECT_CALL(host, sponsorshipKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.sponsorshipKeylet(
+            bytesOf(malformedSponsor), bytesOf(malformedSponsee), out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(SponsorshipKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, sponsorshipKeylet(sponsor, sponsee))
+        .WillOnce(testing::Throw(std::runtime_error{"sponsorship keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.sponsorshipKeylet(bytesOf(sponsorBytes), bytesOf(sponseeBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("sponsorship keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("sponsorshipKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(SponsorshipKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, sponsorshipKeylet(sponsor, sponsee)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.sponsorshipKeylet(bytesOf(sponsorBytes), bytesOf(sponseeBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(SponsorshipKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, sponsorshipKeylet(sponsor, sponsee)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.sponsorshipKeylet(bytesOf(sponsorBytes), bytesOf(sponseeBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(SponsorshipKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, sponsorshipKeylet(sponsor, sponsee)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.sponsorshipKeylet(bytesOf(sponsorBytes), bytesOf(sponseeBytes), out.slice()),
+        0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/TicketKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/TicketKeylet.cpp
new file mode 100644
index 0000000000..62cf2ca316
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/TicketKeylet.cpp
@@ -0,0 +1,127 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct TicketKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                             0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+    std::uint32_t const seq = 12345;
+};
+
+TEST_F(TicketKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, ticketKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(TicketKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, ticketKeylet(account, seq))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(TicketKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const shortAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, ticketKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ticketKeylet(bytesOf(shortAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(TicketKeyletCall, LongAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const longAccount(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, ticketKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ticketKeylet(bytesOf(longAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(TicketKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, ticketKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ticketKeylet(bytesOf(Bytes{}), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(TicketKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, ticketKeylet(account, seq))
+        .WillOnce(testing::Throw(std::runtime_error{"ticket keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("ticket keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("ticketKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(TicketKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, ticketKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(TicketKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, ticketKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(TicketKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, ticketKeylet(account, seq)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.ticketKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/Trace.cpp b/src/tests/libxrpl/tx/wasm/host_context/Trace.cpp
new file mode 100644
index 0000000000..2ac0ebacf3
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/Trace.cpp
@@ -0,0 +1,216 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct TraceDirectCall : HostContextTest
+{
+};
+
+// The catch sits in `trace` itself, not in `guarded`. It logs at trace level, below the
+// fixture's default threshold, so the threshold is lowered to observe it.
+TEST_F(TraceDirectCall, HostExceptionIsSwallowedRatherThanEscaping)
+{
+    sink.threshold(beast::Severity::Trace);
+    EXPECT_CALL(host, trace).WillOnce(testing::Throw(std::runtime_error{"trace sink came apart"}));
+
+    hostContext.trace("note", bytesOf(Bytes{'h', 'i'}), TraceDataType::AsText);
+
+    EXPECT_THAT(logged(), testing::HasSubstr("trace sink came apart"));
+}
+
+// The cap is on message and data together, not on data alone.
+TEST_F(TraceDirectCall, MessagePlusDataPastCapIsDroppedWithoutAskingHost)
+{
+    Bytes const data(kMaxWasmDataLength, 0x41);
+    EXPECT_CALL(host, trace).Times(0);
+
+    hostContext.trace("x", bytesOf(data), TraceDataType::AsText);
+}
+
+TEST_F(TraceDirectCall, CodesNamingNoTypeAreDropped)
+{
+    // Either side of the seven that name a type, and the ends of the range the guest's `i32`
+    // can hold.
+    constexpr std::array kCodesNamingNoType{
+        std::numeric_limits::min(),
+        -1,
+        0,
+        8,
+        9,
+        std::numeric_limits::max()};
+    // Bytes several of the types render, so a drop is the code's doing rather than the buffer's.
+    Bytes const data(8, 0xff);
+    EXPECT_CALL(host, trace).Times(0);
+
+    for (auto const code : kCodesNamingNoType)
+    {
+        hostContext.trace("note", bytesOf(data), static_cast(code));
+    }
+}
+
+// The only buffer `AsText` cannot take verbatim: an empty `Slice` has a null `data()`, which
+// `std::string` may not be handed.
+TEST_F(TraceDirectCall, EmptyBufferIsRenderedAsEmptyText)
+{
+    EXPECT_CALL(host, trace(std::string_view("note"), std::string_view("")));
+
+    hostContext.trace("note", bytesOf(Bytes{}), TraceDataType::AsText);
+}
+
+// The exception to the widths below: `floatToString` renders an undecodable buffer as text
+// rather than refusing it, so this is the one type whose malformed data still reaches the host.
+TEST_F(TraceDirectCall, XfloatOfTheWrongWidthReachesHostAsInvalidData)
+{
+    EXPECT_CALL(host, trace(std::string_view("note"), std::string_view("Invalid data: FFFFFFFF")));
+
+    hostContext.trace("note", bytesOf(Bytes(4, 0xff)), TraceDataType::Xfloat);
+}
+
+// An amount is read rather than measured: the deserializer takes what it needs and is not asked
+// whether anything is left, so trailing bytes are ignored rather than refused.
+TEST_F(TraceDirectCall, AmountPastItsWidthIsReadFromTheFrontOfTheBuffer)
+{
+    Bytes const data{0x40, 0, 0, 0, 0, 0, 0x03, 0xe8, 0xff, 0xff, 0xff, 0xff};
+    EXPECT_CALL(host, trace(std::string_view("note"), std::string_view("1000/XRP")));
+
+    hostContext.trace("note", bytesOf(data), TraceDataType::Amount);
+}
+
+struct TraceRenderingBundle
+{
+    std::string name;
+    TraceDataType type;
+    Bytes data;
+    std::string text;
+};
+
+struct TraceRendering : HostContextTest, testing::WithParamInterface
+{
+};
+
+TEST_P(TraceRendering, DataIsRenderedAsItsTypeNames)
+{
+    auto const& rendering = GetParam();
+    EXPECT_CALL(host, trace(std::string_view("note"), std::string_view(rendering.text)));
+
+    hostContext.trace("note", bytesOf(rendering.data), rendering.type);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    EveryDataType,
+    TraceRendering,
+    testing::ValuesIn({
+        TraceRenderingBundle{
+            .name = "Int64",
+            .type = TraceDataType::Int64,
+            .data = Bytes(8, 0xff),
+            .text = "-1"},
+        TraceRenderingBundle{
+            .name = "Uint64",
+            .type = TraceDataType::Uint64,
+            .data = Bytes(8, 0xff),
+            .text = "18446744073709551615"},
+        TraceRenderingBundle{
+            .name = "Xfloat",
+            .type = TraceDataType::Xfloat,
+            .data = Bytes{0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0},
+            .text = "42"},
+        TraceRenderingBundle{
+            .name = "Account",
+            .type = TraceDataType::Account,
+            .data = Bytes(AccountID::size(), 0),
+            .text = "rrrrrrrrrrrrrrrrrrrrrhoLvTp"},
+        TraceRenderingBundle{
+            .name = "Amount",
+            .type = TraceDataType::Amount,
+            .data = Bytes{0x40, 0, 0, 0, 0, 0, 0x03, 0xe8},
+            .text = "1000/XRP"},
+        TraceRenderingBundle{
+            .name = "AsHex",
+            .type = TraceDataType::AsHex,
+            .data = Bytes{0x07, 0x08, 0xff},
+            .text = "0708FF"},
+        TraceRenderingBundle{
+            .name = "AsText",
+            .type = TraceDataType::AsText,
+            .data = Bytes{'h', 'e', 'l', 'l', 'o'},
+            .text = "hello"},
+    }),
+    [](testing::TestParamInfo const& info) { return info.param.name; });
+
+// A buffer that does not hold what its type claims. The width is part of the type, and bytes
+// that are not it hold no value to print.
+struct TraceRefusalBundle
+{
+    std::string name;
+    TraceDataType type;
+    Bytes data;
+};
+
+struct TraceRefusal : HostContextTest, testing::WithParamInterface
+{
+};
+
+// A trace answers the guest nothing, so a buffer it cannot read is dropped rather than reported.
+TEST_P(TraceRefusal, DataThatDoesNotHoldItsTypeIsDropped)
+{
+    auto const& refusal = GetParam();
+    EXPECT_CALL(host, trace).Times(0);
+
+    hostContext.trace("note", bytesOf(refusal.data), refusal.type);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    EveryWidth,
+    TraceRefusal,
+    testing::ValuesIn({
+        TraceRefusalBundle{
+            .name = "Int64Short",
+            .type = TraceDataType::Int64,
+            .data = Bytes(7, 0xff)},
+        TraceRefusalBundle{
+            .name = "Int64Long",
+            .type = TraceDataType::Int64,
+            .data = Bytes(9, 0xff)},
+        TraceRefusalBundle{.name = "Int64Empty", .type = TraceDataType::Int64, .data = Bytes{}},
+        TraceRefusalBundle{
+            .name = "Uint64Short",
+            .type = TraceDataType::Uint64,
+            .data = Bytes(7, 0xff)},
+        TraceRefusalBundle{
+            .name = "Uint64Long",
+            .type = TraceDataType::Uint64,
+            .data = Bytes(9, 0xff)},
+        TraceRefusalBundle{
+            .name = "AccountShort",
+            .type = TraceDataType::Account,
+            .data = Bytes(AccountID::size() - 1, 0)},
+        TraceRefusalBundle{
+            .name = "AccountLong",
+            .type = TraceDataType::Account,
+            .data = Bytes(AccountID::size() + 1, 0)},
+        // `STAmount`'s deserializer rejects these by throwing, which must not escape the run.
+        TraceRefusalBundle{
+            .name = "AmountMalformed",
+            .type = TraceDataType::Amount,
+            .data = Bytes(3, 0xff)},
+        TraceRefusalBundle{.name = "AmountEmpty", .type = TraceDataType::Amount, .data = Bytes{}},
+    }),
+    [](testing::TestParamInfo const& info) { return info.param.name; });
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/TrustLineKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/TrustLineKeylet.cpp
new file mode 100644
index 0000000000..18a4d8d34a
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/TrustLineKeylet.cpp
@@ -0,0 +1,180 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+//
+// `account1` and `account2` are distinct byte patterns: a happy path built from two copies of
+// the same account would still pass if the two were swapped.
+struct TrustLineKeyletCall : HostContextTest
+{
+    Bytes const account1Bytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                              0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    Bytes const account2Bytes{0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,
+                              0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44};
+    Bytes const currencyBytes{0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,
+                              0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74};
+    AccountID const account1 = AccountID::fromVoid(account1Bytes.data());
+    AccountID const account2 = AccountID::fromVoid(account2Bytes.data());
+    Currency const currency = Currency::fromVoid(currencyBytes.data());
+};
+
+TEST_F(TrustLineKeyletCall, AccountsAndCurrencyAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, trustLineKeylet(account1, account2, currency))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.trustLineKeylet(
+            bytesOf(account1Bytes), bytesOf(account2Bytes), bytesOf(currencyBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(TrustLineKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, trustLineKeylet(account1, account2, currency))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.trustLineKeylet(
+            bytesOf(account1Bytes), bytesOf(account2Bytes), bytesOf(currencyBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(TrustLineKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, trustLineKeylet(account1, account2, currency))
+        .WillOnce(testing::Throw(std::runtime_error{"trust line keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.trustLineKeylet(
+            bytesOf(account1Bytes), bytesOf(account2Bytes), bytesOf(currencyBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("trust line keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("trustLineKeylet"));
+}
+
+TEST_F(TrustLineKeyletCall, MalformedAccount1IsRefusedWithoutAskingHost)
+{
+    Bytes const malformedAccount1(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, trustLineKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.trustLineKeylet(
+            bytesOf(malformedAccount1),
+            bytesOf(account2Bytes),
+            bytesOf(currencyBytes),
+            out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(TrustLineKeyletCall, MalformedAccount2IsRefusedWithoutAskingHost)
+{
+    Bytes const malformedAccount2(AccountID::size() + 1, 0x31);
+    EXPECT_CALL(host, trustLineKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.trustLineKeylet(
+            bytesOf(account1Bytes),
+            bytesOf(malformedAccount2),
+            bytesOf(currencyBytes),
+            out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(TrustLineKeyletCall, MalformedCurrencyIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedCurrency(Currency::size() - 1, 0x61);
+    EXPECT_CALL(host, trustLineKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.trustLineKeylet(
+            bytesOf(account1Bytes),
+            bytesOf(account2Bytes),
+            bytesOf(malformedCurrency),
+            out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// The currency length is checked before either account's, but every malformed shape answers
+// the same `InvalidParams`, so a call malformed in both places cannot show which check fired.
+// What's observable: the host is never asked.
+TEST_F(TrustLineKeyletCall, CurrencyAndAccountBothMalformedIsRefusedWithoutAskingHost)
+{
+    Bytes const malformedCurrency(Currency::size() - 1, 0x61);
+    Bytes const malformedAccount1(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, trustLineKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.trustLineKeylet(
+            bytesOf(malformedAccount1),
+            bytesOf(account2Bytes),
+            bytesOf(malformedCurrency),
+            out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(TrustLineKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, trustLineKeylet(account1, account2, currency))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.trustLineKeylet(
+            bytesOf(account1Bytes), bytesOf(account2Bytes), bytesOf(currencyBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(TrustLineKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, trustLineKeylet(account1, account2, currency))
+        .WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.trustLineKeylet(
+            bytesOf(account1Bytes), bytesOf(account2Bytes), bytesOf(currencyBytes), out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(TrustLineKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, trustLineKeylet(account1, account2, currency))
+        .WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.trustLineKeylet(
+            bytesOf(account1Bytes), bytesOf(account2Bytes), bytesOf(currencyBytes), out.slice()),
+        0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/TxArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_context/TxArrayLen.cpp
new file mode 100644
index 0000000000..887842ef7a
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/TxArrayLen.cpp
@@ -0,0 +1,56 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// `getTxArrayLen` answers its count directly rather than through an out region: no axis E, no
+// `OutRegion`, and the happy path asserts the returned count.
+struct TxArrayLenCall : HostContextTest
+{
+    std::int32_t fieldCode = sfBalance.getCode();
+};
+
+TEST_F(TxArrayLenCall, FieldCodeBecomesSFieldHostIsAskedFor)
+{
+    EXPECT_CALL(host, getTxArrayLen(testing::Ref(sfBalance))).WillOnce(testing::Return(5));
+
+    EXPECT_EQ(hostContext.getTxArrayLen(fieldCode), 5);
+}
+
+// `NoArray` is what a field that is not an array actually answers, so it stands in for axis B
+// here rather than an arbitrary code.
+TEST_F(TxArrayLenCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getTxArrayLen(testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::NoArray)));
+
+    EXPECT_EQ(hostContext.getTxArrayLen(fieldCode), hfErrorToInt(HostFunctionError::NoArray));
+}
+
+TEST_F(TxArrayLenCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getTxArrayLen(testing::Ref(sfBalance)))
+        .WillOnce(testing::Throw(std::runtime_error{"tx array len came apart"}));
+
+    EXPECT_EQ(hostContext.getTxArrayLen(fieldCode), hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("tx array len came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getTxArrayLen"));
+}
+
+TEST_F(TxArrayLenCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
+{
+    fieldCode = 0x7fff'0000;  // a code nothing is registered under
+    EXPECT_CALL(host, getTxArrayLen).Times(0);
+
+    EXPECT_EQ(hostContext.getTxArrayLen(fieldCode), hfErrorToInt(HostFunctionError::InvalidField));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/TxField.cpp b/src/tests/libxrpl/tx/wasm/host_context/TxField.cpp
new file mode 100644
index 0000000000..84a3d694a4
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/TxField.cpp
@@ -0,0 +1,126 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
+// side, not here.
+struct TxFieldCall : HostContextTest
+{
+    std::int32_t fieldCode = sfBalance.getCode();
+};
+
+TEST_F(TxFieldCall, FieldCodeBecomesSFieldHostIsAskedFor)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getTxField(fieldCode, out.slice()), static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(TxFieldCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getTxField(testing::Ref(sfBalance)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::FieldNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getTxField(fieldCode, out.slice()),
+        hfErrorToInt(HostFunctionError::FieldNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(TxFieldCall, UnknownFieldCodeIsRefusedWithoutAskingHost)
+{
+    fieldCode = 0x7fff'0000;  // a code nothing is registered under
+    EXPECT_CALL(host, getTxField).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getTxField(fieldCode, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidField));
+}
+
+TEST_F(TxFieldCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getTxField(testing::Ref(sfBalance)))
+        .WillOnce(testing::Throw(std::runtime_error{"balance field came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getTxField(fieldCode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("balance field came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getTxField"));
+}
+
+// `guarded`'s `catch (...)` arm, for a thrown value that is not a `std::exception`.
+TEST_F(TxFieldCall, NonStandardThrowBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Throw(42));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getTxField(fieldCode, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("getTxField"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(TxFieldCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(value));
+
+    OutRegion out{value.size() - 1};
+    EXPECT_EQ(
+        hostContext.getTxField(fieldCode, out.slice()), static_cast(value.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(TxFieldCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(value));
+
+    OutRegion out{value.size()};
+    EXPECT_EQ(
+        hostContext.getTxField(fieldCode, out.slice()), static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+// `kMaxWasmDataLength` is the engine's cap, not `HostContext`'s: a length past it crosses
+// unchanged here, where the sibling engine test sees `DataFieldTooLarge` instead.
+TEST_F(TxFieldCall, LengthPastProtocolCapCrossesUnchanged)
+{
+    Bytes const value(kMaxWasmDataLength + 1, 0xab);
+    EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(value));
+
+    OutRegion out{value.size()};
+    EXPECT_EQ(
+        hostContext.getTxField(fieldCode, out.slice()), static_cast(value.size()));
+}
+
+TEST_F(TxFieldCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, getTxField(testing::Ref(sfBalance))).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getTxField(fieldCode, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/TxNestedArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_context/TxNestedArrayLen.cpp
new file mode 100644
index 0000000000..5d550cc623
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/TxNestedArrayLen.cpp
@@ -0,0 +1,76 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
+// side, not here.
+//
+// No out region and no axis E: `getTxNestedArrayLen` answers the array's element count
+// directly rather than through a written buffer.
+struct TxNestedArrayLenCall : HostContextTest
+{
+    std::vector const steps{5, -12, 130};
+    Bytes const locatorBytes = bytesOfSteps(steps);
+};
+
+TEST_F(TxNestedArrayLenCall, LocatorBytesBecomeFieldLocatorHostReturnsCount)
+{
+    EXPECT_CALL(host, getTxNestedArrayLen(LocatorEquals(steps))).WillOnce(testing::Return(7));
+
+    EXPECT_EQ(hostContext.getTxNestedArrayLen(bytesOf(locatorBytes)), 7);
+}
+
+// `NoArray` - the field the locator resolves to is not an array - is the error this shape
+// most plausibly returns, so it stands in for axis B.
+TEST_F(TxNestedArrayLenCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getTxNestedArrayLen(LocatorEquals(steps)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::NoArray)));
+
+    EXPECT_EQ(
+        hostContext.getTxNestedArrayLen(bytesOf(locatorBytes)),
+        hfErrorToInt(HostFunctionError::NoArray));
+}
+
+TEST_F(TxNestedArrayLenCall, EmptyLocatorIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, getTxNestedArrayLen).Times(0);
+
+    EXPECT_EQ(
+        hostContext.getTxNestedArrayLen(bytesOf(Bytes{})),
+        hfErrorToInt(HostFunctionError::LocatorMalformed));
+}
+
+// Distinct from an empty locator: `invokeWithLocator` checks the two conditions separately.
+TEST_F(TxNestedArrayLenCall, MisalignedLocatorLengthIsRefusedWithoutAskingHost)
+{
+    Bytes const oddLength{1, 2, 3};
+    EXPECT_CALL(host, getTxNestedArrayLen).Times(0);
+
+    EXPECT_EQ(
+        hostContext.getTxNestedArrayLen(bytesOf(oddLength)),
+        hfErrorToInt(HostFunctionError::LocatorMalformed));
+}
+
+TEST_F(TxNestedArrayLenCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getTxNestedArrayLen(LocatorEquals(steps)))
+        .WillOnce(testing::Throw(std::runtime_error{"tx nested array len came apart"}));
+
+    EXPECT_EQ(
+        hostContext.getTxNestedArrayLen(bytesOf(locatorBytes)),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("tx nested array len came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getTxNestedArrayLen"));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/TxNestedField.cpp b/src/tests/libxrpl/tx/wasm/host_context/TxNestedField.cpp
new file mode 100644
index 0000000000..43351b6884
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/TxNestedField.cpp
@@ -0,0 +1,116 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, the field cap, guest memory - are tested on the Rust
+// side, not here.
+struct TxNestedFieldCall : HostContextTest
+{
+    std::vector const steps{5, -12, 130};
+    Bytes const locatorBytes = bytesOfSteps(steps);
+};
+
+TEST_F(TxNestedFieldCall, LocatorBytesBecomeFieldLocatorHostIsAskedFor)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getTxNestedField(LocatorEquals(steps))).WillOnce(testing::Return(value));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getTxNestedField(bytesOf(locatorBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(TxNestedFieldCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, getTxNestedField(LocatorEquals(steps)))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::NotLeafField)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getTxNestedField(bytesOf(locatorBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::NotLeafField));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(TxNestedFieldCall, EmptyLocatorIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, getTxNestedField).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getTxNestedField(bytesOf(Bytes{}), out.slice()),
+        hfErrorToInt(HostFunctionError::LocatorMalformed));
+}
+
+// Distinct from an empty locator: `invokeWithLocator` checks the two conditions separately.
+TEST_F(TxNestedFieldCall, MisalignedLocatorLengthIsRefusedWithoutAskingHost)
+{
+    Bytes const oddLength{1, 2, 3};
+    EXPECT_CALL(host, getTxNestedField).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getTxNestedField(bytesOf(oddLength), out.slice()),
+        hfErrorToInt(HostFunctionError::LocatorMalformed));
+}
+
+TEST_F(TxNestedFieldCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, getTxNestedField(LocatorEquals(steps)))
+        .WillOnce(testing::Throw(std::runtime_error{"nested field came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.getTxNestedField(bytesOf(locatorBytes), out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("nested field came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("getTxNestedField"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(TxNestedFieldCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getTxNestedField(LocatorEquals(steps))).WillOnce(testing::Return(value));
+
+    OutRegion out{value.size() - 1};
+    EXPECT_EQ(
+        hostContext.getTxNestedField(bytesOf(locatorBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(TxNestedFieldCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const value{1, 2, 3};
+    EXPECT_CALL(host, getTxNestedField(LocatorEquals(steps))).WillOnce(testing::Return(value));
+
+    OutRegion out{value.size()};
+    EXPECT_EQ(
+        hostContext.getTxNestedField(bytesOf(locatorBytes), out.slice()),
+        static_cast(value.size()));
+    EXPECT_TRUE(out.holds(bytesOf(value)));
+}
+
+TEST_F(TxNestedFieldCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, getTxNestedField(LocatorEquals(steps))).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.getTxNestedField(bytesOf(locatorBytes), out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/UpdateData.cpp b/src/tests/libxrpl/tx/wasm/host_context/UpdateData.cpp
new file mode 100644
index 0000000000..11ee5760ed
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/UpdateData.cpp
@@ -0,0 +1,58 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The other non-`const` host method; it answers the byte count stored directly, with no out
+// region.
+struct UpdateDataCall : HostContextTest
+{
+    Bytes const data{'h', 'e', 'l', 'l', 'o'};
+};
+
+TEST_F(UpdateDataCall, DataForwardedByteCountReturned)
+{
+    EXPECT_CALL(host, updateData(BytesAre("hello"))).WillOnce(testing::Return(5));
+
+    EXPECT_EQ(hostContext.updateData(bytesOf(data)), 5);
+}
+
+TEST_F(UpdateDataCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, updateData(BytesAre("hello")))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::DataFieldTooLarge)));
+
+    EXPECT_EQ(
+        hostContext.updateData(bytesOf(data)), hfErrorToInt(HostFunctionError::DataFieldTooLarge));
+}
+
+TEST_F(UpdateDataCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, updateData(BytesAre("hello")))
+        .WillOnce(testing::Throw(std::runtime_error{"update data came apart"}));
+
+    EXPECT_EQ(
+        hostContext.updateData(bytesOf(data)), hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("update data came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("updateData"));
+}
+
+// An empty `rust::Slice` has a null `data()`; `updateData` forwards it as an empty `Slice`
+// rather than treating it as malformed.
+TEST_F(UpdateDataCall, EmptyInputRegionForwardsAsEmptySlice)
+{
+    EXPECT_CALL(host, updateData(testing::Property(&Slice::empty, true)))
+        .WillOnce(testing::Return(0));
+
+    EXPECT_EQ(hostContext.updateData(bytesOf(Bytes{})), 0);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_context/VaultKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_context/VaultKeylet.cpp
new file mode 100644
index 0000000000..6ebefb2437
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_context/VaultKeylet.cpp
@@ -0,0 +1,127 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+// The engine's own rules - buffer-fit, guest memory - are tested on the Rust side, not here.
+struct VaultKeyletCall : HostContextTest
+{
+    Bytes const accountBytes{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
+                             0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14};
+    AccountID const account = AccountID::fromVoid(accountBytes.data());
+    std::uint32_t const seq = 12345;
+};
+
+TEST_F(VaultKeyletCall, AccountAndSeqAreForwardedKeyletIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, vaultKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(VaultKeyletCall, HostErrorBecomesContractReturnValue)
+{
+    EXPECT_CALL(host, vaultKeylet(account, seq))
+        .WillOnce(testing::Return(std::unexpected(HostFunctionError::LedgerObjNotFound)));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::LedgerObjNotFound));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(VaultKeyletCall, ShortAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const shortAccount(AccountID::size() - 1, 0x01);
+    EXPECT_CALL(host, vaultKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.vaultKeylet(bytesOf(shortAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(VaultKeyletCall, LongAccountIsRefusedWithoutAskingHost)
+{
+    Bytes const longAccount(AccountID::size() + 1, 0x01);
+    EXPECT_CALL(host, vaultKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.vaultKeylet(bytesOf(longAccount), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(VaultKeyletCall, EmptyAccountIsRefusedWithoutAskingHost)
+{
+    EXPECT_CALL(host, vaultKeylet).Times(0);
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.vaultKeylet(bytesOf(Bytes{}), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InvalidParams));
+}
+
+TEST_F(VaultKeyletCall, HostExceptionBecomesInternalFatalAndIsLogged)
+{
+    EXPECT_CALL(host, vaultKeylet(account, seq))
+        .WillOnce(testing::Throw(std::runtime_error{"vault keylet came apart"}));
+
+    OutRegion out{32};
+    EXPECT_EQ(
+        hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()),
+        hfErrorToInt(HostFunctionError::InternalFatal));
+    EXPECT_THAT(logged(), testing::HasSubstr("vault keylet came apart"));
+    EXPECT_THAT(logged(), testing::HasSubstr("vaultKeylet"));
+}
+
+// The out-region contract: write only if the whole value fits, and return the true length
+// either way.
+TEST_F(VaultKeyletCall, ShortOutRegionWritesNothingAndReturnsTrueLength)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, vaultKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size() - 1};
+    EXPECT_EQ(
+        hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_FALSE(out.wasWritten());
+}
+
+TEST_F(VaultKeyletCall, OutRegionOfExactSizeIsWritten)
+{
+    Bytes const keylet(32, 0xab);
+    EXPECT_CALL(host, vaultKeylet(account, seq)).WillOnce(testing::Return(keylet));
+
+    OutRegion out{keylet.size()};
+    EXPECT_EQ(
+        hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()),
+        static_cast(keylet.size()));
+    EXPECT_TRUE(out.holds(bytesOf(keylet)));
+}
+
+TEST_F(VaultKeyletCall, EmptyResultAnswersZeroAndWritesNothing)
+{
+    EXPECT_CALL(host, vaultKeylet(account, seq)).WillOnce(testing::Return(Bytes{}));
+
+    OutRegion out{32};
+    EXPECT_EQ(hostContext.vaultKeylet(bytesOf(accountBytes), seq, out.slice()), 0);
+    EXPECT_FALSE(out.wasWritten());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/AccountKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/AccountKeylet.cpp
new file mode 100644
index 0000000000..a846f8807e
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/AccountKeylet.cpp
@@ -0,0 +1,33 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct AccountKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(AccountKeyletImpl, MatchesAccountKeyletFunction)
+{
+    auto const owner = fund("owner");
+
+    expectKeyletMatches(makeHost()->accountKeylet(owner), keylet::account(owner.id()));
+}
+
+TEST_F(AccountKeyletImpl, NonExistentAccountStillComputesKeylet)
+{
+    auto const nobody = Account{"nobody"};
+    expectKeyletMatches(makeHost()->accountKeylet(nobody), keylet::account(nobody.id()));
+}
+
+TEST_F(AccountKeyletImpl, UnsetAccountIsInvalidAccount)
+{
+    expectError(makeHost()->accountKeylet(AccountID{}), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/AmmKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/AmmKeylet.cpp
new file mode 100644
index 0000000000..cf393f7b89
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/AmmKeylet.cpp
@@ -0,0 +1,38 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct AmmKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(AmmKeyletImpl, MatchesAmmKeyletFunction)
+{
+    auto const owner = fund("owner");
+
+    auto usdIssue = Issue{toCurrency("USD"), owner.id()};
+
+    expectKeyletMatches(
+        makeHost()->ammKeylet(usdIssue, xrpIssue()), keylet::amm(xrpIssue(), usdIssue));
+}
+
+TEST_F(AmmKeyletImpl, InvalidParameters)
+{
+    auto const owner = fund("owner");
+
+    auto baseMpt = makeMptID(1, owner.id());
+
+    auto h = makeHost();
+    expectError(h->ammKeylet(xrpIssue(), xrpIssue()), HostFunctionError::InvalidParams);
+    expectError(h->ammKeylet(xrpIssue(), baseMpt), HostFunctionError::InvalidParams);
+    expectError(h->ammKeylet(baseMpt, xrpIssue()), HostFunctionError::InvalidParams);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/BaseFee.cpp b/src/tests/libxrpl/tx/wasm/host_functions/BaseFee.cpp
new file mode 100644
index 0000000000..0bba608633
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/BaseFee.cpp
@@ -0,0 +1,15 @@
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct BaseFeeImpl : RealHostFixture
+{
+};
+
+TEST_F(BaseFeeImpl, MatchesLedger)
+{
+    expectValue(makeHost()->getBaseFee(), ledger.getOpenLedger().fees().base.drops());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/CacheLedgerObj.cpp b/src/tests/libxrpl/tx/wasm/host_functions/CacheLedgerObj.cpp
new file mode 100644
index 0000000000..649bd6e5ba
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/CacheLedgerObj.cpp
@@ -0,0 +1,98 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct CacheLedgerObjImpl : RealHostFixture
+{
+    void
+    runMatchesLedger(bool implicit)
+    {
+        auto const owner = Account{"owner"};
+        ledger.createAccount(owner, XRP(1000));
+
+        auto h = makeHost();
+        auto const key = keylet::account(owner.id()).key;
+
+        for (auto i = int32_t{1}; i < 257; ++i)
+        {
+            auto const slot = h->cacheLedgerObj(key, implicit ? 0 : i);
+            ASSERT_TRUE(slot.has_value()) << "cacheLedgerObj should find the created account";
+            EXPECT_EQ(*slot, i);
+
+            auto const account = h->getLedgerObjField(*slot, sfAccount);
+            ASSERT_TRUE(account.has_value());
+            Bytes const ownerBytes{owner.id().begin(), owner.id().end()};
+            EXPECT_EQ(*account, ownerBytes);
+
+            auto const sle = ledger.getOpenLedger().read(keylet::account(owner.id()));
+            ASSERT_NE(sle, nullptr);
+            auto const& ledgerAccount = sle->getAccountID(sfAccount);
+            EXPECT_EQ(*account, (Bytes{ledgerAccount.begin(), ledgerAccount.end()}));
+        }
+
+        // Every slot is now occupied, so asking to auto-allocate (cacheIdx == 0) has nowhere
+        // to put the object.
+        auto const result = h->cacheLedgerObj(key, 0);
+        ASSERT_FALSE(result.has_value());
+        EXPECT_EQ(result.error(), HostFunctionError::SlotsFull);
+    }
+};
+
+TEST_F(CacheLedgerObjImpl, MatchesLedgerExplicitIndices)
+{
+    runMatchesLedger(false);
+}
+
+TEST_F(CacheLedgerObjImpl, MatchesLedgerImplicitIndices)
+{
+    runMatchesLedger(true);
+}
+
+TEST_F(CacheLedgerObjImpl, OutOfRange)
+{
+    auto h = makeHost();
+    auto result = h->cacheLedgerObj(uint256{}, -1);
+    ASSERT_FALSE(result.has_value());
+    EXPECT_EQ(result.error(), HostFunctionError::SlotOutRange);
+
+    result = h->cacheLedgerObj(uint256{}, 257);
+    ASSERT_FALSE(result.has_value());
+    EXPECT_EQ(result.error(), HostFunctionError::SlotOutRange);
+}
+
+TEST_F(CacheLedgerObjImpl, LedgerObjNotFound)
+{
+    auto const ghost = keylet::account(Account{"ghost"}.id()).key;
+    auto result = makeHost()->cacheLedgerObj(ghost, 0);
+    ASSERT_FALSE(result.has_value());
+    EXPECT_EQ(result.error(), HostFunctionError::LedgerObjNotFound);
+}
+
+// Two hosts built from the same fixture are fully independent: each owns its own slot
+// table, so caching into one leaves the other's slots empty. (This is what the `WasmHost`
+// handle buys over the old shared-fixture-state design.)
+TEST_F(CacheLedgerObjImpl, IndependentHostsDoNotShareSlots)
+{
+    auto const owner = fund("owner");
+    auto const key = keylet::account(owner.id()).key;
+
+    auto a = makeHost();
+    auto b = makeHost();
+
+    ASSERT_TRUE(a->cacheLedgerObj(key, 1).has_value());
+    expectValue(a->getLedgerObjField(1, sfAccount), RealHostFixture::toBytes(owner.id()));
+    // `b` never cached anything, so its slot 1 is still empty.
+    expectError(b->getLedgerObjField(1, sfAccount), HostFunctionError::EmptySlot);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/CheckKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/CheckKeylet.cpp
new file mode 100644
index 0000000000..7e2571fac5
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/CheckKeylet.cpp
@@ -0,0 +1,30 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct CheckKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(CheckKeyletImpl, MatchesCheckKeyletFunction)
+{
+    auto const owner = fund("owner");
+
+    expectKeyletMatches(
+        makeHost()->checkKeylet(owner.id(), 1u),
+        keylet::check(owner.id(), SeqProxy::rawSequence(1u)));
+}
+
+TEST_F(CheckKeyletImpl, UnsetAccountIsInvalidAccount)
+{
+    expectError(makeHost()->checkKeylet(AccountID{}, 1u), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/CheckSignature.cpp b/src/tests/libxrpl/tx/wasm/host_functions/CheckSignature.cpp
new file mode 100644
index 0000000000..7bfd7be73a
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/CheckSignature.cpp
@@ -0,0 +1,80 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct CheckSignatureImpl : RealHostFixture
+{
+};
+
+TEST_F(CheckSignatureImpl, ValidSignature)
+{
+    auto const kp = generateKeyPair(KeyType::Secp256k1, randomSeed());
+    auto const& pk = kp.first;
+    auto const& sk = kp.second;
+    auto const& message = std::string{"hello signature"};
+    auto const sig = sign(pk, sk, Slice(message.data(), message.size()));
+
+    auto const result = makeHost()->checkSignature(
+        Slice{message.data(), message.size()}, Slice{sig.data(), sig.size()}, pk);
+    expectValue(result, std::int32_t{1});
+}
+
+TEST_F(CheckSignatureImpl, InvalidSignature)
+{
+    auto const kp = generateKeyPair(KeyType::Secp256k1, randomSeed());
+    auto const& pk = kp.first;
+    auto const& sk = kp.second;
+    auto const& message = std::string{"hello signature"};
+    auto const sig = sign(pk, sk, Slice(message.data(), message.size()));
+    auto const badSignature = std::string(sig.size(), 0xFF);
+
+    auto const result = makeHost()->checkSignature(
+        Slice{message.data(), message.size()}, Slice{badSignature.data(), badSignature.size()}, pk);
+    expectValue(result, std::int32_t{0});
+}
+
+TEST_F(CheckSignatureImpl, InvalidPublicKey)
+{
+    auto const kp = generateKeyPair(KeyType::Secp256k1, randomSeed());
+    auto const kp2 = generateKeyPair(KeyType::Secp256k1, randomSeed());
+    auto const& pk = kp.first;
+    auto const& sk = kp.second;
+    auto const& message = std::string{"hello signature"};
+    auto const sig = sign(pk, sk, Slice(message.data(), message.size()));
+
+    auto const result = makeHost()->checkSignature(
+        Slice{message.data(), message.size()}, Slice{sig.data(), sig.size()}, kp2.first);
+    expectValue(result, std::int32_t{0});
+}
+
+TEST_F(CheckSignatureImpl, EmptySignature)
+{
+    auto const kp = generateKeyPair(KeyType::Secp256k1, randomSeed());
+    auto const& pk = kp.first;
+    auto const& message = std::string{"hello signature"};
+
+    auto const result =
+        makeHost()->checkSignature(Slice{message.data(), message.size()}, Slice{}, pk);
+    expectValue(result, std::int32_t{0});
+}
+
+TEST_F(CheckSignatureImpl, EmptyMessage)
+{
+    auto const kp = generateKeyPair(KeyType::Secp256k1, randomSeed());
+    auto const& pk = kp.first;
+    auto const& sk = kp.second;
+    auto const& message = std::string{"hello signature"};
+    auto const sig = sign(pk, sk, Slice(message.data(), message.size()));
+
+    auto const result = makeHost()->checkSignature(Slice{}, Slice{sig.data(), sig.size()}, pk);
+    expectValue(result, std::int32_t{0});
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/CredentialKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/CredentialKeylet.cpp
new file mode 100644
index 0000000000..d3521ad256
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/CredentialKeylet.cpp
@@ -0,0 +1,59 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct CredentialKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(CredentialKeyletImpl, MatchesCredentialKeyletFunction)
+{
+    auto const owner = fund("owner");
+
+    auto const credTypeStr = std::string{"test"};
+    auto const credType = Slice{credTypeStr.data(), credTypeStr.size()};
+
+    expectKeyletMatches(
+        makeHost()->credentialKeylet(owner.id(), owner.id(), credType),
+        keylet::credential(owner.id(), owner.id(), credType));
+}
+
+TEST_F(CredentialKeyletImpl, CredentialTypeStringTooLong)
+{
+    auto const owner = fund("owner");
+
+    auto constexpr credTypeStr = std::string_view{
+        "abcdefghijklmnopqrstuvwxyz01234567890qwertyuiop[]"
+        "asdfghjkl;'zxcvbnm8237tr28weufwldebvfv8734t07p"};
+    static_assert(credTypeStr.size() > kMaxCredentialTypeLength);
+    auto const credType = Slice{credTypeStr.data(), credTypeStr.size()};
+
+    expectError(
+        makeHost()->credentialKeylet(owner.id(), owner.id(), credType),
+        HostFunctionError::InvalidParams);
+}
+
+TEST_F(CredentialKeyletImpl, InvalidAccount)
+{
+    auto const owner = fund("owner");
+
+    auto const credTypeStr = std::string{"test"};
+    auto const credType = Slice{credTypeStr.data(), credTypeStr.size()};
+
+    auto h = makeHost();
+    expectError(
+        h->credentialKeylet(AccountID{}, owner.id(), credType), HostFunctionError::InvalidAccount);
+
+    expectError(
+        h->credentialKeylet(owner.id(), AccountID{}, credType), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/CurrentLedgerObjArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_functions/CurrentLedgerObjArrayLen.cpp
new file mode 100644
index 0000000000..72c3491e85
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/CurrentLedgerObjArrayLen.cpp
@@ -0,0 +1,57 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct CurrentLedgerObjArrayLenImpl : RealHostFixture
+{
+    using RealHostFixture::makeHost;
+
+    WasmHost
+    makeHost(Account const& acct)
+    {
+        makeSignerList(acct, 2, {{Account{"alice"}, 1}, {Account{"becky"}, 1}});
+        auto assembler = bareTx();
+        return makeHost(keylet::signerList(acct.id()), assembler.type, std::move(assembler.build));
+    }
+};
+
+TEST_F(CurrentLedgerObjArrayLenImpl, SignerEntriesLength)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(h->getCurrentLedgerObjArrayLen(sfSignerEntries), 2);
+}
+
+TEST_F(CurrentLedgerObjArrayLenImpl, NonArrayFieldNoArray)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(h->getCurrentLedgerObjArrayLen(sfAccount), HostFunctionError::NoArray);
+}
+
+TEST_F(CurrentLedgerObjArrayLenImpl, MissingArrayFieldNotFound)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(h->getCurrentLedgerObjArrayLen(sfMemos), HostFunctionError::FieldNotFound);
+}
+
+TEST_F(CurrentLedgerObjArrayLenImpl, MissingCurrentObjectNotFound)
+{
+    auto const owner = fund("owner");
+    auto assembler = bareTx();
+    auto h = makeHost(keylet::signerList(owner.id()), assembler.type, std::move(assembler.build));
+    expectError(
+        h->getCurrentLedgerObjArrayLen(sfSignerEntries), HostFunctionError::LedgerObjNotFound);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/CurrentLedgerObjField.cpp b/src/tests/libxrpl/tx/wasm/host_functions/CurrentLedgerObjField.cpp
new file mode 100644
index 0000000000..c3c249396f
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/CurrentLedgerObjField.cpp
@@ -0,0 +1,98 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct CurrentLedgerObjFieldImpl : RealHostFixture
+{
+    // Create an escrow owned by `owner` and return its keylet (the object the host will
+    // read as its "current" object).
+    Keylet
+    makeEscrow(Account const& owner, Account const& dest, uint256* transactionId = nullptr)
+    {
+        ledger.createAccount(owner, XRP(1000));
+        ledger.createAccount(dest, XRP(1000));
+
+        auto const ownerSeq = ledger.getAccountRoot(owner.id()).getSequence();
+        // A finish time comfortably after the genesis close time.
+        auto const r = ledger.submit(
+            transactions::EscrowCreateBuilder{owner.id(), dest.id(), XRP(100)}.setFinishAfter(
+                900'000'000),
+            owner);
+        EXPECT_EQ(r.ter, tesSUCCESS) << transToken(r.ter);
+        if (transactionId != nullptr)
+        {
+            *transactionId = r.tx->getTransactionID();
+        }
+        ledger.close();
+        return keylet::escrow(owner.id(), SeqProxy::rawSequence(ownerSeq));
+    }
+};
+
+TEST_F(CurrentLedgerObjFieldImpl, ReadsfAccount)
+{
+    auto const owner = Account{"owner"};
+    auto const escrow = makeEscrow(owner, Account{"dest"});
+    ASSERT_NE(ledger.getOpenLedger().read(escrow), nullptr) << "escrow object should exist";
+
+    expectValue(
+        makeHost(escrow)->getCurrentLedgerObjField(sfAccount),
+        RealHostFixture::toBytes(owner.id()));
+}
+
+TEST_F(CurrentLedgerObjFieldImpl, ReadsfAccountDummyEscrow)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    auto const ownerSeq = ledger.getAccountRoot(owner.id()).getSequence();
+    auto const escrow = keylet::escrow(owner.id(), SeqProxy::rawSequence(ownerSeq));
+
+    expectError(
+        makeHost(escrow)->getCurrentLedgerObjField(sfAccount),
+        HostFunctionError::LedgerObjNotFound);
+}
+
+TEST_F(CurrentLedgerObjFieldImpl, ReadAmount)
+{
+    auto const owner = Account{"owner"};
+    auto const escrow = makeEscrow(owner, Account{"dest"});
+    ASSERT_NE(ledger.getOpenLedger().read(escrow), nullptr) << "escrow object should exist";
+
+    expectValue(
+        makeHost(escrow)->getCurrentLedgerObjField(sfAmount), RealHostFixture::toBytes(XRP(100)));
+}
+
+TEST_F(CurrentLedgerObjFieldImpl, ReadPreviousTxnID)
+{
+    auto const owner = Account{"owner"};
+    auto transactionId = uint256{};
+    auto const escrow = makeEscrow(owner, Account{"dest"}, &transactionId);
+    ASSERT_NE(ledger.getOpenLedger().read(escrow), nullptr) << "escrow object should exist";
+
+    expectValue(
+        makeHost(escrow)->getCurrentLedgerObjField(sfPreviousTxnID),
+        RealHostFixture::toBytes(transactionId));
+}
+
+TEST_F(CurrentLedgerObjFieldImpl, ReadOwner)
+{
+    auto const owner = Account{"owner"};
+    auto const escrow = makeEscrow(owner, Account{"dest"});
+    ASSERT_NE(ledger.getOpenLedger().read(escrow), nullptr) << "escrow object should exist";
+
+    expectError(
+        makeHost(escrow)->getCurrentLedgerObjField(sfOwner), HostFunctionError::FieldNotFound);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/CurrentLedgerObjNestedArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_functions/CurrentLedgerObjNestedArrayLen.cpp
new file mode 100644
index 0000000000..40ce10c841
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/CurrentLedgerObjNestedArrayLen.cpp
@@ -0,0 +1,62 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct CurrentLedgerObjNestedArrayLenImpl : RealHostFixture
+{
+    using RealHostFixture::makeHost;
+
+    WasmHost
+    makeHost(Account const& acct)
+    {
+        makeSignerList(acct, 2, {{Account{"alice"}, 1}, {Account{"becky"}, 1}});
+        auto assembler = bareTx();
+        return makeHost(keylet::signerList(acct.id()), assembler.type, std::move(assembler.build));
+    }
+};
+
+TEST_F(CurrentLedgerObjNestedArrayLenImpl, SignerEntriesLength)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(h->getCurrentLedgerObjNestedArrayLen(FieldLocator{{sfSignerEntries.getCode()}}), 2);
+}
+
+TEST_F(CurrentLedgerObjNestedArrayLenImpl, NonArrayFieldNoArray)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getCurrentLedgerObjNestedArrayLen(FieldLocator{{sfSignerQuorum.getCode()}}),
+        HostFunctionError::NoArray);
+}
+
+TEST_F(CurrentLedgerObjNestedArrayLenImpl, MissingFieldNotFound)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getCurrentLedgerObjNestedArrayLen(FieldLocator{{sfSigners.getCode()}}),
+        HostFunctionError::FieldNotFound);
+}
+
+TEST_F(CurrentLedgerObjNestedArrayLenImpl, MissingCurrentObjectNotFound)
+{
+    auto const owner = fund("owner");
+    auto assembler = bareTx();
+    auto h = makeHost(keylet::signerList(owner.id()), assembler.type, std::move(assembler.build));
+    expectError(
+        h->getCurrentLedgerObjNestedArrayLen(FieldLocator{{sfSignerEntries.getCode()}}),
+        HostFunctionError::LedgerObjNotFound);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/CurrentLedgerObjNestedField.cpp b/src/tests/libxrpl/tx/wasm/host_functions/CurrentLedgerObjNestedField.cpp
new file mode 100644
index 0000000000..c1a9cab3da
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/CurrentLedgerObjNestedField.cpp
@@ -0,0 +1,123 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct CurrentLedgerObjNestedFieldImpl : RealHostFixture
+{
+    using RealHostFixture::makeHost;
+
+    WasmHost
+    makeHost(Account const& acct)
+    {
+        makeSignerList(acct, 2, {{Account{"alice"}, 1}, {Account{"becky"}, 1}});
+        auto assembler = bareTx();
+        return makeHost(keylet::signerList(acct.id()), assembler.type, std::move(assembler.build));
+    }
+};
+
+TEST_F(CurrentLedgerObjNestedFieldImpl, MatchesNestedSignerQuorum)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(
+        h->getCurrentLedgerObjNestedField(FieldLocator{{sfSignerQuorum.getCode()}}),
+        RealHostFixture::toBytes(static_cast(2)));
+}
+
+TEST_F(CurrentLedgerObjNestedFieldImpl, MatchesNestedSignerWeight)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(
+        h->getCurrentLedgerObjNestedField(
+            FieldLocator{{sfSignerEntries.getCode(), 0, sfSignerWeight.getCode()}}),
+        RealHostFixture::toBytes(static_cast(1)));
+}
+
+TEST_F(CurrentLedgerObjNestedFieldImpl, MatchesNestedSignerAccount)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+
+    auto const sle = ledger.getOpenLedger().read(keylet::signerList(owner.id()));
+    ASSERT_NE(sle, nullptr);
+    auto const& entry0 = sle->getFieldArray(sfSignerEntries)[0];
+
+    expectValue(
+        h->getCurrentLedgerObjNestedField(
+            FieldLocator{{sfSignerEntries.getCode(), 0, sfAccount.getCode()}}),
+        RealHostFixture::toBytes(entry0.getAccountID(sfAccount)));
+}
+
+TEST_F(CurrentLedgerObjNestedFieldImpl, MissingFieldNotFound)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getCurrentLedgerObjNestedField(
+            FieldLocator{{sfSigners.getCode(), 0, sfAccount.getCode()}}),
+        HostFunctionError::FieldNotFound);
+}
+
+TEST_F(CurrentLedgerObjNestedFieldImpl, IndexOutOfBounds)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    auto const err = HostFunctionError::IndexOutOfBounds;
+
+    expectError(
+        h->getCurrentLedgerObjNestedField(
+            FieldLocator{{sfSignerEntries.getCode(), 2, sfAccount.getCode()}}),
+        err);
+    expectError(
+        h->getCurrentLedgerObjNestedField(
+            FieldLocator{{sfSignerEntries.getCode(), -1, sfAccount.getCode()}}),
+        err);
+}
+
+TEST_F(CurrentLedgerObjNestedFieldImpl, UnknownFieldCodeInvalidField)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    auto const err = HostFunctionError::InvalidField;
+
+    expectError(h->getCurrentLedgerObjNestedField(FieldLocator{{fieldCode(20000, 20000)}}), err);
+    expectError(
+        h->getCurrentLedgerObjNestedField(
+            FieldLocator{{sfSignerEntries.getCode(), 0, fieldCode(20000, 20000)}}),
+        err);
+}
+
+TEST_F(CurrentLedgerObjNestedFieldImpl, NestIntoNonContainerMalformed)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getCurrentLedgerObjNestedField(
+            FieldLocator{{sfSignerQuorum.getCode(), 0, sfAccount.getCode()}}),
+        HostFunctionError::LocatorMalformed);
+}
+
+TEST_F(CurrentLedgerObjNestedFieldImpl, MissingCurrentObjectNotFound)
+{
+    auto const owner = fund("owner");
+    auto assembler = bareTx();
+    auto h = makeHost(keylet::signerList(owner.id()), assembler.type, std::move(assembler.build));
+    expectError(
+        h->getCurrentLedgerObjNestedField(FieldLocator{{sfSignerQuorum.getCode()}}),
+        HostFunctionError::LedgerObjNotFound);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/DelegateKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/DelegateKeylet.cpp
new file mode 100644
index 0000000000..afe148ed53
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/DelegateKeylet.cpp
@@ -0,0 +1,42 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct DelegateKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(DelegateKeyletImpl, MatchesDelegateKeyletFunction)
+{
+    auto const owner = fund("owner");
+    auto const delegate = fund("delegate");
+
+    expectKeyletMatches(
+        makeHost()->delegateKeylet(owner.id(), delegate.id()),
+        keylet::delegate(owner.id(), delegate.id()));
+}
+
+TEST_F(DelegateKeyletImpl, CantDelegateToSelf)
+{
+    auto const owner = fund("owner");
+
+    expectError(
+        makeHost()->delegateKeylet(owner.id(), owner.id()), HostFunctionError::InvalidParams);
+}
+
+TEST_F(DelegateKeyletImpl, InvalidAccount)
+{
+    auto const owner = fund("owner");
+
+    auto h = makeHost();
+    expectError(h->delegateKeylet(AccountID{}, owner.id()), HostFunctionError::InvalidAccount);
+    expectError(h->delegateKeylet(owner.id(), AccountID{}), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/DepositPreauthKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/DepositPreauthKeylet.cpp
new file mode 100644
index 0000000000..a7a3fec8c4
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/DepositPreauthKeylet.cpp
@@ -0,0 +1,44 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct DepositPreauthKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(DepositPreauthKeyletImpl, MatchesDepositPreauthKeyletFunction)
+{
+    auto const owner = fund("owner");
+    auto const destination = fund("destination");
+
+    expectKeyletMatches(
+        makeHost()->depositPreauthKeylet(owner.id(), destination.id()),
+        keylet::depositPreauth(owner.id(), destination.id()));
+}
+
+TEST_F(DepositPreauthKeyletImpl, CantPreauthToSelf)
+{
+    auto const owner = fund("owner");
+
+    expectError(
+        makeHost()->depositPreauthKeylet(owner.id(), owner.id()), HostFunctionError::InvalidParams);
+}
+
+TEST_F(DepositPreauthKeyletImpl, InvalidAccount)
+{
+    auto const owner = fund("owner");
+
+    auto h = makeHost();
+    expectError(
+        h->depositPreauthKeylet(AccountID{}, owner.id()), HostFunctionError::InvalidAccount);
+    expectError(
+        h->depositPreauthKeylet(owner.id(), AccountID{}), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/DidKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/DidKeylet.cpp
new file mode 100644
index 0000000000..5ba5a9585c
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/DidKeylet.cpp
@@ -0,0 +1,27 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct DidKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(DidKeyletImpl, MatchesDidKeyletFunction)
+{
+    auto const owner = fund("owner");
+
+    expectKeyletMatches(makeHost()->didKeylet(owner.id()), keylet::did(owner.id()));
+}
+
+TEST_F(DidKeyletImpl, InvalidAccount)
+{
+    expectError(makeHost()->didKeylet(AccountID{}), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/EscrowKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/EscrowKeylet.cpp
new file mode 100644
index 0000000000..e8bbaa1145
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/EscrowKeylet.cpp
@@ -0,0 +1,43 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct EscrowKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(EscrowKeyletImpl, MatchesLedgerKeyletFunction)
+{
+    auto const owner = Account{"owner"};
+    auto const seq = std::uint32_t{42};
+
+    expectKeyletMatches(
+        makeHost()->escrowKeylet(owner.id(), seq),
+        keylet::escrow(owner.id(), SeqProxy::rawSequence(seq)));
+}
+
+TEST_F(EscrowKeyletImpl, DifferentAccountsGiveDifferentKeylets)
+{
+    auto h = makeHost();
+    auto const a = h->escrowKeylet(Account{"alice"}.id(), 7);
+    auto const b = h->escrowKeylet(Account{"becky"}.id(), 7);
+
+    ASSERT_TRUE(a.has_value() && b.has_value());
+    EXPECT_NE(*a, *b);
+}
+
+TEST_F(EscrowKeyletImpl, UnsetAccountIsInvalidAccount)
+{
+    expectError(makeHost()->escrowKeylet(AccountID{}, 1), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatAdd.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatAdd.cpp
new file mode 100644
index 0000000000..c594950cad
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatAdd.cpp
@@ -0,0 +1,49 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct FloatAddImpl : FloatTest
+{
+};
+
+TEST_F(FloatAddImpl, BadModeIsMalformed)
+{
+    expectError(
+        makeHost()->floatAdd(slice(FloatTest::kOne), slice(FloatTest::kOne), -1),
+        HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatAddImpl, MalformedInput)
+{
+    expectError(
+        makeHost()->floatAdd(slice(FloatTest::kOne), Slice{}, 0),
+        HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatAddImpl, MaxIouPlusMaxExpIsMaxExp)
+{
+    expectValue(
+        makeHost()->floatAdd(slice(FloatTest::kMaxIOU), slice(FloatTest::kMaxExp), 0),
+        FloatTest::kMaxExp);
+}
+
+TEST_F(FloatAddImpl, MinPlusZeroIsMin)
+{
+    expectValue(
+        makeHost()->floatAdd(slice(FloatTest::kIntMin), slice(FloatTest::kIntZero), 0),
+        FloatTest::kIntMin);
+}
+
+TEST_F(FloatAddImpl, MaxPlusMinIsZero)
+{
+    expectValue(
+        makeHost()->floatAdd(slice(FloatTest::kIntMax), slice(FloatTest::kIntMin), 0),
+        FloatTest::kIntZero);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatCompare.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatCompare.cpp
new file mode 100644
index 0000000000..21acd3f915
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatCompare.cpp
@@ -0,0 +1,48 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct FloatCompareImpl : FloatTest
+{
+};
+
+TEST_F(FloatCompareImpl, MalformedInputs)
+{
+    // A wrong-size (here empty) buffer is malformed; the impl normalizes any well-formed
+    // 12-byte buffer, so size is the only rejection.
+    auto h = makeHost();
+    expectError(h->floatCompare(Slice{}, Slice{}), HostFunctionError::FloatInputMalformed);
+    expectError(
+        h->floatCompare(slice(FloatTest::kOne), Slice{}), HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatCompareImpl, Less)
+{
+    expectValue(makeHost()->floatCompare(slice(FloatTest::kIntMin), slice(FloatTest::kIntZero)), 2);
+}
+
+TEST_F(FloatCompareImpl, Greater)
+{
+    expectValue(makeHost()->floatCompare(slice(FloatTest::kIntMax), slice(FloatTest::kIntZero)), 1);
+}
+
+TEST_F(FloatCompareImpl, Equal)
+{
+    expectValue(makeHost()->floatCompare(slice(FloatTest::kOne), slice(FloatTest::kOne)), 0);
+}
+
+// A non-canonical encoding of 10 (mantissa 100000, exponent -4) is normalized on decode, so
+// it compares equal to the canonical 10 — the impl accepts any well-formed 12-byte buffer.
+TEST_F(FloatCompareImpl, NonCanonicalNormalizes)
+{
+    Bytes const nonCanonicalTen{
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x86, 0xA0, 0xFF, 0xFF, 0xFF, 0xFC};
+    expectValue(makeHost()->floatCompare(slice(nonCanonicalTen), slice(FloatTest::kTen)), 0);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatDivide.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatDivide.cpp
new file mode 100644
index 0000000000..8e48cfcafe
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatDivide.cpp
@@ -0,0 +1,73 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct FloatDivideImpl : FloatTest
+{
+};
+
+TEST_F(FloatDivideImpl, BadModeIsMalformed)
+{
+    expectError(
+        makeHost()->floatDivide(slice(FloatTest::kOne), slice(FloatTest::kOne), -1),
+        HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatDivideImpl, MalformedInput)
+{
+    expectError(
+        makeHost()->floatDivide(slice(FloatTest::kOne), Slice{}, 0),
+        HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatDivideImpl, DivideByZeroIsComputationError)
+{
+    expectError(
+        makeHost()->floatDivide(slice(FloatTest::kOne), slice(FloatTest::kIntZero), 0),
+        HostFunctionError::FloatComputationError);
+}
+
+TEST_F(FloatDivideImpl, OverflowIsComputationError)
+{
+    // A divisor just below 1, so max / it overflows.
+    auto h = makeHost();
+    auto const y = h->floatFromMantExp(STAmount::kMaxValue, -FloatTest::kNormalExp - 1, 0);
+    ASSERT_TRUE(y.has_value());
+    expectError(
+        h->floatDivide(slice(FloatTest::kMax), slice(*y), 0),
+        HostFunctionError::FloatComputationError);
+}
+
+TEST_F(FloatDivideImpl, ZeroDividedByOneIsZero)
+{
+    expectValue(
+        makeHost()->floatDivide(slice(FloatTest::kIntZero), slice(FloatTest::kOne), 0),
+        FloatTest::kIntZero);
+}
+
+TEST_F(FloatDivideImpl, MaxExpDividedByTenIsPreMaxExp)
+{
+    expectValue(
+        makeHost()->floatDivide(slice(FloatTest::kMaxExp), slice(FloatTest::kTen), 0),
+        FloatTest::kPreMaxExp);
+}
+
+// The rounding mode changes an inexact result: 1/3 rounded Downward differs from Upward.
+TEST_F(FloatDivideImpl, RoundingModeAffectsInexactResult)
+{
+    auto h = makeHost();
+    auto const three = h->floatFromInt(3, 0);
+    ASSERT_TRUE(three.has_value());
+    auto const down = h->floatDivide(slice(FloatTest::kOne), slice(*three), 2);
+    auto const up = h->floatDivide(slice(FloatTest::kOne), slice(*three), 3);
+    ASSERT_TRUE(down.has_value() && up.has_value());
+    EXPECT_NE(*down, *up);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatFromInt.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatFromInt.cpp
new file mode 100644
index 0000000000..cc972cbde3
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatFromInt.cpp
@@ -0,0 +1,35 @@
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct FloatFromIntImpl : FloatTest
+{
+};
+
+TEST_F(FloatFromIntImpl, BadModeIsMalformed)
+{
+    auto h = makeHost();
+    expectError(h->floatFromInt(kMin64, -1), HostFunctionError::FloatInputMalformed);
+    expectError(h->floatFromInt(kMin64, 4), HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatFromIntImpl, MinInt)
+{
+    expectValue(makeHost()->floatFromInt(kMin64, 0), FloatTest::kIntMin);
+}
+
+TEST_F(FloatFromIntImpl, Zero)
+{
+    expectValue(makeHost()->floatFromInt(0, 0), FloatTest::kIntZero);
+}
+
+TEST_F(FloatFromIntImpl, MaxInt)
+{
+    expectValue(makeHost()->floatFromInt(kMax64, 0), FloatTest::kIntMax);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatFromMantExp.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatFromMantExp.cpp
new file mode 100644
index 0000000000..705c6b3fa1
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatFromMantExp.cpp
@@ -0,0 +1,69 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct FloatFromMantExpImpl : FloatTest
+{
+    static constexpr int kMaxRawExp = Number::kMaxExponent + FloatTest::kNormalExp;
+    static constexpr int kMinRawExp = Number::kMinExponent + FloatTest::kNormalExp;
+};
+
+TEST_F(FloatFromMantExpImpl, BadModeIsMalformed)
+{
+    auto h = makeHost();
+    expectError(h->floatFromMantExp(1, 0, -1), HostFunctionError::FloatInputMalformed);
+    expectError(h->floatFromMantExp(1, 0, 4), HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatFromMantExpImpl, ExponentTooHighIsMalformed)
+{
+    expectError(
+        makeHost()->floatFromMantExp(1, kMaxRawExp + 1, 0), HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatFromMantExpImpl, UnderflowIsZero)
+{
+    expectValue(makeHost()->floatFromMantExp(1, kMinRawExp - 1, 0), FloatTest::kIntZero);
+}
+
+TEST_F(FloatFromMantExpImpl, MaxExponent)
+{
+    expectValue(makeHost()->floatFromMantExp(1, kMaxRawExp, 0), FloatTest::kMaxExp);
+}
+
+TEST_F(FloatFromMantExpImpl, MinusMaxExponent)
+{
+    expectValue(makeHost()->floatFromMantExp(-1, kMaxRawExp, 0), FloatTest::kMinusMaxExp);
+}
+
+TEST_F(FloatFromMantExpImpl, PreMaxExponent)
+{
+    expectValue(makeHost()->floatFromMantExp(1, kMaxRawExp - 1, 0), FloatTest::kPreMaxExp);
+}
+
+TEST_F(FloatFromMantExpImpl, MaxIou)
+{
+    expectValue(
+        makeHost()->floatFromMantExp(STAmount::kMaxValue, STAmount::kMaxOffset, 0),
+        FloatTest::kMaxIOU);
+}
+
+TEST_F(FloatFromMantExpImpl, MinExponent)
+{
+    expectValue(
+        makeHost()->floatFromMantExp(1, Number::kMinExponent - FloatTest::kNormalExp, 0),
+        FloatTest::kMinExp);
+}
+
+TEST_F(FloatFromMantExpImpl, TenTimesTenthIsOne)
+{
+    expectValue(makeHost()->floatFromMantExp(10, -1, 0), FloatTest::kOne);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatFromStAmount.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatFromStAmount.cpp
new file mode 100644
index 0000000000..36666ba2d3
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatFromStAmount.cpp
@@ -0,0 +1,71 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct FloatFromStAmountImpl : FloatTest
+{
+    static Issue
+    usd()
+    {
+        return Issue{toCurrency("USD"), Account{"gw"}.id()};
+    }
+};
+
+TEST_F(FloatFromStAmountImpl, BadModeIsMalformed)
+{
+    auto h = makeHost();
+    auto const amount = STAmount{XRP(100)};
+    expectError(h->floatFromSTAmount(amount, -1), HostFunctionError::FloatInputMalformed);
+    expectError(h->floatFromSTAmount(amount, 4), HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatFromStAmountImpl, ZeroXrp)
+{
+    expectValue(makeHost()->floatFromSTAmount(STAmount{XRP(0)}, 0), FloatTest::kIntZero);
+}
+
+TEST_F(FloatFromStAmountImpl, MinusOneXrp)
+{
+    // -1 XRP == -1'000'000 drops.
+    auto h = makeHost();
+    auto const expected = h->floatFromMantExp(-1'000'000, 0, 0);
+    ASSERT_TRUE(expected.has_value());
+    expectValue(h->floatFromSTAmount(STAmount{XRP(-1)}, 0), *expected);
+}
+
+TEST_F(FloatFromStAmountImpl, MaxDrops)
+{
+    auto h = makeHost();
+    static constexpr int64_t kTestValue{9'223'372'036'854'776};
+    auto const expected = h->floatFromMantExp(kTestValue, 3, 0);
+    ASSERT_TRUE(expected.has_value());
+    expectValue(h->floatFromSTAmount(STAmount{noIssue(), kMax64}, 0), *expected);
+}
+
+TEST_F(FloatFromStAmountImpl, MinIou)
+{
+    auto const amount = STAmount{
+        IOUAmount{static_cast(STAmount::kMinValue), STAmount::kMinOffset}, usd()};
+    expectValue(makeHost()->floatFromSTAmount(amount, 0), FloatTest::kMinIOU);
+}
+
+TEST_F(FloatFromStAmountImpl, MaxIou)
+{
+    auto const amount = STAmount{
+        IOUAmount{static_cast(STAmount::kMaxValue), STAmount::kMaxOffset}, usd()};
+    expectValue(makeHost()->floatFromSTAmount(amount, 0), FloatTest::kMaxIOU);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatFromStNumber.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatFromStNumber.cpp
new file mode 100644
index 0000000000..40c9f30fa5
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatFromStNumber.cpp
@@ -0,0 +1,40 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct FloatFromStNumberImpl : FloatTest
+{
+};
+
+TEST_F(FloatFromStNumberImpl, BadModeIsMalformed)
+{
+    auto h = makeHost();
+    auto const n = STNumber{sfNumber, Number(123, 0)};
+    expectError(h->floatFromSTNumber(n, -1), HostFunctionError::FloatInputMalformed);
+    expectError(h->floatFromSTNumber(n, 4), HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatFromStNumberImpl, MaxUint)
+{
+    auto const n = STNumber{
+        sfNumber, Number(std::numeric_limits::max(), 0, Number::Normalized{})};
+    expectValue(makeHost()->floatFromSTNumber(n, 0), FloatTest::kUintMax);
+}
+
+TEST_F(FloatFromStNumberImpl, MinusMaxExponent)
+{
+    auto const n = STNumber{sfNumber, Number(-1, Number::kMaxExponent + FloatTest::kNormalExp)};
+    expectValue(makeHost()->floatFromSTNumber(n, 0), FloatTest::kMinusMaxExp);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatFromUint.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatFromUint.cpp
new file mode 100644
index 0000000000..c407831448
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatFromUint.cpp
@@ -0,0 +1,34 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct FloatFromUintImpl : FloatTest
+{
+    static constexpr std::uint64_t kMaxU64 = std::numeric_limits::max();
+};
+
+TEST_F(FloatFromUintImpl, BadModeIsMalformed)
+{
+    auto h = makeHost();
+    expectError(h->floatFromUint(0, -1), HostFunctionError::FloatInputMalformed);
+    expectError(h->floatFromUint(0, 4), HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatFromUintImpl, Zero)
+{
+    expectValue(makeHost()->floatFromUint(0, 0), FloatTest::kIntZero);
+}
+
+TEST_F(FloatFromUintImpl, MaxUint)
+{
+    expectValue(makeHost()->floatFromUint(kMaxU64, 0), FloatTest::kUintMax);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatMultiply.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatMultiply.cpp
new file mode 100644
index 0000000000..cb91211dd4
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatMultiply.cpp
@@ -0,0 +1,56 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct FloatMultiplyImpl : FloatTest
+{
+};
+
+TEST_F(FloatMultiplyImpl, BadModeIsMalformed)
+{
+    expectError(
+        makeHost()->floatMultiply(slice(FloatTest::kOne), slice(FloatTest::kOne), -1),
+        HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatMultiplyImpl, MalformedInput)
+{
+    expectError(
+        makeHost()->floatMultiply(slice(FloatTest::kOne), Slice{}, 0),
+        HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatMultiplyImpl, OverflowIsComputationError)
+{
+    expectError(
+        makeHost()->floatMultiply(slice(FloatTest::kMax), slice(FloatTest::kOneMore), 0),
+        HostFunctionError::FloatComputationError);
+}
+
+TEST_F(FloatMultiplyImpl, OneTimesOneIsOne)
+{
+    expectValue(
+        makeHost()->floatMultiply(slice(FloatTest::kOne), slice(FloatTest::kOne), 0),
+        FloatTest::kOne);
+}
+
+TEST_F(FloatMultiplyImpl, ZeroTimesMaxIouIsZero)
+{
+    expectValue(
+        makeHost()->floatMultiply(slice(FloatTest::kIntZero), slice(FloatTest::kMaxIOU), 0),
+        FloatTest::kIntZero);
+}
+
+TEST_F(FloatMultiplyImpl, TenTimesPreMaxExpIsMaxExp)
+{
+    expectValue(
+        makeHost()->floatMultiply(slice(FloatTest::kTen), slice(FloatTest::kPreMaxExp), 0),
+        FloatTest::kMaxExp);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatPower.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatPower.cpp
new file mode 100644
index 0000000000..44e01add62
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatPower.cpp
@@ -0,0 +1,75 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct FloatPowerImpl : FloatTest
+{
+};
+
+TEST_F(FloatPowerImpl, BadModeIsMalformed)
+{
+    expectError(
+        makeHost()->floatPower(slice(FloatTest::kOne), 2, -1),
+        HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatPowerImpl, MalformedInput)
+{
+    expectError(makeHost()->floatPower(Slice{}, 3, 0), HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatPowerImpl, NegativeDegreeIsMalformed)
+{
+    expectError(
+        makeHost()->floatPower(slice(FloatTest::kOne), -2, 0),
+        HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatPowerImpl, OverflowIsComputationError)
+{
+    expectError(
+        makeHost()->floatPower(slice(FloatTest::kMax), 2, 0),
+        HostFunctionError::FloatComputationError);
+}
+
+TEST_F(FloatPowerImpl, DegreeTooLargeIsMalformed)
+{
+    expectError(
+        makeHost()->floatPower(slice(FloatTest::kMax), Number::kMaxExponent + 1, 0),
+        HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatPowerImpl, DegreeZeroIsOne)
+{
+    expectValue(makeHost()->floatPower(slice(FloatTest::kMaxIOU), 0, 0), FloatTest::kOne);
+}
+
+TEST_F(FloatPowerImpl, DegreeOneIsIdentity)
+{
+    expectValue(makeHost()->floatPower(slice(FloatTest::kMaxIOU), 1, 0), FloatTest::kMaxIOU);
+}
+
+TEST_F(FloatPowerImpl, TenSquaredIsHundred)
+{
+    auto h = makeHost();
+    auto const hundred = h->floatFromMantExp(100, 0, 0);
+    ASSERT_TRUE(hundred.has_value());
+    expectValue(h->floatPower(slice(FloatTest::kTen), 2, 0), *hundred);
+}
+
+TEST_F(FloatPowerImpl, TenthSquaredIsHundredth)
+{
+    auto h = makeHost();
+    auto const tenth = h->floatFromMantExp(1, -1, 0);
+    auto const hundredth = h->floatFromMantExp(1, -2, 0);
+    ASSERT_TRUE(tenth.has_value() && hundredth.has_value());
+    expectValue(h->floatPower(slice(*tenth), 2, 0), *hundredth);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatSubtract.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatSubtract.cpp
new file mode 100644
index 0000000000..4e2a1d9d95
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatSubtract.cpp
@@ -0,0 +1,49 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct FloatSubtractImpl : FloatTest
+{
+};
+
+TEST_F(FloatSubtractImpl, BadModeIsMalformed)
+{
+    expectError(
+        makeHost()->floatSubtract(slice(FloatTest::kOne), slice(FloatTest::kOne), -1),
+        HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatSubtractImpl, MalformedInput)
+{
+    expectError(
+        makeHost()->floatSubtract(slice(FloatTest::kOne), Slice{}, 0),
+        HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatSubtractImpl, MinusMaxExpMinusMaxIouIsMinusMaxExp)
+{
+    expectValue(
+        makeHost()->floatSubtract(slice(FloatTest::kMinusMaxExp), slice(FloatTest::kMaxIOU), 0),
+        FloatTest::kMinusMaxExp);
+}
+
+TEST_F(FloatSubtractImpl, MinMinusZeroIsMin)
+{
+    expectValue(
+        makeHost()->floatSubtract(slice(FloatTest::kIntMin), slice(FloatTest::kIntZero), 0),
+        FloatTest::kIntMin);
+}
+
+TEST_F(FloatSubtractImpl, ZeroMinusOneIsMinusOne)
+{
+    expectValue(
+        makeHost()->floatSubtract(slice(FloatTest::kIntZero), slice(FloatTest::kOne), 0),
+        FloatTest::kMinusOne);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatToInt.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatToInt.cpp
new file mode 100644
index 0000000000..c8fbe6c54b
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatToInt.cpp
@@ -0,0 +1,70 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct FloatToIntImpl : FloatTest
+{
+};
+
+TEST_F(FloatToIntImpl, BadModeIsMalformed)
+{
+    auto h = makeHost();
+    expectError(h->floatToInt(slice(FloatTest::kOne), -1), HostFunctionError::FloatInputMalformed);
+    expectError(h->floatToInt(slice(FloatTest::kOne), 4), HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatToIntImpl, MalformedInputs)
+{
+    expectError(makeHost()->floatToInt(Slice{}, 0), HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatToIntImpl, Zero)
+{
+    expectValue(makeHost()->floatToInt(slice(FloatTest::kIntZero), 0), std::int64_t{0});
+}
+
+TEST_F(FloatToIntImpl, One)
+{
+    expectValue(makeHost()->floatToInt(slice(FloatTest::kOne), 0), std::int64_t{1});
+}
+
+TEST_F(FloatToIntImpl, MinusOne)
+{
+    expectValue(makeHost()->floatToInt(slice(FloatTest::kMinusOne), 0), std::int64_t{-1});
+}
+
+TEST_F(FloatToIntImpl, Max)
+{
+    expectValue(makeHost()->floatToInt(slice(FloatTest::kIntMax), 0), kMax64);
+}
+
+TEST_F(FloatToIntImpl, Min)
+{
+    // floatIntMin rounds to -(2^63-1), i.e. -kMax64.
+    expectValue(makeHost()->floatToInt(slice(FloatTest::kIntMin), 0), -kMax64);
+}
+
+TEST_F(FloatToIntImpl, OverflowsInt64IsComputationError)
+{
+    expectError(
+        makeHost()->floatToInt(slice(FloatTest::kUintMax), 0),
+        HostFunctionError::FloatComputationError);
+}
+
+TEST_F(FloatToIntImpl, PiRoundsByMode)
+{
+    auto h = makeHost();
+    expectValue(h->floatToInt(slice(FloatTest::kPi), 0), std::int64_t{3});  // ToNearest
+    expectValue(h->floatToInt(slice(FloatTest::kPi), 1), std::int64_t{3});  // TowardsZero
+    expectValue(h->floatToInt(slice(FloatTest::kPi), 2), std::int64_t{3});  // Downward
+    expectValue(h->floatToInt(slice(FloatTest::kPi), 3), std::int64_t{4});  // Upward
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/FloatToMantExp.cpp b/src/tests/libxrpl/tx/wasm/host_functions/FloatToMantExp.cpp
new file mode 100644
index 0000000000..5c8a89e5d0
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/FloatToMantExp.cpp
@@ -0,0 +1,80 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct FloatToMantExpImpl : FloatTest
+{
+    static constexpr std::int32_t kExpMin = std::numeric_limits::min();
+
+    static FloatPair
+    pair(std::int64_t mantissa, std::int32_t exponent)
+    {
+        return FloatPair{mantissa, exponent};
+    }
+};
+
+TEST_F(FloatToMantExpImpl, MalformedInput)
+{
+    expectError(makeHost()->floatToMantExp(Slice{}), HostFunctionError::FloatInputMalformed);
+}
+
+TEST_F(FloatToMantExpImpl, Zero)
+{
+    expectValue(makeHost()->floatToMantExp(slice(FloatTest::kIntZero)), pair(0, kExpMin));
+}
+
+TEST_F(FloatToMantExpImpl, One)
+{
+    expectValue(
+        makeHost()->floatToMantExp(slice(FloatTest::kOne)),
+        pair(1'000'000'000'000'000'000, -FloatTest::kNormalExp));
+}
+
+TEST_F(FloatToMantExpImpl, MinusOne)
+{
+    expectValue(
+        makeHost()->floatToMantExp(slice(FloatTest::kMinusOne)),
+        pair(-1'000'000'000'000'000'000, -FloatTest::kNormalExp));
+}
+
+TEST_F(FloatToMantExpImpl, Ten)
+{
+    expectValue(
+        makeHost()->floatToMantExp(slice(FloatTest::kTen)),
+        pair(1'000'000'000'000'000'000, -FloatTest::kNormalExp + 1));
+}
+
+TEST_F(FloatToMantExpImpl, Pi)
+{
+    expectValue(
+        makeHost()->floatToMantExp(slice(FloatTest::kPi)),
+        pair(3'141'592'653'589'793'000, -FloatTest::kNormalExp));
+}
+
+TEST_F(FloatToMantExpImpl, IntMax)
+{
+    expectValue(makeHost()->floatToMantExp(slice(FloatTest::kIntMax)), pair(kMax64, 0));
+}
+
+TEST_F(FloatToMantExpImpl, IntMin)
+{
+    expectValue(makeHost()->floatToMantExp(slice(FloatTest::kIntMin)), pair(-kMax64, 0));
+}
+
+TEST_F(FloatToMantExpImpl, Max)
+{
+    expectValue(
+        makeHost()->floatToMantExp(slice(FloatTest::kMax)),
+        pair(Number::kMaxRep, Number::kMaxExponent));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/GetNFT.cpp b/src/tests/libxrpl/tx/wasm/host_functions/GetNFT.cpp
new file mode 100644
index 0000000000..e6d152a06b
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/GetNFT.cpp
@@ -0,0 +1,54 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct GetNFTImpl : NFTTest
+{
+};
+
+TEST_F(GetNFTImpl, UnsetAccountIsInvalidAccount)
+{
+    auto const issuer = Account{"issuer"};
+    expectError(
+        makeHost()->getNFT(AccountID{}, makeNftId(issuer.id())), HostFunctionError::InvalidAccount);
+}
+
+TEST_F(GetNFTImpl, ZeroIdIsInvalidParams)
+{
+    auto const owner = fund("owner");
+    expectError(makeHost()->getNFT(owner.id(), uint256{}), HostFunctionError::InvalidParams);
+}
+
+TEST_F(GetNFTImpl, MissingTokenIsNotFound)
+{
+    auto const owner = fund("owner");
+    expectError(
+        makeHost()->getNFT(owner.id(), makeNftId(owner.id())),
+        HostFunctionError::LedgerObjNotFound);
+}
+
+TEST_F(GetNFTImpl, ReturnsUri)
+{
+    auto const owner = fund("owner");
+    auto const uri = std::string_view{"https://example.com/nft"};
+    auto const id = mintNFT(owner, uri);
+    expectValue(makeHost()->getNFT(owner.id(), id), RealHostFixture::toBytes(uri));
+}
+
+TEST_F(GetNFTImpl, WithoutUriFieldNotFound)
+{
+    auto const owner = fund("owner");
+    auto const id = mintNFT(owner);
+    expectError(makeHost()->getNFT(owner.id(), id), HostFunctionError::FieldNotFound);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/IsAmendmentEnabled.cpp b/src/tests/libxrpl/tx/wasm/host_functions/IsAmendmentEnabled.cpp
new file mode 100644
index 0000000000..8c9c34a0df
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/IsAmendmentEnabled.cpp
@@ -0,0 +1,45 @@
+#include 
+#include 
+
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct IsAmendmentEnabledImpl : RealHostFixture
+{
+};
+
+TEST_F(IsAmendmentEnabledImpl, EnabledAmendmentByIdReadsOne)
+{
+    auto const id = getRegisteredFeature("TokenEscrow");
+    ASSERT_TRUE(id.has_value());
+    auto const result = makeHost()->isAmendmentEnabled(id.value_or(uint256{}));
+    expectValue(result, 1);
+}
+
+TEST_F(IsAmendmentEnabledImpl, EnabledAmendmentByNameReadsOne)
+{
+    auto const result = makeHost()->isAmendmentEnabled(std::string_view{"TokenEscrow"});
+
+    expectValue(result, 1);
+}
+
+TEST_F(IsAmendmentEnabledImpl, UnknownAmendmentByIdReadsZero)
+{
+    auto const result = makeHost()->isAmendmentEnabled(
+        uint256{"DEADBEEF00000000000000000000000000000000000000000000000000000000"});
+
+    expectValue(result, 0);
+}
+
+TEST_F(IsAmendmentEnabledImpl, UnknownAmendmentNameReadsZero)
+{
+    auto const result = makeHost()->isAmendmentEnabled(std::string_view{"DEADBEEF"});
+
+    expectValue(result, 0);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/LedgerObjArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_functions/LedgerObjArrayLen.cpp
new file mode 100644
index 0000000000..b63dfa2657
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/LedgerObjArrayLen.cpp
@@ -0,0 +1,60 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct LedgerObjArrayLenImpl : RealHostFixture
+{
+    using RealHostFixture::makeHost;
+
+    WasmHost
+    makeHost(Account const& acct)
+    {
+        makeSignerList(acct, 2, {{Account{"alice"}, 1}, {Account{"becky"}, 1}});
+        auto assembler = bareTx();
+        auto h = makeHost(keylet::account(AccountID{}), assembler.type, std::move(assembler.build));
+        EXPECT_TRUE(h->cacheLedgerObj(keylet::signerList(acct.id()).key, 1).has_value());
+        return h;
+    }
+};
+
+TEST_F(LedgerObjArrayLenImpl, SignerEntriesLength)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(h->getLedgerObjArrayLen(1, sfSignerEntries), 2);
+}
+
+TEST_F(LedgerObjArrayLenImpl, NonArrayFieldNoArray)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(h->getLedgerObjArrayLen(1, sfAccount), HostFunctionError::NoArray);
+}
+
+TEST_F(LedgerObjArrayLenImpl, MissingArrayFieldNotFound)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(h->getLedgerObjArrayLen(1, sfMemos), HostFunctionError::FieldNotFound);
+}
+
+TEST_F(LedgerObjArrayLenImpl, SlotErrors)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(h->getLedgerObjArrayLen(0, sfSignerEntries), HostFunctionError::SlotOutRange);
+    expectError(h->getLedgerObjArrayLen(257, sfSignerEntries), HostFunctionError::SlotOutRange);
+    expectError(h->getLedgerObjArrayLen(2, sfSignerEntries), HostFunctionError::EmptySlot);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/LedgerObjField.cpp b/src/tests/libxrpl/tx/wasm/host_functions/LedgerObjField.cpp
new file mode 100644
index 0000000000..8deed246f5
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/LedgerObjField.cpp
@@ -0,0 +1,83 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct LedgerObjFieldImpl : RealHostFixture
+{
+    template 
+    void
+    checkCachedField(
+        Account const& acct,
+        std::uint32_t index,
+        SField const& field,
+        TxAssembler assembler,
+        Functor&& f)
+    {
+        auto const accountKeylet = keylet::account(acct.id());
+        auto h = makeHost(accountKeylet, assembler.type, std::move(assembler.build));
+        EXPECT_TRUE(h->cacheLedgerObj(accountKeylet.key, 1).has_value());
+        expectValue(h->getLedgerObjField(index, field), f());
+    }
+
+    void
+    checkCachedFieldError(
+        Account const& acct,
+        std::uint32_t index,
+        SField const& field,
+        TxAssembler assembler,
+        HostFunctionError error)
+    {
+        auto const accountKeylet = keylet::account(acct.id());
+        auto h = makeHost(accountKeylet, assembler.type, std::move(assembler.build));
+        EXPECT_TRUE(h->cacheLedgerObj(accountKeylet.key, 1).has_value());
+        expectError(h->getLedgerObjField(index, field), error);
+    }
+};
+
+TEST_F(LedgerObjFieldImpl, MatchesAccount)
+{
+    auto const owner = fund("owner");
+    checkCachedField(
+        owner, 1, sfAccount, bareTx(), [&] { return RealHostFixture::toBytes(owner.id()); });
+}
+
+TEST_F(LedgerObjFieldImpl, MatchesBalance)
+{
+    auto const owner = fund("owner");
+    auto const root = ledger.getOpenLedger().read(keylet::account(owner.id()));
+    checkCachedField(owner, 1, sfBalance, bareTx(), [&] {
+        return RealHostFixture::toBytes(root->getFieldAmount(sfBalance));
+    });
+}
+
+TEST_F(LedgerObjFieldImpl, MatchesAccountSlotOutOfRange)
+{
+    auto const owner = fund("owner");
+    checkCachedFieldError(owner, 0, sfAccount, bareTx(), HostFunctionError::SlotOutRange);
+    checkCachedFieldError(owner, 257, sfAccount, bareTx(), HostFunctionError::SlotOutRange);
+}
+
+TEST_F(LedgerObjFieldImpl, MatchesAccountEmptySlot)
+{
+    auto const owner = fund("owner");
+    checkCachedFieldError(owner, 2, sfAccount, bareTx(), HostFunctionError::EmptySlot);
+}
+
+TEST_F(LedgerObjFieldImpl, MatchesOwnerFieldNotFound)
+{
+    auto const owner = fund("owner");
+    checkCachedFieldError(owner, 1, sfOwner, bareTx(), HostFunctionError::FieldNotFound);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/LedgerObjNestedArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_functions/LedgerObjNestedArrayLen.cpp
new file mode 100644
index 0000000000..d7e2fa8939
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/LedgerObjNestedArrayLen.cpp
@@ -0,0 +1,80 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct LedgerObjNestedArrayLenImpl : RealHostFixture
+{
+    using RealHostFixture::makeHost;
+
+    WasmHost
+    makeHost(Account const& acct)
+    {
+        makeSignerList(acct, 2, {{Account{"alice"}, 1}, {Account{"becky"}, 1}});
+        auto assembler = bareTx();
+        auto h = makeHost(keylet::account(AccountID{}), assembler.type, std::move(assembler.build));
+        EXPECT_TRUE(h->cacheLedgerObj(keylet::signerList(acct.id()).key, 1).has_value());
+        return h;
+    }
+};
+
+TEST_F(LedgerObjNestedArrayLenImpl, SignerEntriesLength)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(h->getLedgerObjNestedArrayLen(1, FieldLocator{{sfSignerEntries.getCode()}}), 2);
+}
+
+TEST_F(LedgerObjNestedArrayLenImpl, NonArrayFieldNoArray)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getLedgerObjNestedArrayLen(1, FieldLocator{{sfSignerQuorum.getCode()}}),
+        HostFunctionError::NoArray);
+}
+
+TEST_F(LedgerObjNestedArrayLenImpl, MissingFieldNotFound)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getLedgerObjNestedArrayLen(1, FieldLocator{{sfSigners.getCode()}}),
+        HostFunctionError::FieldNotFound);
+}
+
+TEST_F(LedgerObjNestedArrayLenImpl, SlotErrors)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getLedgerObjNestedArrayLen(0, FieldLocator{{sfSignerEntries.getCode()}}),
+        HostFunctionError::SlotOutRange);
+    expectError(
+        h->getLedgerObjNestedArrayLen(257, FieldLocator{{sfSignerEntries.getCode()}}),
+        HostFunctionError::SlotOutRange);
+    expectError(
+        h->getLedgerObjNestedArrayLen(2, FieldLocator{{sfSignerEntries.getCode()}}),
+        HostFunctionError::EmptySlot);
+}
+
+TEST_F(LedgerObjNestedArrayLenImpl, NestIntoNonContainerMalformed)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getLedgerObjNestedArrayLen(
+            1, FieldLocator{{sfSignerQuorum.getCode(), 0, sfAccount.getCode()}}),
+        HostFunctionError::LocatorMalformed);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/LedgerObjNestedField.cpp b/src/tests/libxrpl/tx/wasm/host_functions/LedgerObjNestedField.cpp
new file mode 100644
index 0000000000..c8e5d844e7
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/LedgerObjNestedField.cpp
@@ -0,0 +1,156 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct LedgerObjNestedFieldImpl : RealHostFixture
+{
+    using RealHostFixture::makeHost;
+
+    WasmHost
+    makeHost(Account const& acct)
+    {
+        makeSignerList(acct, 2, {{Account{"alice"}, 1}, {Account{"becky"}, 1}});
+        auto assembler = bareTx();
+        auto h = makeHost(keylet::account(AccountID{}), assembler.type, std::move(assembler.build));
+        EXPECT_TRUE(h->cacheLedgerObj(keylet::signerList(acct.id()).key, 1).has_value());
+        return h;
+    }
+};
+
+TEST_F(LedgerObjNestedFieldImpl, MatchesNestedSignerAccountsByIndex)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+
+    auto const sle = ledger.getOpenLedger().read(keylet::signerList(owner.id()));
+    ASSERT_NE(sle, nullptr);
+    auto const& entries = sle->getFieldArray(sfSignerEntries);
+
+    expectValue(
+        h->getLedgerObjNestedField(
+            1, FieldLocator{{sfSignerEntries.getCode(), 0, sfAccount.getCode()}}),
+        RealHostFixture::toBytes(entries[0].getAccountID(sfAccount)));
+    expectValue(
+        h->getLedgerObjNestedField(
+            1, FieldLocator{{sfSignerEntries.getCode(), 1, sfAccount.getCode()}}),
+        RealHostFixture::toBytes(entries[1].getAccountID(sfAccount)));
+    EXPECT_NE(entries[0].getAccountID(sfAccount), entries[1].getAccountID(sfAccount));
+}
+
+TEST_F(LedgerObjNestedFieldImpl, MatchesNestedSignerWeight)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(
+        h->getLedgerObjNestedField(
+            1, FieldLocator{{sfSignerEntries.getCode(), 0, sfSignerWeight.getCode()}}),
+        RealHostFixture::toBytes(static_cast(1)));
+}
+
+TEST_F(LedgerObjNestedFieldImpl, MatchesBaseSignerQuorum)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(
+        h->getLedgerObjNestedField(1, FieldLocator{{sfSignerQuorum.getCode()}}),
+        RealHostFixture::toBytes(static_cast(2)));
+}
+
+TEST_F(LedgerObjNestedFieldImpl, MissingFieldNotFound)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    auto const err = HostFunctionError::FieldNotFound;
+
+    expectError(
+        h->getLedgerObjNestedField(1, FieldLocator{{sfSigners.getCode(), 0, sfAccount.getCode()}}),
+        err);
+    expectError(
+        h->getLedgerObjNestedField(
+            1, FieldLocator{{sfSignerEntries.getCode(), 0, sfDestination.getCode()}}),
+        err);
+}
+
+TEST_F(LedgerObjNestedFieldImpl, IndexOutOfBounds)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    auto const err = HostFunctionError::IndexOutOfBounds;
+
+    expectError(
+        h->getLedgerObjNestedField(
+            1, FieldLocator{{sfSignerEntries.getCode(), 2, sfAccount.getCode()}}),
+        err);
+    expectError(
+        h->getLedgerObjNestedField(
+            1, FieldLocator{{sfSignerEntries.getCode(), -1, sfAccount.getCode()}}),
+        err);
+}
+
+TEST_F(LedgerObjNestedFieldImpl, UnknownFieldCodeInvalidField)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    auto const err = HostFunctionError::InvalidField;
+
+    expectError(
+        h->getLedgerObjNestedField(
+            1, FieldLocator{{fieldCode(99999, 99999), 0, sfAccount.getCode()}}),
+        err);
+    expectError(
+        h->getLedgerObjNestedField(
+            1, FieldLocator{{sfSignerEntries.getCode(), 0, fieldCode(99999, 99999)}}),
+        err);
+}
+
+TEST_F(LedgerObjNestedFieldImpl, SlotErrors)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+
+    // 0 and 257 are outside the 1..256 slot range.
+    expectError(
+        h->getLedgerObjNestedField(0, FieldLocator{{sfSignerQuorum.getCode()}}),
+        HostFunctionError::SlotOutRange);
+    expectError(
+        h->getLedgerObjNestedField(257, FieldLocator{{sfSignerQuorum.getCode()}}),
+        HostFunctionError::SlotOutRange);
+    // Slot 2 is in range but nothing was cached there.
+    expectError(
+        h->getLedgerObjNestedField(2, FieldLocator{{sfSignerQuorum.getCode()}}),
+        HostFunctionError::EmptySlot);
+}
+
+TEST_F(LedgerObjNestedFieldImpl, ContainerWithoutIndexNotLeaf)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getLedgerObjNestedField(1, FieldLocator{{sfSignerEntries.getCode()}}),
+        HostFunctionError::NotLeafField);
+}
+
+TEST_F(LedgerObjNestedFieldImpl, NestIntoNonContainerMalformed)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getLedgerObjNestedField(
+            1, FieldLocator{{sfSignerQuorum.getCode(), 0, sfAccount.getCode()}}),
+        HostFunctionError::LocatorMalformed);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/LedgerSqn.cpp b/src/tests/libxrpl/tx/wasm/host_functions/LedgerSqn.cpp
new file mode 100644
index 0000000000..a88c81a586
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/LedgerSqn.cpp
@@ -0,0 +1,15 @@
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct LedgerSqnImpl : RealHostFixture
+{
+};
+
+TEST_F(LedgerSqnImpl, MatchesLedger)
+{
+    expectValue(makeHost()->getLedgerSqn(), ledger.getOpenLedger().header().seq);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/LoanBrokerKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/LoanBrokerKeylet.cpp
new file mode 100644
index 0000000000..3d0b028eac
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/LoanBrokerKeylet.cpp
@@ -0,0 +1,30 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct LoanBrokerKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(LoanBrokerKeyletImpl, MatchesLoanBrokerKeyletFunction)
+{
+    auto const owner = fund("owner");
+
+    expectKeyletMatches(
+        makeHost()->loanBrokerKeylet(owner.id(), 1u),
+        keylet::loanBroker(owner.id(), SeqProxy::rawSequence(1u)));
+}
+
+TEST_F(LoanBrokerKeyletImpl, InvalidAccount)
+{
+    expectError(makeHost()->loanBrokerKeylet(AccountID{}, 1u), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/LoanKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/LoanKeylet.cpp
new file mode 100644
index 0000000000..594a8d37ac
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/LoanKeylet.cpp
@@ -0,0 +1,32 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct LoanKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(LoanKeyletImpl, MatchesLoanKeyletFunction)
+{
+    Bytes const loanBrokerIdBytes{0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b,
+                                  0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
+                                  0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70};
+    uint256 const loanBrokerId = uint256::fromVoid(loanBrokerIdBytes.data());
+
+    expectKeyletMatches(
+        makeHost()->loanKeylet(loanBrokerId, 1u),
+        keylet::loan(loanBrokerId, SeqProxy::rawSequence(1u)));
+}
+
+TEST_F(LoanKeyletImpl, InvalidLoanBrokerId)
+{
+    expectError(makeHost()->loanKeylet(uint256{}, 1u), HostFunctionError::InvalidParams);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/MptokenIssuanceKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/MptokenIssuanceKeylet.cpp
new file mode 100644
index 0000000000..d2960a0747
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/MptokenIssuanceKeylet.cpp
@@ -0,0 +1,30 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct MptokenIssuanceKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(MptokenIssuanceKeyletImpl, MatchesMptokenIssuanceKeyletFunction)
+{
+    auto const owner = fund("owner");
+
+    expectKeyletMatches(
+        makeHost()->mptokenIssuanceKeylet(owner.id(), 1u),
+        keylet::mptokenIssuance(makeMptID(1u, owner.id())));
+}
+
+TEST_F(MptokenIssuanceKeyletImpl, InvalidAccount)
+{
+    expectError(
+        makeHost()->mptokenIssuanceKeylet(AccountID{}, 1u), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/MptokenKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/MptokenKeylet.cpp
new file mode 100644
index 0000000000..eb3544c2a5
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/MptokenKeylet.cpp
@@ -0,0 +1,40 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct MptokenKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(MptokenKeyletImpl, MatchesMptokenKeyletFunction)
+{
+    auto const owner = fund("owner");
+    auto const anotherAccount = fund("account");
+
+    auto const mpt = makeMptID(1u, owner.id());
+    expectKeyletMatches(
+        makeHost()->mptokenKeylet(mpt, anotherAccount.id()),
+        keylet::mptoken(mpt, anotherAccount.id()));
+}
+
+TEST_F(MptokenKeyletImpl, InvalidMpt)
+{
+    auto const owner = fund("owner");
+    expectError(makeHost()->mptokenKeylet(MPTID{}, owner.id()), HostFunctionError::InvalidParams);
+}
+
+TEST_F(MptokenKeyletImpl, InvalidAccount)
+{
+    auto const owner = fund("owner");
+
+    auto const mpt = makeMptID(1u, owner.id());
+    expectError(makeHost()->mptokenKeylet(mpt, AccountID{}), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/NFTFlags.cpp b/src/tests/libxrpl/tx/wasm/host_functions/NFTFlags.cpp
new file mode 100644
index 0000000000..c69871dc0d
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/NFTFlags.cpp
@@ -0,0 +1,26 @@
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct NFTFlagsImpl : NFTTest
+{
+};
+
+TEST_F(NFTFlagsImpl, FlagsDecodeFromId)
+{
+    auto const issuer = Account{"issuer"};
+    expectValue(makeHost()->getNFTFlags(makeNftId(issuer.id())), std::int32_t{kFlags});
+}
+
+TEST_F(NFTFlagsImpl, FlagsShouldBeZeroWithZeroNftId)
+{
+    expectValue(makeHost()->getNFTFlags(uint256{}), std::int32_t{});
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/NFTIssuer.cpp b/src/tests/libxrpl/tx/wasm/host_functions/NFTIssuer.cpp
new file mode 100644
index 0000000000..ef65a4fa4a
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/NFTIssuer.cpp
@@ -0,0 +1,29 @@
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct NFTIssuerImpl : NFTTest
+{
+};
+
+TEST_F(NFTIssuerImpl, IssuerDecodesFromId)
+{
+    auto const issuer = Account{"issuer"};
+    expectValue(
+        makeHost()->getNFTIssuer(makeNftId(issuer.id())), RealHostFixture::toBytes(issuer.id()));
+}
+
+TEST_F(NFTIssuerImpl, IssuerZeroIsInvalidParams)
+{
+    expectError(makeHost()->getNFTIssuer(makeNftId(AccountID{})), HostFunctionError::InvalidParams);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/NFTSequence.cpp b/src/tests/libxrpl/tx/wasm/host_functions/NFTSequence.cpp
new file mode 100644
index 0000000000..28cba45a68
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/NFTSequence.cpp
@@ -0,0 +1,26 @@
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct NFTSequenceImpl : NFTTest
+{
+};
+
+TEST_F(NFTSequenceImpl, SequenceDecodesFromId)
+{
+    auto const issuer = Account{"issuer"};
+    expectValue(makeHost()->getNFTSequence(makeNftId(issuer.id())), kSequence);
+}
+
+TEST_F(NFTSequenceImpl, SequenceShouldBeZeroWithZeroNftId)
+{
+    expectValue(makeHost()->getNFTSequence(uint256{}), std::int32_t{});
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/NFTTaxon.cpp b/src/tests/libxrpl/tx/wasm/host_functions/NFTTaxon.cpp
new file mode 100644
index 0000000000..3b7641d9e1
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/NFTTaxon.cpp
@@ -0,0 +1,19 @@
+
+#include 
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct NFTTaxonImpl : NFTTest
+{
+};
+
+TEST_F(NFTTaxonImpl, TaxonDecodesFromId)
+{
+    auto const issuer = Account{"issuer"};
+    expectValue(makeHost()->getNFTTaxon(makeNftId(issuer.id())), kTaxon);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/NFTTransferFee.cpp b/src/tests/libxrpl/tx/wasm/host_functions/NFTTransferFee.cpp
new file mode 100644
index 0000000000..e69eee0192
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/NFTTransferFee.cpp
@@ -0,0 +1,26 @@
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct NFTTransferFeeImpl : NFTTest
+{
+};
+
+TEST_F(NFTTransferFeeImpl, TransferFeeDecodesFromId)
+{
+    auto const issuer = Account{"issuer"};
+    expectValue(makeHost()->getNFTTransferFee(makeNftId(issuer.id())), std::int32_t{kFee});
+}
+
+TEST_F(NFTTransferFeeImpl, TransferFeeShouldBeZeroWithZeroNftId)
+{
+    expectValue(makeHost()->getNFTTransferFee(uint256{}), std::int32_t{});
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/NftokenOfferKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/NftokenOfferKeylet.cpp
new file mode 100644
index 0000000000..18e669e949
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/NftokenOfferKeylet.cpp
@@ -0,0 +1,30 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct NftokenOfferKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(NftokenOfferKeyletImpl, MatchesNftokenOfferFunction)
+{
+    auto const owner = fund("owner");
+
+    expectKeyletMatches(
+        makeHost()->nftokenOfferKeylet(owner.id(), 1u),
+        keylet::nftokenOffer(owner.id(), SeqProxy::rawSequence(1u)));
+}
+
+TEST_F(NftokenOfferKeyletImpl, InvalidAccount)
+{
+    expectError(makeHost()->nftokenOfferKeylet(AccountID{}, 1u), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/OfferKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/OfferKeylet.cpp
new file mode 100644
index 0000000000..82737c9a11
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/OfferKeylet.cpp
@@ -0,0 +1,30 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct OfferKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(OfferKeyletImpl, MatchesOfferFunction)
+{
+    auto const owner = fund("owner");
+
+    expectKeyletMatches(
+        makeHost()->offerKeylet(owner.id(), 1u),
+        keylet::offer(owner.id(), SeqProxy::rawSequence(1u)));
+}
+
+TEST_F(OfferKeyletImpl, InvalidAccount)
+{
+    expectError(makeHost()->offerKeylet(AccountID{}, 1u), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/OracleKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/OracleKeylet.cpp
new file mode 100644
index 0000000000..69b0792fe4
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/OracleKeylet.cpp
@@ -0,0 +1,27 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct OracleKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(OracleKeyletImpl, MatchesOracleFunction)
+{
+    auto const owner = fund("owner");
+
+    expectKeyletMatches(makeHost()->oracleKeylet(owner.id(), 1u), keylet::oracle(owner.id(), 1u));
+}
+
+TEST_F(OracleKeyletImpl, InvalidAccount)
+{
+    expectError(makeHost()->oracleKeylet(AccountID{}, 1u), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/ParentLedgerHash.cpp b/src/tests/libxrpl/tx/wasm/host_functions/ParentLedgerHash.cpp
new file mode 100644
index 0000000000..d5b39bde46
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/ParentLedgerHash.cpp
@@ -0,0 +1,15 @@
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct ParentLedgerHashImpl : RealHostFixture
+{
+};
+
+TEST_F(ParentLedgerHashImpl, MatchesLedger)
+{
+    expectValue(makeHost()->getParentLedgerHash(), ledger.getOpenLedger().header().parentHash);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/ParentLedgerTime.cpp b/src/tests/libxrpl/tx/wasm/host_functions/ParentLedgerTime.cpp
new file mode 100644
index 0000000000..3abdef7916
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/ParentLedgerTime.cpp
@@ -0,0 +1,17 @@
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct ParentLedgerTimeImpl : RealHostFixture
+{
+};
+
+TEST_F(ParentLedgerTimeImpl, MatchesLedger)
+{
+    expectValue(
+        makeHost()->getParentLedgerTime(),
+        ledger.getOpenLedger().parentCloseTime().time_since_epoch().count());
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/PaychannelKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/PaychannelKeylet.cpp
new file mode 100644
index 0000000000..8695f51605
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/PaychannelKeylet.cpp
@@ -0,0 +1,47 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct PaychannelKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(PaychannelKeyletImpl, MatchesPaychannelFunction)
+{
+    auto const owner = fund("owner");
+    auto const destination = fund("destination");
+
+    expectKeyletMatches(
+        makeHost()->paychannelKeylet(owner.id(), destination.id(), 1u),
+        keylet::payChannel(owner.id(), destination.id(), SeqProxy::rawSequence(1u)));
+}
+
+TEST_F(PaychannelKeyletImpl, CantUseSelf)
+{
+    auto const owner = fund("owner");
+
+    expectError(
+        makeHost()->paychannelKeylet(owner.id(), owner.id(), 1u), HostFunctionError::InvalidParams);
+}
+
+TEST_F(PaychannelKeyletImpl, InvalidAccount)
+{
+    auto const owner = fund("owner");
+
+    auto h = makeHost();
+
+    expectError(
+        h->paychannelKeylet(AccountID{}, owner.id(), 1u), HostFunctionError::InvalidAccount);
+
+    expectError(
+        h->paychannelKeylet(owner.id(), AccountID{}, 1u), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/PermissionedDomainedKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/PermissionedDomainedKeylet.cpp
new file mode 100644
index 0000000000..fd659f24a7
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/PermissionedDomainedKeylet.cpp
@@ -0,0 +1,31 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct PermissionedDomainKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(PermissionedDomainKeyletImpl, MatchesPermissionedDomainFunction)
+{
+    auto const owner = fund("owner");
+
+    expectKeyletMatches(
+        makeHost()->permissionedDomainKeylet(owner.id(), 1u),
+        keylet::permissionedDomain(owner.id(), SeqProxy::rawSequence(1u)));
+}
+
+TEST_F(PermissionedDomainKeyletImpl, InvalidAccount)
+{
+    expectError(
+        makeHost()->permissionedDomainKeylet(AccountID{}, 1u), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/Sha512Half.cpp b/src/tests/libxrpl/tx/wasm/host_functions/Sha512Half.cpp
new file mode 100644
index 0000000000..7cf56f4550
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/Sha512Half.cpp
@@ -0,0 +1,21 @@
+#include 
+
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct Sha512HalfImpl : RealHostFixture
+{
+};
+
+TEST_F(Sha512HalfImpl, LogsMessageAndData)
+{
+    static constexpr auto data = std::string_view{"hello world"};
+    auto const result = makeHost()->computeSha512HalfHash({data.data(), data.size()});
+    expectValue(result, sha512Half(Slice{data.data(), data.size()}));
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/SignerListKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/SignerListKeylet.cpp
new file mode 100644
index 0000000000..4d640a7a25
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/SignerListKeylet.cpp
@@ -0,0 +1,27 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct SignerListKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(SignerListKeyletImpl, MatchesSignerListFunction)
+{
+    auto const owner = fund("owner");
+
+    expectKeyletMatches(makeHost()->signerListKeylet(owner.id()), keylet::signerList(owner.id()));
+}
+
+TEST_F(SignerListKeyletImpl, InvalidAccount)
+{
+    expectError(makeHost()->signerListKeylet(AccountID{}), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/SponsorshipKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/SponsorshipKeylet.cpp
new file mode 100644
index 0000000000..0c6d72457c
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/SponsorshipKeylet.cpp
@@ -0,0 +1,43 @@
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct SponsorshipKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(SponsorshipKeyletImpl, MatchesSponsorshipKeyletFunction)
+{
+    auto const sponsor = fund("sponsor");
+    auto const sponsee = fund("sponsee");
+
+    expectKeyletMatches(
+        makeHost()->sponsorshipKeylet(sponsor.id(), sponsee.id()),
+        keylet::sponsorship(sponsor.id(), sponsee.id()));
+}
+
+TEST_F(SponsorshipKeyletImpl, CantSponsorSelf)
+{
+    auto const sponsor = fund("sponsor");
+
+    expectError(
+        makeHost()->sponsorshipKeylet(sponsor.id(), sponsor.id()),
+        HostFunctionError::InvalidParams);
+}
+
+TEST_F(SponsorshipKeyletImpl, InvalidAccount)
+{
+    auto const sponsor = fund("sponsor");
+
+    auto h = makeHost();
+    expectError(h->sponsorshipKeylet(AccountID{}, sponsor.id()), HostFunctionError::InvalidAccount);
+    expectError(h->sponsorshipKeylet(sponsor.id(), AccountID{}), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/TicketKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/TicketKeylet.cpp
new file mode 100644
index 0000000000..93be84aff7
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/TicketKeylet.cpp
@@ -0,0 +1,30 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct TicketKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(TicketKeyletImpl, MatchesTicketFunction)
+{
+    auto const owner = fund("owner");
+
+    expectKeyletMatches(
+        makeHost()->ticketKeylet(owner.id(), 1u),
+        keylet::ticket(owner.id(), SeqProxy::rawTicket(1u)));
+}
+
+TEST_F(TicketKeyletImpl, InvalidAccount)
+{
+    expectError(makeHost()->ticketKeylet(AccountID{}, 1u), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/Trace.cpp b/src/tests/libxrpl/tx/wasm/host_functions/Trace.cpp
new file mode 100644
index 0000000000..0db8539c1d
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/Trace.cpp
@@ -0,0 +1,30 @@
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct TraceImpl : RealHostFixture
+{
+};
+
+TEST_F(TraceImpl, LogsMessageAndData)
+{
+    auto h = makeTracingHost();
+    h->trace("hello", "world");
+    EXPECT_NE(logged().find("hello world"), std::string::npos) << logged();
+}
+
+TEST_F(TraceImpl, NothingLoggedBelowTraceSeverity)
+{
+    CaptureSink sink{beast::Severity::Error};
+    auto h = makeHost(beast::Journal{sink});
+    h->trace("hello", "world");
+    EXPECT_TRUE(sink.messages().empty()) << sink.messages();
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/TrustLineKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/TrustLineKeylet.cpp
new file mode 100644
index 0000000000..c8c428eadf
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/TrustLineKeylet.cpp
@@ -0,0 +1,63 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct TrustlineKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(TrustlineKeyletImpl, MatchesTrustlineKeyletFunction)
+{
+    auto const owner = fund("owner");
+    auto const destination = fund("destination");
+
+    auto const usd = toCurrency("USD");
+
+    expectKeyletMatches(
+        makeHost()->trustLineKeylet(owner.id(), destination.id(), usd),
+        keylet::trustLine(owner.id(), destination.id(), usd));
+}
+
+TEST_F(TrustlineKeyletImpl, InvalidCurrency)
+{
+    auto const owner = fund("owner");
+    auto const destination = fund("destination");
+
+    expectError(
+        makeHost()->trustLineKeylet(owner.id(), destination.id(), toCurrency("")),
+        HostFunctionError::InvalidParams);
+}
+
+TEST_F(TrustlineKeyletImpl, CantTrustlineToSelf)
+{
+    auto const owner = fund("owner");
+
+    auto const usd = toCurrency("USD");
+
+    expectError(
+        makeHost()->trustLineKeylet(owner.id(), owner.id(), usd), HostFunctionError::InvalidParams);
+}
+
+TEST_F(TrustlineKeyletImpl, InvalidAccount)
+{
+    auto const owner = fund("owner");
+
+    auto const usd = toCurrency("USD");
+
+    auto h = makeHost();
+
+    expectError(
+        h->trustLineKeylet(AccountID{}, owner.id(), usd), HostFunctionError::InvalidAccount);
+
+    expectError(
+        h->trustLineKeylet(owner.id(), AccountID{}, usd), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/TxArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_functions/TxArrayLen.cpp
new file mode 100644
index 0000000000..97e5d1d8b9
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/TxArrayLen.cpp
@@ -0,0 +1,63 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct TxArrayLenImpl : RealHostFixture
+{
+    using RealHostFixture::makeHost;
+
+    WasmHost
+    makeHost(Account const& acct)
+    {
+        auto assembler = escrowFinishTx(ledger, acct);
+        assembler.build = [inner = std::move(assembler.build)](STObject& obj) {
+            inner(obj);
+            auto memos = STArray{};
+            memos.push_back(makeMemo(RealHostFixture::toBytes("hello")));
+            memos.push_back(makeMemo(RealHostFixture::toBytes("world")));
+            obj.setFieldArray(sfMemos, memos);
+        };
+        return makeHost(keylet::account(acct.id()), assembler.type, std::move(assembler.build));
+    }
+};
+
+TEST_F(TxArrayLenImpl, MemosLength)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(h->getTxArrayLen(sfMemos), 2);
+}
+
+TEST_F(TxArrayLenImpl, CredentialIdsLength)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(h->getTxArrayLen(sfCredentialIDs), 1);
+}
+
+TEST_F(TxArrayLenImpl, NonArrayFieldNoArray)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(h->getTxArrayLen(sfAccount), HostFunctionError::NoArray);
+}
+
+TEST_F(TxArrayLenImpl, MissingArrayFieldNotFound)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(h->getTxArrayLen(sfSigners), HostFunctionError::FieldNotFound);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/TxField.cpp b/src/tests/libxrpl/tx/wasm/host_functions/TxField.cpp
new file mode 100644
index 0000000000..c0dd8efdc7
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/TxField.cpp
@@ -0,0 +1,172 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct TxFieldImpl : RealHostFixture
+{
+    template 
+    void
+    checkTxField(Account const& acct, SField const& field, TxAssembler assembler, Functor&& f)
+    {
+        auto h = makeHost(keylet::account(acct.id()), assembler.type, std::move(assembler.build));
+        expectValue(h->getTxField(field), f());
+    }
+
+    void
+    checkTxFieldError(
+        Account const& acct,
+        SField const& field,
+        TxAssembler assembler,
+        HostFunctionError error)
+    {
+        auto h = makeHost(keylet::account(acct.id()), assembler.type, std::move(assembler.build));
+        expectError(h->getTxField(field), error);
+    }
+};
+
+TEST_F(TxFieldImpl, MPTokenIssuanceCreateTxMatchesScale)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    auto const expectedScale = std::uint8_t{8};
+    checkTxField(owner, sfAssetScale, mptIssuanceCreateTx(owner, expectedScale), [&] {
+        return RealHostFixture::toBytes(expectedScale);
+    });
+}
+
+TEST_F(TxFieldImpl, AmmDepositTxUSDMatchesAsset)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    auto usdIssue = Issue{toCurrency("USD"), owner.id()};
+    checkTxField(
+        owner, sfAsset, ammDepositTx(owner, xrpIssue(), usdIssue), [&] { return Bytes(20, 0); });
+}
+
+TEST_F(TxFieldImpl, AmmDepositTxUSDMatchesAsset2)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    auto usdIssue = Issue{toCurrency("USD"), owner.id()};
+    checkTxField(owner, sfAsset2, ammDepositTx(owner, xrpIssue(), usdIssue), [&] {
+        return RealHostFixture::toBytes(Asset{usdIssue});
+    });
+}
+
+TEST_F(TxFieldImpl, AmmDepositTxGBPMatchesAsset)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    auto gbpIssue = Issue{toCurrency("GBP"), owner.id()};
+    auto mptId = makeMptID(1, owner);
+    auto mptIssue = MPTIssue{mptId};
+    checkTxField(owner, sfAsset, ammDepositTx(owner, gbpIssue, mptIssue), [&] {
+        return RealHostFixture::toBytes(Asset{gbpIssue});
+    });
+}
+
+TEST_F(TxFieldImpl, AmmDepositTxGBPMatchesAsset2)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    auto gbpIssue = Issue{toCurrency("GBP"), owner.id()};
+    auto mptId = makeMptID(1, owner);
+    auto mptIssue = MPTIssue{mptId};
+    checkTxField(owner, sfAsset2, ammDepositTx(owner, gbpIssue, mptIssue), [&] {
+        return RealHostFixture::toBytes(Asset{mptId});
+    });
+}
+
+TEST_F(TxFieldImpl, EscrowTxMatchesAccount)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    checkTxField(owner, sfAccount, escrowFinishTx(ledger, owner), [&] {
+        return Bytes{std::begin(owner.id()), std::end(owner.id())};
+    });
+}
+
+TEST_F(TxFieldImpl, EscrowTxMatchesOwner)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    checkTxField(owner, sfOwner, escrowFinishTx(ledger, owner), [&] {
+        return Bytes{std::begin(owner.id()), std::end(owner.id())};
+    });
+}
+
+TEST_F(TxFieldImpl, EscrowTxMatchesTransactionType)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    checkTxField(owner, sfTransactionType, escrowFinishTx(ledger, owner), [] {
+        return RealHostFixture::toBytes(ttESCROW_FINISH);
+    });
+}
+
+TEST_F(TxFieldImpl, EscrowTxMatchesOfferSequence)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    checkTxField(owner, sfOfferSequence, escrowFinishTx(ledger, owner), [&] {
+        return RealHostFixture::toBytes(ledger.getAccountRoot(owner.id()).getSequence());
+    });
+}
+
+TEST_F(TxFieldImpl, EscrowTxMatchesDestination)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    checkTxFieldError(
+        owner, sfDestination, escrowFinishTx(ledger, owner), HostFunctionError::FieldNotFound);
+}
+
+TEST_F(TxFieldImpl, EscrowTxMatchesMemos)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    checkTxFieldError(
+        owner, sfMemos, escrowFinishTx(ledger, owner), HostFunctionError::NotLeafField);
+}
+
+TEST_F(TxFieldImpl, EscrowTxMatchesCredentialIDs)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    checkTxFieldError(
+        owner, sfCredentialIDs, escrowFinishTx(ledger, owner), HostFunctionError::NotLeafField);
+}
+
+TEST_F(TxFieldImpl, EscrowTxMatchesInvalid)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    checkTxFieldError(
+        owner, sfInvalid, escrowFinishTx(ledger, owner), HostFunctionError::FieldNotFound);
+}
+
+TEST_F(TxFieldImpl, EscrowTxMatchesGeneric)
+{
+    auto const owner = Account{"owner"};
+    ledger.createAccount(owner, XRP(1000));
+    checkTxFieldError(
+        owner, sfGeneric, escrowFinishTx(ledger, owner), HostFunctionError::FieldNotFound);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/TxNestedArrayLen.cpp b/src/tests/libxrpl/tx/wasm/host_functions/TxNestedArrayLen.cpp
new file mode 100644
index 0000000000..f697ceeed3
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/TxNestedArrayLen.cpp
@@ -0,0 +1,65 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+
+struct TxNestedArrayLenImpl : RealHostFixture
+{
+    using RealHostFixture::makeHost;
+
+    WasmHost
+    makeHost(Account const& acct)
+    {
+        auto assembler = escrowFinishTx(ledger, acct);
+        assembler.build = [inner = std::move(assembler.build)](STObject& obj) {
+            inner(obj);
+            auto memos = STArray{};
+            memos.push_back(makeMemo(RealHostFixture::toBytes("hello")));
+            obj.setFieldArray(sfMemos, memos);
+        };
+        return makeHost(keylet::account(acct.id()), assembler.type, std::move(assembler.build));
+    }
+};
+
+TEST_F(TxNestedArrayLenImpl, MemosLength)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(h->getTxNestedArrayLen(FieldLocator{{sfMemos.getCode()}}), 1);
+}
+
+TEST_F(TxNestedArrayLenImpl, CredentialIdsLength)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(h->getTxNestedArrayLen(FieldLocator{{sfCredentialIDs.getCode()}}), 1);
+}
+
+TEST_F(TxNestedArrayLenImpl, NonArrayFieldNoArray)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getTxNestedArrayLen(FieldLocator{{sfAccount.getCode()}}), HostFunctionError::NoArray);
+}
+
+TEST_F(TxNestedArrayLenImpl, MissingFieldNotFound)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getTxNestedArrayLen(FieldLocator{{sfSigners.getCode()}}),
+        HostFunctionError::FieldNotFound);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/TxNestedField.cpp b/src/tests/libxrpl/tx/wasm/host_functions/TxNestedField.cpp
new file mode 100644
index 0000000000..d801d46f44
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/TxNestedField.cpp
@@ -0,0 +1,137 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct TxNestedFieldImpl : RealHostFixture
+{
+    TxAssembler
+    assemble(Account const& acct)
+    {
+        auto assembler = escrowFinishTx(ledger, acct);
+        assembler.build = [inner = std::move(assembler.build)](STObject& obj) {
+            inner(obj);
+            auto memos = STArray{};
+            auto memo = STObject::makeInnerObject(sfMemo);
+            memo.setFieldVL(sfMemoData, Slice{"hello", 5});
+            memos.push_back(std::move(memo));
+            obj.setFieldArray(sfMemos, memos);
+        };
+        return assembler;
+    }
+
+    using RealHostFixture::makeHost;
+
+    WasmHost
+    makeHost(Account const& acct)
+    {
+        auto assembler = assemble(acct);
+        return makeHost(keylet::account(acct.id()), assembler.type, std::move(assembler.build));
+    }
+};
+
+TEST_F(TxNestedFieldImpl, MatchesNestedMemo)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(
+        h->getTxNestedField(FieldLocator{{sfMemos.getCode(), 0, sfMemoData.getCode()}}),
+        RealHostFixture::toBytes("hello"));
+}
+
+TEST_F(TxNestedFieldImpl, MatchesCredId)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(
+        h->getTxNestedField(FieldLocator{{sfCredentialIDs.getCode(), 0}}),
+        RealHostFixture::toBytes(credentialId()));
+}
+
+TEST_F(TxNestedFieldImpl, MatchesBaseFieldViaNestedLocator)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectValue(
+        h->getTxNestedField(FieldLocator{{sfAccount.getCode()}}),
+        RealHostFixture::toBytes(owner.id()));
+}
+
+TEST_F(TxNestedFieldImpl, MissingFieldNotFound)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    auto const err = HostFunctionError::FieldNotFound;
+
+    expectError(
+        h->getTxNestedField(FieldLocator{{sfSigners.getCode(), 0, sfAccount.getCode()}}), err);
+    expectError(h->getTxNestedField(FieldLocator{{sfMemos.getCode(), 0, sfURI.getCode()}}), err);
+    expectError(h->getTxNestedField(FieldLocator{{sfMemos.getCode(), 0, -1}}), err);
+    expectError(h->getTxNestedField(FieldLocator{{-1, 0, sfAccount.getCode()}}), err);
+    expectError(h->getTxNestedField(FieldLocator{{0, 0, sfAccount.getCode()}}), err);
+}
+
+TEST_F(TxNestedFieldImpl, IndexOutOfBounds)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    auto const err = HostFunctionError::IndexOutOfBounds;
+
+    expectError(
+        h->getTxNestedField(FieldLocator{{sfMemos.getCode(), 1, sfMemoData.getCode()}}), err);
+    expectError(h->getTxNestedField(FieldLocator{{sfCredentialIDs.getCode(), 1}}), err);
+    expectError(
+        h->getTxNestedField(FieldLocator{{sfMemos.getCode(), -1, sfMemoData.getCode()}}), err);
+    expectError(h->getTxNestedField(FieldLocator{{sfCredentialIDs.getCode(), -1}}), err);
+}
+
+TEST_F(TxNestedFieldImpl, UnknownFieldCodeInvalidField)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    auto const err = HostFunctionError::InvalidField;
+
+    expectError(
+        h->getTxNestedField(FieldLocator{{fieldCode(20000, 20000), 0, sfAccount.getCode()}}), err);
+    expectError(
+        h->getTxNestedField(FieldLocator{{sfMemos.getCode(), 0, fieldCode(20000, 20000)}}), err);
+    // Far-negative code: not in the SField map at all.
+    expectError(
+        h->getTxNestedField(
+            FieldLocator{{std::numeric_limits::min(), 0, sfAccount.getCode()}}),
+        err);
+}
+
+TEST_F(TxNestedFieldImpl, ContainerWithoutIndexNotLeaf)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    auto const err = HostFunctionError::NotLeafField;
+
+    expectError(h->getTxNestedField(FieldLocator{{sfMemos.getCode()}}), err);
+    expectError(h->getTxNestedField(FieldLocator{{sfCredentialIDs.getCode()}}), err);
+}
+
+TEST_F(TxNestedFieldImpl, NestIntoNonContainerMalformed)
+{
+    auto const owner = fund("owner");
+    auto h = makeHost(owner);
+    expectError(
+        h->getTxNestedField(FieldLocator{{sfAccount.getCode(), 0, sfAccount.getCode()}}),
+        HostFunctionError::LocatorMalformed);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/UpdateData.cpp b/src/tests/libxrpl/tx/wasm/host_functions/UpdateData.cpp
new file mode 100644
index 0000000000..295956d6fb
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/UpdateData.cpp
@@ -0,0 +1,30 @@
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct UpdateDataImpl : RealHostFixture
+{
+};
+
+TEST_F(UpdateDataImpl, SmallData)
+{
+    auto h = makeHost();
+    auto data = Bytes(10, 0x42);
+    expectValue(h->updateData(Slice{data.data(), data.size()}), data.size());
+    // TODO: getData() does not seem to be called when the smart escrow finishes.
+    EXPECT_EQ(h->getData(), data);
+}
+
+TEST_F(UpdateDataImpl, LargeData)
+{
+    auto h = makeHost();
+    auto data = Bytes(kMaxWasmDataLength + 1, 0x42);
+    expectError(
+        h->updateData(Slice{data.data(), data.size()}), HostFunctionError::DataFieldTooLarge);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/host_functions/VaultKeylet.cpp b/src/tests/libxrpl/tx/wasm/host_functions/VaultKeylet.cpp
new file mode 100644
index 0000000000..1026ed448e
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/host_functions/VaultKeylet.cpp
@@ -0,0 +1,30 @@
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+
+struct VaultKeyletImpl : RealHostFixture
+{
+};
+
+TEST_F(VaultKeyletImpl, MatchesVaultFunction)
+{
+    auto const owner = fund("owner");
+
+    expectKeyletMatches(
+        makeHost()->vaultKeylet(owner.id(), 1u),
+        keylet::vault(owner.id(), SeqProxy::rawSequence(1u)));
+}
+
+TEST_F(VaultKeyletImpl, InvalidAccount)
+{
+    expectError(makeHost()->vaultKeylet(AccountID{}, 1u), HostFunctionError::InvalidAccount);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/transactor/BytecodePreflight.cpp b/src/tests/libxrpl/tx/wasm/transactor/BytecodePreflight.cpp
new file mode 100644
index 0000000000..428cdf6e08
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/transactor/BytecodePreflight.cpp
@@ -0,0 +1,198 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+
+namespace xrpl::test {
+namespace {
+
+// What `EscrowCreate` refuses before a contract ever runs. Everything here is decided in
+// preflight, so the shape of the transaction is the whole subject: no ledger state matters
+// beyond the account existing.
+struct BytecodePreflight : testing::Test
+{
+    Account const alice{"alice"};
+    Account const carol{"carol"};
+
+    // An `EscrowCreate` with everything a valid one needs, for callers to spoil one field at
+    // a time. `cancelAfter` is set because without an expiry the transaction is refused for
+    // that reason first, and every bytecode case would report `temBAD_EXPIRATION` instead of
+    // what it meant to check.
+    transactions::EscrowCreateBuilder
+    escrowCreate(TxTest const& env, Bytes const& bytecode)
+    {
+        auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(500)}};
+        builder.setBytecode(makeSlice(bytecode));
+        builder.setCancelAfter(closeTimeOffset(env, 100));
+        return builder;
+    }
+};
+
+TEST_F(BytecodePreflight, BytecodeIsRefusedWhileSmartEscrowIsDisabled)
+{
+    auto env = TxTest{allFeatures() - featureSmartEscrow};
+    createAccounts(env, XRP(5'000), alice, carol);
+
+    auto const wasm = assembleWat(kReadsLedgerSqn);
+    auto const fee = escrowCreateFee(env, wasm);
+
+    EXPECT_EQ(env.submit(escrowCreate(env, wasm), alice, fee).ter, temDISABLED);
+
+    // Also with a data field, which is the other half of the feature's surface.
+    auto builder = escrowCreate(env, wasm);
+    builder.setData(makeSlice(Bytes{0x00, 0x11, 0x22, 0x33}));
+    EXPECT_EQ(env.submit(builder, alice, fee).ter, temDISABLED);
+}
+
+// A zero limit is how fee voting turns the runtime off, and it has to be distinguishable
+// from "your contract is too big" — `temTEMP_DISABLED` says come back later, `temMALFORMED`
+// says never.
+TEST_F(BytecodePreflight, AZeroSizeLimitDisablesUploadsRatherThanRejectingThem)
+{
+    auto fees = TestServiceRegistry::defaultFees();
+    fees.bytecodeSizeLimit = 0;
+    auto env = TxTest{std::nullopt, fees};
+    createAccounts(env, XRP(5'000), alice, carol);
+
+    auto const wasm = assembleWat(kReadsLedgerSqn);
+    EXPECT_EQ(
+        env.submit(escrowCreate(env, wasm), alice, escrowCreateFee(env, wasm)).ter,
+        temTEMP_DISABLED);
+}
+
+TEST_F(BytecodePreflight, AZeroGasLimitDisablesUploads)
+{
+    auto fees = TestServiceRegistry::defaultFees();
+    fees.gasLimit = 0;
+    auto env = TxTest{std::nullopt, fees};
+    createAccounts(env, XRP(5'000), alice, carol);
+
+    auto const wasm = assembleWat(kReadsLedgerSqn);
+    EXPECT_EQ(
+        env.submit(escrowCreate(env, wasm), alice, escrowCreateFee(env, wasm)).ter,
+        temTEMP_DISABLED);
+}
+
+TEST_F(BytecodePreflight, EmptyBytecodeIsRefused)
+{
+    auto env = TxTest{};
+    createAccounts(env, XRP(5'000), alice, carol);
+
+    EXPECT_EQ(
+        env.submit(escrowCreate(env, Bytes{}), alice, escrowCreateFee(env, Bytes{})).ter,
+        temMALFORMED);
+}
+
+// Screening reaches into the module: this one is structurally valid wasm that asks for a
+// host function nobody serves.
+TEST_F(BytecodePreflight, BytecodeImportingAnUnknownHostFunctionIsRefused)
+{
+    auto env = TxTest{};
+    createAccounts(env, XRP(5'000), alice, carol);
+
+    auto const wasm = assembleWat(kImportsUnknownHostFunction);
+    EXPECT_EQ(
+        env.submit(escrowCreate(env, wasm), alice, escrowCreateFee(env, wasm)).ter,
+        temINVALID_BYTECODE);
+}
+
+TEST_F(BytecodePreflight, DataWithoutBytecodeIsRefused)
+{
+    auto env = TxTest{};
+    createAccounts(env, XRP(5'000), alice, carol);
+
+    auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(500)}};
+    builder.setData(makeSlice(Bytes{0x41, 0x41, 0x41, 0x41}));
+    builder.setCancelAfter(closeTimeOffset(env, 100));
+
+    EXPECT_EQ(env.submit(builder, alice, XRPAmount{100'000}).ter, temMALFORMED);
+}
+
+TEST_F(BytecodePreflight, DataPastItsMaximumIsRefused)
+{
+    auto env = TxTest{};
+    createAccounts(env, XRP(5'000), alice, carol);
+
+    auto const wasm = assembleWat(kReadsLedgerSqn);
+    auto builder = escrowCreate(env, wasm);
+    builder.setData(makeSlice(Bytes(kMaxWasmDataLength + 1, 0x42)));
+
+    EXPECT_EQ(env.submit(builder, alice, XRPAmount{100'000}).ter, temMALFORMED);
+}
+
+// A contract needs a deadline. Without `CancelAfter` the escrow could never be reclaimed if
+// the contract never approves, so every combination lacking it is refused — including the
+// ones that look complete because they carry a `FinishAfter` or a condition.
+TEST_F(BytecodePreflight, BytecodeWithoutACancelTimeIsRefused)
+{
+    auto env = TxTest{};
+    createAccounts(env, XRP(5'000), alice, carol);
+
+    auto const wasm = assembleWat(kReadsLedgerSqn);
+    auto const fee = escrowCreateFee(env, wasm);
+
+    auto bare = [&] {
+        auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(500)}};
+        builder.setBytecode(makeSlice(wasm));
+        return builder;
+    };
+
+    EXPECT_EQ(env.submit(bare(), alice, fee).ter, temBAD_EXPIRATION);
+
+    auto withFinish = bare();
+    withFinish.setFinishAfter(closeTimeOffset(env, 2));
+    EXPECT_EQ(env.submit(withFinish, alice, fee).ter, temBAD_EXPIRATION);
+}
+
+TEST_F(BytecodePreflight, BytecodeWithACancelTimeIsAccepted)
+{
+    auto env = TxTest{};
+    createAccounts(env, XRP(5'000), alice, carol);
+
+    auto const wasm = assembleWat(kReadsLedgerSqn);
+    EXPECT_EQ(
+        env.submit(escrowCreate(env, wasm), alice, escrowCreateFee(env, wasm)).ter, tesSUCCESS);
+}
+
+TEST_F(BytecodePreflight, BytecodeWithAFinishAndCancelTimeIsAccepted)
+{
+    auto env = TxTest{};
+    createAccounts(env, XRP(5'000), alice, carol);
+
+    auto const wasm = assembleWat(kReadsLedgerSqn);
+    auto builder = escrowCreate(env, wasm);
+    builder.setFinishAfter(closeTimeOffset(env, 2));
+
+    EXPECT_EQ(env.submit(builder, alice, escrowCreateFee(env, wasm)).ter, tesSUCCESS);
+}
+
+// The per-byte charge is enforced, not advisory. One drop short is refused — which also
+// confirms the fee helper the other tests rely on is computing the real number rather than
+// something merely generous.
+TEST_F(BytecodePreflight, AFeeOneDropShortIsRefused)
+{
+    TxTest env;
+    createAccounts(env, XRP(5'000), alice, carol);
+
+    auto const wasm = assembleWat(kReadsLedgerSqn);
+    auto const fee = escrowCreateFee(env, wasm);
+
+    EXPECT_EQ(env.submit(escrowCreate(env, wasm), alice, fee - XRPAmount{1}).ter, telINSUF_FEE_P);
+}
+
+}  // namespace
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/transactor/BytecodeRun.cpp b/src/tests/libxrpl/tx/wasm/transactor/BytecodeRun.cpp
new file mode 100644
index 0000000000..d58d20aaac
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/transactor/BytecodeRun.cpp
@@ -0,0 +1,192 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+namespace {
+
+// A contract deciding whether an escrow releases, end to end through the transactor. The
+// other transactor files are about refusals; this one is about the feature working.
+
+constexpr std::uint32_t kAllowance = 10'000;
+
+// A preimage-sha256 pair, copied from jtx because `src/test/jtx` is not linked here.
+constexpr auto kFulfillment = std::array{{0xA0, 0x02, 0x80, 0x00}};
+constexpr auto kCondition = std::array{
+    {0xA0, 0x25, 0x80, 0x20, 0xE3, 0xB0, 0xC4, 0x42, 0x98, 0xFC, 0x1C, 0x14, 0x9A,
+     0xFB, 0xF4, 0xC8, 0x99, 0x6F, 0xB9, 0x24, 0x27, 0xAE, 0x41, 0xE4, 0x64, 0x9B,
+     0x93, 0x4C, 0xA4, 0x95, 0x99, 0x1B, 0x78, 0x52, 0xB8, 0x55, 0x81, 0x01, 0x00}};
+
+struct BytecodeRun : testing::Test
+{
+    TxTest env;
+    Account const alice{"alice"};
+    Account const carol{"carol"};
+
+    BytecodeRun()
+    {
+        createAccounts(env, XRP(5'000), alice, carol);
+    }
+
+    std::uint32_t
+    currentSeq() const
+    {
+        return env.getOpenLedger().header().seq;
+    }
+
+    struct Created
+    {
+        std::uint32_t seq;
+        XRPAmount fee;
+    };
+
+    Created
+    createEscrow(Bytes const& wasm, bool withCondition = false)
+    {
+        auto const seq = env.getAccountRoot(alice).getSequence();
+
+        auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(1'000)}};
+        builder.setBytecode(makeSlice(wasm));
+        builder.setCancelAfter(closeTimeOffset(env, 1'000));
+        if (withCondition)
+        {
+            builder.setCondition(makeSlice(kCondition));
+        }
+
+        auto const fee = escrowCreateFee(env, wasm);
+        EXPECT_EQ(env.submit(builder, alice, fee).ter, tesSUCCESS);
+        env.close();
+        return Created{.seq = seq, .fee = fee};
+    }
+
+    [[nodiscard]] ClosedResult
+    finish(std::uint32_t seq, bool withFulfillment = false)
+    {
+        auto builder = transactions::EscrowFinishBuilder{carol, alice, seq};
+        builder.setGas(kAllowance);
+
+        auto fee = escrowFinishFee(env, kAllowance);
+        if (withFulfillment)
+        {
+            builder.setCondition(makeSlice(kCondition));
+            builder.setFulfillment(makeSlice(kFulfillment));
+            fee += env.getOpenLedger().fees().base * (32 + (kFulfillment.size() / 16));
+        }
+
+        return env.submitAndClose(builder, carol, fee);
+    }
+
+    bool
+    escrowExists(std::uint32_t seq) const
+    {
+        return env.getOpenLedger().read(keylet::escrow(alice, SeqProxy::rawSequence(seq))) !=
+            nullptr;
+    }
+};
+
+// The whole point: it refuses while its predicate is false and releases once the ledger
+// makes it true, with nothing resubmitted differently.
+TEST_F(BytecodeRun, AContractRejectsUntilItsConditionHoldsThenReleases)
+{
+    auto const threshold = currentSeq() + 3;
+    auto const wasm = assembleWat(gatedOnLedgerSqn(threshold));
+    auto const created = createEscrow(wasm);
+
+    ASSERT_LT(currentSeq(), threshold);
+    auto const rejected = finish(created.seq);
+    EXPECT_EQ(rejected.ter, tecBYTECODE_REJECTED);
+    EXPECT_TRUE(escrowExists(created.seq)) << "a rejected escrow must survive";
+
+    ASSERT_TRUE(rejected.meta.has_value());
+    // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+    EXPECT_EQ(rejected.meta->getAsObject().getFieldI32(sfVMReturnCode), 0);
+
+    while (currentSeq() < threshold)
+    {
+        env.close();
+    }
+
+    auto const approved = finish(created.seq);
+    EXPECT_EQ(approved.ter, tesSUCCESS);
+    EXPECT_FALSE(escrowExists(created.seq)) << "an approved escrow must be destroyed";
+
+    ASSERT_TRUE(approved.meta.has_value());
+    // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+    auto const meta = approved.meta->getAsObject();
+    EXPECT_EQ(meta.getFieldI32(sfVMReturnCode), 5);
+    EXPECT_TRUE(meta.isFieldPresent(sfGasUsed));
+}
+
+TEST_F(BytecodeRun, TheBytecodeReserveIsHeldWhileTheEscrowLivesAndReleasedWhenItGoes)
+{
+    EXPECT_EQ(env.getOwnerCount(alice), 0U);
+
+    auto const threshold = currentSeq() + 2;
+    auto const wasm = assembleWat(gatedOnLedgerSqn(threshold));
+    auto const created = createEscrow(wasm);
+
+    // `calculateAdditionalReserve`: one increment for the escrow, plus one per 500 bytes.
+    auto const expected = 1U + static_cast(wasm.size() / 500);
+    EXPECT_EQ(env.getOwnerCount(alice), expected);
+
+    while (currentSeq() < threshold)
+    {
+        env.close();
+    }
+    ASSERT_EQ(finish(created.seq).ter, tesSUCCESS);
+
+    EXPECT_EQ(env.getOwnerCount(alice), 0U);
+}
+
+TEST_F(BytecodeRun, CreatingChargesTheAmountAndTheFee)
+{
+    auto const before = env.getXrpBalance(alice);
+
+    auto const wasm = assembleWat(gatedOnLedgerSqn(currentSeq() + 2));
+    auto const created = createEscrow(wasm);
+
+    EXPECT_EQ(env.getXrpBalance(alice), before - XRP(1'000) - created.fee);
+    EXPECT_EQ(env.getXrpBalance(carol), XRP(5'000));
+}
+
+// The condition is the outer gate: without a fulfillment the contract is never reached, even
+// though it would have approved.
+TEST_F(BytecodeRun, AConditionIsCheckedBeforeTheContractRuns)
+{
+    auto const threshold = currentSeq() + 2;
+    auto const wasm = assembleWat(gatedOnLedgerSqn(threshold));
+    auto const created = createEscrow(wasm, /*withCondition*/ true);
+
+    while (currentSeq() < threshold)
+    {
+        env.close();
+    }
+
+    EXPECT_EQ(finish(created.seq).ter, tecCRYPTOCONDITION_ERROR);
+    EXPECT_TRUE(escrowExists(created.seq));
+
+    auto const approved = finish(created.seq, /*withFulfillment*/ true);
+    EXPECT_EQ(approved.ter, tesSUCCESS);
+    EXPECT_FALSE(escrowExists(created.seq));
+}
+
+}  // namespace
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/transactor/BytecodeSize.cpp b/src/tests/libxrpl/tx/wasm/transactor/BytecodeSize.cpp
new file mode 100644
index 0000000000..c9cbd05cc9
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/transactor/BytecodeSize.cpp
@@ -0,0 +1,122 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+namespace {
+
+TER
+createEscrowWith(TxTest& env, Account const& account, Bytes const& bytecode)
+{
+    auto builder = transactions::EscrowCreateBuilder{account, account, STAmount{XRP(1'000)}};
+    builder.setBytecode(makeSlice(bytecode));
+    builder.setCancelAfter(closeTimeOffset(env, 100));
+
+    return env.submit(builder, account, escrowCreateFee(env, bytecode)).ter;
+}
+
+// Rich enough for the owner reserve a 200 KB contract demands: 401 increments, 802 XRP.
+Account
+fundedAccount(TxTest& env)
+{
+    auto const alice = Account{"alice"};
+    env.createAccount(alice, XRP(2'000'000));
+    return alice;
+}
+
+}  // namespace
+
+// The transactor screens `sfBytecode` against `bytecodeSizeLimit` before the module reaches
+// the engine. Compilation is unmetered, so that limit is the only thing bounding it.
+
+// Footing for the rest: without this, "too big" and "malformed" are indistinguishable.
+TEST(BytecodeSize, TheBuildersProduceAModuleTheEngineAccepts)
+{
+    auto env = TxTest{};
+    auto const alice = fundedAccount(env);
+
+    EXPECT_EQ(createEscrowWith(env, alice, codeHeavyModule(1'000)), tesSUCCESS);
+    EXPECT_EQ(createEscrowWith(env, alice, dataHeavyModule(1'000)), tesSUCCESS);
+}
+
+TEST(BytecodeSize, AModuleUnderTheLimitIsAccepted)
+{
+    auto env = TxTest{};
+    auto const alice = fundedAccount(env);
+
+    auto const wasm = codeHeavyModule(90'000);
+    ASSERT_LT(wasm.size(), env.getOpenLedger().fees().bytecodeSizeLimit);
+
+    EXPECT_EQ(createEscrowWith(env, alice, wasm), tesSUCCESS);
+}
+
+TEST(BytecodeSize, AModuleOverTheLimitIsRefused)
+{
+    auto env = TxTest{};
+    auto const alice = fundedAccount(env);
+
+    auto const wasm = codeHeavyModule(110'000);
+    ASSERT_GT(wasm.size(), env.getOpenLedger().fees().bytecodeSizeLimit);
+
+    EXPECT_EQ(createEscrowWith(env, alice, wasm), temMALFORMED);
+}
+
+// The limit is on the module, not the code section — moving the bulk into a data segment
+// does not walk around it.
+TEST(BytecodeSize, ADataSegmentCountsTowardTheLimit)
+{
+    auto env = TxTest{};
+    auto const alice = fundedAccount(env);
+
+    auto const wasm = dataHeavyModule(110'000);
+    ASSERT_GT(wasm.size(), env.getOpenLedger().fees().bytecodeSizeLimit);
+
+    EXPECT_EQ(createEscrowWith(env, alice, wasm), temMALFORMED);
+}
+
+// The limit is a fee setting, so it moves. If raising it admits nothing new, some other cap
+// is really in charge.
+TEST(BytecodeSize, RaisingTheLimitAdmitsALargerModule)
+{
+    auto fees = TestServiceRegistry::defaultFees();
+    fees.bytecodeSizeLimit = kMaxBytecodeSizeLimit;
+    auto env = TxTest{std::nullopt, fees};
+    auto const alice = fundedAccount(env);
+
+    auto const wasm = codeHeavyModule(150'000);
+    ASSERT_GT(wasm.size(), TestServiceRegistry::defaultFees().bytecodeSizeLimit);
+    ASSERT_LT(wasm.size(), kMaxBytecodeSizeLimit);
+
+    EXPECT_EQ(createEscrowWith(env, alice, wasm), tesSUCCESS);
+}
+
+// No per-function limit sits below the module limit: one function may occupy the entire
+// module. `wasmparser` defines `MAX_WASM_FUNCTION_SIZE` = 128 KiB, but nothing on this path
+// appears to enforce it — a lone body of a million instructions is accepted.
+//
+// So `bytecodeSizeLimit` is not defence in depth; it is the only bound on how much there is
+// to compile, and raising it raises the worst case with nothing behind it. A failure here
+// means a second limit has appeared, and that reasoning needs revisiting.
+TEST(BytecodeSize, ASingleFunctionBodyIsNotSeparatelyCapped)
+{
+    auto const wasm = codeHeavyModule(1'000'000);
+    ASSERT_GT(wasm.size(), 128U * 1024U) << "the module must exceed MAX_WASM_FUNCTION_SIZE";
+
+    EXPECT_EQ(preflightEscrowWasm(wasm, beast::Journal{beast::Journal::getNullSink()}), tesSUCCESS);
+}
+
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/transactor/DataOnReject.cpp b/src/tests/libxrpl/tx/wasm/transactor/DataOnReject.cpp
new file mode 100644
index 0000000000..b2ecb6298d
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/transactor/DataOnReject.cpp
@@ -0,0 +1,122 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+namespace {
+
+// Writes "Data" and then rejects. `set_data` stores through the host, and the `-256` return
+// puts `EscrowFinish` on its `reValue <= 0` path — a contract-defined rejection, distinct
+// from a fault. The escrow survives, so whether the write survives with it is the question.
+constexpr auto kWritesThenRejects = std::string_view{R"wat(
+(module
+  (import "host_lib" "set_data" (func $set_data (param i32 i32) (result i32)))
+  (memory (export "memory") 1)
+  (data (i32.const 0) "Data")
+  (func (export "escrow_finish") (result i32)
+    (drop (call $set_data (i32.const 0) (i32.const 4)))
+    (i32.const -256)))
+)wat"};
+
+constexpr std::int32_t kRejectCode = -256;
+
+// Enough to run the contract; the test is about persistence, not budgets.
+constexpr std::uint32_t kAllowance = 100'000;
+
+struct DataOnReject : testing::Test
+{
+    TxTest env;
+    Account const alice{"alice"};
+    std::uint32_t escrowSeq{};
+
+    void
+    SetUp() override
+    {
+        testing::Test::SetUp();
+        env.createAccount(alice, XRP(5'000));
+
+        auto const wasm = assembleWat(kWritesThenRejects);
+        escrowSeq = env.getAccountRoot(alice).getSequence();
+
+        auto builder = transactions::EscrowCreateBuilder{alice, alice, STAmount{XRP(1'000)}};
+        builder.setBytecode(makeSlice(wasm));
+        builder.setCancelAfter(closeTimeOffset(env, 1'000));
+
+        ASSERT_EQ(env.submit(builder, alice, escrowCreateFee(env, wasm)).ter, tesSUCCESS);
+        env.close();
+    }
+
+    [[nodiscard]] ClosedResult
+    finish()
+    {
+        auto builder = transactions::EscrowFinishBuilder{alice, alice, escrowSeq};
+        builder.setGas(kAllowance);
+
+        return env.submitAndClose(builder, alice, escrowFinishFee(env, kAllowance));
+    }
+};
+
+// The point of the whole shape: a contract that rejects can still leave a record of why.
+// `EscrowFinish` writes `sfData` *before* returning `tecBYTECODE_REJECTED`, and a `tec`
+// keeps its ledger changes, so the escrow survives carrying what the contract wrote.
+TEST_F(DataOnReject, ARejectingContractStillPersistsItsData)
+{
+    auto const result = finish();
+    EXPECT_EQ(result.ter, tecBYTECODE_REJECTED);
+
+    auto const sle =
+        env.getOpenLedger().read(keylet::escrow(alice, SeqProxy::rawSequence(escrowSeq)));
+    ASSERT_NE(sle, nullptr) << "a rejected finish must leave the escrow in place";
+    ASSERT_TRUE(sle->isFieldPresent(sfData));
+
+    auto const data = sle->getFieldVL(sfData);
+    EXPECT_EQ(std::string(data.begin(), data.end()), "Data") << strHex(data);
+}
+
+// The reject code reaches the metadata, which is the only way a client learns *which*
+// rejection it was — every contract-defined reject shares one TER.
+TEST_F(DataOnReject, TheRejectCodeIsReportedInTheMetadata)
+{
+    auto const result = finish();
+    ASSERT_TRUE(result.meta.has_value());
+    // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+    auto const meta = result.meta->getAsObject();
+    ASSERT_TRUE(meta.isFieldPresent(sfVMReturnCode));
+
+    EXPECT_EQ(meta.getFieldI32(sfVMReturnCode), kRejectCode);
+}
+
+// Gas is reported even though the run ended in a rejection: the engine has a trustworthy
+// number whenever the contract ran to completion, and a reject is a completed run.
+TEST_F(DataOnReject, GasIsChargedAndReportedForARejectedRun)
+{
+    auto const result = finish();
+    ASSERT_TRUE(result.meta.has_value());
+    // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+    auto const meta = result.meta->getAsObject();
+    ASSERT_TRUE(meta.isFieldPresent(sfGasUsed));
+
+    auto const used = meta.getFieldU32(sfGasUsed);
+    EXPECT_GT(used, 0U);
+    EXPECT_LE(used, kAllowance) << "the engine cannot spend more than it was given";
+}
+
+}  // namespace
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/transactor/FinishFailures.cpp b/src/tests/libxrpl/tx/wasm/transactor/FinishFailures.cpp
new file mode 100644
index 0000000000..f9b06d426b
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/transactor/FinishFailures.cpp
@@ -0,0 +1,198 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+namespace xrpl::test {
+namespace {
+
+// The ways an `EscrowFinish` against a contract-bearing escrow fails, and what each reports.
+// A rejection, a fault, and running out of gas are three outcomes; only one carries a return
+// code.
+struct FinishFailures : testing::Test
+{
+    TxTest env;
+    Account const alice{"alice"};
+    Account const carol{"carol"};
+
+    FinishFailures()
+    {
+        createAccounts(env, XRP(5'000), alice, carol);
+    }
+
+    std::uint32_t
+    createEscrow(std::string_view wat)
+    {
+        auto const wasm = assembleWat(wat);
+        auto const seq = env.getAccountRoot(alice).getSequence();
+
+        auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(500)}};
+        builder.setBytecode(makeSlice(wasm));
+        builder.setCancelAfter(closeTimeOffset(env, 1'000));
+
+        EXPECT_EQ(env.submit(builder, alice, escrowCreateFee(env, wasm)).ter, tesSUCCESS);
+        env.close();
+        return seq;
+    }
+
+    std::uint32_t
+    createPlainEscrow()
+    {
+        auto const seq = env.getAccountRoot(alice).getSequence();
+
+        auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(500)}};
+        // A contract-free escrow needs a `FinishAfter` or a condition; `CancelAfter` alone is
+        // `temMALFORMED`.
+        builder.setFinishAfter(closeTimeOffset(env, 1));
+        builder.setCancelAfter(closeTimeOffset(env, 1'000));
+
+        EXPECT_EQ(env.submit(builder, alice, XRPAmount{100'000}).ter, tesSUCCESS);
+        env.close();
+        env.close();  // past the finish time
+        return seq;
+    }
+
+    [[nodiscard]] ClosedResult
+    finish(std::uint32_t seq, std::optional allowance, XRPAmount fee)
+    {
+        auto builder = transactions::EscrowFinishBuilder{carol, alice, seq};
+        if (allowance)
+        {
+            builder.setGas(*allowance);
+        }
+
+        return env.submitAndClose(builder, carol, fee);
+    }
+};
+
+TEST_F(FinishFailures, FinishIsRefusedWhileSmartEscrowIsDisabled)
+{
+    auto disabled = TxTest{allFeatures() - featureSmartEscrow};
+    createAccounts(disabled, XRP(5'000), alice, carol);
+
+    auto builder = transactions::EscrowFinishBuilder{carol, alice, 1};
+    builder.setGas(4);
+
+    EXPECT_EQ(disabled.submit(builder, carol, XRPAmount{100'000}).ter, temDISABLED);
+}
+
+// Execution cannot be bought unbounded just by asking for it.
+TEST_F(FinishFailures, AnAllowancePastTheGasLimitIsRefused)
+{
+    auto fees = TestServiceRegistry::defaultFees();
+    fees.gasLimit = 1'000;
+    env.getServiceRegistry().setFees(fees);
+
+    auto builder = transactions::EscrowFinishBuilder{carol, alice, 1};
+    builder.setGas(1'001);
+
+    EXPECT_EQ(env.submit(builder, carol, XRPAmount{10'000'000}).ter, temBAD_LIMIT);
+}
+
+// A zero gas limit turns the runtime off.
+TEST_F(FinishFailures, AZeroGasLimitDisablesFinishing)
+{
+    auto const seq = createEscrow(kReadsLedgerSqn);
+
+    auto fees = TestServiceRegistry::defaultFees();
+    fees.gasLimit = 0;
+    env.getServiceRegistry().setFees(fees);
+
+    auto builder = transactions::EscrowFinishBuilder{carol, alice, seq};
+    builder.setGas(1'000);
+
+    EXPECT_EQ(env.submit(builder, carol, XRPAmount{10'000'000}).ter, temTEMP_DISABLED);
+}
+
+TEST_F(FinishFailures, AFinishWithoutAGasFieldIsRefused)
+{
+    auto const seq = createEscrow(kReadsLedgerSqn);
+
+    EXPECT_EQ(finish(seq, std::nullopt, XRPAmount{100'000}).ter, tefBYTECODE_NOT_INCLUDED);
+}
+
+TEST_F(FinishFailures, AZeroAllowanceIsRefused)
+{
+    auto const seq = createEscrow(kReadsLedgerSqn);
+
+    auto builder = transactions::EscrowFinishBuilder{carol, alice, seq};
+    builder.setGas(0);
+
+    EXPECT_EQ(env.submit(builder, carol, XRPAmount{100'000}).ter, temBAD_LIMIT);
+}
+
+// The allowance is paid up front, so under-paying is caught before anything runs.
+TEST_F(FinishFailures, AFeeThatDoesNotCoverTheAllowanceIsRefused)
+{
+    auto const seq = createEscrow(kReadsLedgerSqn);
+    constexpr std::uint32_t kAllowance = 1'000;
+
+    auto const fee = escrowFinishFee(env, kAllowance) - XRPAmount{1};
+    EXPECT_EQ(finish(seq, kAllowance, fee).ter, telINSUF_FEE_P);
+}
+
+TEST_F(FinishFailures, GasAgainstAnEscrowWithoutBytecodeIsRefused)
+{
+    auto const seq = createPlainEscrow();
+    constexpr std::uint32_t kAllowance = 100;
+
+    EXPECT_EQ(finish(seq, kAllowance, escrowFinishFee(env, kAllowance)).ter, tefNO_BYTECODE);
+}
+
+// A band rather than an equality: the meter stops at the last instruction it could afford,
+// leaving a few units unspent. That band is what separates this from a trap, which stops
+// early and reports a small fraction.
+TEST_F(FinishFailures, RunningOutOfGasConsumesEssentiallyTheWholeAllowanceAndReportsNoReturnCode)
+{
+    auto const seq = createEscrow(kLoopsForever);
+    constexpr std::uint32_t kAllowance = 10'000;
+
+    auto const result = finish(seq, kAllowance, escrowFinishFee(env, kAllowance));
+    EXPECT_EQ(result.ter, tecOUT_OF_GAS);
+
+    ASSERT_TRUE(result.meta.has_value());
+    // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+    auto const meta = result.meta->getAsObject();
+    ASSERT_TRUE(meta.isFieldPresent(sfGasUsed));
+
+    auto const used = meta.getFieldU32(sfGasUsed);
+    EXPECT_LE(used, kAllowance) << "the engine cannot spend more than it was given";
+    EXPECT_GT(used, kAllowance - (kAllowance / 100));
+    EXPECT_FALSE(meta.isFieldPresent(sfVMReturnCode));
+}
+
+// A trap is a fault, not a rejection: gas actually burned, and no return code.
+TEST_F(FinishFailures, ATrapReportsPartialGasAndNoReturnCode)
+{
+    auto const seq = createEscrow(kTraps);
+    constexpr std::uint32_t kAllowance = 1'000;
+
+    auto const result = finish(seq, kAllowance, escrowFinishFee(env, kAllowance));
+    EXPECT_EQ(result.ter, tecFAILED_PROCESSING);
+
+    ASSERT_TRUE(result.meta.has_value());
+    // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+    auto const meta = result.meta->getAsObject();
+    ASSERT_TRUE(meta.isFieldPresent(sfGasUsed));
+    EXPECT_LT(meta.getFieldU32(sfGasUsed), kAllowance);
+    EXPECT_FALSE(meta.isFieldPresent(sfVMReturnCode));
+}
+
+}  // namespace
+}  // namespace xrpl::test
diff --git a/src/tests/libxrpl/tx/wasm/transactor/GasFees.cpp b/src/tests/libxrpl/tx/wasm/transactor/GasFees.cpp
new file mode 100644
index 0000000000..1fa5a329a4
--- /dev/null
+++ b/src/tests/libxrpl/tx/wasm/transactor/GasFees.cpp
@@ -0,0 +1,99 @@
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::test {
+namespace {
+
+// allowance × gasPrice is a product of two 32-bit values, so a narrow intermediate would wrap
+// and let a large allowance be bought for almost nothing. These pin that it costs a big fee.
+
+// Near the default gas limit, so the product is as large as the transactor ever computes.
+constexpr std::uint32_t kBigAllowance = 996'433;
+
+struct GasFees : testing::Test
+{
+    TxTest env;
+    Account const alice{"alice"};
+    Account const carol{"carol"};
+    std::uint32_t escrowSeq{};
+
+    void
+    SetUp() override
+    {
+        testing::Test::SetUp();
+        createAccounts(env, XRP(5'000), alice, carol);
+
+        auto const wasm = assembleWat(kReadsLedgerSqn);
+        escrowSeq = env.getAccountRoot(alice).getSequence();
+
+        auto builder = transactions::EscrowCreateBuilder{alice, carol, STAmount{XRP(1'000)}};
+        builder.setBytecode(makeSlice(wasm));
+        builder.setCancelAfter(closeTimeOffset(env, 1'000));
+
+        ASSERT_EQ(env.submit(builder, alice, escrowCreateFee(env, wasm)).ter, tesSUCCESS);
+        env.close();
+    }
+
+    [[nodiscard]] TER
+    finishPaying(XRPAmount fee)
+    {
+        auto builder = transactions::EscrowFinishBuilder{carol, alice, escrowSeq};
+        builder.setGas(kBigAllowance);
+        return env.submit(builder, carol, fee).ter;
+    }
+};
+
+// If the product ever wrapped, this is the test that notices: 30 drops would start looking
+// sufficient.
+TEST_F(GasFees, ALargeAllowanceCannotBeBoughtForAFewDrops)
+{
+    auto const owed = escrowFinishFee(env, kBigAllowance);
+    ASSERT_GT(owed.drops(), kBigAllowance) << "the fee must scale with the allowance";
+
+    EXPECT_EQ(finishPaying(XRPAmount{30}), telINSUF_FEE_P);
+}
+
+TEST_F(GasFees, AFeeOneDropShortOfTheAllowanceIsRefused)
+{
+    EXPECT_EQ(finishPaying(escrowFinishFee(env, kBigAllowance) - XRPAmount{1}), telINSUF_FEE_P);
+}
+
+// Otherwise the two refusals above prove nothing: any fee at all might be rejected.
+TEST_F(GasFees, TheExactFeeIsAccepted)
+{
+    EXPECT_EQ(finishPaying(escrowFinishFee(env, kBigAllowance)), tesSUCCESS);
+}
+
+// Asking for a near-limit budget does not mean spending it.
+TEST_F(GasFees, OnlyTheGasActuallyUsedIsReported)
+{
+    auto builder = transactions::EscrowFinishBuilder{carol, alice, escrowSeq};
+    builder.setGas(kBigAllowance);
+
+    auto const result = env.submitAndClose(builder, carol, escrowFinishFee(env, kBigAllowance));
+    ASSERT_EQ(result.ter, tesSUCCESS);
+
+    ASSERT_TRUE(result.meta.has_value());
+    // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
+    auto const obj = result.meta->getAsObject();
+    ASSERT_TRUE(obj.isFieldPresent(sfGasUsed));
+
+    EXPECT_LT(obj.getFieldU32(sfGasUsed), kBigAllowance);
+    EXPECT_EQ(obj.getFieldI32(sfVMReturnCode), 5);
+}
+
+}  // namespace
+}  // namespace xrpl::test
diff --git a/src/xrpld/app/ledger/LedgerMaster.h b/src/xrpld/app/ledger/LedgerMaster.h
index 32163fd57b..140b12fa59 100644
--- a/src/xrpld/app/ledger/LedgerMaster.h
+++ b/src/xrpld/app/ledger/LedgerMaster.h
@@ -123,7 +123,10 @@ public:
     failedSave(std::uint32_t seq, uint256 const& hash);
 
     std::string
-    getCompleteLedgers();
+    getCompleteLedgers() const;
+
+    std::size_t
+    missingFromCompleteLedgerRange(LedgerIndex first, LedgerIndex last) const;
 
     /**
      * Apply held transactions to the open ledger
@@ -190,7 +193,7 @@ public:
     fixMismatch(ReadView const& ledger);
 
     bool
-    haveLedger(std::uint32_t seq);
+    haveLedger(std::uint32_t seq) const;
     void
     clearLedger(std::uint32_t seq);
     bool
@@ -348,7 +351,7 @@ private:
     // A set of transactions to replay during the next close
     std::unique_ptr replayData_;
 
-    std::recursive_mutex completeLock_;
+    std::recursive_mutex mutable completeLock_;
     RangeSet completeLedgers_;
 
     // Publish thread is running.
diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp b/src/xrpld/app/ledger/detail/LedgerMaster.cpp
index 83d76bcd2a..878b257b69 100644
--- a/src/xrpld/app/ledger/detail/LedgerMaster.cpp
+++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp
@@ -57,6 +57,7 @@
 #include 
 #include 
 
+#include 
 #include 
 
 #include 
@@ -492,7 +493,7 @@ LedgerMaster::setBuildingLedger(LedgerIndex i)
 }
 
 bool
-LedgerMaster::haveLedger(std::uint32_t seq)
+LedgerMaster::haveLedger(std::uint32_t seq) const
 {
     std::scoped_lock const sl(completeLock_);
     return boost::icl::contains(completeLedgers_, seq);
@@ -1576,12 +1577,36 @@ LedgerMaster::getPublishedLedger()
 }
 
 std::string
-LedgerMaster::getCompleteLedgers()
+LedgerMaster::getCompleteLedgers() const
 {
     std::scoped_lock const sl(completeLock_);
     return to_string(completeLedgers_);
 }
 
+std::size_t
+LedgerMaster::missingFromCompleteLedgerRange(LedgerIndex first, LedgerIndex last) const
+{
+    if (first > last)
+    {
+        // In expected usage, this will never happen because "first" is generally initialized to
+        // "last", "last" is guaranteed to grow monotonically, and "first" either doesn't change
+        // or grows more slowly.
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::LedgerMaster::missingFromCompleteLedgerRange : invalid parameters");
+        return 0;
+        // LCOV_EXCL_STOP
+    }
+
+    RangeSet const target{range(first, last)};
+
+    auto const missing = [&target, this] {
+        std::scoped_lock const sl(completeLock_);
+        return target - completeLedgers_;
+    }();
+
+    return boost::icl::size(missing);
+}
+
 std::optional
 LedgerMaster::getCloseTimeBySeq(LedgerIndex ledgerIndex)
 {
diff --git a/src/xrpld/app/ledger/detail/LedgerNodeHelpers.cpp b/src/xrpld/app/ledger/detail/LedgerNodeHelpers.cpp
index 531dba59f9..230c802022 100644
--- a/src/xrpld/app/ledger/detail/LedgerNodeHelpers.cpp
+++ b/src/xrpld/app/ledger/detail/LedgerNodeHelpers.cpp
@@ -75,11 +75,10 @@ getSHAMapNodeID(protocol::TMLedgerNode const& ledgerNode, SHAMapTreeNode const&
     if (treeNode.isLeaf())
     {
         auto const key = leafKey(treeNode);
-        auto const expectedID = SHAMapNodeID::createID(static_cast(nodeID->getDepth()), key);
         SOMETIMES(
-            nodeID->getNodeID() != expectedID.getNodeID(),
+            !nodeID->isPrefixOf(key),
             "xrpl::getSHAMapNodeID : legacy leaf ID inconsistent with key");
-        if (nodeID->getNodeID() != expectedID.getNodeID())
+        if (!nodeID->isPrefixOf(key))
             return std::nullopt;
     }
 
diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp
index 9d3820e9f7..1d789aa604 100644
--- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp
+++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp
@@ -4,8 +4,7 @@
 #include 
 #include 
 #include 
-#include 
-#include 
+#include 
 
 #include 
 #include 
@@ -19,6 +18,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -141,19 +141,12 @@ fillJsonTx(
         {
             txJson[jss::meta] = stMeta->getJson(JsonOptions::Values::None);
 
-            // If applicable, insert delivered amount
-            if (txnType == ttPAYMENT || txnType == ttCHECK_CASH)
-            {
-                rpc::insertDeliveredAmount(
-                    txJson[jss::meta],
-                    fill.ledger,
-                    txn,
-                    {txn->getTransactionID(), fill.ledger.seq(), *stMeta});
-            }
-
-            // If applicable, insert mpt issuance id
-            rpc::insertMPTokenIssuanceID(
-                txJson[jss::meta], txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta});
+            // Insert all synthetic fields
+            rpc::insertAllSyntheticInJson(
+                txJson[jss::meta],
+                fill.ledger,
+                txn,
+                {txn->getTransactionID(), fill.ledger.seq(), *stMeta});
         }
 
         if (!fill.ledger.open())
@@ -177,19 +170,12 @@ fillJsonTx(
         {
             txJson[jss::metaData] = stMeta->getJson(JsonOptions::Values::None);
 
-            // If applicable, insert delivered amount
-            if (txnType == ttPAYMENT || txnType == ttCHECK_CASH)
-            {
-                rpc::insertDeliveredAmount(
-                    txJson[jss::metaData],
-                    fill.ledger,
-                    txn,
-                    {txn->getTransactionID(), fill.ledger.seq(), *stMeta});
-            }
-
-            // If applicable, insert mpt issuance id
-            rpc::insertMPTokenIssuanceID(
-                txJson[jss::metaData], txn, {txn->getTransactionID(), fill.ledger.seq(), *stMeta});
+            // Insert all synthetic fields
+            rpc::insertAllSyntheticInJson(
+                txJson[jss::metaData],
+                fill.ledger,
+                txn,
+                {txn->getTransactionID(), fill.ledger.seq(), *stMeta});
         }
     }
 
@@ -208,6 +194,7 @@ fillJsonTx(
                 account,
                 amount,
                 FreezeHandling::IgnoreFreeze,
+                AuthHandling::IgnoreAuth,
                 beast::Journal{beast::Journal::getNullSink()});
             txJson[jss::owner_funds] = ownerFunds.getText();
         }
diff --git a/src/xrpld/app/main/GRPCServer.cpp b/src/xrpld/app/main/GRPCServer.cpp
index 1b20ff1d49..c1ea5e874b 100644
--- a/src/xrpld/app/main/GRPCServer.cpp
+++ b/src/xrpld/app/main/GRPCServer.cpp
@@ -9,6 +9,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -24,7 +25,6 @@
 #include 
 #include 
 
-#include 
 #include 
 #include 
 #include 
@@ -49,6 +49,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -371,7 +372,7 @@ GRPCServerImpl::GRPCServerImpl(Application& app)
                 std::string ip;
                 while (std::getline(ss, ip, ','))
                 {
-                    boost::algorithm::trim(ip);
+                    ip = trimWhitespace(ip);
                     auto const addr = boost::asio::ip::make_address(ip);
 
                     if (addr.is_unspecified())
@@ -615,7 +616,7 @@ GRPCServerImpl::createServerCredentials()
 
     try
     {
-        boost::system::error_code ec;
+        std::error_code ec;
         grpc::SslServerCredentialsOptions sslOpts;
         grpc::SslServerCredentialsOptions::PemKeyCertPair keyCertPair;
 
diff --git a/src/xrpld/app/main/Main.cpp b/src/xrpld/app/main/Main.cpp
index a23b84f2e8..ba6520db5f 100644
--- a/src/xrpld/app/main/Main.cpp
+++ b/src/xrpld/app/main/Main.cpp
@@ -6,6 +6,7 @@
 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -21,7 +22,6 @@
 
 #include 
 #include 
-#include 
 #include 
 #include   // IWYU pragma: keep
 #include 
@@ -211,7 +211,7 @@ public:
         boost::split(v, patterns, boost::algorithm::is_any_of(","));
         selectors_.reserve(v.size());
         std::ranges::for_each(v, [this](std::string s) {
-            boost::trim(s);
+            s = trimWhitespace(s);
             if (selectors_.empty() || !s.empty())
                 selectors_.emplace_back(beast::unit_test::Selector::ModeT::Automatch, s);
         });
@@ -614,7 +614,7 @@ run(int argc, char** argv)
                 std::vector result;
                 for (auto& s : strVec)
                 {
-                    boost::trim(s);
+                    s = trimWhitespace(s);
                     if (!s.empty())
                         result.push_back(std::stoi(s));
                 }
diff --git a/src/xrpld/app/misc/FeeVoteImpl.cpp b/src/xrpld/app/misc/FeeVoteImpl.cpp
index 2bfb24b121..26bf6bd24b 100644
--- a/src/xrpld/app/misc/FeeVoteImpl.cpp
+++ b/src/xrpld/app/misc/FeeVoteImpl.cpp
@@ -326,50 +326,46 @@ FeeVoteImpl::doVoting(
     }
 
     // choose our positions
-    // TODO: Use structured binding once LLVM 16 is the minimum supported
-    // version. See also: https://github.com/llvm/llvm-project/issues/48582
-    // https://github.com/llvm/llvm-project/commit/127bf44385424891eb04cff8e52d3f157fc2cb7c
-    auto const baseFee = baseFeeVote.getVotes();
-    auto const baseReserve = baseReserveVote.getVotes();
-    auto const incReserve = incReserveVote.getVotes();
-    auto const gasLimit = gasLimitVote.getVotes();
-    auto const bytecodeSizeLimit = bytecodeSizeLimitVote.getVotes();
-    auto const gasPrice = gasPriceVote.getVotes();
+    auto const [baseFee, baseFeeChanged] = baseFeeVote.getVotes();
+    auto const [baseReserve, baseReserveChanged] = baseReserveVote.getVotes();
+    auto const [incReserve, incReserveChanged] = incReserveVote.getVotes();
+    auto const [gasLimit, gasLimitChanged] = gasLimitVote.getVotes();
+    auto const [bytecodeSizeLimit, bytecodeSizeLimitChanged] = bytecodeSizeLimitVote.getVotes();
+    auto const [gasPrice, gasPriceChanged] = gasPriceVote.getVotes();
 
     auto const seq = lastClosedLedger->header().seq + 1;
 
     // add transactions to our position
-    if (baseFee.second || baseReserve.second || incReserve.second || gasLimit.second ||
-        bytecodeSizeLimit.second || gasPrice.second)
+    if (baseFeeChanged || baseReserveChanged || incReserveChanged || gasLimitChanged ||
+        bytecodeSizeLimitChanged || gasPriceChanged)
     {
-        JLOG(journal_.warn()) << "We are voting for a fee change: " << baseFee.first << "/"
-                              << baseReserve.first << "/" << incReserve.first;
+        JLOG(journal_.warn()) << "We are voting for a fee change: " << baseFee << "/" << baseReserve
+                              << "/" << incReserve;
 
         STTx const feeTx(ttFEE, [=, &rules](auto& obj) {
             obj[sfAccount] = AccountID();
             obj[sfLedgerSequence] = seq;
             if (rules.enabled(featureXRPFees))
             {
-                obj[sfBaseFeeDrops] = baseFee.first;
-                obj[sfReserveBaseDrops] = baseReserve.first;
-                obj[sfReserveIncrementDrops] = incReserve.first;
+                obj[sfBaseFeeDrops] = baseFee;
+                obj[sfReserveBaseDrops] = baseReserve;
+                obj[sfReserveIncrementDrops] = incReserve;
             }
             else
             {
                 // Without the featureXRPFees amendment, these fields are
                 // required.
-                obj[sfBaseFee] = baseFee.first.dropsAs(baseFeeVote.current());
-                obj[sfReserveBase] =
-                    baseReserve.first.dropsAs(baseReserveVote.current());
+                obj[sfBaseFee] = baseFee.dropsAs(baseFeeVote.current());
+                obj[sfReserveBase] = baseReserve.dropsAs(baseReserveVote.current());
                 obj[sfReserveIncrement] =
-                    incReserve.first.dropsAs(incReserveVote.current());
+                    incReserve.dropsAs(incReserveVote.current());
                 obj[sfReferenceFeeUnits] = kFeeUnitsDeprecated;
             }
             if (rules.enabled(featureSmartEscrow))
             {
-                obj[sfGasLimit] = gasLimit.first;
-                obj[sfBytecodeSizeLimit] = bytecodeSizeLimit.first;
-                obj[sfGasPrice] = gasPrice.first;
+                obj[sfGasLimit] = gasLimit;
+                obj[sfBytecodeSizeLimit] = bytecodeSizeLimit;
+                obj[sfGasPrice] = gasPrice;
             }
         });
 
diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp
index cded5a9194..8d0c1e2de3 100644
--- a/src/xrpld/app/misc/NetworkOPs.cpp
+++ b/src/xrpld/app/misc/NetworkOPs.cpp
@@ -28,9 +28,8 @@
 #include 
 #include 
 #include 
-#include 
-#include 
 #include 
+#include 
 
 #include 
 #include 
@@ -74,10 +73,11 @@
 #include 
 #include 
 #include 
-#include 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -85,10 +85,12 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
+#include 
+#include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -1465,11 +1467,17 @@ NetworkOPsImp::preProcessTransaction(std::shared_ptr& transaction)
 
     // NOTE ximinez - I think this check is redundant,
     // but I'm not 100% sure yet.
-    // If so, only cost is looking up HashRouter flags.
-    auto const [validity, reason] =
-        checkValidity(registry_.get().getHashRouter(), sttx, view->rules());
-    XRPL_ASSERT(
-        validity == Validity::Valid, "xrpl::NetworkOPsImp::processTransaction : valid validity");
+    //
+    // For an ordinary transaction it is: the relay and submit paths have
+    // already run checkValidity, so this costs a HashRouter lookup. It is not
+    // redundant for a role-signature transaction while fixCleanup3_4_0 is
+    // activating. Those paths verify against the validated rules, which lag
+    // the open ledger rules used here, and checkValidity scopes a cached
+    // verdict to the rules that reached it, so this call can verify the
+    // signature again and come to a different answer. SigBad is therefore
+    // reachable, and the handler below is the correct response to it.
+    auto const& viewRules = view->rules();
+    auto const [validity, reason] = checkValidity(registry_.get().getHashRouter(), sttx, viewRules);
 
     // Not concerned with local checks at this point.
     if (validity == Validity::SigBad)
@@ -1477,7 +1485,15 @@ NetworkOPsImp::preProcessTransaction(std::shared_ptr& transaction)
         JLOG(journal_.info()) << "Transaction has bad signature: " << reason;
         transaction->setStatus(TransStatus::INVALID);
         transaction->setResult(temBAD_SIGNATURE);
-        registry_.get().getHashRouter().setFlags(transaction->getID(), HashRouterFlags::BAD);
+        // See the matching guard in PeerImp::checkTransaction: only cache
+        // BAD for a role-signature transaction once fixCleanup3_4_0 is
+        // enabled on this node. Remove together with the amendment.
+        if (viewRules.enabled(fixCleanup3_4_0) ||
+            (!sttx.isFieldPresent(sfSponsorSignature) &&
+             !sttx.isFieldPresent(sfCounterpartySignature)))
+        {
+            registry_.get().getHashRouter().setFlags(transaction->getID(), HashRouterFlags::BAD);
+        }
         return false;
     }
 
@@ -2559,7 +2575,7 @@ NetworkOPsImp::pubConsensus(ConsensusPhase phase)
 void
 NetworkOPsImp::pubContractEvent(std::string const& name, STJson const& event)
 {
-    std::lock_guard const sl(subLock_);
+    std::scoped_lock const sl(streamLock_);
 
     auto& streamMap = streamMaps_[SContractEvents];
     if (!streamMap.empty())
@@ -3526,9 +3542,7 @@ NetworkOPsImp::transJson(
     if (meta)
     {
         jvObj[jss::meta] = meta->get().getJson(JsonOptions::Values::None);
-        rpc::insertDeliveredAmount(jvObj[jss::meta], *ledger, transaction, meta->get());
-        rpc::insertNFTSyntheticInJson(jvObj, transaction, meta->get());
-        rpc::insertMPTokenIssuanceID(jvObj[jss::meta], transaction, meta->get());
+        rpc::insertAllSyntheticInJson(jvObj[jss::meta], *ledger, transaction, meta->get());
     }
 
     // add CTID where the needed data for it exists
@@ -4769,7 +4783,7 @@ NetworkOPsImp::unsubConsensus(std::uint64_t uSeq)
 bool
 NetworkOPsImp::subContractEvent(InfoSub::ref isrListener)
 {
-    std::lock_guard const sl(subLock_);
+    std::scoped_lock const sl(streamLock_);
     return streamMaps_[SContractEvents].emplace(isrListener->getSeq(), isrListener).second;
 }
 
@@ -4777,8 +4791,8 @@ NetworkOPsImp::subContractEvent(InfoSub::ref isrListener)
 bool
 NetworkOPsImp::unsubContractEvent(std::uint64_t uSeq)
 {
-    std::lock_guard const sl(subLock_);
-    return streamMaps_[SContractEvents].erase(uSeq);
+    std::scoped_lock const sl(streamLock_);
+    return streamMaps_[SContractEvents].erase(uSeq) != 0u;
 }
 
 InfoSub::pointer
@@ -4873,8 +4887,7 @@ NetworkOPsImp::getBookPage(
 
     ReadView const& view = *lpLedger;
 
-    bool const bGlobalFreeze =
-        isGlobalFrozen(view, book.out.getIssuer()) || isGlobalFrozen(view, book.in.getIssuer());
+    bool const bGlobalFreeze = isGlobalFrozen(view, book.out) || isGlobalFrozen(view, book.in);
 
     bool bDone = false;
     bool bDirectAdvance = true;
@@ -4884,7 +4897,7 @@ NetworkOPsImp::getBookPage(
     unsigned int uBookEntry = 0;
     STAmount saDirRate;
 
-    auto const rate = transferRate(view, book.out.getIssuer());
+    auto const rate = transferRate(view, book.out);
     auto viewJ = registry_.get().getJournal("View");
 
     while (!bDone && iLimit-- > 0)
@@ -4933,12 +4946,37 @@ NetworkOPsImp::getBookPage(
                 auto const& saTakerPays = sleOffer->getFieldAmount(sfTakerPays);
                 STAmount saOwnerFunds;
                 bool firstOwnerOffer(true);
+                auto foundBalance = [&]() {
+                    auto umBalanceEntry = umBalance.find(uOfferOwnerID);
+                    if (umBalanceEntry == umBalance.end())
+                        return false;
+
+                    // Found in running balance table.
+                    saOwnerFunds = umBalanceEntry->second;
+                    firstOwnerOffer = false;
+                    return true;
+                };
 
                 if (book.out.getIssuer() == uOfferOwnerID)
                 {
-                    // If an offer is selling issuer's own IOUs, it is fully
-                    // funded.
-                    saOwnerFunds = saTakerGets;
+                    book.out.visit(
+                        [&](Issue const&) {
+                            // If an offer is selling issuer's own IOUs, it is
+                            // fully funded.
+                            saOwnerFunds = saTakerGets;
+                        },
+                        [&](MPTIssue const& issue) {
+                            // MPT issuers have bounded self-issuance. Use the
+                            // running balance table so multiple issuer-owned
+                            // offers share the same remaining issuance
+                            // headroom.
+                            if (!foundBalance())
+                            {
+                                // Did not find balance in table.
+
+                                saOwnerFunds = issuerFundsToSelfIssue(view, issue);
+                            }
+                        });
                 }
                 else if (bGlobalFreeze)
                 {
@@ -4948,15 +4986,7 @@ NetworkOPsImp::getBookPage(
                 }
                 else
                 {
-                    auto umBalanceEntry = umBalance.find(uOfferOwnerID);
-                    if (umBalanceEntry != umBalance.end())
-                    {
-                        // Found in running balance table.
-
-                        saOwnerFunds = umBalanceEntry->second;
-                        firstOwnerOffer = false;
-                    }
-                    else
+                    if (!foundBalance())
                     {
                         // Did not find balance in table.
 
@@ -4992,7 +5022,28 @@ NetworkOPsImp::getBookPage(
                 {
                     // Need to charge a transfer fee to offer owner.
                     offerRate = rate;
-                    saOwnerFundsLimit = divide(saOwnerFunds, offerRate);
+                    // Why MPT does not use divide(): divide() is built for an
+                    // IOU mantissa, which is always normalized into
+                    // [1e15, 1e16]. An MPT mantissa is the raw int64 balance,
+                    // and divide() scales the numerator by 1e17, so a balance
+                    // over ~1.8e17 leaves uint64 range and throws -- failing
+                    // the whole RPC rather than this one offer.
+                    //
+                    // Why mulRatio is safe: it evaluates in 128 bits, and here
+                    // it cannot overflow either. offerRate is
+                    // 1e9 + 10'000 * TransferFee, so this branch runs only with
+                    // offerRate > kParityRate, making the quotient smaller than
+                    // saOwnerFunds. Rounded down, so reported liquidity is
+                    // never overstated.
+                    saOwnerFundsLimit = saOwnerFunds.holds()
+                        ? toSTAmount(
+                              mulRatio(
+                                  saOwnerFunds.mpt(),
+                                  kParityRate.value,
+                                  offerRate.value,
+                                  /*roundUp*/ false),
+                              saOwnerFunds.asset())
+                        : divide(saOwnerFunds, offerRate);
                 }
 
                 if (saOwnerFundsLimit >= saTakerGets)
@@ -5049,6 +5100,8 @@ NetworkOPsImp::getBookPage(
 
 // This is the new code that uses the book iterators
 // It has temporarily been disabled
+// If this path is re-enabled, add MPT support mirroring the functional
+// getBookPage() implementation above.
 
 void
 NetworkOPsImp::getBookPage(
diff --git a/src/xrpld/app/misc/SHAMapStore.h b/src/xrpld/app/misc/SHAMapStore.h
index eeb04df53d..9d50f988b5 100644
--- a/src/xrpld/app/misc/SHAMapStore.h
+++ b/src/xrpld/app/misc/SHAMapStore.h
@@ -8,6 +8,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -34,8 +35,8 @@ public:
     virtual void
     start() = 0;
 
-    virtual void
-    rendezvous() const = 0;
+    [[nodiscard]] virtual bool
+    rendezvous(std::optional const& timeout = {}) const = 0;
 
     virtual void
     stop() = 0;
diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp b/src/xrpld/app/misc/SHAMapStoreImp.cpp
index e41837d206..e19df597a2 100644
--- a/src/xrpld/app/misc/SHAMapStoreImp.cpp
+++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp
@@ -6,8 +6,10 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -27,12 +29,12 @@
 #include 
 
 #include 
-#include 
-#include 
-#include 
 
 #include 
+#include 
+#include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -128,22 +130,6 @@ SHAMapStoreImp::SHAMapStoreImp(
 
     if (deleteInterval_ != 0u)
     {
-        // Configuration that affects the behavior of online delete
-        getIfExists(section, Keys::kDeleteBatch, deleteBatch_);
-        std::uint32_t temp = 0;
-        if (getIfExists(section, Keys::kBackOffMilliseconds, temp) ||
-            // Included for backward compatibility with an undocumented setting
-            getIfExists(section, Keys::kBackOff, temp))
-        {
-            backOff_ = std::chrono::milliseconds{temp};
-        }
-        if (getIfExists(section, Keys::kAgeThresholdSeconds, temp))
-            ageThreshold_ = std::chrono::seconds{temp};
-        if (getIfExists(section, Keys::kRecoveryWaitSeconds, temp))
-            recoveryWaitTime_ = std::chrono::seconds{temp};
-
-        getIfExists(section, Keys::kAdvisoryDelete, advisoryDelete_);
-
         auto const minInterval =
             config.standalone() ? kMinimumDeletionIntervalSa : kMinimumDeletionInterval;
         if (deleteInterval_ < minInterval)
@@ -160,6 +146,40 @@ SHAMapStoreImp::SHAMapStoreImp(
                 std::to_string(config.ledgerHistory) + ")");
         }
 
+        // Configuration that affects the behavior of online delete
+        getIfExists(section, Keys::kDeleteBatch, deleteBatch_);
+        std::uint32_t temp = 0;
+        if (getIfExists(section, Keys::kBackOffMilliseconds, temp) ||
+            // Included for backward compatibility with an undocumented setting
+            getIfExists(section, Keys::kBackOff, temp))
+        {
+            backOff_ = std::chrono::milliseconds{temp};
+        }
+        if (getIfExists(section, Keys::kAgeThresholdSeconds, temp))
+            ageThreshold_ = std::chrono::seconds{temp};
+        if (getIfExists(section, Keys::kRecoveryWaitSeconds, temp))
+            recoveryWaitTime_ = std::chrono::seconds{temp};
+        if (recoveryWaitTime_ < std::chrono::seconds{1})
+            Throw("recovery_wait_seconds must be at least 1 second");
+
+        getIfExists(section, Keys::kAdvisoryDelete, advisoryDelete_);
+
+        if (getIfExists(section, Keys::kMaxWaitingLedgers, temp))
+        {
+            maxWaitingLedgers_ = temp;
+        }
+        else
+        {
+            maxWaitingLedgers_ = deleteInterval_;
+        }
+
+        auto const minWaiting = minInterval / 4;
+        if (maxWaitingLedgers_ < minWaiting)
+        {
+            Throw(
+                "max_waiting_ledgers must be at least " + std::to_string(minWaiting));
+        }
+
         stateDb_.init(config, dbName_);
         dbPaths();
     }
@@ -236,14 +256,22 @@ SHAMapStoreImp::onLedgerClosed(std::shared_ptr const& ledger)
     cond_.notify_one();
 }
 
-void
-SHAMapStoreImp::rendezvous() const
+[[nodiscard]]
+bool
+SHAMapStoreImp::rendezvous(std::optional const& timeout) const
 {
     if (!working_)
-        return;
+        return true;
+
+    auto notWorking = [&] { return !working_; };
 
     std::unique_lock lock(mutex_);
-    rendezvous_.wait(lock, [&] { return !working_; });
+    if (timeout)
+    {
+        return rendezvous_.wait_for(lock, *timeout, notWorking);
+    }
+    rendezvous_.wait(lock, notWorking);
+    return true;
 }
 
 int
@@ -276,7 +304,7 @@ SHAMapStoreImp::copyNode(std::uint64_t& nodeCount, SHAMapTreeNode const& node)
     }
     if ((++nodeCount % checkHealthInterval_) == 0u)
     {
-        if (healthWait() == HealthResult::Stopping)
+        if (healthWait() != HealthResult::KeepGoing)
             return false;
     }
 
@@ -327,9 +355,35 @@ SHAMapStoreImp::run()
             stateDb_.setLastRotated(lastRotated);
         }
 
+        // We're starting a new cycle, so reset back to the default.
+        lastSuccessfulHealthCheck_ = 0;
+
         bool const readyToRotate = validatedSeq >= lastRotated + deleteInterval_ &&
             canDelete_ >= lastRotated - 1 && healthWait() == HealthResult::KeepGoing;
 
+        {
+            // Note that this is set after the healthWait() check, so that we
+            // don't start the rotation until the validated ledger is fully
+            // processed. It is not guaranteed to be done at this point. It also
+            // allows the testLedgerGaps unit test to work.
+            std::unique_lock lock(mutex_);
+            if (newLedger_)
+            {
+                // It is possible, though very unlikely outside of tests which manipulate internals,
+                // that healthWait() took so long that the validated ledger (newLedger_) has moved
+                // on from where we started. If that's the case, update lastGoodValidatedLedger_
+                // to that ledger's sequence number.
+                lastGoodValidatedLedger_ = newLedger_->header().seq;
+            }
+            else
+            {
+                lastGoodValidatedLedger_ = validatedSeq;
+            }
+            auto const l = lastGoodValidatedLedger_;
+            lock.unlock();
+            JLOG(journal_.trace()) << "run: Set lastGoodValidatedLedger_ to " << l;
+        }
+
         // will delete up to (not including) lastRotated
         if (readyToRotate)
         {
@@ -337,11 +391,19 @@ SHAMapStoreImp::run()
                                   << lastRotated << " deleteInterval " << deleteInterval_
                                   << " canDelete_ " << canDelete_ << " state "
                                   << app_.getOPs().strOperatingMode(false) << " age "
-                                  << ledgerMaster_->getValidatedLedgerAge().count() << 's';
+                                  << ledgerMaster_->getValidatedLedgerAge().count()
+                                  << "s. Complete ledgers: " << ledgerMaster_->getCompleteLedgers();
 
             clearPrior(lastRotated);
-            if (healthWait() == HealthResult::Stopping)
-                return;
+            switch (healthWait())
+            {
+                case HealthResult::Stopping:
+                    return;
+                case HealthResult::Expired:
+                    continue;
+                case HealthResult::KeepGoing:
+                    break;
+            }
 
             JLOG(journal_.debug()) << "copying ledger " << validatedSeq;
             std::uint64_t nodeCount = 0;
@@ -360,8 +422,15 @@ SHAMapStoreImp::run()
                 continue;
             }
 
-            if (healthWait() == HealthResult::Stopping)
-                return;
+            switch (healthWait())
+            {
+                case HealthResult::Stopping:
+                    return;
+                case HealthResult::Expired:
+                    continue;
+                case HealthResult::KeepGoing:
+                    break;
+            }
             // Only log if we completed without a "health" abort
             JLOG(journal_.debug())
                 << "copied ledger " << validatedSeq << " nodecount " << nodeCount;
@@ -385,8 +454,15 @@ SHAMapStoreImp::run()
 
             JLOG(journal_.debug()) << "freshening caches";
             freshenCaches();
-            if (healthWait() == HealthResult::Stopping)
-                return;
+            switch (healthWait())
+            {
+                case HealthResult::Stopping:
+                    return;
+                case HealthResult::Expired:
+                    continue;
+                case HealthResult::KeepGoing:
+                    break;
+            }
             // Only log if we completed without a "health" abort
             JLOG(journal_.debug()) << validatedSeq << " freshened caches";
 
@@ -395,8 +471,15 @@ SHAMapStoreImp::run()
             JLOG(journal_.debug()) << validatedSeq << " new backend " << newBackend->getName();
 
             clearCaches(validatedSeq);
-            if (healthWait() == HealthResult::Stopping)
-                return;
+            switch (healthWait())
+            {
+                case HealthResult::Stopping:
+                    return;
+                case HealthResult::Expired:
+                    continue;
+                case HealthResult::KeepGoing:
+                    break;
+            }
 
             lastRotated = validatedSeq;
 
@@ -412,7 +495,9 @@ SHAMapStoreImp::run()
                     clearCaches(validatedSeq);
                 });
 
-            JLOG(journal_.warn()) << "finished rotation " << validatedSeq;
+            JLOG(journal_.warn()) << "finished rotation. validatedSeq: " << validatedSeq
+                                  << ", lastRotated: " << lastRotated
+                                  << ". Complete ledgers: " << ledgerMaster_->getCompleteLedgers();
         }
     }
 }
@@ -426,10 +511,10 @@ SHAMapStoreImp::dbPaths()
     if (boost::iequals(get(section, Keys::kType), "memory"))
         return;
 
-    boost::filesystem::path dbPath = get(section, Keys::kPath);
-    if (boost::filesystem::exists(dbPath))
+    std::filesystem::path dbPath = get(section, Keys::kPath);
+    if (std::filesystem::exists(dbPath))
     {
-        if (!boost::filesystem::is_directory(dbPath))
+        if (!std::filesystem::is_directory(dbPath))
         {
             journal_.error() << "node db path must be a directory. " << dbPath.string();
             Throw("node db path must be a directory.");
@@ -437,7 +522,7 @@ SHAMapStoreImp::dbPaths()
     }
     else
     {
-        boost::filesystem::create_directories(dbPath);
+        std::filesystem::create_directories(dbPath);
     }
 
     SavedState state = stateDb_.getState();
@@ -448,8 +533,8 @@ SHAMapStoreImp::dbPaths()
                 return false;
 
             // Check if configured "path" matches stored directory path
-            using namespace boost::filesystem;
-            auto const stored{path(sPath)};
+            using namespace std::filesystem;
+            auto const stored{std::filesystem::path(sPath)};
             if (stored.parent_path() == dbPath)
                 return false;
 
@@ -467,9 +552,9 @@ SHAMapStoreImp::dbPaths()
     bool writableDbExists = false;
     bool archiveDbExists = false;
 
-    std::vector pathsToDelete;
-    for (boost::filesystem::directory_iterator it(dbPath);
-         it != boost::filesystem::directory_iterator();
+    std::vector pathsToDelete;
+    for (std::filesystem::directory_iterator it(dbPath);
+         it != std::filesystem::directory_iterator();
          ++it)
     {
         if (state.writableDb == it->path().string())
@@ -490,7 +575,7 @@ SHAMapStoreImp::dbPaths()
         (!archiveDbExists && !state.archiveDb.empty()) || (writableDbExists != archiveDbExists) ||
         state.writableDb.empty() != state.archiveDb.empty())
     {
-        boost::filesystem::path stateDbPathName = app_.config().legacy(Sections::kDatabasePath);
+        std::filesystem::path stateDbPathName = app_.config().legacy(Sections::kDatabasePath);
         stateDbPathName /= dbName_;
         stateDbPathName += "*";
 
@@ -512,15 +597,15 @@ SHAMapStoreImp::dbPaths()
     }
 
     // The necessary directories exist. Now, remove any others.
-    for (boost::filesystem::path const& p : pathsToDelete)
-        boost::filesystem::remove_all(p);
+    for (std::filesystem::path const& p : pathsToDelete)
+        std::filesystem::remove_all(p);
 }
 
 std::unique_ptr
 SHAMapStoreImp::makeBackendRotating(std::string path)
 {
     Section section{app_.config().section(Sections::kNodeDatabase)};
-    boost::filesystem::path newPath;
+    std::filesystem::path newPath;
 
     if (!path.empty())
     {
@@ -528,10 +613,7 @@ SHAMapStoreImp::makeBackendRotating(std::string path)
     }
     else
     {
-        boost::filesystem::path p = get(section, Keys::kPath);
-        p /= dbPrefix_;
-        p += ".%%%%";
-        newPath = boost::filesystem::unique_path(p);
+        newPath = uniqueRandomPath(get(section, Keys::kPath), dbPrefix_ + ".");
     }
     section.set(Keys::kPath, newPath.string());
 
@@ -563,7 +645,7 @@ SHAMapStoreImp::clearSql(
         min = *m;
     }
 
-    if (min > lastRotated || healthWait() == HealthResult::Stopping)
+    if (min > lastRotated || healthWait() != HealthResult::KeepGoing)
         return;
     if (min == lastRotated)
     {
@@ -576,18 +658,19 @@ SHAMapStoreImp::clearSql(
                            << lastRotated;
     while (min < lastRotated)
     {
+        // The very first sleep is, arguably wasted, but clearSql is called multiple times for
+        // different tables, so the time is amortized among all the operations. This results in
+        // a backoff in between each set of tables, too.
+        std::this_thread::sleep_for(backOff_);
+        if (healthWait() != HealthResult::KeepGoing)
+            return;
+
         min = std::min(lastRotated, min + deleteBatch_);
         JLOG(journal_.trace()) << "Begin: Delete up to " << deleteBatch_
                                << " rows with LedgerSeq < " << min << " from: " << tableName;
         deleteBeforeSeq(min);
         JLOG(journal_.trace()) << "End: Delete up to " << deleteBatch_ << " rows with LedgerSeq < "
                                << min << " from: " << tableName;
-        if (healthWait() == HealthResult::Stopping)
-            return;
-        if (min < lastRotated)
-            std::this_thread::sleep_for(backOff_);
-        if (healthWait() == HealthResult::Stopping)
-            return;
     }
     JLOG(journal_.debug()) << "finished deleting from: " << tableName;
 }
@@ -620,7 +703,7 @@ SHAMapStoreImp::clearPrior(LedgerIndex lastRotated)
     JLOG(journal_.trace()) << "Begin: Clear internal ledgers up to " << lastRotated;
     ledgerMaster_->clearPriorLedgers(lastRotated);
     JLOG(journal_.trace()) << "End: Clear internal ledgers up to " << lastRotated;
-    if (healthWait() == HealthResult::Stopping)
+    if (healthWait() != HealthResult::KeepGoing)
         return;
 
     auto& db = app_.getRelationalDatabase();
@@ -630,7 +713,7 @@ SHAMapStoreImp::clearPrior(LedgerIndex lastRotated)
         "Ledgers",
         [&db]() -> std::optional { return db.getMinLedgerSeq(); },
         [&db](LedgerIndex min) -> void { db.deleteBeforeLedgerSeq(min); });
-    if (healthWait() == HealthResult::Stopping)
+    if (healthWait() != HealthResult::KeepGoing)
         return;
 
     if (!app_.config().useTxTables())
@@ -641,7 +724,7 @@ SHAMapStoreImp::clearPrior(LedgerIndex lastRotated)
         "Transactions",
         [&db]() -> std::optional { return db.getTransactionsMinLedgerSeq(); },
         [&db](LedgerIndex min) -> void { db.deleteTransactionsBeforeLedgerSeq(min); });
-    if (healthWait() == HealthResult::Stopping)
+    if (healthWait() != HealthResult::KeepGoing)
         return;
 
     clearSql(
@@ -649,30 +732,136 @@ SHAMapStoreImp::clearPrior(LedgerIndex lastRotated)
         "AccountTransactions",
         [&db]() -> std::optional { return db.getAccountTransactionsMinLedgerSeq(); },
         [&db](LedgerIndex min) -> void { db.deleteAccountTransactionsBeforeLedgerSeq(min); });
-    if (healthWait() == HealthResult::Stopping)
+    if (healthWait() != HealthResult::KeepGoing)
         return;
 }
 
 SHAMapStoreImp::HealthResult
 SHAMapStoreImp::healthWait()
 {
-    auto age = ledgerMaster_->getValidatedLedgerAge();
-    OperatingMode mode = netOPs_->getOperatingMode();
-    std::unique_lock lock(mutex_);
-    while (!stop_ && (mode != OperatingMode::FULL || age > ageThreshold_))
-    {
-        lock.unlock();
-        JLOG(journal_.warn()) << "Waiting " << recoveryWaitTime_.count()
-                              << "s for node to stabilize. state: "
-                              << app_.getOPs().strOperatingMode(mode, false) << ". age "
-                              << age.count() << 's';
-        std::this_thread::sleep_for(recoveryWaitTime_);
+    // Gets the current status of the server from ledgerMaster_ and netOPs_. Must be called
+    // while mutex_ is unlocked to avoid unlikely, but possible, deadlock with ledgerMaster_'s
+    // completeLock_.
+    // Releasing the lock may mean that status will be slightly out of date when the lock is
+    // reacquired, but it's close enough. In a normal rotation, healthWait() is called frequently,
+    // so a false positive will be detected on the next call, and a false negative will be detected
+    // in the next loop iteration. Database rotation is important, but not timely, so an extra
+    // delay is fine.
+    auto readServerStatus = [this](
+                                LedgerIndex& index,
+                                bool& buildingIndex,
+                                std::chrono::seconds& age,
+                                OperatingMode& mode,
+                                std::size_t& numMissing,
+                                LedgerIndex const lowerBound,
+                                ScopeUnlock const&) {
+        index = ledgerMaster_->getValidLedgerIndex();
+        bool const haveIndex = ledgerMaster_->haveLedger(index);
         age = ledgerMaster_->getValidatedLedgerAge();
         mode = netOPs_->getOperatingMode();
-        lock.lock();
+
+        numMissing =
+            lowerBound == 0 ? 0 : ledgerMaster_->missingFromCompleteLedgerRange(lowerBound, index);
+
+        buildingIndex = (numMissing == 1 && !haveIndex);
+    };
+
+    // Tracked server status properties
+    LedgerIndex index = 0;
+    bool buildingIndex = false;
+    std::chrono::seconds age;
+    OperatingMode mode = OperatingMode::DISCONNECTED;
+    std::size_t numMissing = 0;
+
+    std::unique_lock lock(mutex_);
+
+    auto const waitTime = recoveryWaitTime_;
+    auto const ageThreshold = ageThreshold_;
+    {
+        auto const lowerBound = lastGoodValidatedLedger_;
+
+        ScopeUnlock const unlock(lock);
+
+        readServerStatus(index, buildingIndex, age, mode, numMissing, lowerBound, unlock);
+    }
+    // If index gets past this point without the health check succeeding, return
+    // HealthWait::Expired. This depends on index being initialized, so it must be after
+    // readServerStatus().
+    auto const lastSuccess = lastSuccessfulHealthCheck_ == 0 ? index : lastSuccessfulHealthCheck_;
+    auto const circuitBreaker = lastSuccess + maxWaitingLedgers_;
+
+    auto healthy = [&] {
+        // Special case: If the server is disconnected, it's not doing any ledger I/O, because
+        // it's focused on trying to get peers. A disconnected state is should never be caused by
+        // the activity of the server. It's usually limited to hardware or connectivity issues. Take
+        // advantage of that to run as much rotation I/O as possible before it comes back online.
+        if (mode == OperatingMode::DISCONNECTED)
+            return true;
+        if (age > ageThreshold)
+            return false;
+        if (numMissing > 0)
+            return false;
+        if (mode != OperatingMode::FULL)
+            return false;
+        return true;
+    };
+
+    while (!stop_ && !healthy() && index < circuitBreaker)
+    {
+        // Future-proofing: this value shouldn't change while we are sleeping, but grab it while we
+        // have the lock in case it does.
+        auto const lowerBound = lastGoodValidatedLedger_;
+
+        ScopeUnlock const unlock(lock);
+
+        auto const [stream, waitMs] = std::invoke(
+            [mode, age, ageThreshold, buildingIndex, waitTime, index, lastSuccess, this]
+            -> std::pair {
+                if (mode != OperatingMode::FULL || age > ageThreshold ||
+                    (index - lastSuccess > maxWaitingLedgers_ / 4))
+                    return {journal_.warn(), waitTime};
+                if (buildingIndex)
+                {
+                    // We expect this ledger to be built soon, so log at a lower level, and don't
+                    // wait as long.
+                    return {
+                        journal_.trace(),
+                        std::chrono::duration_cast(waitTime) / 10};
+                }
+                return {journal_.info(), waitTime};
+            });
+        JLOG(stream) << "Waiting " << waitMs.count() << "ms for node to stabilize. state: "
+                     << app_.getOPs().strOperatingMode(mode, false) << ". age " << age.count()
+                     << "s. Missing ledgers: " << numMissing << ". Expect: " << lowerBound << "-"
+                     << index << ". Complete ledgers: " << ledgerMaster_->getCompleteLedgers();
+        std::this_thread::sleep_for(waitMs);
+
+        [[maybe_unused]]
+        LedgerIndex const lastLedger = index;
+        readServerStatus(index, buildingIndex, age, mode, numMissing, lowerBound, unlock);
+        SOMETIMES(
+            index > lastLedger, "SHAMapStoreImp::healthWait : validated ledger index changed");
     }
 
-    return stop_ ? HealthResult::Stopping : HealthResult::KeepGoing;
+    auto const result = std::invoke([index, circuitBreaker, this]() -> HealthResult {
+        if (stop_)
+            return HealthResult::Stopping;
+        if (index < circuitBreaker)
+            return HealthResult::KeepGoing;
+        JLOG(journal_.error()) << "online_delete rotation has been unable to make progress for "
+                               << maxWaitingLedgers_ << " ledgers. "
+                               << "validated ledger index: " << index
+                               << ", last successful health check index: "
+                               << lastSuccessfulHealthCheck_
+                               << ", circuit breaker index: " << circuitBreaker;
+        return HealthResult::Expired;
+    });
+
+    XRPL_ASSERT(lock.owns_lock(), "SHAMapStoreImp::healthWait : lock held");
+    if (result == HealthResult::KeepGoing)
+        lastSuccessfulHealthCheck_ = index;
+
+    return result;
 }
 
 void
diff --git a/src/xrpld/app/misc/SHAMapStoreImp.h b/src/xrpld/app/misc/SHAMapStoreImp.h
index 8a1b7504b9..c1e9199665 100644
--- a/src/xrpld/app/misc/SHAMapStoreImp.h
+++ b/src/xrpld/app/misc/SHAMapStoreImp.h
@@ -88,6 +88,13 @@ private:
     std::thread thread_;
     bool stop_ = false;
     bool healthy_ = true;
+    // Used to prevent ledger gaps from forming during online deletion. Keeps
+    // track of the last validated ledger that was processed without gaps. There
+    // are no guarantees about gaps while online delete is not running. For
+    // that, use advisory_delete and check for gaps externally.
+    LedgerIndex lastGoodValidatedLedger_ = 0;
+    // Used to prevent the circuit breaker from tripping too quickly.
+    LedgerIndex lastSuccessfulHealthCheck_ = 0;
     mutable std::condition_variable cond_;
     mutable std::condition_variable rendezvous_;
     mutable std::mutex mutex_;
@@ -102,12 +109,18 @@ private:
     std::chrono::milliseconds backOff_{100};
     std::chrono::seconds ageThreshold_{60};
     /**
-     * If  the node is out of sync during an online_delete healthWait()
-     * call, sleep the thread for this time, and continue checking until
-     * recovery.
+     * If the node is out of sync, or any recent ledgers are not
+     * available during an online_delete healthWait() call, sleep
+     * the thread for this time, and continue checking until recovery.
      * See also: "recovery_wait_seconds" in xrpld-example.cfg
      */
-    std::chrono::seconds recoveryWaitTime_{5};
+    std::chrono::seconds recoveryWaitTime_{2};
+    /**
+     * If the rotation stays "unhealthy" for a very long time, the process is aborted, and tried
+     * again later. This value represents the number of ledgers that must be validated without
+     * making rotation progress before the process is aborted.
+     */
+    std::uint32_t maxWaitingLedgers_ = deleteBatch_;
 
     // these do not exist upon SHAMapStore creation, but do exist
     // as of run() or before
@@ -163,8 +176,9 @@ public:
     void
     onLedgerClosed(std::shared_ptr const& ledger) override;
 
-    void
-    rendezvous() const override;
+    [[nodiscard]]
+    bool
+    rendezvous(std::optional const& timeout = {}) const override;
     int
     fdRequired() const override;
 
@@ -192,7 +206,7 @@ private:
         for (auto const& key : cache.getKeys())
         {
             dbRotating_->fetchNodeObject(key, 0, node_store::FetchType::Synchronous, true);
-            if (!(++check % checkHealthInterval_) && healthWait() == HealthResult::Stopping)
+            if (!(++check % checkHealthInterval_) && healthWait() != HealthResult::KeepGoing)
                 return true;
         }
 
@@ -220,11 +234,11 @@ private:
     /**
      * This is a health check for online deletion that waits until xrpld is
      * stable before returning. It returns an indication of whether the server
-     * is stopping.
+     * is stopping, or if this attempt should be abandoned.
      *
      * @return Whether the server is stopping.
      */
-    enum class HealthResult { Stopping, KeepGoing };
+    enum class HealthResult { Stopping, Expired, KeepGoing };
     [[nodiscard]] HealthResult
     healthWait();
 
diff --git a/src/xrpld/app/misc/Transaction.h b/src/xrpld/app/misc/Transaction.h
index b6b6d1a8d5..61951fbb59 100644
--- a/src/xrpld/app/misc/Transaction.h
+++ b/src/xrpld/app/misc/Transaction.h
@@ -15,6 +15,10 @@
 #include 
 #include 
 
+// boost::optional (not std::optional) appears in the declarations below,
+// because SOCI's into()/use() bindings only support boost::optional.
+#include 
+
 #include 
 #include 
 #include 
diff --git a/src/xrpld/app/misc/ValidatorList.h b/src/xrpld/app/misc/ValidatorList.h
index 3f9039eab8..4e001affe8 100644
--- a/src/xrpld/app/misc/ValidatorList.h
+++ b/src/xrpld/app/misc/ValidatorList.h
@@ -17,6 +17,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -29,7 +30,6 @@
 #include 
 
 namespace protocol {
-class TMValidatorList;
 class TMValidatorListCollection;
 }  // namespace protocol
 
@@ -238,7 +238,7 @@ class ValidatorList
     ManifestCache& validatorManifests_;
     ManifestCache& publisherManifests_;
     TimeKeeper& timeKeeper_;
-    boost::filesystem::path const dataPath_;
+    std::filesystem::path const dataPath_;
     beast::Journal const j_;
     std::shared_mutex mutable mutex_;
     using scoped_lock = std::scoped_lock;
@@ -370,9 +370,6 @@ public:
     static std::vector
     parseBlobs(std::uint32_t version, json::Value const& body);
 
-    static std::vector
-    parseBlobs(protocol::TMValidatorList const& body);
-
     static std::vector
     parseBlobs(protocol::TMValidatorListCollection const& body);
 
@@ -390,7 +387,6 @@ public:
 
     [[nodiscard]] static std::pair
     buildValidatorListMessages(
-        std::size_t messageVersion,
         std::uint64_t peerSequence,
         std::size_t maxSequence,
         std::uint32_t rawVersion,
@@ -866,7 +862,7 @@ private:
     /**
      * Get the filename used for caching UNLs
      */
-    boost::filesystem::path
+    std::filesystem::path
     getCacheFileName(scoped_lock const&, PublicKey const& pubKey) const;
 
     /**
@@ -986,14 +982,6 @@ hash_append(Hasher& h, std::map const& blobs)
 
 namespace protocol {
 
-template 
-void
-hash_append(Hasher& h, TMValidatorList const& msg)
-{
-    using beast::hash_append;
-    hash_append(h, msg.manifest(), msg.blob(), msg.signature(), msg.version());
-}
-
 template 
 void
 hash_append(Hasher& h, TMValidatorListCollection const& msg)
diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp
index e355cfacab..f099ebf059 100644
--- a/src/xrpld/app/misc/detail/ValidatorList.cpp
+++ b/src/xrpld/app/misc/detail/ValidatorList.cpp
@@ -29,12 +29,8 @@
 #include 
 #include 
 
-#include 
 #include 
 #include 
-#include 
-#include 
-#include 
 
 #include 
 
@@ -43,6 +39,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -54,6 +51,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -288,7 +286,7 @@ ValidatorList::load(
     return true;
 }
 
-boost::filesystem::path
+std::filesystem::path
 ValidatorList::getCacheFileName(ValidatorList::scoped_lock const&, PublicKey const& pubKey) const
 {
     return dataPath_ / (kFilePrefix + strHex(pubKey));
@@ -372,9 +370,9 @@ ValidatorList::cacheValidatorFile(ValidatorList::scoped_lock const& lock, Public
     if (dataPath_.empty())
         return;
 
-    boost::filesystem::path const filename = getCacheFileName(lock, pubKey);
+    std::filesystem::path const filename = getCacheFileName(lock, pubKey);
 
-    boost::system::error_code ec;
+    std::error_code ec;
 
     json::Value value = buildFileData(strHex(pubKey), publisherLists_.at(pubKey), j_);
     // xrpld should be the only process writing to this file, so
@@ -451,13 +449,6 @@ ValidatorList::parseBlobs(std::uint32_t version, json::Value const& body)
     }
 }
 
-// static
-std::vector
-ValidatorList::parseBlobs(protocol::TMValidatorList const& body)
-{
-    return {{.blob = body.blob(), .signature = body.signature(), .manifest = {}}};
-}
-
 // static
 std::vector
 ValidatorList::parseBlobs(protocol::TMValidatorListCollection const& body)
@@ -478,7 +469,7 @@ ValidatorList::parseBlobs(protocol::TMValidatorListCollection const& body)
     }
     XRPL_ASSERT(
         result.size() == body.blobs_size(),
-        "xrpl::ValidatorList::parseBlobs(TMValidatorList) : result size "
+        "xrpl::ValidatorList::parseBlobs(TMValidatorListCollection) : result size "
         "match");
     return result;
 }
@@ -522,29 +513,6 @@ splitMessageParts(
 {
     if (end <= begin)
         return 0;
-    if (end - begin == 1)
-    {
-        protocol::TMValidatorList smallMsg;
-        smallMsg.set_version(1);
-        smallMsg.set_manifest(largeMsg.manifest());
-
-        auto const& blob = largeMsg.blobs(begin);
-        smallMsg.set_blob(blob.blob());
-        smallMsg.set_signature(blob.signature());
-        // This is only possible if "downgrading" a v2 UNL to v1.
-        if (blob.has_manifest())
-            smallMsg.set_manifest(blob.manifest());
-
-        XRPL_ASSERT(
-            Message::totalSize(smallMsg) <= kMaximumMessageSize,
-            "xrpl::splitMessageParts : maximum message size");
-
-        messages.emplace_back(
-            std::make_shared(smallMsg, protocol::mtVALIDATOR_LIST),
-            sha512Half(smallMsg),
-            1);
-        return messages.back().numVLs;
-    }
 
     std::optional smallMsg;
     smallMsg.emplace();
@@ -556,13 +524,29 @@ splitMessageParts(
         *smallMsg->add_blobs() = largeMsg.blobs(i);
     }
 
-    if (Message::totalSize(*smallMsg) > maxSize)
+    auto const size = Message::totalSize(*smallMsg);
+
+    // Split until each message fits, but a single blob can't be split any
+    // further, so stop recursing at that point regardless of maxSize.
+    if (size > maxSize && end - begin > 1)
     {
         // free up the message space
         smallMsg.reset();
         return splitMessage(messages, largeMsg, maxSize, begin, end);
     }
 
+    // An unsplittable blob is still bounded by the protocol limit: peers drop
+    // messages exceeding it on receipt, so don't waste the bandwidth. maxSize
+    // only ever tightens this (it defaults to kMaximumMessageSize), so a blob
+    // reaching here can exceed maxSize but never the protocol limit.
+    if (size > kMaximumMessageSize)
+    {
+        // LCOV_EXCL_START
+        UNREACHABLE("xrpl::splitMessageParts : maximum message size exceeded");
+        return 0;
+        // LCOV_EXCL_STOP
+    }
+
     messages.emplace_back(
         std::make_shared(*smallMsg, protocol::mtVALIDATOR_LIST_COLLECTION),
         sha512Half(*smallMsg),
@@ -570,37 +554,6 @@ splitMessageParts(
     return messages.back().numVLs;
 }
 
-// Build a v1 protocol message using only the current VL
-std::size_t
-buildValidatorListMessage(
-    std::vector& messages,
-    std::uint32_t rawVersion,
-    std::string const& rawManifest,
-    ValidatorBlobInfo const& currentBlob,
-    std::size_t maxSize)
-{
-    XRPL_ASSERT(
-        messages.empty(),
-        "xrpl::buildValidatorListMessage(ValidatorBlobInfo) : empty messages "
-        "input");
-    protocol::TMValidatorList msg;
-    auto const manifest = currentBlob.manifest ? *currentBlob.manifest : rawManifest;
-    auto const version = 1;
-    msg.set_manifest(manifest);
-    msg.set_blob(currentBlob.blob);
-    msg.set_signature(currentBlob.signature);
-    // Override the version
-    msg.set_version(version);
-
-    XRPL_ASSERT(
-        Message::totalSize(msg) <= kMaximumMessageSize,
-        "xrpl::buildValidatorListMessage(ValidatorBlobInfo) : maximum "
-        "message size");
-    messages.emplace_back(
-        std::make_shared(msg, protocol::mtVALIDATOR_LIST), sha512Half(msg), 1);
-    return 1;
-}
-
 // Build a v2 protocol message using all the VLs with sequence larger than the
 // peer's
 std::size_t
@@ -652,7 +605,6 @@ buildValidatorListMessage(
 // static
 std::pair
 ValidatorList::buildValidatorListMessages(
-    std::size_t messageVersion,
     std::uint64_t peerSequence,
     std::size_t maxSequence,
     std::uint32_t rawVersion,
@@ -665,14 +617,12 @@ ValidatorList::buildValidatorListMessages(
         !blobInfos.empty(),
         "xrpl::ValidatorList::buildValidatorListMessages : empty messages "
         "input");
-    auto const& [currentSeq, currentBlob] = *blobInfos.begin();
     auto numVLs = std::accumulate(
         messages.begin(), messages.end(), 0, [](std::size_t total, MessageWithHash const& m) {
             return total + m.numVLs;
         });
-    if (messageVersion == 2 && peerSequence < maxSequence)
+    if (peerSequence < maxSequence)
     {
-        // Version 2
         if (messages.empty())
         {
             numVLs = buildValidatorListMessage(
@@ -680,36 +630,13 @@ ValidatorList::buildValidatorListMessages(
             if (messages.empty())
             {
                 // No message was generated. Create an empty placeholder so we
-                // dont' repeat the work later.
+                // don't repeat the work later.
                 messages.emplace_back();
             }
         }
 
-        // Don't send it next time.
         return {maxSequence, numVLs};
     }
-    if (messageVersion == 1 && peerSequence < currentSeq)
-    {
-        // Version 1
-        if (messages.empty())
-        {
-            numVLs = buildValidatorListMessage(
-                messages,
-                rawVersion,
-                currentBlob.manifest ? *currentBlob.manifest : rawManifest,
-                currentBlob,
-                maxSize);
-            if (messages.empty())
-            {
-                // No message was generated. Create an empty placeholder so we
-                // dont' repeat the work later.
-                messages.emplace_back();
-            }
-        }
-
-        // Don't send it next time.
-        return {currentSeq, numVLs};
-    }
     return {0, 0};
 }
 
@@ -727,19 +654,8 @@ ValidatorList::sendValidatorList(
     HashRouter& hashRouter,
     beast::Journal j)
 {
-    std::size_t messageVersion = 0;
-    if (peer.supportsFeature(ProtocolFeature::ValidatorList2Propagation))
-    {
-        messageVersion = 2;
-    }
-    else if (peer.supportsFeature(ProtocolFeature::ValidatorListPropagation))
-    {
-        messageVersion = 1;
-    }
-    if (messageVersion == 0u)
-        return;
     auto const [newPeerSequence, numVLs] = buildValidatorListMessages(
-        messageVersion, peerSequence, maxSequence, rawVersion, rawManifest, blobInfos, messages);
+        peerSequence, maxSequence, rawVersion, rawManifest, blobInfos, messages);
     if (newPeerSequence != 0u)
     {
         XRPL_ASSERT(
@@ -766,24 +682,11 @@ ValidatorList::sendValidatorList(
             "xrpl::ValidatorList::sendValidatorList : sent or one message");
         if (sent)
         {
-            if (messageVersion > 1)
-            {
-                JLOG(j.debug()) << "Sent " << messages.size()
-                                << " validator list collection(s) containing " << numVLs
-                                << " validator list(s) for " << strHex(publisherKey)
-                                << " with sequence range " << peerSequence << ", "
-                                << newPeerSequence << " to " << peer.fingerprint();
-            }
-            else
-            {
-                XRPL_ASSERT(
-                    numVLs == 1,
-                    "xrpl::ValidatorList::sendValidatorList : one validator "
-                    "list");
-                JLOG(j.debug()) << "Sent validator list for " << strHex(publisherKey)
-                                << " with sequence " << newPeerSequence << " to "
-                                << peer.fingerprint();
-            }
+            JLOG(j.debug()) << "Sent " << messages.size()
+                            << " validator list collection(s) containing " << numVLs
+                            << " validator list(s) for " << strHex(publisherKey)
+                            << " with sequence range " << peerSequence << ", " << newPeerSequence
+                            << " to " << peer.fingerprint();
         }
     }
 }
@@ -858,16 +761,9 @@ ValidatorList::broadcastBlobs(
 
     if (toSkip)
     {
-        // We don't know what messages or message versions we're sending
-        // until we examine our peer's properties. Build the message(s) on
-        // demand, but reuse them when possible.
-
-        // This will hold a v1 message with only the current VL if we have
-        // any peers that don't support v2
-        std::vector messages1;
-        // This will hold v2 messages indexed by the peer's
-        // `publisherListSequence`. For each `publisherListSequence`, we'll
-        // only send the VLs with higher sequences.
+        // Build v2 messages on demand and reuse them when possible. Messages
+        // are indexed by the peer's `publisherListSequence`; for each sequence,
+        // we only send VLs with higher sequences.
         std::map> messages2;
         // If any peers are found that are worth considering, this list will
         // be built to hold info for all of the valid VLs.
@@ -887,8 +783,6 @@ ValidatorList::broadcastBlobs(
                 {
                     if (blobInfos.empty())
                         buildBlobInfos(blobInfos, lists);
-                    auto const v2 =
-                        peer->supportsFeature(ProtocolFeature::ValidatorList2Propagation);
                     sendValidatorList(
                         *peer,
                         peerSequence,
@@ -897,11 +791,10 @@ ValidatorList::broadcastBlobs(
                         lists.rawVersion,
                         lists.rawManifest,
                         blobInfos,
-                        v2 ? messages2[peerSequence] : messages1,
+                        messages2[peerSequence],
                         hashRouter,
                         j);
-                    // Even if the peer doesn't support the messages,
-                    // suppress it so it'll be ignored next time.
+                    // Don't send it next time.
                     hashRouter.addSuppressionPeer(hash, peer->id());
                 }
             }
@@ -1295,8 +1188,7 @@ std::vector
 ValidatorList::loadLists()
 {
     using namespace std::string_literals;
-    using namespace boost::filesystem;
-    using namespace boost::system::errc;
+    using namespace std::filesystem;
 
     std::scoped_lock const lock{mutex_};
 
@@ -1304,12 +1196,12 @@ ValidatorList::loadLists()
     sites.reserve(publisherLists_.size());
     for (auto const& [pubKey, publisherCollection] : publisherLists_)
     {
-        boost::system::error_code ec;
+        std::error_code ec;
 
         if (publisherCollection.status == PublisherStatus::Available)
             continue;
 
-        boost::filesystem::path const filename = getCacheFileName(lock, pubKey);
+        std::filesystem::path const filename = getCacheFileName(lock, pubKey);
 
         auto const fullPath{canonical(filename, ec)};
         if (ec)
@@ -1320,7 +1212,7 @@ ValidatorList::loadLists()
         {
             // Treat an empty file as a missing file, because
             // nobody else is going to write it.
-            ec = make_error_code(no_such_file_or_directory);
+            ec = make_error_code(std::errc::no_such_file_or_directory);
         }
         if (ec)
             continue;
diff --git a/src/xrpld/app/misc/detail/WorkSSL.cpp b/src/xrpld/app/misc/detail/WorkSSL.cpp
index e8d24b55d6..48231b147e 100644
--- a/src/xrpld/app/misc/detail/WorkSSL.cpp
+++ b/src/xrpld/app/misc/detail/WorkSSL.cpp
@@ -10,8 +10,8 @@
 #include 
 #include 
 #include 
-#include 
 
+#include 
 #include 
 #include 
 
@@ -38,7 +38,7 @@ WorkSSL::WorkSSL(
 {
     auto ec = context_.preConnectVerify(stream_, host_);
     if (ec)
-        Throw(boost::str(boost::format("preConnectVerify: %s") % ec.message()));
+        Throw(std::format("preConnectVerify: {}", ec.message()));
 }
 
 void
diff --git a/src/xrpld/app/misc/detail/WorkSSL.h b/src/xrpld/app/misc/detail/WorkSSL.h
index d4b3b9ff25..e4b7586054 100644
--- a/src/xrpld/app/misc/detail/WorkSSL.h
+++ b/src/xrpld/app/misc/detail/WorkSSL.h
@@ -7,7 +7,6 @@
 #include 
 
 #include 
-#include 
 
 #include 
 #include 
diff --git a/src/xrpld/app/rdb/backend/detail/Node.cpp b/src/xrpld/app/rdb/backend/detail/Node.cpp
index b2f14c71ea..be4c5d29e5 100644
--- a/src/xrpld/app/rdb/backend/detail/Node.cpp
+++ b/src/xrpld/app/rdb/backend/detail/Node.cpp
@@ -40,8 +40,6 @@
 #include 
 #include 
 
-#include 
-#include 
 #include   // IWYU pragma: keep
 #include 
 
@@ -58,6 +56,8 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -66,6 +66,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -108,18 +109,16 @@ makeLedgerDBs(
     // ledger database
     auto lgr{std::make_unique(
         setup, kLgrDbName, setup.lgrPragma, kLgrDbInit, checkpointerSetup, j)};
-    lgr->getSession() << boost::str(
-        boost::format("PRAGMA cache_size=-%d;") %
-        kilobytes(config.getValueFor(SizedItem::LgrDbCache)));
+    lgr->getSession() << std::format(
+        "PRAGMA cache_size=-{};", kilobytes(config.getValueFor(SizedItem::LgrDbCache)));
 
     if (config.useTxTables())
     {
         // transaction database
         auto tx{std::make_unique(
             setup, kTxDbName, setup.txPragma, kTxDbInit, checkpointerSetup, j)};
-        tx->getSession() << boost::str(
-            boost::format("PRAGMA cache_size=-%d;") %
-            kilobytes(config.getValueFor(SizedItem::TxnDbCache)));
+        tx->getSession() << std::format(
+            "PRAGMA cache_size=-{};", kilobytes(config.getValueFor(SizedItem::TxnDbCache)));
 
         if (!setup.standAlone || setup.startUp == StartUpType::Load ||
             setup.startUp == StartUpType::LoadFile || setup.startUp == StartUpType::Replay)
@@ -279,15 +278,17 @@ saveValidatedLedger(
     }
 
     {
-        static boost::format kDeleteLedger("DELETE FROM Ledgers WHERE LedgerSeq = %u;");
-        static boost::format kDeleteTranS1("DELETE FROM Transactions WHERE LedgerSeq = %u;");
-        static boost::format kDeleteTranS2("DELETE FROM AccountTransactions WHERE LedgerSeq = %u;");
-        static boost::format kDeleteAcctTrans(
-            "DELETE FROM AccountTransactions WHERE TransID = '%s';");
+        static constexpr char const* kDeleteLedger = "DELETE FROM Ledgers WHERE LedgerSeq = {};";
+        static constexpr char const* kDeleteTranS1 =
+            "DELETE FROM Transactions WHERE LedgerSeq = {};";
+        static constexpr char const* kDeleteTranS2 =
+            "DELETE FROM AccountTransactions WHERE LedgerSeq = {};";
+        static constexpr char const* kDeleteAcctTrans =
+            "DELETE FROM AccountTransactions WHERE TransID = '{}';";
 
         {
             auto db = ldgDB.checkoutDb();
-            *db << boost::str(kDeleteLedger % seq);
+            *db << std::format(kDeleteLedger, seq);
         }
 
         if (app.config().useTxTables())
@@ -304,19 +305,19 @@ saveValidatedLedger(
 
             soci::transaction tr(*db);
 
-            *db << boost::str(kDeleteTranS1 % seq);
-            *db << boost::str(kDeleteTranS2 % seq);
+            *db << std::format(kDeleteTranS1, seq);
+            *db << std::format(kDeleteTranS2, seq);
 
             std::string const ledgerSeq(std::to_string(seq));
 
             for (auto const& acceptedLedgerTx : *aLedger)
             {
-                uint256 transactionID = acceptedLedgerTx->getTransactionID();
+                uint256 const transactionID = acceptedLedgerTx->getTransactionID();
 
                 std::string const txnId(to_string(transactionID));
                 std::string const txnSeq(std::to_string(acceptedLedgerTx->getTxnSeq()));
 
-                *db << boost::str(kDeleteAcctTrans % transactionID);
+                *db << std::format(kDeleteAcctTrans, txnId);
 
                 auto const& accts = acceptedLedgerTx->getAffected();
 
@@ -628,11 +629,11 @@ getHashesByIndex(soci::session& session, LedgerIndex minSeq, LedgerIndex maxSeq,
 std::pair>, int>
 getTxHistory(soci::session& session, Application& app, LedgerIndex startIndex, int quantity)
 {
-    std::string const sql = boost::str(
-        boost::format(
-            "SELECT LedgerSeq, Status, RawTxn "
-            "FROM Transactions ORDER BY LedgerSeq DESC LIMIT %u,%u;") %
-        startIndex % quantity);
+    std::string const sql = std::format(
+        "SELECT LedgerSeq, Status, RawTxn "
+        "FROM Transactions ORDER BY LedgerSeq DESC LIMIT {},{};",
+        startIndex,
+        quantity);
 
     std::vector> txs;
     int total = 0;
@@ -729,41 +730,50 @@ transactionsSQL(
 
     if (options.ledgerRange.max != 0u)
     {
-        maxClause = boost::str(
-            boost::format("AND AccountTransactions.LedgerSeq <= '%u'") % options.ledgerRange.max);
+        maxClause =
+            std::format("AND AccountTransactions.LedgerSeq <= '{}'", options.ledgerRange.max);
     }
 
     if (options.ledgerRange.min != 0u)
     {
-        minClause = boost::str(
-            boost::format("AND AccountTransactions.LedgerSeq >= '%u'") % options.ledgerRange.min);
+        minClause =
+            std::format("AND AccountTransactions.LedgerSeq >= '{}'", options.ledgerRange.min);
     }
 
     std::string sql;
 
     if (count)
     {
-        sql = boost::str(
-            boost::format(
-                "SELECT %s FROM AccountTransactions "
-                "WHERE Account = '%s' %s %s LIMIT %u, %u;") %
-            selection % toBase58(options.account) % maxClause % minClause % options.offset %
+        sql = std::format(
+            "SELECT {} FROM AccountTransactions "
+            "WHERE Account = '{}' {} {} LIMIT {}, {};",
+            selection,
+            toBase58(options.account),
+            maxClause,
+            minClause,
+            options.offset,
             numberOfResults);
     }
     else
     {
-        sql = boost::str(
-            boost::format(
-                "SELECT %s FROM "
-                "AccountTransactions INNER JOIN Transactions "
-                "ON Transactions.TransID = AccountTransactions.TransID "
-                "WHERE Account = '%s' %s %s "
-                "ORDER BY AccountTransactions.LedgerSeq %s, "
-                "AccountTransactions.TxnSeq %s, AccountTransactions.TransID %s "
-                "LIMIT %u, %u;") %
-            selection % toBase58(options.account) % maxClause % minClause %
-            (descending ? "DESC" : "ASC") % (descending ? "DESC" : "ASC") %
-            (descending ? "DESC" : "ASC") % options.offset % numberOfResults);
+        char const* const order = descending ? "DESC" : "ASC";
+        sql = std::format(
+            "SELECT {} FROM "
+            "AccountTransactions INNER JOIN Transactions "
+            "ON Transactions.TransID = AccountTransactions.TransID "
+            "WHERE Account = '{}' {} {} "
+            "ORDER BY AccountTransactions.LedgerSeq {}, "
+            "AccountTransactions.TxnSeq {}, AccountTransactions.TransID {} "
+            "LIMIT {}, {};",
+            selection,
+            toBase58(options.account),
+            maxClause,
+            minClause,
+            order,
+            order,
+            order,
+            options.offset,
+            numberOfResults);
     }
     JLOG(j.trace()) << "txSQL query: " << sql;
     return sql;
@@ -1104,14 +1114,6 @@ accountTxPage(
 
     std::optional newmarker;
 
-    static std::string const kPrefix(
-        R"(SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq,
-          Status,RawTxn,TxnMeta
-          FROM AccountTransactions INNER JOIN Transactions
-          ON Transactions.TransID = AccountTransactions.TransID
-          AND AccountTransactions.Account = '%s' WHERE
-          )");
-
     std::string sql;
 
     // SQL's BETWEEN uses a closed interval ([a,b])
@@ -1120,13 +1122,22 @@ accountTxPage(
 
     if (findLedger == 0)
     {
-        sql = boost::str(
-            boost::format(kPrefix + R"(AccountTransactions.LedgerSeq BETWEEN %u AND %u
-             ORDER BY AccountTransactions.LedgerSeq %s,
-             AccountTransactions.TxnSeq %s
-             LIMIT %u;)") %
-            toBase58(options.account) % options.ledgerRange.min % options.ledgerRange.max % order %
-            order % queryLimit);
+        sql = std::format(
+            R"(SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq,
+          Status,RawTxn,TxnMeta
+          FROM AccountTransactions INNER JOIN Transactions
+          ON Transactions.TransID = AccountTransactions.TransID
+          AND AccountTransactions.Account = '{}' WHERE
+          AccountTransactions.LedgerSeq BETWEEN {} AND {}
+             ORDER BY AccountTransactions.LedgerSeq {},
+             AccountTransactions.TxnSeq {}
+             LIMIT {};)",
+            toBase58(options.account),
+            options.ledgerRange.min,
+            options.ledgerRange.max,
+            order,
+            order,
+            queryLimit);
     }
     else
     {
@@ -1135,27 +1146,34 @@ accountTxPage(
         std::uint32_t const maxLedger = forward ? options.ledgerRange.max : findLedger - 1;
 
         auto b58acct = toBase58(options.account);
-        sql = boost::str(
-            boost::format(
-                R"(SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq,
+        sql = std::format(
+            R"(SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq,
             Status,RawTxn,TxnMeta
             FROM AccountTransactions, Transactions WHERE
             (AccountTransactions.TransID = Transactions.TransID AND
-            AccountTransactions.Account = '%s' AND
-            AccountTransactions.LedgerSeq BETWEEN %u AND %u)
+            AccountTransactions.Account = '{}' AND
+            AccountTransactions.LedgerSeq BETWEEN {} AND {})
             UNION
             SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq,Status,RawTxn,TxnMeta
             FROM AccountTransactions, Transactions WHERE
             (AccountTransactions.TransID = Transactions.TransID AND
-            AccountTransactions.Account = '%s' AND
-            AccountTransactions.LedgerSeq = %u AND
-            AccountTransactions.TxnSeq %s %u)
-            ORDER BY AccountTransactions.LedgerSeq %s,
-            AccountTransactions.TxnSeq %s
-            LIMIT %u;
-            )") %
-            b58acct % minLedger % maxLedger % b58acct % findLedger % compare % findSeq % order %
-            order % queryLimit);
+            AccountTransactions.Account = '{}' AND
+            AccountTransactions.LedgerSeq = {} AND
+            AccountTransactions.TxnSeq {} {})
+            ORDER BY AccountTransactions.LedgerSeq {},
+            AccountTransactions.TxnSeq {}
+            LIMIT {};
+            )",
+            b58acct,
+            minLedger,
+            maxLedger,
+            b58acct,
+            findLedger,
+            compare,
+            findSeq,
+            order,
+            order,
+            queryLimit);
     }
 
     {
@@ -1393,8 +1411,8 @@ getTransaction(
 bool
 dbHasSpace(soci::session& session, Config const& config, beast::Journal j)
 {
-    boost::filesystem::space_info const space =
-        boost::filesystem::space(config.legacy(Sections::kDatabasePath));
+    std::filesystem::space_info const space =
+        std::filesystem::space(config.legacy(Sections::kDatabasePath));
 
     if (space.available < megabytes(512))
     {
@@ -1405,9 +1423,9 @@ dbHasSpace(soci::session& session, Config const& config, beast::Journal j)
     if (config.useTxTables())
     {
         DatabaseCon::Setup const dbSetup = setupDatabaseCon(config);
-        boost::filesystem::path const dbPath = dbSetup.dataDir / kTxDbName;
-        boost::system::error_code ec;
-        std::optional dbSize = boost::filesystem::file_size(dbPath, ec);
+        std::filesystem::path const dbPath = dbSetup.dataDir / kTxDbName;
+        std::error_code ec;
+        std::optional dbSize = std::filesystem::file_size(dbPath, ec);
         if (ec)
         {
             JLOG(j.error()) << "Error checking transaction db file size: " << ec.message();
diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h
index 0b6e7e6f74..852a9d9f4b 100644
--- a/src/xrpld/core/Config.h
+++ b/src/xrpld/core/Config.h
@@ -11,11 +11,10 @@
 #include 
 #include 
 
-#include   // VFALCO FIX: This include should not be here
-
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -112,17 +111,17 @@ public:
     /**
      * Returns the full path and filename of the debug log file.
      */
-    [[nodiscard]] boost::filesystem::path
+    [[nodiscard]] std::filesystem::path
     getDebugLogFile() const;
 
 private:
-    boost::filesystem::path configFile_;
+    std::filesystem::path configFile_;
 
 public:
-    boost::filesystem::path configDir;
+    std::filesystem::path configDir;
 
 private:
-    boost::filesystem::path debugLogfile_;
+    std::filesystem::path debugLogfile_;
 
     void
     load();
diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp
index 25dce385e4..d8002f483c 100644
--- a/src/xrpld/core/detail/Config.cpp
+++ b/src/xrpld/core/detail/Config.cpp
@@ -22,21 +22,19 @@
 #include 
 #include 
 #include 
-#include 
-#include 
-#include 
 #include 
 #include 
 #include   // IWYU pragma: keep
 #include 
 #include 
-#include 
 
 #include 
 #include 
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -46,6 +44,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -186,7 +185,7 @@ parseIniFile(std::string const& strInput, bool const bTrim)
     for (auto& strValue : vLines)
     {
         if (bTrim)
-            boost::algorithm::trim(strValue);
+            strValue = trimWhitespace(strValue);
 
         if (strValue.empty() || strValue[0] == '#')
         {
@@ -314,13 +313,13 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand
     // directory, use the current working directory as the
     // config directory and that with "db" as the data
     // directory.
-    boost::filesystem::path dataDir;
+    std::filesystem::path dataDir;
 
     if (!strConf.empty())
     {
         // --conf= : everything is relative that file.
         configFile_ = strConf;
-        configDir = boost::filesystem::absolute(configFile_);
+        configDir = std::filesystem::absolute(configFile_);
         configDir.remove_filename();
         dataDir = configDir / kDatabaseDirName;
     }
@@ -331,13 +330,13 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand
             // Check if either of the config files exist in the current working
             // directory, in which case the databases will be stored in a
             // subdirectory.
-            configDir = boost::filesystem::current_path();
+            configDir = std::filesystem::current_path();
             dataDir = configDir / kDatabaseDirName;
             configFile_ = configDir / kConfigFileName;
-            if (boost::filesystem::exists(configFile_))
+            if (std::filesystem::exists(configFile_))
                 break;
             configFile_ = configDir / kConfigLegacyName;
-            if (boost::filesystem::exists(configFile_))
+            if (std::filesystem::exists(configFile_))
                 break;
 
             // Check if the home directory is set, and optionally the XDG config
@@ -364,10 +363,10 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand
                 dataDir = strXdgDataHome + "/" + systemName();
                 configDir = strXdgConfigHome + "/" + systemName();
                 configFile_ = configDir / kConfigFileName;
-                if (boost::filesystem::exists(configFile_))
+                if (std::filesystem::exists(configFile_))
                     break;
                 configFile_ = configDir / kConfigLegacyName;
-                if (boost::filesystem::exists(configFile_))
+                if (std::filesystem::exists(configFile_))
                     break;
             }
 
@@ -375,7 +374,7 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand
             dataDir = "/var/lib/" + systemName();
             configDir = "/etc/" + systemName();
             configFile_ = configDir / kConfigFileName;
-            if (boost::filesystem::exists(configFile_))
+            if (std::filesystem::exists(configFile_))
                 break;
             configFile_ = configDir / kConfigLegacyName;
         } while (false);
@@ -388,7 +387,7 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand
         std::string const dbPath(legacy(Sections::kDatabasePath));
         if (!dbPath.empty())
         {
-            dataDir = boost::filesystem::path(dbPath);
+            dataDir = std::filesystem::path(dbPath);
         }
         else if (runStandalone_)
         {
@@ -398,13 +397,13 @@ Config::setup(std::string const& strConf, bool bQuiet, bool bSilent, bool bStand
 
     if (!dataDir.empty())
     {
-        boost::system::error_code ec;
-        boost::filesystem::create_directories(dataDir, ec);
+        std::error_code ec;
+        std::filesystem::create_directories(dataDir, ec);
 
         if (ec)
-            Throw(boost::str(boost::format("Can not create %s") % dataDir));
+            Throw(std::format("Can not create {}", dataDir.string()));
 
-        legacy(Sections::kDatabasePath, boost::filesystem::absolute(dataDir).string());
+        legacy(Sections::kDatabasePath, std::filesystem::absolute(dataDir).string());
     }
 
     HTTPClient::initializeSSLContext(this->sslVerifyDir, this->sslVerifyFile, this->sslVerify, j_);
@@ -456,7 +455,7 @@ Config::load()
     if (!quiet_)
         std::cerr << "Loading: " << configFile_ << "\n";
 
-    boost::system::error_code ec;
+    std::error_code ec;
     auto const fileContents = getFileContents(ec, configFile_);
 
     if (ec)
@@ -509,8 +508,8 @@ Config::loadFromString(std::string const& fileContents)
         std::string dbPath;
         if (getSingleSection(secConfig, Sections::kDatabasePath, dbPath, j_))
         {
-            boost::filesystem::path const p(dbPath);
-            legacy(Sections::kDatabasePath, boost::filesystem::absolute(p).string());
+            std::filesystem::path const p(dbPath);
+            legacy(Sections::kDatabasePath, std::filesystem::absolute(p).string());
         }
     }
 
@@ -1012,7 +1011,7 @@ Config::loadFromString(std::string const& fileContents)
         // If no path was specified, then look for validators.txt
         // in the same directory as the config file, but don't complain
         // if we can't find it.
-        boost::filesystem::path validatorsFile;
+        std::filesystem::path validatorsFile;
 
         if (getSingleSection(secConfig, Sections::kValidatorsFile, strTemp, j_))
         {
@@ -1027,7 +1026,7 @@ Config::loadFromString(std::string const& fileContents)
             if (!validatorsFile.is_absolute() && !configDir.empty())
                 validatorsFile = configDir / validatorsFile;
 
-            if (!boost::filesystem::exists(validatorsFile))
+            if (!std::filesystem::exists(validatorsFile))
             {
                 Throw(
                     std::string("The file specified in [") + Sections::kValidatorsFile +
@@ -1036,8 +1035,8 @@ Config::loadFromString(std::string const& fileContents)
                     validatorsFile.string());
             }
             else if (
-                !boost::filesystem::is_regular_file(validatorsFile) &&
-                !boost::filesystem::is_symlink(validatorsFile))
+                !std::filesystem::is_regular_file(validatorsFile) &&
+                !std::filesystem::is_symlink(validatorsFile))
             {
                 Throw(
                     std::string("Invalid file specified in [") + Sections::kValidatorsFile +
@@ -1050,20 +1049,20 @@ Config::loadFromString(std::string const& fileContents)
 
             if (!validatorsFile.empty())
             {
-                if (!boost::filesystem::exists(validatorsFile) ||
-                    (!boost::filesystem::is_regular_file(validatorsFile) &&
-                     !boost::filesystem::is_symlink(validatorsFile)))
+                if (!std::filesystem::exists(validatorsFile) ||
+                    (!std::filesystem::is_regular_file(validatorsFile) &&
+                     !std::filesystem::is_symlink(validatorsFile)))
                 {
                     validatorsFile.clear();
                 }
             }
         }
 
-        if (!validatorsFile.empty() && boost::filesystem::exists(validatorsFile) &&
-            (boost::filesystem::is_regular_file(validatorsFile) ||
-             boost::filesystem::is_symlink(validatorsFile)))
+        if (!validatorsFile.empty() && std::filesystem::exists(validatorsFile) &&
+            (std::filesystem::is_regular_file(validatorsFile) ||
+             std::filesystem::is_symlink(validatorsFile)))
         {
-            boost::system::error_code ec;
+            std::error_code ec;
             auto const data = getFileContents(ec, validatorsFile);
             if (ec)
             {
@@ -1196,7 +1195,7 @@ Config::loadFromString(std::string const& fileContents)
     }
 }
 
-boost::filesystem::path
+std::filesystem::path
 Config::getDebugLogFile() const
 {
     auto logFile = debugLogfile_;
@@ -1205,17 +1204,17 @@ Config::getDebugLogFile() const
     {
         // Unless an absolute path for the log file is specified, the
         // path is relative to the config file directory.
-        logFile = boost::filesystem::absolute(logFile, configDir);
+        logFile = std::filesystem::absolute(configDir / logFile);
     }
 
     if (!logFile.empty())
     {
         auto logDir = logFile.parent_path();
 
-        if (!boost::filesystem::is_directory(logDir))
+        if (!std::filesystem::is_directory(logDir))
         {
-            boost::system::error_code ec;
-            boost::filesystem::create_directories(logDir, ec);
+            std::error_code ec;
+            std::filesystem::create_directories(logDir, ec);
 
             // If we fail, we warn but continue so that the calling code can
             // decide how to handle this situation.
@@ -1323,8 +1322,7 @@ setupDatabaseCon(Config const& c, std::optional j)
                 boost::iequals(journalMode, "truncate") || boost::iequals(journalMode, "persist") ||
                 boost::iequals(journalMode, "wal"))
             {
-                result->emplace_back(
-                    boost::str(boost::format(kCommonDbPragmaJournal) % journalMode));
+                result->emplace_back(commonDbPragmaJournal(journalMode));
             }
             else
             {
@@ -1345,7 +1343,7 @@ setupDatabaseCon(Config const& c, std::optional j)
             if (higherRisk || boost::iequals(synchronous, "normal") ||
                 boost::iequals(synchronous, "full") || boost::iequals(synchronous, "extra"))
             {
-                result->emplace_back(boost::str(boost::format(kCommonDbPragmaSync) % synchronous));
+                result->emplace_back(commonDbPragmaSync(synchronous));
             }
             else
             {
@@ -1366,7 +1364,7 @@ setupDatabaseCon(Config const& c, std::optional j)
             if (higherRisk || boost::iequals(tempStore, "default") ||
                 boost::iequals(tempStore, "file"))
             {
-                result->emplace_back(boost::str(boost::format(kCommonDbPragmaTemp) % tempStore));
+                result->emplace_back(commonDbPragmaTemp(tempStore));
             }
             else
             {
diff --git a/src/xrpld/overlay/Peer.h b/src/xrpld/overlay/Peer.h
index 87750ed40e..6c4cf1dff1 100644
--- a/src/xrpld/overlay/Peer.h
+++ b/src/xrpld/overlay/Peer.h
@@ -20,8 +20,6 @@ class Charge;
 }  // namespace resource
 
 enum class ProtocolFeature {
-    ValidatorListPropagation,
-    ValidatorList2Propagation,
     LedgerReplay,
     LedgerNodeDepth,
 };
diff --git a/src/xrpld/overlay/detail/Message.cpp b/src/xrpld/overlay/detail/Message.cpp
index c6e0511515..a6af525620 100644
--- a/src/xrpld/overlay/detail/Message.cpp
+++ b/src/xrpld/overlay/detail/Message.cpp
@@ -82,7 +82,6 @@ Message::compress()
             case protocol::mtGET_LEDGER:
             case protocol::mtLEDGER_DATA:
             case protocol::mtGET_OBJECTS:
-            case protocol::mtVALIDATOR_LIST:
             case protocol::mtVALIDATOR_LIST_COLLECTION:
             case protocol::mtREPLAY_DELTA_RESPONSE:
             case protocol::mtTRANSACTIONS:
diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp
index 726002fce4..c56ea5797f 100644
--- a/src/xrpld/overlay/detail/PeerImp.cpp
+++ b/src/xrpld/overlay/detail/PeerImp.cpp
@@ -44,6 +44,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -542,10 +543,6 @@ PeerImp::supportsFeature(ProtocolFeature f) const
 {
     switch (f)
     {
-        case ProtocolFeature::ValidatorListPropagation:
-            return protocol_ >= makeProtocol(2, 1);
-        case ProtocolFeature::ValidatorList2Propagation:
-            return protocol_ >= makeProtocol(2, 2);
         case ProtocolFeature::LedgerNodeDepth:
             return protocol_ >= makeProtocol(2, 3);
         case ProtocolFeature::LedgerReplay:
@@ -885,7 +882,7 @@ PeerImp::doProtocolStart()
     onReadMessage(error_code(), 0);
 
     // Send all the validator lists that have been loaded
-    if (inbound_ && supportsFeature(ProtocolFeature::ValidatorListPropagation))
+    if (inbound_)
     {
         app_.getValidators().forEachAvailable(
             [&](std::string const& manifest,
@@ -2422,43 +2419,11 @@ PeerImp::onValidatorListMessage(
     }
 }
 
-void
-PeerImp::onMessage(std::shared_ptr const& m)
-{
-    try
-    {
-        if (!supportsFeature(ProtocolFeature::ValidatorListPropagation))
-        {
-            JLOG(pJournal_.debug()) << "ValidatorList: received validator list from peer using "
-                                    << "protocol version " << to_string(protocol_)
-                                    << " which shouldn't support this feature.";
-            fee_.update(resource::kFeeUselessData, "unsupported peer");
-            return;
-        }
-        onValidatorListMessage(
-            "ValidatorList", m->manifest(), m->version(), ValidatorList::parseBlobs(*m));
-    }
-    catch (std::exception const& e)
-    {
-        JLOG(pJournal_.warn()) << "ValidatorList: Exception, " << e.what();
-        using namespace std::string_literals;
-        fee_.update(resource::kFeeInvalidData, e.what());
-    }
-}
-
 void
 PeerImp::onMessage(std::shared_ptr const& m)
 {
     try
     {
-        if (!supportsFeature(ProtocolFeature::ValidatorList2Propagation))
-        {
-            JLOG(pJournal_.debug()) << "ValidatorListCollection: received validator list from peer "
-                                    << "using protocol version " << to_string(protocol_)
-                                    << " which shouldn't support this feature.";
-            fee_.update(resource::kFeeUselessData, "unsupported peer");
-            return;
-        }
         if (m->version() < 2)
         {
             JLOG(pJournal_.debug())
@@ -3123,8 +3088,9 @@ PeerImp::checkTransaction(
         if (checkSignature)
         {
             // Check the signature before handing off to the job queue.
-            if (auto [valid, validReason] = checkValidity(
-                    app_.getHashRouter(), *stx, app_.getLedgerMaster().getValidatedRules());
+            auto const& validatedRules = app_.getLedgerMaster().getValidatedRules();
+            if (auto [valid, validReason] =
+                    checkValidity(app_.getHashRouter(), *stx, validatedRules);
                 valid != Validity::Valid)
             {
                 if (!validReason.empty())
@@ -3132,9 +3098,20 @@ PeerImp::checkTransaction(
                     JLOG(pJournal_.debug()) << "Exception checking transaction: " << validReason;
                 }
 
-                // Probably not necessary to set HashRouterFlags::BAD, but
-                // doesn't hurt.
-                app_.getHashRouter().setFlags(stx->getTransactionID(), HashRouterFlags::BAD);
+                // For a role-signature transaction, only cache BAD once
+                // fixCleanup3_4_0 is enabled on this node: the SigBad verdict
+                // then covers the post-fix prefix and cannot flip back.
+                // Before the amendment activates, checkValidity's own
+                // era-scoped cache handles the repeat lookups; setting BAD
+                // would block a correctly new-prefix-signed transaction until
+                // the router entry ages out. Remove the guard together with
+                // the amendment.
+                if (validatedRules.enabled(fixCleanup3_4_0) ||
+                    (!stx->isFieldPresent(sfSponsorSignature) &&
+                     !stx->isFieldPresent(sfCounterpartySignature)))
+                {
+                    app_.getHashRouter().setFlags(stx->getTransactionID(), HashRouterFlags::BAD);
+                }
                 charge(resource::kFeeInvalidSignature, "check transaction signature failure");
                 return;
             }
diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h
index 7078d6fb56..0f229bf9d8 100644
--- a/src/xrpld/overlay/detail/PeerImp.h
+++ b/src/xrpld/overlay/detail/PeerImp.h
@@ -623,8 +623,6 @@ public:
     void
     onMessage(std::shared_ptr const& m);
     void
-    onMessage(std::shared_ptr const& m);
-    void
     onMessage(std::shared_ptr const& m);
     void
     onMessage(std::shared_ptr const& m);
diff --git a/src/xrpld/overlay/detail/ProtocolMessage.h b/src/xrpld/overlay/detail/ProtocolMessage.h
index f7d5e26272..88f50e1e2e 100644
--- a/src/xrpld/overlay/detail/ProtocolMessage.h
+++ b/src/xrpld/overlay/detail/ProtocolMessage.h
@@ -71,8 +71,6 @@ protocolMessageName(int type)
             return "status";
         case protocol::mtHAVE_SET:
             return "have_set";
-        case protocol::mtVALIDATOR_LIST:
-            return "validator_list";
         case protocol::mtVALIDATOR_LIST_COLLECTION:
             return "validator_list_collection";
         case protocol::mtVALIDATION:
@@ -424,9 +422,6 @@ invokeProtocolMessage(Buffers const& buffers, Handler& handler, std::size_t& hin
         case protocol::mtVALIDATION:
             success = detail::invoke(*header, buffers, handler);
             break;
-        case protocol::mtVALIDATOR_LIST:
-            success = detail::invoke(*header, buffers, handler);
-            break;
         case protocol::mtVALIDATOR_LIST_COLLECTION:
             success =
                 detail::invoke(*header, buffers, handler);
diff --git a/src/xrpld/overlay/detail/ProtocolVersion.cpp b/src/xrpld/overlay/detail/ProtocolVersion.cpp
index 2d5d0a56f7..74dad61828 100644
--- a/src/xrpld/overlay/detail/ProtocolVersion.cpp
+++ b/src/xrpld/overlay/detail/ProtocolVersion.cpp
@@ -14,7 +14,9 @@
 #include 
 #include 
 #include 
+#include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
@@ -27,36 +29,21 @@ namespace xrpl {
  */
 
 constexpr ProtocolVersion const kSupportedProtocolList[]{
-    {2, 1},
     {2, 2},
     {2, 3},
 };
 
-// This ugly construct ensures that supportedProtocolList is sorted in strictly
-// ascending order and doesn't contain any duplicates.
-// FIXME: With C++20 we can use std::is_sorted with an appropriate comparator
+// There should be at least one protocol we're willing to speak.
 static_assert(
-    []() constexpr -> bool {
-        auto const len =
-            std::distance(std::begin(kSupportedProtocolList), std::end(kSupportedProtocolList));
+    !std::ranges::empty(kSupportedProtocolList),
+    "There must be at least one supported protocol.");
 
-        // There should be at least one protocol we're willing to speak.
-        if (len == 0)
-            return false;
-
-        // A list with only one entry is, by definition, sorted so we don't
-        // need to check it.
-        if (len != 1)
-        {
-            for (auto i = 0; i != len - 1; ++i)
-            {
-                if (kSupportedProtocolList[i] >= kSupportedProtocolList[i + 1])
-                    return false;
-            }
-        }
-
-        return true;
-    }(),
+// Searching for an adjacent pair where the first element is not less than the
+// second one proves the list is sorted in strictly ascending order, which in
+// turn means it holds no duplicates.
+static_assert(
+    std::ranges::adjacent_find(kSupportedProtocolList, std::ranges::greater_equal{}) ==
+        std::ranges::end(kSupportedProtocolList),
     "The list of supported protocols isn't properly sorted.");
 
 std::string
@@ -66,7 +53,7 @@ to_string(ProtocolVersion const& p)
 }
 
 std::vector
-parseProtocolVersions(boost::beast::string_view const& value)
+parseProtocolVersions(std::string_view value)
 {
     static boost::regex const kRE(
         "^"                        // start of line
@@ -133,7 +120,7 @@ negotiateProtocolVersion(std::vector const& versions)
 }
 
 std::optional
-negotiateProtocolVersion(boost::beast::string_view const& versions)
+negotiateProtocolVersion(std::string_view versions)
 {
     auto const them = parseProtocolVersions(versions);
 
diff --git a/src/xrpld/overlay/detail/ProtocolVersion.h b/src/xrpld/overlay/detail/ProtocolVersion.h
index b56871318a..5c05f63e2a 100644
--- a/src/xrpld/overlay/detail/ProtocolVersion.h
+++ b/src/xrpld/overlay/detail/ProtocolVersion.h
@@ -1,10 +1,9 @@
 #pragma once
 
-#include 
-
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -43,7 +42,7 @@ to_string(ProtocolVersion const& p);
  *       no duplicates and will be sorted in ascending protocol order.
  */
 std::vector
-parseProtocolVersions(boost::beast::string_view const& s);
+parseProtocolVersions(std::string_view s);
 
 /**
  * Given a list of supported protocol versions, choose the one we prefer.
@@ -55,7 +54,7 @@ negotiateProtocolVersion(std::vector const& versions);
  * Given a list of supported protocol versions, choose the one we prefer.
  */
 std::optional
-negotiateProtocolVersion(boost::beast::string_view const& versions);
+negotiateProtocolVersion(std::string_view versions);
 
 /**
  * The list of all the protocol versions we support.
diff --git a/src/xrpld/overlay/detail/TrafficCount.cpp b/src/xrpld/overlay/detail/TrafficCount.cpp
index bdce9e68f0..90d5c0b4ff 100644
--- a/src/xrpld/overlay/detail/TrafficCount.cpp
+++ b/src/xrpld/overlay/detail/TrafficCount.cpp
@@ -14,7 +14,6 @@ std::unordered_map const kTypeLoo
     {protocol::mtMANIFESTS, TrafficCount::Category::Manifests},
     {protocol::mtENDPOINTS, TrafficCount::Category::Overlay},
     {protocol::mtTRANSACTION, TrafficCount::Category::Transaction},
-    {protocol::mtVALIDATOR_LIST, TrafficCount::Category::Validatorlist},
     {protocol::mtVALIDATOR_LIST_COLLECTION, TrafficCount::Category::Validatorlist},
     {protocol::mtVALIDATION, TrafficCount::Category::Validation},
     {protocol::mtPROPOSE_LEDGER, TrafficCount::Category::Proposal},
diff --git a/src/xrpld/perflog/detail/PerfLogImp.cpp b/src/xrpld/perflog/detail/PerfLogImp.cpp
index 3aa7e38ea2..2777e0dcdb 100644
--- a/src/xrpld/perflog/detail/PerfLogImp.cpp
+++ b/src/xrpld/perflog/detail/PerfLogImp.cpp
@@ -17,11 +17,9 @@
 #include 
 #include 
 
-#include 
-#include 
-
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -29,6 +27,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -220,10 +219,10 @@ PerfLogImp::openLog()
         logFile_.close();
 
     auto logDir = setup_.perfLog.parent_path();
-    if (!boost::filesystem::is_directory(logDir))
+    if (!std::filesystem::is_directory(logDir))
     {
-        boost::system::error_code ec;
-        boost::filesystem::create_directories(logDir, ec);
+        std::error_code ec;
+        std::filesystem::create_directories(logDir, ec);
         if (ec)
         {
             JLOG(j_.fatal()) << "Unable to create performance log "
@@ -478,17 +477,17 @@ PerfLogImp::stop()
 //-----------------------------------------------------------------------------
 
 PerfLog::Setup
-setupPerfLog(Section const& section, boost::filesystem::path const& configDir)
+setupPerfLog(Section const& section, std::filesystem::path const& configDir)
 {
     PerfLog::Setup setup;
     std::string perfLog;
     set(perfLog, "perf_log", section);
     if (!perfLog.empty())
     {
-        setup.perfLog = boost::filesystem::path(perfLog);
+        setup.perfLog = std::filesystem::path(perfLog);
         if (setup.perfLog.is_relative())
         {
-            setup.perfLog = boost::filesystem::absolute(setup.perfLog, configDir);
+            setup.perfLog = std::filesystem::absolute(configDir / setup.perfLog);
         }
     }
 
diff --git a/src/xrpld/rpc/AGENTS.md b/src/xrpld/rpc/AGENTS.md
new file mode 100644
index 0000000000..14fdd7a03e
--- /dev/null
+++ b/src/xrpld/rpc/AGENTS.md
@@ -0,0 +1,5 @@
+# AGENTS.md — rpc
+
+See the repo-level [AGENTS.md](../../../AGENTS.md) for general build/test/style guidance.
+
+Any change to a public RPC method's behavior (new/changed/removed fields, parameters, or error conditions) needs a corresponding entry in [`API-CHANGELOG.md`](../../../API-CHANGELOG.md), under the `## Unreleased` section (`### Additions`, `### Deprecations`, etc. as appropriate).
diff --git a/src/xrpld/rpc/BookChanges.h b/src/xrpld/rpc/BookChanges.h
index 16f7ea8e43..1912b0512e 100644
--- a/src/xrpld/rpc/BookChanges.h
+++ b/src/xrpld/rpc/BookChanges.h
@@ -7,6 +7,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -18,6 +19,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 
@@ -50,6 +52,36 @@ computeBookChanges(std::shared_ptr const& lpAccepted)
             std::optional>>  // optional: domain id
         tally;
 
+    // Accumulating volume can exceed what the asset can represent, and the two
+    // types fail differently: STAmount's IOU addition throws, while its MPT
+    // addition is a raw int64 add that wraps past kMaxMpTokenAmount to a
+    // negative amount. Reject both so that one extreme crossing cannot poison
+    // this ledger's report, which is otherwise permanent -- the ledger is
+    // immutable and the computation deterministic.
+    auto const checkedAdd = [](STAmount& acc, STAmount const& delta) {
+        return acc.asset().visit(
+            [&](Issue const&) {
+                try
+                {
+                    acc += delta;
+                }
+                catch (std::overflow_error const&)
+                {
+                    return false;
+                }
+                return true;
+            },
+            [&](MPTIssue const&) {
+                // Both volumes are non-negative by the time they reach the
+                // tally, so this cannot underflow.
+                auto const room = static_cast(kMaxMpTokenAmount) - acc.mpt().value();
+                if (delta.mpt().value() > room)
+                    return false;
+                acc += delta;
+                return true;
+            });
+    };
+
     for (auto& tx : lpAccepted->txs)
     {
         if (!tx.first || !tx.second || !tx.first->isFieldPresent(sfTransactionType))
@@ -123,7 +155,16 @@ computeBookChanges(std::shared_ptr const& lpAccepted)
             if (second == beast::kZero)
                 continue;
 
-            STAmount const rate = divide(first, second, noIssue());
+            std::optional maybeRate;
+            try
+            {
+                maybeRate = divide(first, second, noIssue());
+            }
+            catch (std::overflow_error const&)
+            {
+                continue;
+            }
+            STAmount const rate = *maybeRate;
 
             if (first < beast::kZero)
                 first = -first;
@@ -161,8 +202,15 @@ computeBookChanges(std::shared_ptr const& lpAccepted)
                 // increment volume
                 auto& entry = tally[key];
 
-                std::get<0>(entry) += first;   // side A vol
-                std::get<1>(entry) += second;  // side B vol
+                // Commit both sides or neither, so an overflow on the second
+                // cannot leave the entry half-updated. Skipping the crossing
+                // matches how an unrepresentable rate is handled above.
+                STAmount volA = std::get<0>(entry);
+                STAmount volB = std::get<1>(entry);
+                if (!checkedAdd(volA, first) || !checkedAdd(volB, second))
+                    continue;
+                std::get<0>(entry) = volA;  // side A vol
+                std::get<1>(entry) = volB;  // side B vol
 
                 if (std::get<2>(entry) < rate)  // high
                     std::get<2>(entry) = rate;
diff --git a/src/xrpld/rpc/CLAUDE.md b/src/xrpld/rpc/CLAUDE.md
new file mode 120000
index 0000000000..47dc3e3d86
--- /dev/null
+++ b/src/xrpld/rpc/CLAUDE.md
@@ -0,0 +1 @@
+AGENTS.md
\ No newline at end of file
diff --git a/src/xrpld/rpc/detail/AccountAssets.cpp b/src/xrpld/rpc/detail/AccountAssets.cpp
index 67b9174fe3..0e71836b74 100644
--- a/src/xrpld/rpc/detail/AccountAssets.cpp
+++ b/src/xrpld/rpc/detail/AccountAssets.cpp
@@ -49,7 +49,7 @@ accountSourceAssets(
     {
         for (auto const& rspEntry : *mpts)
         {
-            if (!rspEntry.isZeroBalance() && !rspEntry.isMaxedOut())
+            if (rspEntry.canSend(account))
                 assets.insert(rspEntry.getMptID());
         }
     }
@@ -86,8 +86,10 @@ accountDestAssets(
     {
         for (auto const& rspEntry : *mpts)
         {
-            if (rspEntry.isZeroBalance() && !rspEntry.isMaxedOut())
-                assets.insert(rspEntry.getMptID());
+            // Any cached MPT entry means this account already has an issuance
+            // or MPToken object. A maxed-out issuance does not prevent
+            // receiving existing MPT from another holder.
+            assets.insert(rspEntry.getMptID());
         }
     }
 
diff --git a/src/xrpld/rpc/detail/MPT.h b/src/xrpld/rpc/detail/MPT.h
index 93c8517539..68054b2d0b 100644
--- a/src/xrpld/rpc/detail/MPT.h
+++ b/src/xrpld/rpc/detail/MPT.h
@@ -1,5 +1,7 @@
 #pragma once
 
+#include 
+#include 
 #include 
 
 namespace xrpl {
@@ -31,14 +33,11 @@ public:
         return mptID_;
     }
     [[nodiscard]] bool
-    isZeroBalance() const
+    canSend(AccountID const& account) const
     {
-        return zeroBalance_;
-    }
-    [[nodiscard]] bool
-    isMaxedOut() const
-    {
-        return maxedOut_;
+        // A maxed-out issuance only prevents the issuer from creating more
+        // MPT. Holders can still send existing balances.
+        return account == getMPTIssuer(mptID_) ? !maxedOut_ : !zeroBalance_;
     }
 };
 
diff --git a/src/xrpld/rpc/detail/PathRequest.cpp b/src/xrpld/rpc/detail/PathRequest.cpp
index fb132199bc..0a01031ae2 100644
--- a/src/xrpld/rpc/detail/PathRequest.cpp
+++ b/src/xrpld/rpc/detail/PathRequest.cpp
@@ -416,20 +416,22 @@ PathRequest::parseJson(json::Value const& jvParams)
                 // If the assets don't match, ignore the source asset.
                 if (srcPathAsset == saSendMax_->asset())
                 {
-                    // If neither is the source and they are not equal, then the
-                    // source issuer is illegal.
-                    if (srcIssuerID != *raSrcAccount_ &&
-                        saSendMax_->getIssuer() != *raSrcAccount_ &&
-                        srcIssuerID != saSendMax_->getIssuer())
-                    {
-                        jvStatus_ = rpcError(RpcSrcIsrMalformed);
-                        return PFR_PJ_INVALID;
-                    }
-
-                    // If both are the source, use the source.
-                    // Otherwise, use the one that's not the source.
-                    srcPathAsset.visit(
+                    auto const status = srcPathAsset.visit(
                         [&](Currency const& currency) {
+                            // If neither is the source and they are not equal,
+                            // then the source issuer is illegal. srcIssuerID
+                            // comes from the optional IOU source_currencies
+                            // issuer field, so this reconciliation is IOU-only.
+                            if (srcIssuerID != *raSrcAccount_ &&
+                                saSendMax_->getIssuer() != *raSrcAccount_ &&
+                                srcIssuerID != saSendMax_->getIssuer())
+                            {
+                                jvStatus_ = rpcError(RpcSrcIsrMalformed);
+                                return PFR_PJ_INVALID;
+                            }
+
+                            // If both are the source, use the source.
+                            // Otherwise, use the one that's not the source.
                             if (srcIssuerID != *raSrcAccount_)
                             {
                                 sciSourceAssets_.insert(Issue{currency, srcIssuerID});
@@ -438,11 +440,18 @@ PathRequest::parseJson(json::Value const& jvParams)
                             {
                                 sciSourceAssets_.insert(Issue{currency, saSendMax_->getIssuer()});
                             }
+                            else
                             {
                                 sciSourceAssets_.insert(Issue{currency, *raSrcAccount_});
                             }
+                            return PFR_PJ_NOCHANGE;
                         },
-                        [&](MPTID const& mpt) { sciSourceAssets_.insert(mpt); });
+                        [&](MPTID const& mpt) {
+                            sciSourceAssets_.insert(mpt);
+                            return PFR_PJ_NOCHANGE;
+                        });
+                    if (status == PFR_PJ_INVALID)
+                        return status;
                 }
             }
             else
diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp b/src/xrpld/rpc/detail/Pathfinder.cpp
index 642b5c4253..1f530a1165 100644
--- a/src/xrpld/rpc/detail/Pathfinder.cpp
+++ b/src/xrpld/rpc/detail/Pathfinder.cpp
@@ -224,7 +224,7 @@ Pathfinder::Pathfinder(
     , dstAmount_(saDstAmount)
     , srcPathAsset_(uSrcPathAsset)
     , srcIssuer_(uSrcIssuer)
-    , srcAmount_(amountFromPathAsset(uSrcPathAsset, uSrcIssuer, uSrcAccount))
+    , srcAmount_(srcAmount.value_or(amountFromPathAsset(uSrcPathAsset, uSrcIssuer, uSrcAccount)))
     , convertAll_(convertAllCheck(dstAmount_))
     , domain_(domain)
     , ledger_(cache->getLedger())
@@ -815,8 +815,8 @@ Pathfinder::getPathsOut(
                 {
                     for (auto const& mpt : *mpts)
                     {
-                        if (pathAsset.get() != mpt.getMptID() || mpt.isZeroBalance() ||
-                            mpt.isMaxedOut() || bAuthRequired)
+                        if (pathAsset.get() != mpt.getMptID() || !mpt.canSend(account) ||
+                            bAuthRequired)
                             continue;
                         if (isDstAsset && dstAccount == getMPTIssuer(mpt))
                         {
@@ -1079,7 +1079,10 @@ Pathfinder::addLink(
                             }
                             if constexpr (kIsMpt)
                             {
-                                return asset.isZeroBalance() || asset.isMaxedOut() ||
+                                // `asset` came from uEndAccount's cached MPTs.
+                                // `acct` is the next issuer hop, not the
+                                // account whose balance is being tested.
+                                return !asset.canSend(uEndAccount) ||
                                     requireAuth(*ledger_, MPTIssue{asset}, acct);
                             }
                         };
diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp
index 619dd44638..a822df2f05 100644
--- a/src/xrpld/rpc/detail/RPCHelpers.cpp
+++ b/src/xrpld/rpc/detail/RPCHelpers.cpp
@@ -42,6 +42,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -429,7 +430,7 @@ parseSubUnsubJson(
     if (jv.isMember(jss::mpt_issuance_id) &&
         (jv.isMember(jss::currency) || jv.isMember(jss::issuer)))
     {
-        JLOG(j.info()) << boost::format("Bad %s currency or MPT.") % name.cStr();
+        JLOG(j.info()) << std::format("Bad {} currency or MPT.", name.cStr());
         return RpcInvalidParams;
     }
 
@@ -440,7 +441,7 @@ parseSubUnsubJson(
         if (!jv.isMember(jss::currency) ||
             !toCurrency(issue.currency, jv[jss::currency].asString()))
         {
-            JLOG(j.info()) << boost::format("Bad %s currency.") % name.cStr();
+            JLOG(j.info()) << std::format("Bad {} currency.", name.cStr());
             return assetError;
         }
 
@@ -450,7 +451,7 @@ parseSubUnsubJson(
             // Don't allow illegal issuers.
             || (!issue.currency != !issue.account) || noAccount() == issue.account)
         {
-            JLOG(j.info()) << boost::format("Bad %s issuer.") % name.cStr();
+            JLOG(j.info()) << std::format("Bad {} issuer.", name.cStr());
             return issuerError;
         }
         asset = issue;
@@ -464,7 +465,7 @@ parseSubUnsubJson(
     }
     else
     {
-        JLOG(j.info()) << boost::format("Neither %s currency or MPT is present.") % name.cStr();
+        JLOG(j.info()) << std::format("Neither {} currency or MPT is present.", name.cStr());
         return assetError;
     }
 
diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp
index 9fc3465047..3e45f52241 100644
--- a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp
+++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp
@@ -9,6 +9,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -28,6 +29,7 @@
 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl::rpc {
@@ -331,6 +333,13 @@ getLedger<>(std::shared_ptr&, LedgerShortcut shortcut, Context c
 template Status
 getLedger<>(std::shared_ptr&, uint256 const&, Context const&);
 
+// explicit instantiation of ledgerFromSpecifier
+template Status
+ledgerFromSpecifier<>(
+    std::shared_ptr&,
+    org::xrpl::rpc::v1::LedgerSpecifier const&,
+    Context const&);
+
 // The previous version of the lookupLedger command would accept the
 // "ledger_index" argument as a string and silently treat it as a request to
 // return the current ledger which, while not strictly wrong, could cause a lot
@@ -520,10 +529,10 @@ injectSLE(json::Value& jv, SLE const& sle)
             auto const& hash = sle.getFieldH128(sfEmailHash);
             Blob const b(hash.begin(), hash.end());
             std::string md5 = strHex(makeSlice(b));
-            boost::to_lower(md5);
+            md5 = toLower(md5);
             // VFALCO TODO Give a name to this constant and move it
             //             to a more visible location.
-            jv[jss::urlgravatar] = str(boost::format("https://www.gravatar.com/avatar/%s") % md5);
+            jv[jss::urlgravatar] = std::format("https://www.gravatar.com/avatar/{}", md5);
         }
     }
     else
diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp b/src/xrpld/rpc/detail/ServerHandler.cpp
index 0181d5b10f..28e7eebd63 100644
--- a/src/xrpld/rpc/detail/ServerHandler.cpp
+++ b/src/xrpld/rpc/detail/ServerHandler.cpp
@@ -8,6 +8,7 @@
 #include 
 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -44,7 +45,6 @@
 #include 
 #include 
 
-#include 
 #include 
 #include 
 #include 
@@ -113,7 +113,7 @@ authorized(Port const& port, std::map const& h)
     if ((it == h.end()) || (!it->second.starts_with("Basic ")))
         return false;
     std::string strUserPass64 = it->second.substr(6);
-    boost::trim(strUserPass64);
+    strUserPass64 = trimWhitespace(strUserPass64);
     std::string const strUserPass = base64Decode(strUserPass64);
     std::string::size_type const nColon = strUserPass.find(':');
     if (nColon == std::string::npos)
@@ -264,7 +264,7 @@ ServerHandler::onHandoff(
 static inline json::Output
 makeOutput(Session& session)
 {
-    return [&](boost::beast::string_view const& b) { session.write(b.data(), b.size()); };
+    return [&](std::string_view b) { session.write(b.data(), b.size()); };
 }
 
 static std::map
@@ -564,11 +564,11 @@ ServerHandler::processSession(
         makeOutput(*session),
         coro,
         forwardedFor(session->request()),
-        [&] {
+        [&] -> std::string_view {
             auto const iter = session->request().find("X-User");
             if (iter != session->request().end())
                 return iter->value();
-            return boost::beast::string_view{};
+            return {};
         }());
 
     if (beast::rfc2616::isKeepAlive(session->request()))
diff --git a/src/xrpld/rpc/detail/SyntheticFields.cpp b/src/xrpld/rpc/detail/SyntheticFields.cpp
new file mode 100644
index 0000000000..9acf3abb36
--- /dev/null
+++ b/src/xrpld/rpc/detail/SyntheticFields.cpp
@@ -0,0 +1,40 @@
+#include 
+
+#include 
+#include 
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl::rpc {
+
+void
+insertAllSyntheticInJson(
+    json::Value& metadata,
+    ReadView const& ledger,
+    std::shared_ptr const& transaction,
+    TxMeta const& transactionMeta)
+{
+    insertDeliveredAmount(metadata, ledger, transaction, transactionMeta);
+    insertNFTokenID(metadata, transaction, transactionMeta);
+    insertNFTokenOfferID(metadata, transaction, transactionMeta);
+    insertMPTokenIssuanceID(metadata, transaction, transactionMeta);
+}
+
+void
+insertAllSyntheticInJson(
+    json::Value& metadata,
+    JsonContext const& context,
+    std::shared_ptr const& transaction,
+    TxMeta const& transactionMeta)
+{
+    insertDeliveredAmount(metadata, context, transaction, transactionMeta);
+    insertNFTokenID(metadata, transaction, transactionMeta);
+    insertNFTokenOfferID(metadata, transaction, transactionMeta);
+    insertMPTokenIssuanceID(metadata, transaction, transactionMeta);
+}
+
+}  // namespace xrpl::rpc
diff --git a/src/xrpld/rpc/detail/SyntheticFields.h b/src/xrpld/rpc/detail/SyntheticFields.h
new file mode 100644
index 0000000000..6ece4bbcd5
--- /dev/null
+++ b/src/xrpld/rpc/detail/SyntheticFields.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include 
+#include 
+#include 
+
+#include 
+
+namespace xrpl {
+
+class ReadView;
+
+namespace rpc {
+
+struct JsonContext;
+
+/**
+ * Adds all synthetic fields to transaction metadata JSON.
+ * This includes delivered amount, NFT synthetic fields, and MPToken issuance
+ * ID.
+ */
+/** @{ */
+void
+insertAllSyntheticInJson(
+    json::Value& metadata,
+    ReadView const&,
+    std::shared_ptr const&,
+    TxMeta const&);
+
+void
+insertAllSyntheticInJson(
+    json::Value& metadata,
+    JsonContext const&,
+    std::shared_ptr const&,
+    TxMeta const&);
+/** @} */
+
+}  // namespace rpc
+}  // namespace xrpl
diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp
index 9c97577b27..3e9f62214e 100644
--- a/src/xrpld/rpc/detail/TransactionSign.cpp
+++ b/src/xrpld/rpc/detail/TransactionSign.cpp
@@ -460,7 +460,8 @@ transactionPreProcessImpl(
     Role role,
     SigningForParams& signingArgs,
     std::chrono::seconds validatedLedgerAge,
-    Application& app)
+    Application& app,
+    Rules const& rules)
 {
     auto j = app.getJournal("RPCHandler");
 
@@ -482,13 +483,16 @@ transactionPreProcessImpl(
     }();
 
     // Make sure the signature target field is valid, if specified, and save the
-    // template for use later
+    // template for use later. Only a field that holds a transaction signature
+    // is a valid target; the signature is bound to that field's role.
     auto const signatureTemplate = signatureTarget
         ? InnerObjectFormats::getInstance().findSOTemplateBySField(*signatureTarget)
         : nullptr;
+    auto const signatureRoleOpt =
+        signatureTarget ? signatureRole(signatureTarget->get()) : SignatureRole::Transaction;
     if (signatureTarget)
     {
-        if (signatureTemplate == nullptr)
+        if (!signatureRoleOpt || signatureTemplate == nullptr)
         {  // Invalid target field
             return rpc::makeError(RpcInvalidParams, signatureTarget->get().getName());
         }
@@ -687,7 +691,8 @@ transactionPreProcessImpl(
     // If multisign then return multiSignature, else set TxnSignature field.
     if (signingArgs.isMultiSigning())
     {
-        Serializer const s = buildMultiSigningData(*stTx, signingArgs.getSigner());
+        Serializer const s = buildMultiSigningData(
+            *stTx, signingArgs.getSigner(), signingPrefix(*signatureRoleOpt, true, rules));
 
         auto multisig = xrpl::sign(pk, sk, s.slice());
 
@@ -695,7 +700,7 @@ transactionPreProcessImpl(
     }
     else if (signingArgs.isSingleSigning())
     {
-        stTx->sign(pk, sk, signatureTarget);
+        stTx->sign(pk, sk, *signatureRoleOpt, rules);
     }
 
     return TransactionPreProcessResult{std::move(stTx)};
@@ -1007,18 +1012,20 @@ transactionSign(
 {
     using namespace detail;
 
+    // Sign and verify against the same ruleset: a ledger close in between
+    // could change the signing prefix of an alternate signature field.
+    std::shared_ptr const ledger = app.getOpenLedger().current();
     auto j = app.getJournal("RPCHandler");
     JLOG(j.debug()) << "transactionSign: " << jvRequest;
 
     // Add and amend fields based on the transaction type.
     SigningForParams signForParams;
-    TransactionPreProcessResult const preprocResult =
-        transactionPreProcessImpl(jvRequest, role, signForParams, validatedLedgerAge, app);
+    TransactionPreProcessResult const preprocResult = transactionPreProcessImpl(
+        jvRequest, role, signForParams, validatedLedgerAge, app, ledger->rules());
 
     if (!preprocResult.second)
         return preprocResult.first;
 
-    std::shared_ptr const ledger = app.getOpenLedger().current();
     // Make sure the STTx makes a legitimate Transaction.
     std::pair const txn =
         transactionConstructImpl(preprocResult.second, ledger->rules(), app);
@@ -1050,8 +1057,8 @@ transactionSubmit(
 
     // Add and amend fields based on the transaction type.
     SigningForParams signForParams;
-    TransactionPreProcessResult const preprocResult =
-        transactionPreProcessImpl(jvRequest, role, signForParams, validatedLedgerAge, app);
+    TransactionPreProcessResult const preprocResult = transactionPreProcessImpl(
+        jvRequest, role, signForParams, validatedLedgerAge, app, ledger->rules());
 
     if (!preprocResult.second)
         return preprocResult.first;
@@ -1218,8 +1225,8 @@ transactionSignFor(
     // Add and amend fields based on the transaction type.
     SigningForParams signForParams(*signerAccountID);
 
-    TransactionPreProcessResult const preprocResult =
-        transactionPreProcessImpl(jvRequest, role, signForParams, validatedLedgerAge, app);
+    TransactionPreProcessResult const preprocResult = transactionPreProcessImpl(
+        jvRequest, role, signForParams, validatedLedgerAge, app, ledger->rules());
 
     if (!preprocResult.second)
         return preprocResult.first;
diff --git a/src/xrpld/rpc/handlers/VaultInfo.cpp b/src/xrpld/rpc/handlers/VaultInfo.cpp
index c216192ab3..0aa5334bd2 100644
--- a/src/xrpld/rpc/handlers/VaultInfo.cpp
+++ b/src/xrpld/rpc/handlers/VaultInfo.cpp
@@ -26,36 +26,48 @@ parseVault(json::Value const& params, json::Value& jvResult)
     uint256 uNodeIndex = beast::kZero;
     if (hasVaultId && !hasOwner && !hasSeq)
     {
-        if (!uNodeIndex.parseHex(params[jss::vault_id].asString()))
+        // asString() throws on an object or an array, so the type comes first.
+        auto const& vaultId = params[jss::vault_id];
+        if (!vaultId.isString() || !uNodeIndex.parseHex(vaultId.asString()))
         {
-            rpc::injectError(RpcInvalidParams, jvResult);
+            rpc::injectError(
+                RpcInvalidParams, rpc::expectedFieldMessage(jss::vault_id, "hex string"), jvResult);
             return std::nullopt;
         }
         // else uNodeIndex holds the value we need
     }
     else if (!hasVaultId && hasOwner && hasSeq)
     {
-        auto const id = parseBase58(params[jss::owner].asString());
+        auto const& owner = params[jss::owner];
+        auto const id = owner.isString() ? parseBase58(owner.asString())
+                                         : std::optional{};
         if (!id)
         {
-            rpc::injectError(RpcActMalformed, jvResult);
-            return std::nullopt;
-        }
-        if (!(params[jss::seq].isInt() || params[jss::seq].isUInt()) ||
-            params[jss::seq].asDouble() <= 0.0 ||
-            params[jss::seq].asDouble() > double(json::Value::kMaxUInt))
-        {
-            rpc::injectError(RpcInvalidParams, jvResult);
+            rpc::injectError(
+                RpcActMalformed, rpc::expectedFieldMessage(jss::owner, "AccountID"), jvResult);
             return std::nullopt;
         }
 
-        auto const seq = SeqProxy::rawSequence(params[jss::seq].asUInt());
+        // Int and UInt are both 32 bits wide, so the type check is the only upper bound needed.
+        auto const& seqField = params[jss::seq];
+        if (!(seqField.isInt() || seqField.isUInt()) || seqField.asDouble() <= 0.0)
+        {
+            rpc::injectError(
+                RpcInvalidParams,
+                rpc::expectedFieldMessage(jss::seq, "a positive 32-bit integer"),
+                jvResult);
+            return std::nullopt;
+        }
+
+        auto const seq = SeqProxy::rawSequence(seqField.asUInt());
         uNodeIndex = keylet::vault(*id, seq).key;
     }
     else
     {
-        // Invalid combination of fields vault_id/owner/seq
-        rpc::injectError(RpcInvalidParams, jvResult);
+        rpc::injectError(
+            RpcInvalidParams,
+            "Must specify either 'vault_id' or both 'owner' and 'seq'.",
+            jvResult);
         return std::nullopt;
     }
 
@@ -71,20 +83,25 @@ doVaultInfo(rpc::JsonContext& context)
     if (!lpLedger)
         return jvResult;
 
-    auto const uNodeIndex = parseVault(context.params, jvResult).value_or(beast::kZero);
-    if (uNodeIndex == beast::kZero)
+    // No key means the request could not be turned into one, and parseVault has already said why.
+    auto const uNodeIndex = parseVault(context.params, jvResult);
+    if (!uNodeIndex)
+        return jvResult;
+
+    // A zero key names an entry that cannot exist, and the ledger refuses to be asked for one.
+    if (*uNodeIndex == beast::kZero)
     {
-        jvResult[jss::error] = "malformedRequest";
+        rpc::injectError(RpcEntryNotFound, jvResult);
         return jvResult;
     }
 
-    auto const sleVault = lpLedger->read(keylet::vault(uNodeIndex));
+    auto const sleVault = lpLedger->read(keylet::vault(*uNodeIndex));
     auto const sleIssuance = sleVault == nullptr  //
         ? nullptr
         : lpLedger->read(keylet::mptokenIssuance(sleVault->at(sfShareMPTID)));
     if (!sleVault || !sleIssuance)
     {
-        jvResult[jss::error] = "entryNotFound";
+        rpc::injectError(RpcEntryNotFound, jvResult);
         return jvResult;
     }
 
diff --git a/src/xrpld/rpc/handlers/account/AccountChannels.cpp b/src/xrpld/rpc/handlers/account/AccountChannels.cpp
index d50bf1cf07..f2da1e31ee 100644
--- a/src/xrpld/rpc/handlers/account/AccountChannels.cpp
+++ b/src/xrpld/rpc/handlers/account/AccountChannels.cpp
@@ -3,6 +3,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -22,9 +23,6 @@
 #include 
 #include 
 
-#include 
-#include 
-
 #include 
 #include 
 #include 
@@ -129,7 +127,7 @@ doAccountChannels(rpc::JsonContext& context)
             return rpc::expectedFieldError(jss::marker, "string");
 
         // Marker is composed of a comma separated index and start hint. The
-        // former will be read as hex, and the latter using boost lexical cast.
+        // former will be read as hex, and the latter as a decimal integer.
         std::stringstream marker(params[jss::marker].asString());
         std::string value;
         if (!std::getline(marker, value, ','))
@@ -141,14 +139,10 @@ doAccountChannels(rpc::JsonContext& context)
         if (!std::getline(marker, value, ','))
             return rpcError(RpcInvalidParams);
 
-        try
-        {
-            startHint = boost::lexical_cast(value);
-        }
-        catch (boost::bad_lexical_cast&)
-        {
+        auto const hint = toUInt64(value);
+        if (!hint.has_value())
             return rpcError(RpcInvalidParams);
-        }
+        startHint = *hint;
 
         // We then must check if the object pointed to by the marker is actually
         // owned by the account in the request.
diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp b/src/xrpld/rpc/handlers/account/AccountInfo.cpp
index 02778abad5..7f48e8f285 100644
--- a/src/xrpld/rpc/handlers/account/AccountInfo.cpp
+++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp
@@ -5,6 +5,8 @@
 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
@@ -22,15 +24,14 @@
 #include 
 #include 
 
-#include 
-#include 
-
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 namespace xrpl {
@@ -87,29 +88,37 @@ doAccountInfo(rpc::JsonContext& context)
     }
     auto const accountID{id.value()};
 
-    static constexpr std::array, 9> kLsFlags{
-        {{"defaultRipple", lsfDefaultRipple},
-         {"depositAuth", lsfDepositAuth},
-         {"disableMasterKey", lsfDisableMaster},
-         {"disallowIncomingXRP", lsfDisallowXRP},
-         {"globalFreeze", lsfGlobalFreeze},
-         {"noFreeze", lsfNoFreeze},
-         {"passwordSpent", lsfPasswordSpent},
-         {"requireAuthorization", lsfRequireAuth},
-         {"requireDestinationTag", lsfRequireDestTag}}};
-
-    static constexpr std::array, 4>
-        kDisallowIncomingFlags{
-            {{"disallowIncomingNFTokenOffer", lsfDisallowIncomingNFTokenOffer},
+    // Flags that are always reported.
+    static constexpr auto kAccountRootFlags =
+        std::to_array>(
+            {{"allowTrustLineClawback", lsfAllowTrustLineClawback},
+             {"defaultRipple", lsfDefaultRipple},
+             {"depositAuth", lsfDepositAuth},
+             {"disableMasterKey", lsfDisableMaster},
              {"disallowIncomingCheck", lsfDisallowIncomingCheck},
+             {"disallowIncomingNFTokenOffer", lsfDisallowIncomingNFTokenOffer},
              {"disallowIncomingPayChan", lsfDisallowIncomingPayChan},
-             {"disallowIncomingTrustline", lsfDisallowIncomingTrustline}}};
+             {"disallowIncomingTrustline", lsfDisallowIncomingTrustline},
+             {"disallowIncomingXRP", lsfDisallowXRP},
+             {"globalFreeze", lsfGlobalFreeze},
+             {"noFreeze", lsfNoFreeze},
+             {"passwordSpent", lsfPasswordSpent},
+             {"requireAuthorization", lsfRequireAuth},
+             {"requireDestinationTag", lsfRequireDestTag}});
 
-    static constexpr std::pair kAllowTrustLineClawbackFlag{
-        "allowTrustLineClawback", lsfAllowTrustLineClawback};
+    // Flags that are only reported when their amendment is enabled. This can't be `constexpr`,
+    // since the amendment IDs are computed at runtime.
+    static auto const kAmendmentGatedFlags =
+        std::to_array>(
+            {{"allowTrustLineLocking", lsfAllowTrustLineLocking, featureTokenEscrow}});
 
-    static constexpr std::pair kAllowTrustLineLockingFlag{
-        "allowTrustLineLocking", lsfAllowTrustLineLocking};
+    // Every `AccountRoot` flag must be reported by `account_info`, so if a new flag is added, it
+    // needs to be added to one of the arrays above. This can't be a `static_assert` because
+    // `getAccountRootFlags()` builds its map at runtime.
+    XRPL_ASSERT_PARTS(
+        kAccountRootFlags.size() + kAmendmentGatedFlags.size() == getAccountRootFlags().size(),
+        "xrpl::doAccountInfo",
+        "number of account flags");
 
     auto const sleAccepted = ledger->read(keylet::account(accountID));
     if (sleAccepted)
@@ -129,19 +138,13 @@ doAccountInfo(rpc::JsonContext& context)
         result[jss::account_data] = jvAccepted;
 
         json::Value acctFlags{json::ValueType::Object};
-        for (auto const& lsf : kLsFlags)
-            acctFlags[lsf.first.data()] = sleAccepted->isFlag(lsf.second);
+        for (auto const& [name, flag] : kAccountRootFlags)
+            acctFlags[name.data()] = sleAccepted->isFlag(flag);
 
-        for (auto const& lsf : kDisallowIncomingFlags)
-            acctFlags[lsf.first.data()] = sleAccepted->isFlag(lsf.second);
-
-        acctFlags[kAllowTrustLineClawbackFlag.first.data()] =
-            sleAccepted->isFlag(kAllowTrustLineClawbackFlag.second);
-
-        if (ledger->rules().enabled(featureTokenEscrow))
+        for (auto const& [name, flag, amendment] : kAmendmentGatedFlags)
         {
-            acctFlags[kAllowTrustLineLockingFlag.first.data()] =
-                sleAccepted->isFlag(kAllowTrustLineLockingFlag.second);
+            if (ledger->rules().enabled(amendment))
+                acctFlags[name.data()] = sleAccepted->isFlag(flag);
         }
 
         result[jss::account_flags] = std::move(acctFlags);
diff --git a/src/xrpld/rpc/handlers/account/AccountLines.cpp b/src/xrpld/rpc/handlers/account/AccountLines.cpp
index f134c8af92..ac98e271b6 100644
--- a/src/xrpld/rpc/handlers/account/AccountLines.cpp
+++ b/src/xrpld/rpc/handlers/account/AccountLines.cpp
@@ -4,6 +4,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -22,9 +23,6 @@
 #include 
 #include 
 
-#include 
-#include 
-
 #include 
 #include 
 #include 
@@ -109,7 +107,12 @@ doAccountLines(rpc::JsonContext& context)
 
     std::string strPeer;
     if (params.isMember(jss::peer))
+    {
+        if (!params[jss::peer].isString())
+            return rpc::invalidFieldError(jss::peer);
+
         strPeer = params[jss::peer].asString();
+    }
 
     auto const raPeerAccount = [&]() -> std::optional {
         return strPeer.empty() ? std::nullopt : parseBase58(strPeer);
@@ -153,7 +156,7 @@ doAccountLines(rpc::JsonContext& context)
             return rpc::expectedFieldError(jss::marker, "string");
 
         // Marker is composed of a comma separated index and start hint. The
-        // former will be read as hex, and the latter using boost lexical cast.
+        // former will be read as hex, and the latter as a decimal integer.
         std::stringstream marker(params[jss::marker].asString());
         std::string value;
         if (!std::getline(marker, value, ','))
@@ -165,14 +168,10 @@ doAccountLines(rpc::JsonContext& context)
         if (!std::getline(marker, value, ','))
             return rpcError(RpcInvalidParams);
 
-        try
-        {
-            startHint = boost::lexical_cast(value);
-        }
-        catch (boost::bad_lexical_cast&)
-        {
+        auto const hint = toUInt64(value);
+        if (!hint.has_value())
             return rpcError(RpcInvalidParams);
-        }
+        startHint = *hint;
 
         // We then must check if the object pointed to by the marker is actually
         // owned by the account in the request.
diff --git a/src/xrpld/rpc/handlers/account/AccountOffers.cpp b/src/xrpld/rpc/handlers/account/AccountOffers.cpp
index 1467b14b48..a7933f65a7 100644
--- a/src/xrpld/rpc/handlers/account/AccountOffers.cpp
+++ b/src/xrpld/rpc/handlers/account/AccountOffers.cpp
@@ -3,6 +3,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -20,9 +21,6 @@
 #include 
 #include 
 
-#include 
-#include 
-
 #include 
 #include 
 #include 
@@ -97,7 +95,7 @@ doAccountOffers(rpc::JsonContext& context)
             return rpc::expectedFieldError(jss::marker, "string");
 
         // Marker is composed of a comma separated index and start hint. The
-        // former will be read as hex, and the latter using boost lexical cast.
+        // former will be read as hex, and the latter as a decimal integer.
         std::stringstream marker(params[jss::marker].asString());
         std::string value;
         if (!std::getline(marker, value, ','))
@@ -109,14 +107,10 @@ doAccountOffers(rpc::JsonContext& context)
         if (!std::getline(marker, value, ','))
             return rpc::invalidFieldError(jss::marker);
 
-        try
-        {
-            startHint = boost::lexical_cast(value);
-        }
-        catch (boost::bad_lexical_cast&)
-        {
+        auto const hint = toUInt64(value);
+        if (!hint.has_value())
             return rpc::invalidFieldError(jss::marker);
-        }
+        startHint = *hint;
 
         // We then must check if the object pointed to by the marker is actually
         // owned by the account in the request.
diff --git a/src/xrpld/rpc/handlers/account/AccountTx.cpp b/src/xrpld/rpc/handlers/account/AccountTx.cpp
index 7b0c34e048..5385776b36 100644
--- a/src/xrpld/rpc/handlers/account/AccountTx.cpp
+++ b/src/xrpld/rpc/handlers/account/AccountTx.cpp
@@ -4,12 +4,11 @@
 #include 
 #include 
 #include 
-#include 
-#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include 
@@ -22,7 +21,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -378,9 +376,7 @@ populateJsonResponse(
                     if (txnMeta)
                     {
                         jvObj[jss::meta] = txnMeta->getJson(JsonOptions::Values::IncludeDate);
-                        insertDeliveredAmount(jvObj[jss::meta], context, txn, *txnMeta);
-                        rpc::insertNFTSyntheticInJson(jvObj, sttx, *txnMeta);
-                        rpc::insertMPTokenIssuanceID(jvObj[jss::meta], sttx, *txnMeta);
+                        rpc::insertAllSyntheticInJson(jvObj[jss::meta], context, sttx, *txnMeta);
                     }
                     else
                     {
diff --git a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp
index ff19d1d1e5..041e878a3f 100644
--- a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp
+++ b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp
@@ -63,6 +63,12 @@ doGatewayBalances(rpc::JsonContext& context)
     if (!(params.isMember(jss::account) || params.isMember(jss::ident)))
         return rpc::missingFieldError(jss::account);
 
+    if (params.isMember(jss::account) && !params[jss::account].isString())
+        return rpc::invalidFieldError(jss::account);
+
+    if (params.isMember(jss::ident) && !params[jss::ident].isString())
+        return rpc::invalidFieldError(jss::ident);
+
     std::string const strIdent(
         params.isMember(jss::account) ? params[jss::account].asString()
                                       : params[jss::ident].asString());
diff --git a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp
index 91db16bb4f..5c96bfb215 100644
--- a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp
+++ b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp
@@ -3,14 +3,13 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 #include 
 #include 
 #include 
 
-#include 
-
 #include 
 #include 
 #include 
@@ -38,7 +37,7 @@ doCanDelete(rpc::JsonContext& context)
         else
         {
             std::string canDeleteStr = canDelete.asString();
-            boost::to_lower(canDeleteStr);
+            canDeleteStr = toLower(canDeleteStr);
 
             if (canDeleteStr.find_first_not_of("0123456789") == std::string::npos)
             {
diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp
index e72b55379b..fdc33baaae 100644
--- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp
+++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp
@@ -864,15 +864,12 @@ parseContractSource(
         return parseObjectID(params, fieldName);
     }
 
-    auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner");
-    if (!id)
-        return std::unexpected(id.error());
+    auto const contractHash =
+        ledger_entry_helpers::requiredUInt256(params, jss::contract_hash, "malformedRequest");
+    if (!contractHash)
+        return std::unexpected(contractHash.error());
 
-    auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest");
-    if (!seq)
-        return std::unexpected(seq.error());
-
-    return keylet::vault(*id, *seq).key;
+    return keylet::contractSource(*contractHash).key;
 }
 
 static std::expected
@@ -886,15 +883,21 @@ parseContract(
         return parseObjectID(params, fieldName);
     }
 
-    auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner");
-    if (!id)
-        return std::unexpected(id.error());
+    auto const contractHash =
+        ledger_entry_helpers::requiredUInt256(params, jss::contract_hash, "malformedRequest");
+    if (!contractHash)
+        return std::unexpected(contractHash.error());
 
-    auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest");
+    auto const owner =
+        ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
+    if (!owner)
+        return std::unexpected(owner.error());
+
+    auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedRequest");
     if (!seq)
         return std::unexpected(seq.error());
 
-    return keylet::vault(*id, *seq).key;
+    return keylet::contract(*contractHash, *owner, *seq).key;
 }
 
 static std::expected
@@ -908,15 +911,17 @@ parseContractData(
         return parseObjectID(params, fieldName);
     }
 
-    auto const id = LedgerEntryHelpers::requiredAccountID(params, jss::owner, "malformedOwner");
-    if (!id)
-        return std::unexpected(id.error());
+    auto const owner =
+        ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner");
+    if (!owner)
+        return std::unexpected(owner.error());
 
-    auto const seq = LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedRequest");
-    if (!seq)
-        return std::unexpected(seq.error());
+    auto const contractAccount = ledger_entry_helpers::requiredAccountID(
+        params, jss::contract_account, "malformedContractAccount");
+    if (!contractAccount)
+        return std::unexpected(contractAccount.error());
 
-    return keylet::vault(*id, *seq).key;
+    return keylet::contractData(*owner, *contractAccount).key;
 }
 
 struct LedgerEntry
diff --git a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp
index ae539a59f3..219c29d53a 100644
--- a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp
+++ b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp
@@ -22,6 +22,7 @@
 #include 
 #include 
 
+#include 
 #include 
 #include 
 
@@ -32,7 +33,7 @@ validateTakerJSON(json::Value const& taker, json::StaticString const& name)
 {
     if (!taker.isMember(jss::currency) && !taker.isMember(jss::mpt_issuance_id))
     {
-        return rpc::missingFieldError((boost::format("%s.currency") % name.cStr()).str());
+        return rpc::missingFieldError(std::format("{}.currency", name.cStr()));
     }
 
     if (taker.isMember(jss::mpt_issuance_id) &&
@@ -44,8 +45,7 @@ validateTakerJSON(json::Value const& taker, json::StaticString const& name)
     if ((taker.isMember(jss::currency) && !taker[jss::currency].isString()) ||
         (taker.isMember(jss::mpt_issuance_id) && !taker[jss::mpt_issuance_id].isString()))
     {
-        return rpc::expectedFieldError(
-            (boost::format("%s.currency") % name.cStr()).str(), "string");
+        return rpc::expectedFieldError(std::format("{}.currency", name.cStr()), "string");
     }
 
     return std::nullopt;
@@ -70,10 +70,9 @@ parseTakerAssetJSON(
 
         if (!toCurrency(issue.currency, taker[jss::currency].asString()))
         {
-            JLOG(j.info()) << boost::format("Bad %s currency.") % name.cStr();
+            JLOG(j.info()) << std::format("Bad {} currency.", name.cStr());
             return rpc::makeError(
-                assetError,
-                (boost::format("Invalid field '%s.currency', bad currency.") % name.cStr()).str());
+                assetError, std::format("Invalid field '{}.currency', bad currency.", name.cStr()));
         }
         asset = issue;
     }
@@ -83,8 +82,7 @@ parseTakerAssetJSON(
         if (!mptid.parseHex(taker[jss::mpt_issuance_id].asString()))
         {
             return rpc::makeError(
-                assetError,
-                (boost::format("Invalid field '%s.mpt_issuance_id'") % name.cStr()).str());
+                assetError, std::format("Invalid field '{}.mpt_issuance_id'", name.cStr()));
         }
         asset = mptid;
     }
@@ -113,24 +111,21 @@ parseTakerIssuerJSON(
         {
             if (!taker[jss::issuer].isString())
             {
-                return rpc::expectedFieldError(
-                    (boost::format("%s.issuer") % name.cStr()).str(), "string");
+                return rpc::expectedFieldError(std::format("{}.issuer", name.cStr()), "string");
             }
 
             if (!toIssuer(issue.account, taker[jss::issuer].asString()))
             {
                 return rpc::makeError(
                     issuerError,
-                    (boost::format("Invalid field '%s.issuer', bad issuer.") % name.cStr()).str());
+                    std::format("Invalid field '{}.issuer', bad issuer.", name.cStr()));
             }
 
             if (issue.account == noAccount())
             {
                 return rpc::makeError(
                     issuerError,
-                    (boost::format("Invalid field '%s.issuer', bad issuer account one.") %
-                     name.cStr())
-                        .str());
+                    std::format("Invalid field '{}.issuer', bad issuer account one.", name.cStr()));
             }
         }
         else
@@ -142,19 +137,17 @@ parseTakerIssuerJSON(
         {
             return rpc::makeError(
                 issuerError,
-                (boost::format(
-                     "Unneeded field '%s.issuer' for XRP currency "
-                     "specification.") %
-                 name.cStr())
-                    .str());
+                std::format(
+                    "Unneeded field '{}.issuer' for XRP currency "
+                    "specification.",
+                    name.cStr()));
         }
 
         if (!isXRP(issue.currency) && isXRP(issue.account))
         {
             return rpc::makeError(
                 issuerError,
-                (boost::format("Invalid field '%s.issuer', expected non-XRP issuer.") % name.cStr())
-                    .str());
+                std::format("Invalid field '{}.issuer', expected non-XRP issuer.", name.cStr()));
         }
     }
 
diff --git a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h
index e03830ae0d..21bf3f8be8 100644
--- a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h
+++ b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h
@@ -93,6 +93,17 @@ enumerateNFTOffers(rpc::JsonContext& context, uint256 const& nftId, Keylet const
         if (!sle || nftId != sle->getFieldH256(sfNFTokenID))
             return rpcError(RpcInvalidParams);
 
+        // Reject a marker that references an offer on the opposite side
+        // (buy vs. sell) of the directory being enumerated.  Without this
+        // check the marker's node hint points into the other directory, so
+        // forEachItemAfter never finds `startAfter` and instead scans every
+        // page of `directory` before returning invalidParams -- turning an
+        // O(1) rejection into an O(directory size) walk.
+        auto const offerDir =
+            sle->isFlag(lsfSellNFToken) ? keylet::nftSells(nftId) : keylet::nftBuys(nftId);
+        if (directory.key != offerDir.key)
+            return rpcError(RpcInvalidParams);
+
         startHint = sle->getFieldU64(sfNFTokenOfferNode);
         appendNftOfferJson(context.app, sle, jsonOffers);
         offers.reserve(reserve);
diff --git a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp
index c730f64494..47cbd86afc 100644
--- a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp
+++ b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp
@@ -2,6 +2,7 @@
 
 #include 
 
+#include 
 #include 
 #include 
 #include 
@@ -14,7 +15,6 @@
 #include 
 #include 
 
-#include 
 #include 
 
 #include 
@@ -64,7 +64,6 @@ ServerDefinitions::translate(std::string const& inp)
         return out;
     };
 
-    // TODO: use string::contains with C++23
     auto contains = [&](std::string_view s) -> bool { return inp.contains(s); };
 
     if (contains("UINT"))
@@ -108,7 +107,7 @@ ServerDefinitions::translate(std::string const& inp)
         std::string token = inpToProcess.substr(0, pos);
         if (token.size() > 1)
         {
-            boost::algorithm::to_lower(token);
+            token = toLower(token);
             token[0] -= ('a' - 'A');
             out += token;
         }
diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp b/src/xrpld/rpc/handlers/transaction/Simulate.cpp
index 8441add08b..61cdddafde 100644
--- a/src/xrpld/rpc/handlers/transaction/Simulate.cpp
+++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp
@@ -4,7 +4,7 @@
 #include 
 #include 
 #include 
-#include 
+#include 
 #include 
 
 #include 
@@ -20,7 +20,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -290,12 +289,8 @@ simulateTxn(rpc::JsonContext& context, std::shared_ptr transaction)
         else
         {
             jvResult[jss::meta] = result.metadata->getJson(JsonOptions::Values::None);
-            rpc::insertDeliveredAmount(
+            rpc::insertAllSyntheticInJson(
                 jvResult[jss::meta], view, transaction->getSTransaction(), *result.metadata);
-            rpc::insertNFTSyntheticInJson(
-                jvResult, transaction->getSTransaction(), *result.metadata);
-            rpc::insertMPTokenIssuanceID(
-                jvResult[jss::meta], transaction->getSTransaction(), *result.metadata);
         }
     }
 
diff --git a/src/xrpld/rpc/handlers/transaction/Tx.cpp b/src/xrpld/rpc/handlers/transaction/Tx.cpp
index ee7110bf6b..cebe427af8 100644
--- a/src/xrpld/rpc/handlers/transaction/Tx.cpp
+++ b/src/xrpld/rpc/handlers/transaction/Tx.cpp
@@ -5,8 +5,9 @@
 #include 
 #include 
 #include 
-#include 
 #include 
+#include 
+#include 
 
 #include 
 #include 
@@ -18,7 +19,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -253,9 +253,7 @@ populateJsonResponse(
             if (meta)
             {
                 response[jss::meta] = meta->getJson(JsonOptions::Values::None);
-                insertDeliveredAmount(response[jss::meta], context, result.txn, *meta);
-                rpc::insertNFTSyntheticInJson(response, sttx, *meta);
-                rpc::insertMPTokenIssuanceID(response[jss::meta], sttx, *meta);
+                rpc::insertAllSyntheticInJson(response[jss::meta], context, sttx, *meta);
             }
         }
         response[jss::validated] = result.validated;