mirror of
https://github.com/XRPLF/rippled.git
synced 2026-08-22 14:50:54 +00:00
refactor: add borrowFromVault helper with state validation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user