Address PR comments

Signed-off-by: JCW <a1q123456@users.noreply.github.com>
This commit is contained in:
JCW
2026-03-24 15:23:14 +00:00
parent 6466e94bb8
commit 4c665f1678
6 changed files with 117 additions and 42 deletions

View File

@@ -57,6 +57,32 @@ public:
{
return this->tx_->at(sfVaultID);
}
/**
* @brief Get sfMemoData (soeOPTIONAL)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getMemoData() const
{
if (hasMemoData())
{
return this->tx_->at(sfMemoData);
}
return std::nullopt;
}
/**
* @brief Check if sfMemoData is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasMemoData() const
{
return this->tx_->isFieldPresent(sfMemoData);
}
};
/**
@@ -112,6 +138,17 @@ public:
return *this;
}
/**
* @brief Set sfMemoData (soeOPTIONAL)
* @return Reference to this builder for method chaining.
*/
VaultDeleteBuilder&
setMemoData(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfMemoData] = value;
return *this;
}
/**
* @brief Build and return the VaultDelete wrapper.
* @param publicKey The public key for signing.

View File

@@ -140,40 +140,39 @@ LoanManage::defaultLoan(
// Apply the First-Loss Capital to the Default Amount
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
TenthBips32 const coverRateLiquidation{brokerSle->at(sfCoverRateLiquidation)};
auto const coverRateLiquidation{brokerSle->at(~sfCoverRateLiquidation)};
auto const covered = [&]() {
auto const defaultCovered = [&]() {
// Always round the minimum required up.
NumberRoundModeGuard mg(Number::upward);
Number covered;
if (view.rules().enabled(fixLendingProtocolV1_1))
if (view.rules().enabled(featureLendingProtocolV1_1) && !coverRateLiquidation)
{
// New formula: DefaultCovered = min(DefaultAmount × CoverRateMinimum, CoverAvailable)
// Round the liquidation amount up
return roundToAsset(vaultAsset, tenthBipsOfValue(totalDefaultAmount, coverRateMinimum), loanScale);
covered = roundToAsset(
vaultAsset, tenthBipsOfValue(totalDefaultAmount, coverRateMinimum), loanScale);
}
else
{
auto const minimumCover = tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
auto const minimumCover =
tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
// Round the liquidation amount up
return roundToAsset(
covered = roundToAsset(
vaultAsset,
/*
* This formula is from the XLS-66 spec, section 3.2.3.2 (State
* Changes), specifically "if the `tfLoanDefault` flag is set" /
* "Apply the First-Loss Capital to the Default Amount"
*/
std::min(tenthBipsOfValue(minimumCover, coverRateLiquidation), totalDefaultAmount),
std::min(
tenthBipsOfValue(minimumCover, TenthBips32{coverRateLiquidation.value_or(0)}),
totalDefaultAmount),
loanScale);
}
}();
auto const defaultCovered = [&]() {
// Always round the minimum required up.
NumberRoundModeGuard mg(Number::upward);
auto const coverAvailable = *brokerSle->at(sfCoverAvailable);
return std::min(covered, coverAvailable);
}();

View File

