diff --git a/include/xrpl/basics/Number.h b/include/xrpl/basics/Number.h
index 01b3adb22d4..70baf5d1e47 100644
--- a/include/xrpl/basics/Number.h
+++ b/include/xrpl/basics/Number.h
@@ -41,6 +41,14 @@ class Number
     int exponent_{std::numeric_limits<int>::lowest()};
 
 public:
+    // The range for the mantissa when normalized
+    constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL;
+    constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL;
+
+    // The range for the exponent when normalized
+    constexpr static int minExponent = -32768;
+    constexpr static int maxExponent = 32768;
+
     struct unchecked
     {
         explicit unchecked() = default;
@@ -191,14 +199,6 @@ class Number
     constexpr bool
     isnormal() const noexcept;
 
-    // The range for the mantissa when normalized
-    constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL;
-    constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL;
-
-    // The range for the exponent when normalized
-    constexpr static int minExponent = -32768;
-    constexpr static int maxExponent = 32768;
-
     class Guard;
 };
 
diff --git a/include/xrpl/protocol/SField.h b/include/xrpl/protocol/SField.h
index 942f2a8654b..01909b19862 100644
--- a/include/xrpl/protocol/SField.h
+++ b/include/xrpl/protocol/SField.h
@@ -49,6 +49,7 @@ template <int>
 class STBitString;
 template <class>
 class STInteger;
+class STNumber;
 class STXChainBridge;
 class STVector256;
 class STCurrency;
@@ -70,8 +71,9 @@ class STCurrency;
     STYPE(STI_AMOUNT, 6)                          \
     STYPE(STI_VL, 7)                              \
     STYPE(STI_ACCOUNT, 8)                         \
+    STYPE(STI_NUMBER, 9)                          \
                                                   \
-    /* 9-13 are reserved */                       \
+    /* 10-13 are reserved */                      \
     STYPE(STI_OBJECT, 14)                         \
     STYPE(STI_ARRAY, 15)                          \
                                                   \
@@ -355,6 +357,7 @@ using SF_ACCOUNT = TypedField<STAccount>;
 using SF_AMOUNT = TypedField<STAmount>;
 using SF_ISSUE = TypedField<STIssue>;
 using SF_CURRENCY = TypedField<STCurrency>;
+using SF_NUMBER = TypedField<STNumber>;
 using SF_VL = TypedField<STBlob>;
 using SF_VECTOR256 = TypedField<STVector256>;
 using SF_XCHAIN_BRIDGE = TypedField<STXChainBridge>;
