Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions include/xrpl/ledger/helpers/LendingHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,24 @@ static constexpr std::uint32_t kSecondsInYear = 365 * 24 * 60 * 60;
Number
loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval);

/**
* Assets a loan earns per second at this principal outstanding.
*
* Equation (27) of XLS-66 is linear in elapsed time, and sfPaymentInterval
* cancels out of it, so a loan's accrual rate depends only on its principal
* and interest rate. Principal is flat between payments, which makes the rate
* piecewise-constant with breakpoints exactly at the events that update it —
* summing it across a vault's loans is therefore exact, not an approximation.
*/
inline Number
loanAccrualRate(Number const& principalOutstanding, TenthBips32 interestRate)
{
if (interestRate == TenthBips32{0} || principalOutstanding <= Number{})
return Number{};

return tenthBipsOfValue(principalOutstanding, interestRate) / Number{kSecondsInYear};
}

/**
* Ensure the periodic payment is always rounded consistently
*/
Expand Down
115 changes: 107 additions & 8 deletions include/xrpl/ledger/helpers/VaultHelpers.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <xrpl/basics/Number.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
Expand All @@ -16,20 +17,111 @@
namespace xrpl {

class STTx;
/**
* Interest the vault's loans have earned since sfLastAccrualTime, capped by
* the sfUnearnedInterest budget still left to recognize.
*
* sfAssetsTotal is only credited when a loan event settles the vault, so
* between those events it lags by this amount. Adding it back at read time
* recognizes interest continuously as it is earned, without writing to
* sfAssetsTotal outside the loan transactions.
*
* Returns zero when nothing is accruing — no rate, no budget left, or a vault
* created before featureLendingProtocolV1_1.
*/
[[nodiscard]] Number
vaultAccruedInterest(ReadView const& view, SLE::const_ref vault);

/**
* Whether a rolling vault is inside a dealing window at this close time.
*
* A rolling vault deals in [SubscriptionDate + k * DealingInterval,
* SubscriptionDate + k * DealingInterval + DealingWindow) for integer k >= 0.
* Returns true for any vault that is not rolling, which has no windows to be
* outside of, and false before the first window opens.
*/
[[nodiscard]] bool
inDealingWindow(ReadView const& view, SLE::const_ref vault);

/**
* End of the dealing window containing this close time.
*
* Only meaningful when inDealingWindow is true for a rolling vault; it is the
* sfStruckUntil written when a window's price is struck.
*/
[[nodiscard]] std::uint32_t
dealingWindowEnd(ReadView const& view, SLE::const_ref vault);

/**
* The price every deal in the current window converts at, in vault asset per
* share, or nullopt when no struck price governs this ledger.
*
* Returns a price only for a rolling vault inside a window whose sfStruckUntil
* matches that window's end. Accrual continues underneath it: the dealing price
* is frozen for the window, the accounting is not.
*/
/**
* The interest recognition method of a vault.
*
* Returns sfAccountingMethod where it is present. A vault created before
* featureVaultContinuousAccrual carries no such field, so the method is derived
* from its schema version instead: CashBasis recognizes interest as it is
* collected, and anything older is Legacy, which recognizes a loan's whole-life
* interest at origination. Every vault that exists today therefore resolves
* without being touched.
*/
[[nodiscard]] std::uint8_t
getAccountingMethod(SLE::const_ref vault);

[[nodiscard]] std::optional<Number>
struckPriceInForce(ReadView const& view, SLE::const_ref vault);

/**
* Strike the price for the current window if it has not been struck yet.
*
* Called by the first deposit or withdrawal of a window. Does nothing for a
* vault that is not rolling, outside a window, or where this window's price is
* already struck, so it is safe to call unconditionally.
*/
void
strikeWindowPrice(ApplyView& view, SLE::ref vault, SLE::const_ref issuance);

/**
* Credit interest earned since the last settlement into sfAssetsTotal, draw
* it out of the sfUnearnedInterest budget, and stamp the current close time.
*
* Must be called before adjusting sfAccrualRate, so the elapsed period is
* charged at the rate that was in effect over it. Only the ttLOAN_*
* transactions may call this: ValidVault requires sfAssetsTotal to move with
* the vault balance on deposit and withdraw, which settling would violate.
* Pricing does not need it — the conversion helpers add elapsed interest
* themselves.
*/
void
accrueVault(ApplyView& view, SLE::ref vault);

/**
* 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
* MPT, this number is integral and always truncated in this calculation.
*
* @param vault The vault SLE.
* @param issuance The MPTokenIssuance SLE for the vault's shares.
* @param assets The amount of assets to convert.
*
* @return The number of shares, or nullopt on error.
* /**
* * 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
* * MPT, this number is integral and always truncated in this calculation.
* *
* * @param vault The vault SLE.
* * @param issuance The MPTokenIssuance SLE for the vault's shares.
* * @param assets The amount of assets to convert.
* *
* * @return The number of shares, or nullopt on error.
*/
[[nodiscard]] std::optional<STAmount>
assetsToSharesDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& assets);
assetsToSharesDeposit(
ReadView const& view,
SLE::const_ref vault,
SLE::const_ref issuance,
STAmount const& assets);