@@ -262,8 +262,7 @@ Batch::preflight(PreflightContext const& ctx)
return temINVALID;
}
if (!ctx.rules.enabled(fixLendingProtocolV1_1) &&
std::any_of(
if (std::any_of(
disabledTxTypes.begin(), disabledTxTypes.end(), [txType](auto const& disabled) {
return txType == disabled;
}))

View File

@@ -2600,18 +2600,16 @@ class Batch_test : public beast::unit_test::suite
{
testcase("loan");
bool const lendingBatchEnabled = !std::any_of(
Batch::disabledTxTypes.begin(), Batch::disabledTxTypes.end(), [](auto const& disabled) {
return disabled == ttLOAN_BROKER_SET;
});
using namespace test::jtx;
test::jtx::Env env{
*this, features | featureSingleAssetVault | featureLendingProtocol | featureMPTokensV1};
bool const lendingBatchEnabled =
!std::any_of(
Batch::disabledTxTypes.begin(),
Batch::disabledTxTypes.end(),
[](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; }) ||
env.enabled(fixLendingProtocolV1_1);
Account const issuer{"issuer"};
// For simplicity, lender will be the sole actor for the vault &
// brokers.

View File

@@ -1946,7 +1946,7 @@ protected:
NumberRoundModeGuard mg(Number::upward);
auto const totalDefaultAmount = state.totalValue - state.managementFeeOutstanding;
auto const defaultAmount = [&] {
if (env.enabled(fixLendingProtocolV1_1))
if (env.enabled(featureLendingProtocolV1_1))
{
// DefaultCovered = min(DefaultAmount × CoverRateMinimum, CoverAvailable)
return roundToAsset(
@@ -1957,13 +1957,14 @@ protected:
else
{
// From XLS-66 spec, section 3.2.3.2:
// DefaultCovered = min(DebtTotal × CoverRateMinimum × CoverRateLiquidation, DefaultAmount,
// CoverAvailable)
// DefaultCovered = min(DebtTotal × CoverRateMinimum × CoverRateLiquidation,
// DefaultAmount, CoverAvailable)
return roundToAsset(
broker.asset,
std::min(
tenthBipsOfValue(
tenthBipsOfValue(brokerSle->at(sfDebtTotal), broker.params.coverRateMin),
tenthBipsOfValue(
brokerSle->at(sfDebtTotal), broker.params.coverRateMin),
broker.params.coverRateLiquidation),
state.totalValue - state.managementFeeOutstanding),
state.loanScale);
@@ -3603,23 +3604,15 @@ protected:
// From FIND-001
testcase << "Batch Bypass Counterparty";
bool const lendingBatchEnabled = !std::any_of(
Batch::disabledTxTypes.begin(), Batch::disabledTxTypes.end(), [](auto const& disabled) {
return disabled == ttLOAN_BROKER_SET;
});
using namespace jtx;
using namespace std::chrono_literals;
Env env(*this, all);
bool const lendingBatchEnabled =
!std::any_of(
Batch::disabledTxTypes.begin(),
Batch::disabledTxTypes.end(),
[](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; }) ||
env.enabled(fixLendingProtocolV1_1);
if (!lendingBatchEnabled)
{
pass();
return;
}
Account const lender{"lender"};
Account const borrower{"borrower"};
@@ -6986,7 +6979,7 @@ protected:
auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal);
auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable);
if (env.enabled(fixLendingProtocolV1_1))
if (env.enabled(featureLendingProtocolV1_1))
{
// Proportional default cover
// Loan 1 Defaults: 20% of Loan A (50,134) = 10,027 seizure
@@ -7023,7 +7016,7 @@ protected:
// Both scenarios: DebtTotal should be 0 after both loans default
BEAST_EXPECT(afterSecondDebtTotal == 0);
if (env.enabled(fixLendingProtocolV1_1))
if (env.enabled(featureLendingProtocolV1_1))
{
// Proportional default cover
// Loan 2 Defaults: 20% of Loan B (50,134) = 10,027 seizure
@@ -7226,7 +7219,7 @@ public:
testLoanPayBrokerOwnerUnauthorizedMPT();
testLoanPayBrokerOwnerNoPermissionedDomainMPT();
testLoanSetBrokerOwnerNoPermissionedDomainMPT();
testSequentialFLCDepletion(all - fixLendingProtocolV1_1);
testSequentialFLCDepletion(all - featureLendingProtocolV1_1);
testSequentialFLCDepletion(all);
}
};

View File

@@ -30,6 +30,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
// Transaction-specific field values
auto const vaultIDValue = canonical_UINT256();
auto const memoDataValue = canonical_VL();
VaultDeleteBuilder builder{
accountValue,
@@ -39,6 +40,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
};
// Set optional fields
builder.setMemoData(memoDataValue);
auto tx = builder.build(publicKey, secretKey);
@@ -62,6 +64,14 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
}
// Verify optional fields
{
auto const& expected = memoDataValue;
auto const actualOpt = tx.getMemoData();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present";
expectEqualField(expected, *actualOpt, "sfMemoData");
EXPECT_TRUE(tx.hasMemoData());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
@@ -79,6 +89,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
// Transaction-specific field values
auto const vaultIDValue = canonical_UINT256();
auto const memoDataValue = canonical_VL();
// Build an initial transaction
VaultDeleteBuilder initialBuilder{
@@ -88,6 +99,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
feeValue
};
initialBuilder.setMemoData(memoDataValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
@@ -112,6 +124,13 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
}
// Verify optional fields
{
auto const& expected = memoDataValue;
auto const actualOpt = rebuiltTx.getMemoData();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present";
expectEqualField(expected, *actualOpt, "sfMemoData");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
@@ -142,5 +161,35 @@ TEST(TransactionsVaultDeleteTests, BuilderThrowsOnWrongTxType)
EXPECT_THROW(VaultDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error);
}
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsVaultDeleteTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::secp256k1, generateSeed("testVaultDeleteNullopt"));
// Common transaction fields
auto const accountValue = calcAccountID(publicKey);
std::uint32_t const sequenceValue = 3;
auto const feeValue = canonical_AMOUNT();
// Transaction-specific required field values
auto const vaultIDValue = canonical_UINT256();
VaultDeleteBuilder builder{
accountValue,
vaultIDValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasMemoData());
EXPECT_FALSE(tx.getMemoData().has_value());
}
}