refactor: add borrowFromVault helper with state validation

This commit is contained in:
Vito
2026-03-24 11:53:04 +01:00
parent c0fc689113
commit 8f7a813ac1
3 changed files with 180 additions and 8 deletions

View File

@@ -2,6 +2,7 @@
#include <xrpl/basics/Expected.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -95,4 +96,29 @@ computeClawback(
Number const& assetsAvailable,
beast::Journal j);
/**
* Updates vault state when a loan is issued: reduces available assets by the
* borrowed amount and increases total assets by the yield (accrued interest).
* With featureLendingProtocolV1_1, also tracks the yield in InterestUnrealized
* and validates vault state after modification.
*
* @param view The ledger view to read issuance from and update.
* @param vault The vault SLE to modify. Must be of type ltVAULT.
* @param amount The principal to borrow. Must be > 0 and <= AssetsAvailable.
* @param yield The accrued interest to add. Must be >= 0. Added to both
* AssetsTotal and InterestUnrealized (v1_1 only).
* @param j Journal for error logging.
*
* @return tesSUCCESS on success, tecINTERNAL on invalid inputs or if the
* resulting vault state fails validation. The caller should validate
* inputs beforehand to return a user-facing error code.
*/
[[nodiscard]] TER
borrowFromVault(
ApplyView& view,
SLE::ref vault,
Number const& amount,
Number const& yield,
beast::Journal j);
} // namespace xrpl::vault

View File