/**
* From the perspective of a vault, return the number of assets to take from
Expand All @@ -43,7 +135,11 @@ assetsToSharesDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount co
* @return The number of assets, or nullopt on error.
*/
[[nodiscard]] std::optional<STAmount>
sharesToAssetsDeposit(SLE::const_ref vault, SLE::const_ref issuance, STAmount const& shares);
sharesToAssetsDeposit(
ReadView const& view,
SLE::const_ref vault,
SLE::const_ref issuance,
STAmount const& shares);

/**
* Adjusts a requested asset change (`delta`) to match the decimal scale of the
Expand Down Expand Up @@ -91,11 +187,12 @@ enum class WaiveUnrealizedLoss : bool { No = false, Yes = true };
* unrealized loss is waived. Used by assetsToSharesWithdraw and
* sharesToAssetsWithdraw as the numerator of the share/asset exchange rate.
*
* @param view The ledger view, for interest accrued since the last settlement.
* @param vault The vault SLE.
* @param waive Whether to skip subtracting the unrealized loss.
*/
[[nodiscard]] Number
assetsTotalForWithdrawal(SLE::const_ref vault, WaiveUnrealizedLoss waive);
assetsTotalForWithdrawal(ReadView const& view, SLE::const_ref vault, WaiveUnrealizedLoss waive);