diff --git a/include/xrpl/protocol/STNumber.h b/include/xrpl/protocol/STNumber.h
new file mode 100644
index 00000000000..c0fce572c8c
--- /dev/null
+++ b/include/xrpl/protocol/STNumber.h
@@ -0,0 +1,88 @@
+//------------------------------------------------------------------------------
+/*
+    This file is part of rippled: https://github.com/ripple/rippled
+    Copyright (c) 2024 Ripple Labs Inc.
+
+    Permission to use, copy, modify, and/or distribute this software for any
+    purpose  with  or without fee is hereby granted, provided that the above
+    copyright notice and this permission notice appear in all copies.
+
+    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
+    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+    ANY  SPECIAL ,  DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+    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.
+*/
+//==============================================================================
+
+#ifndef XRPL_PROTOCOL_STNUMBER_H_INCLUDED
+#define XRPL_PROTOCOL_STNUMBER_H_INCLUDED
+
+#include <xrpl/basics/CountedObject.h>
+#include <xrpl/basics/Number.h>
+#include <xrpl/protocol/STBase.h>
+
+#include <ostream>
+
+namespace ripple {
+
+/**
+ * A serializable number.
+ *
+ * This type is-a `Number`, and can be used everywhere that is accepted.
+ * This type simply integrates `Number` with the serialization framework,
+ * letting it be used for fields in ledger entries and transactions.
+ * It is effectively an `STAmount` sans `Asset`:
+ * it can represent a value of any token type (XRP, IOU, or MPT)
+ * without paying the storage cost of duplicating asset information
+ * that may be deduced from the context.
+ */
+class STNumber : public STBase, public CountedObject<STNumber>
+{
+private:
+    Number value_;
+
+public:
+    using value_type = Number;
+
+    STNumber() = default;
+    explicit STNumber(SField const& field, Number const& value = Number());
+    STNumber(SerialIter& sit, SField const& field);
+
+    SerializedTypeID
+    getSType() const override;
+    std::string
+    getText() const override;
+    void
+    add(Serializer& s) const override;
+
+    Number const&
+    value() const;
+    void
+    setValue(Number const& v);
+
+    bool
+    isEquivalent(STBase const& t) const override;
+    bool
+    isDefault() const override;
+
+    operator Number() const
+    {
+        return value_;
+    }
+
+private:
+    STBase*
+    copy(std::size_t n, void* buf) const override;
+    STBase*
+    move(std::size_t n, void* buf) override;
+};
+
+std::ostream&
+operator<<(std::ostream& out, STNumber const& rhs);
+
+}  // namespace ripple
+
+#endif
diff --git a/include/xrpl/protocol/STObject.h b/include/xrpl/protocol/STObject.h
index c06f109dc56..748a2b5d685 100644
--- a/include/xrpl/protocol/STObject.h
+++ b/include/xrpl/protocol/STObject.h
@@ -245,6 +245,8 @@ class STObject : public STBase, public CountedObject<STObject>
     getFieldArray(SField const& field) const;
     const STCurrency&
     getFieldCurrency(SField const& field) const;
