Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e0e882c
Fix overpayment asserts (#6084)
Tapanito Dec 1, 2025
d6a817c
Test updates - show balances in runLoan()
ximinez Dec 2, 2025
46232c0
MPTTester::operator() parameter should be std::int64_t
ximinez Dec 3, 2025
3a28513
Fix LCOV exclusion
ximinez Dec 4, 2025
e6e0a7c
Fix Overpayment Calculation (#6087)
Tapanito Dec 4, 2025
879185c
Fix test errors
ximinez Dec 13, 2025
28398b7
Fix LoanBrokerSet debtMaximum limits (#6116)
Tapanito Dec 16, 2025
313aa2d
Fix some minor bugs in Lending Protocol (#6101)
Tapanito Dec 17, 2025
d9d6b45
Review feedback from @Tapanito: overpayment value change
ximinez Dec 3, 2025
a4c059a
Update src/xrpld/app/tx/detail/LoanManage.cpp
ximinez Dec 6, 2025
8642738
Review feedback from @shawnxie999: even more rounding
ximinez Dec 6, 2025
162cfa0
Check permissions in LoanSet and LoanPay (#6108)
a1q123456 Dec 17, 2025
19686d2
Disallow pseudo accounts to be Destination for LoanBrokerCoverWithdra…
a1q123456 Dec 17, 2025
b38a692
Ensure vault asset cap is not exceeded (#6124)
Tapanito Dec 17, 2025
ea11f62
Fix Overpayment ValueChange calculation in Lending Protocol (6114)
Tapanito Dec 19, 2025
1c2e7de
fix: Enable LP Deposits when the broker is the asset issuer (#6119)
gregtatcam Dec 19, 2025
a743930
Add a few minor changes (#6158)
ximinez Dec 22, 2025
e5a92fb
Add minimum grace period validation (#6133)
Tapanito Jan 6, 2026
d4a1f16
squashed: Expand Number to support full integer range as of 9526a30dbc
ximinez Jan 8, 2026
33027ef
Fix bugs: frozen pseudo-account, and FLC cutoff (#6170)
Tapanito Jan 8, 2026
f6e6389
Merge branch 'ximinez/lending-3.1' into ximinez/lending-3.1-number
ximinez Jan 8, 2026
5835dff
refactor: Rename raw state to theoretical state (#6187)
Tapanito Jan 9, 2026
3248433
Check if a withdrawal amount exceeds any applicable receiving limit. …
gregtatcam Jan 9, 2026
5db9921
Review feedback from @shawnxie999 and @gregtatcam
ximinez Jan 9, 2026
4cb23d0
Merge branch 'ximinez/lending-3.1' into ximinez/lending-3.1-number
ximinez Jan 9, 2026
ed8f9e5
fixup! Review feedback from @shawnxie999 and @gregtatcam
ximinez Jan 9, 2026
bc12359
fixup! fixup! Review feedback from @shawnxie999 and @gregtatcam
ximinez Jan 9, 2026
33b2b73
VaultClawback: Burn shares of an empty vault (#6120)
Tapanito Jan 9, 2026
9613f3b
additional loanbrokerset tests
Tapanito Jan 9, 2026
e072115
fix bugs in Number
ximinez Jan 10, 2026
4a01a9d
fixup! fix bugs in Number
ximinez Jan 10, 2026
fc0a79a
Merge remote-tracking branch 'mywork/ximinez/vault-fix-3.1' into ximi…
ximinez Jan 11, 2026
8fc2551
Fix test failures from interaction between #6120 and #6133
ximinez Jan 11, 2026
fce4cbe
Merge remote-tracking branch 'mywork/ximinez/lending-3.1' into ximine…
ximinez Jan 11, 2026
e4d3a6b
Fix overpayment result calculation (#6195)
Tapanito Jan 11, 2026
35e456d
fixup! Fix overpayment result calculation (#6195)
ximinez Jan 11, 2026
f5a64b2
Fix potential Number addition overflows
ximinez Jan 11, 2026
4d032bf
Merge branch 'ximinez/lending-3.1' into ximinez/lending-3.1-number
ximinez Jan 11, 2026
96f8550
Address review feedback from Lending Protocol re-review (#6161)
ximinez Jan 12, 2026
449cc00
Merge remote-tracking branch 'XRPLF/ximinez/lending-3.1' into ximinez…
ximinez Jan 12, 2026
ce0175f
Merge remote-tracking branch 'XRPLF/release-3.1' into ximinez/lending…
ximinez Jan 12, 2026
e32455d
Update src/libxrpl/basics/Number.cpp
ximinez Jan 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
529 changes: 491 additions & 38 deletions include/xrpl/basics/Number.h

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion include/xrpl/protocol/AmountConversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ toAmount(
{
if (isXRP(issue))
return STAmount(issue, static_cast<std::int64_t>(n));
return STAmount(issue, n.mantissa(), n.exponent());
return STAmount(issue, n);
}
else
{
Expand Down
21 changes: 13 additions & 8 deletions include/xrpl/protocol/IOUAmount.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ class IOUAmount : private boost::totally_ordered<IOUAmount>,
private boost::additive<IOUAmount>
{
private:
std::int64_t mantissa_;
int exponent_;
using mantissa_type = std::int64_t;
using exponent_type = int;
mantissa_type mantissa_;
exponent_type exponent_;

/** Adjusts the mantissa and exponent to the proper range.

Expand All @@ -57,11 +59,14 @@ class IOUAmount : private boost::totally_ordered<IOUAmount>,
void
normalize();

static IOUAmount
fromNumber(Number const& number);

public:
IOUAmount() = default;
explicit IOUAmount(Number const& other);
IOUAmount(beast::Zero);
IOUAmount(std::int64_t mantissa, int exponent);
IOUAmount(mantissa_type mantissa, exponent_type exponent);

IOUAmount& operator=(beast::Zero);

Expand Down Expand Up @@ -90,10 +95,10 @@ class IOUAmount : private boost::totally_ordered<IOUAmount>,
int
signum() const noexcept;

int
exponent_type
exponent() const noexcept;

std::int64_t
mantissa_type
mantissa() const noexcept;

static IOUAmount
Expand All @@ -111,7 +116,7 @@ inline IOUAmount::IOUAmount(beast::Zero)
*this = beast::zero;
}

inline IOUAmount::IOUAmount(std::int64_t mantissa, int exponent)
inline IOUAmount::IOUAmount(mantissa_type mantissa, exponent_type exponent)
: mantissa_(mantissa), exponent_(exponent)
{
normalize();
Expand Down Expand Up @@ -168,13 +173,13 @@ IOUAmount::signum() const noexcept
return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0);
}

inline int
inline IOUAmount::exponent_type
IOUAmount::exponent() const noexcept
{
return exponent_;
}

inline std::int64_t
inline IOUAmount::mantissa_type
IOUAmount::mantissa() const noexcept
{
return mantissa_;
Expand Down
3 changes: 3 additions & 0 deletions include/xrpl/protocol/Issue.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ class Issue
bool
native() const;

bool
integral() const;

friend constexpr std::weak_ordering
operator<=>(Issue const& lhs, Issue const& rhs);
};
Expand Down
6 changes: 6 additions & 0 deletions include/xrpl/protocol/MPTIssue.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ class MPTIssue
{
return false;
}

bool
integral() const
{
return true;
}
};

constexpr bool
Expand Down
1 change: 1 addition & 0 deletions include/xrpl/protocol/Protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ std::size_t constexpr maxMPTokenMetadataLength = 1024;

/** The maximum amount of MPTokenIssuance */
std::uint64_t constexpr maxMPTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull;
static_assert(Number::maxRep >= maxMPTokenAmount);

/** The maximum length of Data payload */
std::size_t constexpr maxDataPayloadLength = 256;
Expand Down
5 changes: 4 additions & 1 deletion include/xrpl/protocol/SField.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,10 @@ class SField
sMD_Always = 0x10, // value when node containing it is affected at all
sMD_BaseTen = 0x20, // value is treated as base 10, overriding behavior
sMD_PseudoAccount = 0x40, // if this field is set in an ACCOUNT_ROOT
// _only_, then it is a pseudo-account
// _only_, then it is a pseudo-account
sMD_NeedsAsset = 0x80, // This field needs to be associated with an
// asset before it is serialized as a ledger
// object. Intended for STNumber.
sMD_Default =
sMD_ChangeOrig | sMD_ChangeNew | sMD_DeleteFinal | sMD_Create
};
Expand Down
71 changes: 54 additions & 17 deletions include/xrpl/protocol/STAmount.h
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class STAmount final : public STBase, public CountedObject<STAmount>

template <AssetType A>
STAmount(A const& asset, Number const& number)
: STAmount(asset, number.mantissa(), number.exponent())
: STAmount(fromNumber(asset, number))
{
}

Expand Down Expand Up @@ -301,6 +301,10 @@ class STAmount final : public STBase, public CountedObject<STAmount>
mpt() const;

private:
template <AssetType A>
static STAmount
fromNumber(A const& asset, Number const& number);

static std::unique_ptr<STAmount>
construct(SerialIter&, SField const& name);

Expand Down Expand Up @@ -364,10 +368,19 @@ STAmount::STAmount(
, mIsNegative(negative)
{
// mValue is uint64, but needs to fit in the range of int64
XRPL_ASSERT(
mValue <= std::numeric_limits<std::int64_t>::max(),
"ripple::STAmount::STAmount(SField, A, std::uint64_t, int, bool) : "
"maximum mantissa input");
if (Number::getMantissaScale() == MantissaRange::small)
{
XRPL_ASSERT(
mValue <= std::numeric_limits<std::int64_t>::max(),
"xrpl::STAmount::STAmount(SField, A, std::uint64_t, int, bool) : "
"maximum mantissa input");
}
else
{
if (integral() && mValue > std::numeric_limits<std::int64_t>::max())
throw std::overflow_error(
"STAmount mantissa is too large " + std::to_string(mantissa));
}
canonicalize();
}

Expand Down Expand Up @@ -561,14 +574,23 @@ STAmount::operator=(XRPAmount const& amount)
return *this;
}

inline STAmount&
STAmount::operator=(Number const& number)
template <AssetType A>
inline STAmount
STAmount::fromNumber(A const& a, Number const& number)
{
mIsNegative = number.mantissa() < 0;
mValue = mIsNegative ? -number.mantissa() : number.mantissa();
mOffset = number.exponent();
canonicalize();
return *this;
bool const negative = number.mantissa() < 0;
Number const working{negative ? -number : number};
Asset asset{a};
if (asset.integral())
{
std::uint64_t const intValue = static_cast<std::int64_t>(working);
return STAmount{asset, intValue, 0, negative};
}

auto const [mantissa, exponent] =
working.normalizeToRange(cMinValue, cMaxValue);

return STAmount{asset, mantissa, exponent, negative};
}

inline void
Expand Down Expand Up @@ -718,17 +740,32 @@ getRate(STAmount const& offerOut, STAmount const& offerIn);
* @param rounding Optional Number rounding mode
*
*/
STAmount
[[nodiscard]] STAmount
roundToScale(
STAmount const& value,
std::int32_t scale,
Number::rounding_mode rounding = Number::getround());

/** Round an arbitrary precision Number IN PLACE to the precision of a given
* Asset.
*
* This is used to ensure that calculations do not collect dust for IOUs, or
* fractional amounts for the integral types XRP and MPT.
*
* @param asset The relevant asset
* @param value The lvalue to be rounded
*/
template <AssetType A>
void
roundToAsset(A const& asset, Number& value)
{
value = STAmount{asset, value};
}

/** Round an arbitrary precision Number to the precision of a given Asset.
*
* This is used to ensure that calculations do not collect dust beyond the
* precision of the reference value for IOUs, or fractional amounts for the
* integral types XRP and MPT.
* This is used to ensure that calculations do not collect dust beyond specified
* scale for IOUs, or fractional amounts for the integral types XRP and MPT.
*
* @param asset The relevant asset
* @param value The value to be rounded
Expand All @@ -737,7 +774,7 @@ roundToScale(
* @param rounding Optional Number rounding mode
*/
template <AssetType A>
Number
[[nodiscard]] Number
roundToAsset(
A const& asset,
Number const& value,
Expand Down
17 changes: 16 additions & 1 deletion include/xrpl/protocol/STNumber.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/STBase.h>
#include <xrpl/protocol/STTakesAsset.h>

#include <ostream>

Expand All @@ -38,8 +39,19 @@ namespace ripple {
* 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.
*
* STNumber derives from STTakesAsset, so that it can be associated with the
* related Asset during transaction processing. Which asset is relevant depends
* on the object and transaction. As of this writing, only Vault, LoanBroker,
* and Loan objects use STNumber fields. All of those fields represent amounts
* of the Vault's Asset, so they should be associated with the Vault's Asset.
*
* e.g.
* associateAsset(*loanSle, asset);
* associateAsset(*brokerSle, asset);
* associateAsset(*vaultSle, asset);
*/
class STNumber : public STBase, public CountedObject<STNumber>
class STNumber : public STTakesAsset, public CountedObject<STNumber>
{
private:
Number value_;
Expand Down Expand Up @@ -75,6 +87,9 @@ class STNumber : public STBase, public CountedObject<STNumber>
bool
isDefault() const override;

void
associateAsset(Asset const& a) override;

operator Number() const
{
return value_;
Expand Down
63 changes: 63 additions & 0 deletions include/xrpl/protocol/STTakesAsset.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#ifndef XRPL_PROTOCOL_STTAKESASSET_H_INCLUDED
#define XRPL_PROTOCOL_STTAKESASSET_H_INCLUDED

#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/STBase.h>

namespace ripple {

/** Intermediate class for any STBase-derived class to store an Asset.
*
* In the class definition, this class should be specified as a base class
* _instead_ of STBase.
*
* Specifically, the Asset is only stored and used at runtime. It should not be
* serialized to the ledger.
*
* The derived class decides what to do with the Asset, and when. It will not
* necessarily be set at any given time. As of this writing, only STNumber uses
* it to round the stored Number to the Asset's precision both when associated,
* and when serializing the Number.
*/
class STTakesAsset : public STBase
{
protected:
std::optional<Asset> asset_;

public:
using STBase::STBase;
using STBase::operator=;

virtual void
associateAsset(Asset const& a);
};

inline void
STTakesAsset::associateAsset(Asset const& a)
{
asset_.emplace(a);
}

class STLedgerEntry;

/** Associate an Asset with all sMD_NeedsAsset fields in a ledger entry.
*
* This function iterates over all fields in the given ledger entry. For each
* field that is set and has the SField::sMD_NeedsAsset metadata flag, it calls
* `associateAsset` on that field with the given Asset. Such field must be
* derived from STTakesAsset - if it is not, the conversion will throw.
*
* Typically, associateAsset should be called near the end of doApply() of any
* Transactor classes on the SLEs of any new or modified ledger entries
* containing STNumber fields, after doing all of the modifications t the SLEs.
*
* @param sle The ledger entry whose fields will be updated.
* @param asset The Asset to associate with the relevant fields.
*
*/
void
associateAsset(STLedgerEntry& sle, Asset const& asset);

} // namespace ripple

#endif
2 changes: 2 additions & 0 deletions include/xrpl/protocol/SystemParameters.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ systemName()

/** Number of drops in the genesis account. */
constexpr XRPAmount INITIAL_XRP{100'000'000'000 * DROPS_PER_XRP};
static_assert(INITIAL_XRP.drops() == 100'000'000'000'000'000);
static_assert(Number::maxRep >= INITIAL_XRP.drops());

/** Returns true if the amount does not exceed the initial XRP in existence. */
inline bool
Expand Down
20 changes: 10 additions & 10 deletions include/xrpl/protocol/detail/sfields.macro
Original file line number Diff line number Diff line change
Expand Up @@ -226,22 +226,22 @@ TYPED_SFIELD(sfLoanID, UINT256, 38)

// number (common)
TYPED_SFIELD(sfNumber, NUMBER, 1)
TYPED_SFIELD(sfAssetsAvailable, NUMBER, 2)
TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3)
TYPED_SFIELD(sfAssetsTotal, NUMBER, 4)
TYPED_SFIELD(sfLossUnrealized, NUMBER, 5)
TYPED_SFIELD(sfDebtTotal, NUMBER, 6)
TYPED_SFIELD(sfDebtMaximum, NUMBER, 7)
TYPED_SFIELD(sfCoverAvailable, NUMBER, 8)
TYPED_SFIELD(sfAssetsAvailable, NUMBER, 2, SField::sMD_NeedsAsset | SField::sMD_Default)
TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3, SField::sMD_NeedsAsset | SField::sMD_Default)
TYPED_SFIELD(sfAssetsTotal, NUMBER, 4, SField::sMD_NeedsAsset | SField::sMD_Default)
TYPED_SFIELD(sfLossUnrealized, NUMBER, 5, SField::sMD_NeedsAsset | SField::sMD_Default)
TYPED_SFIELD(sfDebtTotal, NUMBER, 6, SField::sMD_NeedsAsset | SField::sMD_Default)
TYPED_SFIELD(sfDebtMaximum, NUMBER, 7, SField::sMD_NeedsAsset | SField::sMD_Default)
TYPED_SFIELD(sfCoverAvailable, NUMBER, 8, SField::sMD_NeedsAsset | SField::sMD_Default)
TYPED_SFIELD(sfLoanOriginationFee, NUMBER, 9)
TYPED_SFIELD(sfLoanServiceFee, NUMBER, 10)
TYPED_SFIELD(sfLatePaymentFee, NUMBER, 11)
TYPED_SFIELD(sfClosePaymentFee, NUMBER, 12)
TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13)
TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13, SField::sMD_NeedsAsset | SField::sMD_Default)
TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15)
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::sMD_NeedsAsset | SField::sMD_Default)
TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16)
TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17)
TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::sMD_NeedsAsset | SField::sMD_Default)

// int32
TYPED_SFIELD(sfLoanScale, INT32, 1)
Expand Down
Loading
Loading