@@ -171,25 +171,43 @@ sharesToAssetsWithdraw(SLE::const_ref vault, SLE::const_ref issuance, STAmount c
} // namespace v1
// v2 vault state validation — checks ledger invariants before math.
// v2 vault state validation — checks ledger invariants.
// Returns tecINTERNAL and logs on invalid state. Returns tesSUCCESS if valid.
TER
validateVaultState(SLE::const_ref vault, SLE::const_ref issuance, beast::Journal j)
{
Number const assetTotal = vault->at(sfAssetsTotal);
if (assetTotal == 0)
Number const assetsTotal = vault->at(sfAssetsTotal);
if (assetsTotal == 0)
return tesSUCCESS;
Number const interestUnrealized = vault->at(sfInterestUnrealized);
Number const lossUnrealized = vault->at(sfLossUnrealized);
auto const interestUnrealized = vault->at(sfInterestUnrealized);
auto const lossUnrealized = vault->at(sfLossUnrealized);
auto const assetsMaximum = vault->at(sfAssetsMaximum);
// Cannot exceed asset cap
if (assetsMaximum > 0 && assetsTotal > assetsMaximum)
{
JLOG(j.error()) << "vault state: assets total exceeds maximum"
<< " (assetsTotal=" << assetsTotal << ", assetsMaximum=" << assetsMaximum
<< ")";
return tecINTERNAL;
}
if (vault->at(sfAssetsAvailable) > assetsTotal)
{
JLOG(j.error()) << "vault state: assets available exceeds assets total"
<< " (assetsAvailable=" << vault->at(sfAssetsAvailable)
<< ", assetsTotal=" << assetsTotal << ")";
return tecINTERNAL;
}
// Deposit NAV excludes loss; withdrawal NAV excludes both.
// Deposit NAV <= 0 means all vault value is unrealized interest, which should be impossible.
Number const depositNAV = assetTotal - interestUnrealized;
Number const depositNAV = assetsTotal - interestUnrealized;
if (depositNAV <= 0)
{
JLOG(j.error()) << "vault state: deposit NAV <= 0"
<< " (assetsTotal=" << assetTotal
<< " (assetsTotal=" << assetsTotal
<< ", interestUnrealized=" << interestUnrealized << ")";
return tecINTERNAL;
}
@@ -198,7 +216,7 @@ validateVaultState(SLE::const_ref vault, SLE::const_ref issuance, beast::Journal
if (withdrawNAV < 0)
{
JLOG(j.error()) << "vault state: withdrawal NAV < 0"
<< " (assetsTotal=" << assetTotal
<< " (assetsTotal=" << assetsTotal
<< ", interestUnrealized=" << interestUnrealized
<< ", lossUnrealized=" << lossUnrealized << ")";
return tecINTERNAL;
@@ -475,4 +493,50 @@ computeClawback(
}
}
[[nodiscard]] TER
borrowFromVault(
ApplyView& view,
SLE::ref vault,
Number const& amount,
Number const& yield,
beast::Journal j)
{
XRPL_ASSERT(vault && vault->getType() == ltVAULT, "xrpl::vault::borrowFromVault : vault SLE");
if (amount <= 0 || yield < 0)
{
JLOG(j.error()) << "borrowFromVault: invalid input"
<< " (amount=" << amount << ", yield=" << yield << ")";
return tecINTERNAL;
}
// Cannot borrow more than available assets
if (vault->at(sfAssetsAvailable) < amount)
{
JLOG(j.error()) << "borrowFromVault: insufficient available assets"
<< " (available=" << *vault->at(sfAssetsAvailable) << ", amount=" << amount
<< ")";
return tecINTERNAL;
}
// Update vault state
if (view.rules().enabled(featureLendingProtocolV1_1))
vault->at(sfInterestUnrealized) += yield;
vault->at(sfAssetsAvailable) -= amount;
vault->at(sfAssetsTotal) += yield;
if (view.rules().enabled(featureLendingProtocolV1_1))
{
std::shared_ptr<SLE const> issuance =
view.read(keylet::mptIssuance(vault->at(sfShareMPTID)));
if (!issuance)
return tecINTERNAL; // LCOV_EXCL_LINE
if (auto const ter = validateVaultState(vault, issuance, j))
return ter;
}
view.update(vault);
return tesSUCCESS;
}
} // namespace xrpl::vault

View File

@@ -1,6 +1,7 @@
#include <test/jtx.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/ledger/VaultHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -706,6 +707,86 @@ class VaultHelpers_test : public beast::unit_test::suite
}
}
// Creates a Sandbox with vault and issuance SLEs inserted.
Sandbox
makeSandbox(SLE::pointer vault, SLE::pointer issuance)
{
Sandbox sb(env_->current().get(), tapNONE);
sb.insert(vault);
sb.insert(issuance);
return sb;
}
void
testBorrowFromVault()
{
using namespace vault;
testcase("borrowFromVault: normal");
{
auto vaultSle = makeVault(1000, 0, 0, 0);
(*vaultSle)[sfAssetsAvailable] = STNumber{sfAssetsAvailable, 1000};
(*vaultSle)[sfAssetsMaximum] = STNumber{sfAssetsMaximum, 0};
auto issuanceSle = makeIssuance(1000);
auto sb = makeSandbox(vaultSle, issuanceSle);
auto const ter = borrowFromVault(sb, vaultSle, 100, 10, env_->journal);
BEAST_EXPECT(ter == tesSUCCESS);
BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == 900);
BEAST_EXPECT(vaultSle->at(sfAssetsTotal) == 1010);
BEAST_EXPECT(vaultSle->at(sfInterestUnrealized) == 10);
}
testcase("borrowFromVault: zero yield");
{
auto vaultSle = makeVault(1000, 0, 0, 0);
(*vaultSle)[sfAssetsAvailable] = STNumber{sfAssetsAvailable, 1000};
(*vaultSle)[sfAssetsMaximum] = STNumber{sfAssetsMaximum, 0};
auto issuanceSle = makeIssuance(1000);
auto sb = makeSandbox(vaultSle, issuanceSle);
auto const ter = borrowFromVault(sb, vaultSle, 100, 0, env_->journal);
BEAST_EXPECT(ter == tesSUCCESS);
BEAST_EXPECT(vaultSle->at(sfAssetsAvailable) == 900);
BEAST_EXPECT(vaultSle->at(sfAssetsTotal) == 1000);
BEAST_EXPECT(vaultSle->at(sfInterestUnrealized) == 0);
}
testcase("borrowFromVault: invalid amount");
{
auto vaultSle = makeVault(1000, 0, 0, 0);
(*vaultSle)[sfAssetsAvailable] = STNumber{sfAssetsAvailable, 1000};
(*vaultSle)[sfAssetsMaximum] = STNumber{sfAssetsMaximum, 0};
auto issuanceSle = makeIssuance(1000);
auto sb = makeSandbox(vaultSle, issuanceSle);
BEAST_EXPECT(borrowFromVault(sb, vaultSle, 0, 10, env_->journal) == tecINTERNAL);
BEAST_EXPECT(borrowFromVault(sb, vaultSle, -1, 10, env_->journal) == tecINTERNAL);
}
testcase("borrowFromVault: negative yield");
{
auto vaultSle = makeVault(1000, 0, 0, 0);
(*vaultSle)[sfAssetsAvailable] = STNumber{sfAssetsAvailable, 1000};
(*vaultSle)[sfAssetsMaximum] = STNumber{sfAssetsMaximum, 0};
auto issuanceSle = makeIssuance(1000);
auto sb = makeSandbox(vaultSle, issuanceSle);
BEAST_EXPECT(borrowFromVault(sb, vaultSle, 100, -1, env_->journal) == tecINTERNAL);
}
testcase("borrowFromVault: insufficient available");
{
auto vaultSle = makeVault(1000, 0, 0, 0);
(*vaultSle)[sfAssetsAvailable] = STNumber{sfAssetsAvailable, 50};
(*vaultSle)[sfAssetsMaximum] = STNumber{sfAssetsMaximum, 0};
auto issuanceSle = makeIssuance(1000);
auto sb = makeSandbox(vaultSle, issuanceSle);
BEAST_EXPECT(borrowFromVault(sb, vaultSle, 100, 10, env_->journal) == tecINTERNAL);
}
}
public:
void
run() override
@@ -725,6 +806,7 @@ public:
testComputeWithdrawByAssets();
testComputeWithdrawByShares();
testComputeClawback();
testBorrowFromVault();
}
};