+    STNumber const&
+    getFieldNumber(SField const& field) const;
 
     /** Get the value of a field.
         @param A TypedField built from an SField value representing the desired
@@ -376,6 +378,8 @@ class STObject : public STBase, public CountedObject<STObject>
     void
     setFieldCurrency(SField const& field, STCurrency const&);
     void
+    setFieldNumber(SField const& field, STNumber const&);
+    void
     setFieldPathSet(SField const& field, STPathSet const&);
     void
     setFieldV256(SField const& field, STVector256 const& v);
diff --git a/include/xrpl/protocol/Serializer.h b/include/xrpl/protocol/Serializer.h
index d8d0b9222e3..0e96078ed14 100644
--- a/include/xrpl/protocol/Serializer.h
+++ b/include/xrpl/protocol/Serializer.h
@@ -83,12 +83,43 @@ class Serializer
     add8(unsigned char i);
     int
     add16(std::uint16_t i);
+
+    template <typename T>
+        requires(std::is_same_v<
+                 std::make_unsigned_t<std::remove_cv_t<T>>,
+                 std::uint32_t>)
     int
-    add32(std::uint32_t i);  // ledger indexes, account sequence, timestamps
+    add32(T i)
+    {
+        int ret = mData.size();
+        mData.push_back(static_cast<unsigned char>((i >> 24) & 0xff));
+        mData.push_back(static_cast<unsigned char>((i >> 16) & 0xff));
+        mData.push_back(static_cast<unsigned char>((i >> 8) & 0xff));
+        mData.push_back(static_cast<unsigned char>(i & 0xff));
+        return ret;
+    }
+
     int
     add32(HashPrefix p);
+
+    template <typename T>
+        requires(std::is_same_v<
+                 std::make_unsigned_t<std::remove_cv_t<T>>,
+                 std::uint64_t>)
     int
-    add64(std::uint64_t i);  // native currency amounts
+    add64(T i)
+    {
+        int ret = mData.size();
+        mData.push_back(static_cast<unsigned char>((i >> 56) & 0xff));
+        mData.push_back(static_cast<unsigned char>((i >> 48) & 0xff));
+        mData.push_back(static_cast<unsigned char>((i >> 40) & 0xff));
+        mData.push_back(static_cast<unsigned char>((i >> 32) & 0xff));
+        mData.push_back(static_cast<unsigned char>((i >> 24) & 0xff));
+        mData.push_back(static_cast<unsigned char>((i >> 16) & 0xff));
+        mData.push_back(static_cast<unsigned char>((i >> 8) & 0xff));
+        mData.push_back(static_cast<unsigned char>(i & 0xff));
+        return ret;
+    }
 
     template <typename Integer>
     int addInteger(Integer);
@@ -353,9 +384,13 @@ class SerialIter
 
     std::uint32_t
     get32();
+    std::int32_t
+    geti32();
 
     std::uint64_t
     get64();
+    std::int64_t
+    geti64();
 
     template <std::size_t Bits, class Tag = void>
     base_uint<Bits, Tag>
diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro
index ccf6350cbfc..8384025ee3b 100644
--- a/include/xrpl/protocol/detail/sfields.macro
+++ b/include/xrpl/protocol/detail/sfields.macro
@@ -191,6 +191,9 @@ TYPED_SFIELD(sfHookHash,                 UINT256,   31)
 TYPED_SFIELD(sfHookNamespace,            UINT256,   32)
 TYPED_SFIELD(sfHookSetTxnID,             UINT256,   33)
 
+// number (common)
+TYPED_SFIELD(sfNumber,                   NUMBER,     1)
+
 // currency amount (common)
 TYPED_SFIELD(sfAmount,                   AMOUNT,     1)
 TYPED_SFIELD(sfBalance,                  AMOUNT,     2)
diff --git a/include/xrpl/protocol/st.h b/include/xrpl/protocol/st.h
index 7b0e3a3b2df..0035deaa1bc 100644
--- a/include/xrpl/protocol/st.h
+++ b/include/xrpl/protocol/st.h
@@ -29,6 +29,7 @@
 #include <xrpl/protocol/STBlob.h>
 #include <xrpl/protocol/STInteger.h>
 #include <xrpl/protocol/STLedgerEntry.h>
+#include <xrpl/protocol/STNumber.h>
 #include <xrpl/protocol/STObject.h>
 #include <xrpl/protocol/STParsedJSON.h>
 #include <xrpl/protocol/STPathSet.h>
diff --git a/src/libxrpl/protocol/STNumber.cpp b/src/libxrpl/protocol/STNumber.cpp
new file mode 100644
index 00000000000..3a92bbb02f9
--- /dev/null
+++ b/src/libxrpl/protocol/STNumber.cpp
@@ -0,0 +1,105 @@
+//------------------------------------------------------------------------------
+/*
+    This file is part of rippled: https://github.com/ripple/rippled
+    Copyright (c) 2023 Ripple Labs Inc.
+
+    Permission to use, copy, modify, and/or distribute this software for any
+    purpose  with  or without fee is hereby granted, provided that the above
+    copyright notice and this permission notice appear in all copies.
+
+    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
+    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+    ANY  SPECIAL ,  DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+    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.
+*/
+//==============================================================================
+
+#include <xrpl/protocol/STNumber.h>
+
+#include <xrpl/protocol/SField.h>
+
+namespace ripple {
+
+STNumber::STNumber(SField const& field, Number const& value)
+    : STBase(field), value_(value)
+{
+}
+
+STNumber::STNumber(SerialIter& sit, SField const& field) : STBase(field)
+{
+    // We must call these methods in separate statements
+    // to guarantee their order of execution.
+    auto mantissa = sit.geti64();
+    auto exponent = sit.geti32();
+    value_ = Number{mantissa, exponent};
+}
+
+SerializedTypeID
+STNumber::getSType() const
+{
+    return STI_NUMBER;
+}
+
+std::string
+STNumber::getText() const
+{
+    return to_string(value_);
+}
+
+void
+STNumber::add(Serializer& s) const
+{
+    assert(getFName().isBinary());
+    assert(getFName().fieldType == getSType());
+    s.add64(value_.mantissa());
+    s.add32(value_.exponent());
+}
+
+Number const&
+STNumber::value() const
+{
+    return value_;
+}
+
+void
+STNumber::setValue(Number const& v)
+{
+    value_ = v;
+}
+
+STBase*
+STNumber::copy(std::size_t n, void* buf) const
+{
+    return emplace(n, buf, *this);
+}
+
+STBase*
+STNumber::move(std::size_t n, void* buf)
+{
+    return emplace(n, buf, std::move(*this));
+}
+
+bool
+STNumber::isEquivalent(STBase const& t) const
+{
+    assert(t.getSType() == this->getSType());
+    STNumber const& v = dynamic_cast<STNumber const&>(t);
+    return value_ == v;
+}
+
+bool
+STNumber::isDefault() const
+{
+    return value_ == Number();
+}
+
+std::ostream&
+operator<<(std::ostream& out, STNumber const& rhs)
+{
+    return out << rhs.getText();
+}
+
+}  // namespace ripple
diff --git a/src/libxrpl/protocol/STObject.cpp b/src/libxrpl/protocol/STObject.cpp
index 7e62fc25bd6..c8fc88348e9 100644
--- a/src/libxrpl/protocol/STObject.cpp
+++ b/src/libxrpl/protocol/STObject.cpp
@@ -25,6 +25,7 @@
 #include <xrpl/protocol/STArray.h>
 #include <xrpl/protocol/STBlob.h>
 #include <xrpl/protocol/STCurrency.h>