/**
* Returns true if debiting `amount` from `total` (the current value of a
Expand Down Expand Up @@ -131,6 +228,7 @@ debitIsNonZeroDust(Asset const& asset, Number const& total, Number const& amount
*/
[[nodiscard]] std::optional<STAmount>
assetsToSharesWithdraw(
ReadView const& view,
SLE::const_ref vault,
SLE::const_ref issuance,
STAmount const& assets,
Expand All @@ -152,6 +250,7 @@ assetsToSharesWithdraw(
*/
[[nodiscard]] std::optional<STAmount>
sharesToAssetsWithdraw(
ReadView const& view,
SLE::const_ref vault,
SLE::const_ref issuance,
STAmount const& shares,
Expand Down
29 changes: 26 additions & 3 deletions include/xrpl/protocol/Protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,19 @@ constexpr std::size_t kMaxDataPayloadLength = 256;
*/
constexpr std::uint8_t kVaultStrategyFirstComeFirstServe = 1;

/**
* Vault interest recognition methods.
*
* Legacy recognizes a loan's whole-life interest at origination and is the
* implicit method for vaults created before featureLendingProtocolV1_1. Cash
* recognizes interest as it is collected; accrual recognizes it continuously
* as it is earned. Fixed at VaultCreate — changing it would reprice every
* outstanding share in a single step.
*/
constexpr std::uint8_t kVaultAccountingLegacy = 0;
constexpr std::uint8_t kVaultAccountingCash = 1;
constexpr std::uint8_t kVaultAccountingAccrual = 2;

/**
* Default IOU scale factor for a Vault
*/
Expand All @@ -316,7 +329,13 @@ constexpr std::uint8_t kVaultDefaultIouScale = 6;
* 1 IOU can be always converted to shares.
* 10^19 > maxMPTokenAmount (2^64-1) > 10^18
*/
constexpr std::uint8_t kVaultMaximumIouScale = 18;
constexpr std::uint8_t kVaultMaximumIouScale =
18; /** Largest deposit or redemption fee a vault may charge, in 1/10 bips.

Matches kMaxTransferFee: half of what is moved is the most any fee may
retain.
*/
constexpr std::uint32_t kMaxVaultFee = 50'000;

/**
* Vault ledger-entry schema versions. Assigned to newly created
Expand All @@ -330,12 +349,16 @@ enum class VaultVersion : uint8_t {
};

/**
* Vault kind. Distinguishes closed-ended vaults from the default open-ended
* kind. Persisted as sfVaultKind (UINT8); absent means OpenEnded.
* Vault kind. Distinguishes closed-ended and rolling vaults from the default
* open-ended kind. Persisted as sfVaultKind (UINT8); absent means OpenEnded.
*
* A rolling vault deals in a window that reopens every sfDealingInterval
* seconds and stays open for sfDealingWindow of them.
*/
enum class VaultKind : std::uint8_t {
OpenEnded = 0,
ClosedEnded = 1,
Rolling = 2,
};

/**
Expand Down
1 change: 1 addition & 0 deletions include/xrpl/protocol/detail/features.macro
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.

XRPL_FEATURE(VaultContinuousAccrual, 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)
Expand Down
12 changes: 12 additions & 0 deletions include/xrpl/protocol/detail/ledger_entries.macro
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfIssuerEncryptedBalance, SoeOptional},
{sfAuditorEncryptedBalance, SoeOptional},
{sfHolderEncryptionKey, SoeOptional},
{sfRedemptionAfter, SoeDefault},
}))

/** A ledger object which tracks Oracle
Expand Down Expand Up @@ -511,13 +512,24 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
{sfAssetsAvailable, SoeDefault},
{sfAssetsMaximum, SoeDefault},
{sfLossUnrealized, SoeDefault},
{sfUnearnedInterest, SoeDefault},
{sfAccrualRate, SoeDefault},
{sfLastAccrualTime, SoeDefault},
{sfShareMPTID, SoeRequired},
{sfWithdrawalPolicy, SoeRequired},
{sfAccountingMethod, SoeDefault},
{sfScale, SoeDefault},
{sfLEVersion, SoeDefault},
{sfVaultKind, SoeDefault},
{sfSubscriptionDate, SoeOptional},
{sfRedemptionDate, SoeOptional},
{sfDealingInterval, SoeDefault},
{sfDealingWindow, SoeDefault},
{sfStruckPrice, SoeDefault},
{sfStruckUntil, SoeDefault},
{sfDepositFee, SoeDefault},
{sfRedemptionFee, SoeDefault},
{sfRedemptionPeriod, SoeDefault},
// no SharesTotal ever (use MPTIssuance.sfOutstandingAmount)
// no PermissionedDomainID ever (use MPTIssuance.sfDomainID)
}))
Expand Down
14 changes: 14 additions & 0 deletions include/xrpl/protocol/detail/sfields.macro
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ TYPED_SFIELD(sfWasLockingChainSend, UINT8, 19)
TYPED_SFIELD(sfWithdrawalPolicy, UINT8, 20)
TYPED_SFIELD(sfContractResult, UINT8, 21)
TYPED_SFIELD(sfVaultKind, UINT8, 22)
TYPED_SFIELD(sfAccountingMethod, UINT8, 23)

// 16-bit integers (common)
TYPED_SFIELD(sfLedgerEntryType, UINT16, 1, SField::kSmdNever)
Expand Down Expand Up @@ -128,6 +129,14 @@ TYPED_SFIELD(sfBytecodeSizeLimit, UINT32, 82)
TYPED_SFIELD(sfGasPrice, UINT32, 83)
TYPED_SFIELD(sfGas, UINT32, 84)
TYPED_SFIELD(sfGasUsed, UINT32, 85)
TYPED_SFIELD(sfLastAccrualTime, UINT32, 86)
TYPED_SFIELD(sfDealingInterval, UINT32, 87)
TYPED_SFIELD(sfDealingWindow, UINT32, 88)
TYPED_SFIELD(sfStruckUntil, UINT32, 89)
TYPED_SFIELD(sfDepositFee, UINT32, 90)
TYPED_SFIELD(sfRedemptionFee, UINT32, 91)
TYPED_SFIELD(sfRedemptionPeriod, UINT32, 92)
TYPED_SFIELD(sfRedemptionAfter, UINT32, 93)

// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
Expand Down Expand Up @@ -239,6 +248,11 @@ TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15, SField::kSmdNeedsAsset | SField::kSmdDefault)
TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16)
TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset | SField::kSmdDefault)
TYPED_SFIELD(sfUnearnedInterest, NUMBER, 18, SField::kSmdNeedsAsset | SField::kSmdDefault)
// Assets earned per second, summed over a vault's performing loans. A rate,
// not an amount, so it carries no asset association.
TYPED_SFIELD(sfAccrualRate, NUMBER, 19, SField::kSmdDefault)
TYPED_SFIELD(sfStruckPrice, NUMBER, 20, SField::kSmdDefault)

// 32-bit signed (common)
TYPED_SFIELD(sfLoanScale, INT32, 1)
Expand Down
9 changes: 9 additions & 0 deletions include/xrpl/protocol/detail/transactions.macro
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,12 @@ TRANSACTION(ttVAULT_CREATE, 65, VaultCreate,
{sfVaultKind, SoeOptional},
{sfSubscriptionDate, SoeOptional},
{sfRedemptionDate, SoeOptional},
{sfDealingInterval, SoeOptional},
{sfDealingWindow, SoeOptional},
{sfDepositFee, SoeOptional},
{sfRedemptionFee, SoeOptional},
{sfRedemptionPeriod, SoeOptional},
{sfAccountingMethod, SoeOptional},
}))

/** This transaction updates a single asset vault. */
Expand All @@ -804,6 +810,9 @@ TRANSACTION(ttVAULT_SET, 66, VaultSet,
{sfAssetsMaximum, SoeOptional},
{sfDomainID, SoeOptional},
{sfData, SoeOptional},
{sfDepositFee, SoeOptional},
{sfRedemptionFee, SoeOptional},
{sfRedemptionPeriod, SoeOptional},
}))

/** This transaction deletes a single asset vault. */
Expand Down
Loading