+#include <xrpl/protocol/STNumber.h>
 #include <xrpl/protocol/STObject.h>
 
 namespace ripple {
@@ -665,6 +666,13 @@ STObject::getFieldCurrency(SField const& field) const
     return getFieldByConstRef<STCurrency>(field, empty);
 }
 
+STNumber const&
+STObject::getFieldNumber(SField const& field) const
+{
+    static STNumber const empty{};
+    return getFieldByConstRef<STNumber>(field, empty);
+}
+
 void
 STObject::set(std::unique_ptr<STBase> v)
 {
@@ -765,6 +773,12 @@ STObject::setFieldIssue(SField const& field, STIssue const& v)
     setFieldUsingAssignment(field, v);
 }
 
+void
+STObject::setFieldNumber(SField const& field, STNumber const& v)
+{
+    setFieldUsingAssignment(field, v);
+}
+
 void
 STObject::setFieldPathSet(SField const& field, STPathSet const& v)
 {
diff --git a/src/libxrpl/protocol/STVar.cpp b/src/libxrpl/protocol/STVar.cpp
index f185595eadb..55927cb33aa 100644
--- a/src/libxrpl/protocol/STVar.cpp
+++ b/src/libxrpl/protocol/STVar.cpp
@@ -29,6 +29,7 @@
 #include <xrpl/protocol/STCurrency.h>
 #include <xrpl/protocol/STInteger.h>
 #include <xrpl/protocol/STIssue.h>
+#include <xrpl/protocol/STNumber.h>
 #include <xrpl/protocol/STObject.h>
 #include <xrpl/protocol/STPathSet.h>
 #include <xrpl/protocol/STVector256.h>
diff --git a/src/libxrpl/protocol/Serializer.cpp b/src/libxrpl/protocol/Serializer.cpp
index b99375f80dd..ceaf76faf34 100644
--- a/src/libxrpl/protocol/Serializer.cpp
+++ b/src/libxrpl/protocol/Serializer.cpp
@@ -21,6 +21,7 @@
 #include <xrpl/basics/contract.h>
 #include <xrpl/protocol/Serializer.h>
 #include <xrpl/protocol/digest.h>
+#include <cstdint>
 #include <type_traits>
 
 namespace ripple {
@@ -34,17 +35,6 @@ Serializer::add16(std::uint16_t i)
     return ret;
 }
 
-int
-Serializer::add32(std::uint32_t i)
-{
-    int ret = mData.size();
-    mData.push_back(static_cast<unsigned char>(i >> 24));
-    mData.push_back(static_cast<unsigned char>((i >> 16) & 0xff));
-    mData.push_back(static_cast<unsigned char>((i >> 8) & 0xff));
-    mData.push_back(static_cast<unsigned char>(i & 0xff));
-    return ret;
-}
-
 int
 Serializer::add32(HashPrefix p)
 {
@@ -56,21 +46,6 @@ Serializer::add32(HashPrefix p)
     return add32(safe_cast<std::uint32_t>(p));
 }
 
-int
-Serializer::add64(std::uint64_t i)
-{
-    int ret = mData.size();
-    mData.push_back(static_cast<unsigned char>(i >> 56));
-    mData.push_back(static_cast<unsigned char>((i >> 48) & 0xff));
-    mData.push_back(static_cast<unsigned char>((i >> 40) & 0xff));
-    mData.push_back(static_cast<unsigned char>((i >> 32) & 0xff));
-    mData.push_back(static_cast<unsigned char>((i >> 24) & 0xff));
-    mData.push_back(static_cast<unsigned char>((i >> 16) & 0xff));
-    mData.push_back(static_cast<unsigned char>((i >> 8) & 0xff));
-    mData.push_back(static_cast<unsigned char>(i & 0xff));
-    return ret;
-}
-
 template <>
 int
 Serializer::addInteger(unsigned char i)
@@ -410,6 +385,30 @@ SerialIter::get64()
         (std::uint64_t(t[6]) << 8) + std::uint64_t(t[7]);
 }
 
+std::int32_t
+SerialIter::geti32()
+{
+    if (remain_ < 4)
+        Throw<std::runtime_error>("invalid SerialIter geti32");
+    auto t = p_;
+    p_ += 4;
+    used_ += 4;
+    remain_ -= 4;
+    return boost::endian::load_big_s32(t);
+}
+
+std::int64_t
+SerialIter::geti64()
+{
+    if (remain_ < 8)
+        Throw<std::runtime_error>("invalid SerialIter geti64");
+    auto t = p_;
+    p_ += 8;
+    used_ += 8;
+    remain_ -= 8;
+    return boost::endian::load_big_s64(t);
+}
+
 void
 SerialIter::getFieldID(int& type, int& name)
 {
diff --git a/src/test/protocol/STNumber_test.cpp b/src/test/protocol/STNumber_test.cpp
new file mode 100644
index 00000000000..ed255e32f1c
--- /dev/null
+++ b/src/test/protocol/STNumber_test.cpp
@@ -0,0 +1,93 @@
+//------------------------------------------------------------------------------
+/*
+    This file is part of rippled: https://github.com/ripple/rippled
+    Copyright (c) 2024 Ripple Labs Inc.
+
+    Permission to use, copy, modify, and/or distribute this software for any
+    purpose  with  or without fee is hereby granted, provided that the above
+    copyright notice and this permission notice appear in all copies.
+
+    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
+    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+    ANY  SPECIAL ,  DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+    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.
+*/
+//==============================================================================
+
+#include <xrpl/beast/unit_test.h>
+#include <xrpl/protocol/Issue.h>
+#include <xrpl/protocol/STAmount.h>
+#include <xrpl/protocol/STNumber.h>
+
+#include <limits>
+#include <ostream>
+
+namespace ripple {
+
+struct STNumber_test : public beast::unit_test::suite
+{
+    void
+    testCombo(Number number)
+    {
+        STNumber const before{sfNumber, number};
+        BEAST_EXPECT(number == before);
+        Serializer s;
+        before.add(s);
+        BEAST_EXPECT(s.size() == 12);
+        SerialIter sit(s.slice());
+        STNumber const after{sit, sfNumber};
+        BEAST_EXPECT(after.isEquivalent(before));
+        BEAST_EXPECT(number == after);
+    }
+
+    void
+    run() override
+    {
+        static_assert(!std::is_convertible_v<STNumber*, Number*>);
+
+        {
+            STNumber const stnum{sfNumber};
+            BEAST_EXPECT(stnum.getSType() == STI_NUMBER);
+            BEAST_EXPECT(stnum.getText() == "0");
+            BEAST_EXPECT(stnum.isDefault() == true);
+            BEAST_EXPECT(stnum.value() == Number{0});
+        }
+
+        std::initializer_list<std::int64_t> const mantissas = {
+            std::numeric_limits<std::int64_t>::min(),
+            -1,
+            0,
+            1,
+            std::numeric_limits<std::int64_t>::max()};
+        for (std::int64_t mantissa : mantissas)
+            testCombo(Number{mantissa});
+
+        std::initializer_list<std::int32_t> const exponents = {
+            Number::minExponent, -1, 0, 1, Number::maxExponent - 1};
+        for (std::int32_t exponent : exponents)
+            testCombo(Number{123, exponent});
+
+        {
+            STAmount const strikePrice{noIssue(), 100};
+            STNumber const factor{sfNumber, 100};
+            auto const iouValue = strikePrice.iou();
+            IOUAmount totalValue{iouValue * factor};
+            STAmount const totalAmount{totalValue, strikePrice.issue()};
+            BEAST_EXPECT(totalAmount == Number{10'000});
+        }
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(STNumber, protocol, ripple);
+
+void
+testCompile(std::ostream& out)
+{
+    STNumber number{sfNumber, 42};
+    out << number;
+}
+
+}  // namespace ripple
diff --git a/src/test/protocol/Serializer_test.cpp b/src/test/protocol/Serializer_test.cpp
new file mode 100644
index 00000000000..d707943856f
--- /dev/null
+++ b/src/test/protocol/Serializer_test.cpp
@@ -0,0 +1,69 @@
+//------------------------------------------------------------------------------
+/*
+    This file is part of rippled: https://github.com/ripple/rippled
+    Copyright (c) 2024 Ripple Labs Inc.
+
+    Permission to use, copy, modify, and/or distribute this software for any
+    purpose  with  or without fee is hereby granted, provided that the above
+    copyright notice and this permission notice appear in all copies.
+
+    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
+    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+    ANY  SPECIAL ,  DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+    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.
+*/
+//==============================================================================
+
+#include <xrpl/beast/unit_test.h>
+#include <xrpl/protocol/Serializer.h>
+
+#include <limits>
+
+namespace ripple {
+
+struct Serializer_test : public beast::unit_test::suite
+{
+    void
+    run() override
+    {
+        {
+            std::initializer_list<std::int32_t> const values = {
+                std::numeric_limits<std::int32_t>::min(),
+                -1,
+                0,
+                1,
+                std::numeric_limits<std::int32_t>::max()};
+            for (std::int32_t value : values)
+            {
+                Serializer s;
+                s.add32(value);
+                BEAST_EXPECT(s.size() == 4);
+                SerialIter sit(s.slice());
+                BEAST_EXPECT(sit.geti32() == value);
+            }
+        }
+        {
+            std::initializer_list<std::int64_t> const values = {
+                std::numeric_limits<std::int64_t>::min(),
+                -1,
+                0,
+                1,
+                std::numeric_limits<std::int64_t>::max()};
+            for (std::int64_t value : values)
+            {
+                Serializer s;
+                s.add64(value);
+                BEAST_EXPECT(s.size() == 8);
+                SerialIter sit(s.slice());
+                BEAST_EXPECT(sit.geti64() == value);
+            }
+        }
+    }
+};
+
+BEAST_DEFINE_TESTSUITE(Serializer, protocol, ripple);
+
+}  // namespace ripple
diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp
index 2ea13ffabc8..2be784306cd 100644
--- a/src/xrpld/app/tx/detail/Payment.cpp
+++ b/src/xrpld/app/tx/detail/Payment.cpp
@@ -88,7 +88,7 @@ Payment::preflight(PreflightContext const& ctx)
 
     if (txFlags & paymentMask)
     {
-        JLOG(j.trace()) << "Malformed transaction: " << "Invalid flags set.";
+        JLOG(j.trace()) << "Malformed transaction: Invalid flags set.";
         return temINVALID_FLAG;
     }
 
@@ -110,9 +110,9 @@ Payment::preflight(PreflightContext const& ctx)
     if ((mptDirect && dstAmount.asset() != maxSourceAmount.asset()) ||
         (!mptDirect && maxSourceAmount.holds<MPTIssue>()))
     {
-        JLOG(j.trace()) << "Malformed transaction: "
-                        << "inconsistent issues: " << dstAmount.getFullText()
-                        << " " << maxSourceAmount.getFullText() << " "
+        JLOG(j.trace()) << "Malformed transaction: inconsistent issues: "
+                        << dstAmount.getFullText() << " "
+                        << maxSourceAmount.getFullText() << " "
                         << deliverMin.value_or(STAmount{}).getFullText();
         return temMALFORMED;
     }
@@ -135,19 +135,19 @@ Payment::preflight(PreflightContext const& ctx)
     }
     if (hasMax && maxSourceAmount <= beast::zero)
     {
-        JLOG(j.trace()) << "Malformed transaction: " << "bad max amount: "
+        JLOG(j.trace()) << "Malformed transaction: bad max amount: "
                         << maxSourceAmount.getFullText();
         return temBAD_AMOUNT;
     }
     if (dstAmount <= beast::zero)
     {
-        JLOG(j.trace()) << "Malformed transaction: "
-                        << "bad dst amount: " << dstAmount.getFullText();
+        JLOG(j.trace()) << "Malformed transaction: bad dst amount: "
+                        << dstAmount.getFullText();
         return temBAD_AMOUNT;
     }
     if (badCurrency() == srcAsset || badCurrency() == dstAsset)
     {
-        JLOG(j.trace()) << "Malformed transaction: " << "Bad currency.";
+        JLOG(j.trace()) << "Malformed transaction: Bad currency.";
         return temBAD_CURRENCY;
     }
     if (account == dstAccountID && equalTokens(srcAsset, dstAsset) && !hasPaths)
@@ -550,7 +550,7 @@ Payment::doApply()
         // Vote no. However the transaction might succeed, if applied in
         // a different order.
         JLOG(j_.trace()) << "Delay transaction: Insufficient funds: "
-                         << " " << to_string(mPriorBalance) << " / "
+                         << to_string(mPriorBalance) << " / "
                          << to_string(dstAmount.xrp() + mmm) << " ("
                          << to_string(reserve) << ")";
 
diff --git a/src/xrpld/ledger/detail/CachedView.cpp b/src/xrpld/ledger/detail/CachedView.cpp
index 5502c40e6d5..645a2c79c13 100644
--- a/src/xrpld/ledger/detail/CachedView.cpp
+++ b/src/xrpld/ledger/detail/CachedView.cpp
@@ -63,20 +63,17 @@ CachedViewImpl::read(Keylet const& k) const
         hits.increment();
     else
         misses.increment();
-    std::lock_guard lock(mutex_);
-    auto const er = map_.emplace(k.key, *digest);
-    bool const inserted = er.second;
-    if (sle && !k.check(*sle))
+
+    if (!cacheHit)
     {
-        if (!inserted)
-        {
-            // On entry, this function did not find this key in map_. Now
-            // something (another thread?) has inserted the sle into the map and
-            // it has the wrong type.
-            LogicError("CachedView::read: wrong type");
-        }
-        return nullptr;
+        // Avoid acquiring this lock unless necessary. It is only necessary if
+        // the key was not found in the map_. The lock is needed to add the key
+        // and digest.
+        std::lock_guard lock(mutex_);
+        map_.emplace(k.key, *digest);
     }
+    if (!sle || !k.check(*sle))
+        return nullptr;
     return sle;
 }