diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index 64b61c2024..8e7fb2e4d4 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -38,6 +38,8 @@ This section contains changes targeting a future version. ### Bugfixes - Peer Crawler: The `port` field in `overlay.active[]` now consistently returns an integer instead of a string for outbound peers. [#6318](https://github.com/XRPLF/rippled/pull/6318) +- `ping`: The `ip` field is no longer returned as an empty string for proxied connections without a forwarded-for header. It is now omitted, consistent with the behavior for identified connections. [#6730](https://github.com/XRPLF/rippled/pull/6730) +- gRPC `GetLedgerDiff`: Fixed error message that incorrectly said "base ledger not validated" when the desired ledger was not validated. [#6730](https://github.com/XRPLF/rippled/pull/6730) ## XRP Ledger server version 3.1.0 diff --git a/include/xrpl/tx/transactors/dex/AMMHelpers.h b/include/xrpl/ledger/helpers/AMMHelpers.h similarity index 100% rename from include/xrpl/tx/transactors/dex/AMMHelpers.h rename to include/xrpl/ledger/helpers/AMMHelpers.h diff --git a/include/xrpl/tx/transactors/dex/AMMUtils.h b/include/xrpl/ledger/helpers/AMMUtils.h similarity index 100% rename from include/xrpl/tx/transactors/dex/AMMUtils.h rename to include/xrpl/ledger/helpers/AMMUtils.h diff --git a/include/xrpl/tx/transactors/delegate/DelegateUtils.h b/include/xrpl/ledger/helpers/DelegateHelpers.h similarity index 100% rename from include/xrpl/tx/transactors/delegate/DelegateUtils.h rename to include/xrpl/ledger/helpers/DelegateHelpers.h diff --git a/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h b/include/xrpl/ledger/helpers/EscrowHelpers.h similarity index 97% rename from src/libxrpl/tx/transactors/escrow/EscrowHelpers.h rename to include/xrpl/ledger/helpers/EscrowHelpers.h index f9eab5e90f..7525fd9ba8 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowHelpers.h +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h @@ -10,7 +10,6 @@ #include #include #include -#include #include @@ -197,8 +196,7 @@ escrowUnlockApplyHelper( return tecINSUFFICIENT_RESERVE; } - if (auto const ter = MPTokenAuthorize::createMPToken(view, mptID, receiver, 0); - !isTesSuccess(ter)) + if (auto const ter = createMPToken(view, mptID, receiver, 0); !isTesSuccess(ter)) { return ter; // LCOV_EXCL_LINE } diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h b/include/xrpl/ledger/helpers/MPTokenHelpers.h index ab487280b9..9f7d639285 100644 --- a/include/xrpl/ledger/helpers/MPTokenHelpers.h +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h @@ -157,4 +157,11 @@ rippleUnlockEscrowMPT( STAmount const& grossAmount, beast::Journal j); +TER +createMPToken( + ApplyView& view, + MPTID const& mptIssuanceID, + AccountID const& account, + std::uint32_t const flags); + } // namespace xrpl diff --git a/include/xrpl/tx/transactors/nft/NFTokenUtils.h b/include/xrpl/ledger/helpers/NFTokenHelpers.h similarity index 99% rename from include/xrpl/tx/transactors/nft/NFTokenUtils.h rename to include/xrpl/ledger/helpers/NFTokenHelpers.h index 33aab068c6..d8dac4caaf 100644 --- a/include/xrpl/tx/transactors/nft/NFTokenUtils.h +++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h @@ -1,12 +1,12 @@ #pragma once +#include #include #include #include #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelHelpers.h b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h similarity index 100% rename from src/libxrpl/tx/transactors/payment_channel/PaymentChannelHelpers.h rename to include/xrpl/ledger/helpers/PaymentChannelHelpers.h diff --git a/include/xrpl/tx/transactors/dex/PermissionedDEXHelpers.h b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h similarity index 100% rename from include/xrpl/tx/transactors/dex/PermissionedDEXHelpers.h rename to include/xrpl/ledger/helpers/PermissionedDEXHelpers.h diff --git a/include/xrpl/tx/paths/AMMLiquidity.h b/include/xrpl/tx/paths/AMMLiquidity.h index 71b8dbb12a..87d6ffe32f 100644 --- a/include/xrpl/tx/paths/AMMLiquidity.h +++ b/include/xrpl/tx/paths/AMMLiquidity.h @@ -3,10 +3,10 @@ #include #include #include +#include +#include #include #include -#include -#include namespace xrpl { diff --git a/include/xrpl/tx/paths/detail/StrandFlow.h b/include/xrpl/tx/paths/detail/StrandFlow.h index fba631c695..21cf04dc07 100644 --- a/include/xrpl/tx/paths/detail/StrandFlow.h +++ b/include/xrpl/tx/paths/detail/StrandFlow.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -13,7 +14,6 @@ #include #include #include -#include #include diff --git a/include/xrpl/tx/transactors/nft/NFTokenMint.h b/include/xrpl/tx/transactors/nft/NFTokenMint.h index d4eeba2bf0..d04f88ed3b 100644 --- a/include/xrpl/tx/transactors/nft/NFTokenMint.h +++ b/include/xrpl/tx/transactors/nft/NFTokenMint.h @@ -1,8 +1,8 @@ #pragma once +#include #include #include -#include namespace xrpl { diff --git a/include/xrpl/tx/transactors/token/MPTokenAuthorize.h b/include/xrpl/tx/transactors/token/MPTokenAuthorize.h index b6a34d4f14..3210608e73 100644 --- a/include/xrpl/tx/transactors/token/MPTokenAuthorize.h +++ b/include/xrpl/tx/transactors/token/MPTokenAuthorize.h @@ -31,13 +31,6 @@ public: static TER preclaim(PreclaimContext const& ctx); - static TER - createMPToken( - ApplyView& view, - MPTID const& mptIssuanceID, - AccountID const& account, - std::uint32_t const flags); - TER doApply() override; }; diff --git a/src/libxrpl/tx/transactors/dex/AMMHelpers.cpp b/src/libxrpl/ledger/helpers/AMMHelpers.cpp similarity index 99% rename from src/libxrpl/tx/transactors/dex/AMMHelpers.cpp rename to src/libxrpl/ledger/helpers/AMMHelpers.cpp index 386608229b..c65bb1ff11 100644 --- a/src/libxrpl/tx/transactors/dex/AMMHelpers.cpp +++ b/src/libxrpl/ledger/helpers/AMMHelpers.cpp @@ -1,4 +1,4 @@ -#include +#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/dex/AMMUtils.cpp b/src/libxrpl/ledger/helpers/AMMUtils.cpp similarity index 99% rename from src/libxrpl/tx/transactors/dex/AMMUtils.cpp rename to src/libxrpl/ledger/helpers/AMMUtils.cpp index 616b6a9902..c43f647ced 100644 --- a/src/libxrpl/tx/transactors/dex/AMMUtils.cpp +++ b/src/libxrpl/ledger/helpers/AMMUtils.cpp @@ -1,11 +1,11 @@ #include #include #include +#include +#include #include #include #include -#include -#include namespace xrpl { diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp index bf8bdcea3e..7b88dc069e 100644 --- a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp @@ -139,7 +139,9 @@ authorizeMPToken( { auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); auto const sleMpt = view.peek(mptokenKey); - if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0) + if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0 || + (view.rules().enabled(fixSecurity3_1_3) && + (*sleMpt)[~sfLockedAmount].value_or(0) != 0)) return tecINTERNAL; // LCOV_EXCL_LINE if (!view.dirRemove( @@ -252,7 +254,8 @@ removeEmptyHolding( // balance, it can not just be deleted, because that will throw the issuance // accounting out of balance, so fail. Since this should be impossible // anyway, I'm not going to put any effort into it. - if (mptoken->at(sfMPTAmount) != 0) + if (mptoken->at(sfMPTAmount) != 0 || + (view.rules().enabled(fixSecurity3_1_3) && (*mptoken)[~sfLockedAmount].value_or(0) != 0)) return tecHAS_OBLIGATIONS; return authorizeMPToken( @@ -749,4 +752,30 @@ rippleUnlockEscrowMPT( return tesSUCCESS; } +TER +createMPToken( + ApplyView& view, + MPTID const& mptIssuanceID, + AccountID const& account, + std::uint32_t const flags) +{ + auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); + + auto const ownerNode = + view.dirInsert(keylet::ownerDir(account), mptokenKey, describeOwnerDir(account)); + + if (!ownerNode) + return tecDIR_FULL; // LCOV_EXCL_LINE + + auto mptoken = std::make_shared(mptokenKey); + (*mptoken)[sfAccount] = account; + (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; + (*mptoken)[sfFlags] = flags; + (*mptoken)[sfOwnerNode] = *ownerNode; + + view.insert(mptoken); + + return tesSUCCESS; +} + } // namespace xrpl diff --git a/src/libxrpl/tx/transactors/nft/NFTokenUtils.cpp b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp similarity index 99% rename from src/libxrpl/tx/transactors/nft/NFTokenUtils.cpp rename to src/libxrpl/ledger/helpers/NFTokenHelpers.cpp index 0d7be77f35..661944e097 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenUtils.cpp +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp @@ -1,15 +1,16 @@ +#include #include #include #include #include #include +#include #include #include #include #include #include #include -#include #include #include diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelHelpers.cpp b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp similarity index 95% rename from src/libxrpl/tx/transactors/payment_channel/PaymentChannelHelpers.cpp rename to src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp index e1aa6a8b4c..5150205720 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelHelpers.cpp +++ b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp @@ -1,10 +1,9 @@ #include #include #include +#include #include -#include - namespace xrpl { TER diff --git a/src/libxrpl/tx/transactors/dex/PermissionedDEXHelpers.cpp b/src/libxrpl/ledger/helpers/PermissionedDEXHelpers.cpp similarity index 97% rename from src/libxrpl/tx/transactors/dex/PermissionedDEXHelpers.cpp rename to src/libxrpl/ledger/helpers/PermissionedDEXHelpers.cpp index d857795e39..4b2bde19f8 100644 --- a/src/libxrpl/tx/transactors/dex/PermissionedDEXHelpers.cpp +++ b/src/libxrpl/ledger/helpers/PermissionedDEXHelpers.cpp @@ -1,5 +1,5 @@ #include -#include +#include namespace xrpl { namespace permissioned_dex { diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index db4a07e41c..bc8e796fa0 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -16,8 +18,6 @@ #include #include #include -#include -#include namespace xrpl { diff --git a/src/libxrpl/tx/invariants/AMMInvariant.cpp b/src/libxrpl/tx/invariants/AMMInvariant.cpp index 96df97016f..0be4bedc07 100644 --- a/src/libxrpl/tx/invariants/AMMInvariant.cpp +++ b/src/libxrpl/tx/invariants/AMMInvariant.cpp @@ -2,9 +2,9 @@ // #include #include +#include +#include #include -#include -#include namespace xrpl { diff --git a/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp b/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp index 2bc9e622ad..8ee0a0deb8 100644 --- a/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp +++ b/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp @@ -175,18 +175,32 @@ ValidLoanBroker::finalize( return false; } auto const& vaultAsset = vault->at(sfAsset); - if (after->at(sfCoverAvailable) < accountHolds( - view, - after->at(sfAccount), - vaultAsset, - FreezeHandling::fhIGNORE_FREEZE, - AuthHandling::ahIGNORE_AUTH, - j)) + auto const pseudoBalance = accountHolds( + view, + after->at(sfAccount), + vaultAsset, + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j); + if (after->at(sfCoverAvailable) < pseudoBalance) { JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available " "is less than pseudo-account asset balance"; return false; } + + if (view.rules().enabled(fixSecurity3_1_3)) + { + // Don't check the balance when LoanBroker is deleted, + // sfCoverAvailable is not zeroed + if (tx.getTxnType() != ttLOAN_BROKER_DELETE && + after->at(sfCoverAvailable) > pseudoBalance) + { + JLOG(j.fatal()) << "Invariant failed: Loan Broker cover available is greater " + "than pseudo-account asset balance"; + return false; + } + } } return true; } diff --git a/src/libxrpl/tx/invariants/NFTInvariant.cpp b/src/libxrpl/tx/invariants/NFTInvariant.cpp index cf00dc9290..e37e55e709 100644 --- a/src/libxrpl/tx/invariants/NFTInvariant.cpp +++ b/src/libxrpl/tx/invariants/NFTInvariant.cpp @@ -2,11 +2,12 @@ // #include #include +#include #include #include #include +#include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/paths/BookStep.cpp b/src/libxrpl/tx/paths/BookStep.cpp index 788db25bbf..4daf383b1c 100644 --- a/src/libxrpl/tx/paths/BookStep.cpp +++ b/src/libxrpl/tx/paths/BookStep.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -13,7 +14,6 @@ #include #include #include -#include #include diff --git a/src/libxrpl/tx/paths/OfferStream.cpp b/src/libxrpl/tx/paths/OfferStream.cpp index acb2df1429..411b5cb05b 100644 --- a/src/libxrpl/tx/paths/OfferStream.cpp +++ b/src/libxrpl/tx/paths/OfferStream.cpp @@ -1,11 +1,11 @@ #include #include #include +#include #include #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp b/src/libxrpl/tx/transactors/account/AccountDelete.cpp index 0a4994a7ab..264447a9cf 100644 --- a/src/libxrpl/tx/transactors/account/AccountDelete.cpp +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -14,7 +15,6 @@ #include #include #include -#include #include #include @@ -293,7 +293,7 @@ AccountDelete::preclaim(PreclaimContext const& ctx) if (!cdirFirst(ctx.view, ownerDirKeylet.key, sleDirNode, uDirEntry, dirEntry)) return tesSUCCESS; - std::int32_t deletableDirEntryCount{0}; + std::uint32_t deletableDirEntryCount{0}; do { // Make sure any directory node types that we find are the kind diff --git a/src/libxrpl/tx/transactors/account/AccountSet.cpp b/src/libxrpl/tx/transactors/account/AccountSet.cpp index e1e356ae92..a63d3c94e6 100644 --- a/src/libxrpl/tx/transactors/account/AccountSet.cpp +++ b/src/libxrpl/tx/transactors/account/AccountSet.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -8,7 +9,6 @@ #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp index d9d74a1212..862fcf280c 100644 --- a/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp +++ b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp @@ -1,5 +1,5 @@ +#include #include -#include namespace xrpl { NotTEC diff --git a/src/libxrpl/tx/transactors/dex/AMMBid.cpp b/src/libxrpl/tx/transactors/dex/AMMBid.cpp index 5392333201..60c73dff7d 100644 --- a/src/libxrpl/tx/transactors/dex/AMMBid.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMBid.cpp @@ -1,13 +1,13 @@ #include #include +#include +#include #include #include #include #include #include #include -#include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp index 9fe7620c91..eaacbc3017 100644 --- a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp @@ -1,13 +1,13 @@ #include #include +#include +#include #include #include #include #include #include #include -#include -#include #include #include diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp index c61656a1a6..c826d53ef4 100644 --- a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp @@ -1,14 +1,14 @@ #include #include #include +#include +#include #include #include #include #include #include #include -#include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/dex/AMMDelete.cpp b/src/libxrpl/tx/transactors/dex/AMMDelete.cpp index 0fbaa4e23e..41e88e6a07 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDelete.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDelete.cpp @@ -1,9 +1,9 @@ #include +#include #include #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp index 77983684ce..d6445d22fc 100644 --- a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp @@ -1,12 +1,12 @@ #include #include +#include +#include #include #include #include #include #include -#include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/dex/AMMVote.cpp b/src/libxrpl/tx/transactors/dex/AMMVote.cpp index 73e0830b1a..3d8a521cb1 100644 --- a/src/libxrpl/tx/transactors/dex/AMMVote.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMVote.cpp @@ -1,8 +1,8 @@ #include +#include #include #include #include -#include #include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp index 3d52a52393..e6dd74c19d 100644 --- a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp @@ -1,10 +1,10 @@ #include #include +#include +#include #include #include #include -#include -#include #include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp index 410a061253..9c9ac9e46f 100644 --- a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -13,7 +14,6 @@ #include #include #include -#include namespace xrpl { TxConsequences diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp index fe9202bf78..2770368113 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -9,8 +10,6 @@ #include #include -#include - namespace xrpl { NotTEC diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp index 77e8eccf54..5b1f4824bc 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -15,8 +16,6 @@ #include #include -#include - namespace xrpl { // During an EscrowFinish, the transaction must specify both diff --git a/src/libxrpl/tx/transactors/lending/LoanManage.cpp b/src/libxrpl/tx/transactors/lending/LoanManage.cpp index adef5374b9..8c3e625963 100644 --- a/src/libxrpl/tx/transactors/lending/LoanManage.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp @@ -386,21 +386,29 @@ LoanManage::doApply() return tefBAD_LEDGER; // LCOV_EXCL_LINE auto const vaultAsset = vaultSle->at(sfAsset); - // Valid flag combinations are checked in preflight. No flags is valid - - // just a noop. - if (tx.isFlag(tfLoanDefault)) - return defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_); - if (tx.isFlag(tfLoanImpair)) - return impairLoan(view, loanSle, vaultSle, vaultAsset, j_); - if (tx.isFlag(tfLoanUnimpair)) - return unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_); - // Noop, as described above. + auto const result = [&]() -> TER { + // Valid flag combinations are checked in preflight. No flags is valid - + // just a noop. + if (tx.isFlag(tfLoanDefault)) + return defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_); + if (tx.isFlag(tfLoanImpair)) + return impairLoan(view, loanSle, vaultSle, vaultAsset, j_); + if (tx.isFlag(tfLoanUnimpair)) + return unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_); + // Noop, as described above. + return tesSUCCESS; + }(); - associateAsset(*loanSle, vaultAsset); - associateAsset(*brokerSle, vaultAsset); - associateAsset(*vaultSle, vaultAsset); + // Pre-amendment, associateAsset was only called on the noop (no flags) + // path. Post-amendment, we call associateAsset on all successful paths. + if (view.rules().enabled(fixSecurity3_1_3) && isTesSuccess(result)) + { + associateAsset(*loanSle, vaultAsset); + associateAsset(*brokerSle, vaultAsset); + associateAsset(*vaultSle, vaultAsset); + } - return tesSUCCESS; + return result; } //------------------------------------------------------------------------------ diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp b/src/libxrpl/tx/transactors/lending/LoanPay.cpp index d2a4364d66..7c8c2bcfce 100644 --- a/src/libxrpl/tx/transactors/lending/LoanPay.cpp +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp @@ -156,7 +156,7 @@ LoanPay::preclaim(PreclaimContext const& ctx) if (tx.isFlag(tfLoanOverpayment) && !loanSle->isFlag(lsfLoanOverpayment)) { JLOG(ctx.j.warn()) << "Requested overpayment on a loan that doesn't allow it"; - return temINVALID_FLAG; + return ctx.view.rules().enabled(fixSecurity3_1_3) ? TER{tecNO_PERMISSION} : temINVALID_FLAG; } auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp index 8c3246dda8..ac4c7fa8ca 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp @@ -1,11 +1,11 @@ #include #include +#include #include #include #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp b/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp index cd6f44c562..59999b90ce 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp @@ -1,9 +1,9 @@ #include +#include #include #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp index df0561e076..699714e0ac 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp @@ -1,8 +1,8 @@ #include +#include #include #include #include -#include #include diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp index f5fdc89550..19bf34c560 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp @@ -1,8 +1,8 @@ #include +#include #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp b/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp index b3bb66cbc4..e45658735b 100644 --- a/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp +++ b/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp @@ -1,8 +1,8 @@ #include +#include #include #include #include -#include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp b/src/libxrpl/tx/transactors/payment/Payment.cpp index 0fc6477962..44a71989e9 100644 --- a/src/libxrpl/tx/transactors/payment/Payment.cpp +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp @@ -1,7 +1,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -9,8 +11,6 @@ #include #include #include -#include -#include #include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp index dd397d4869..1370364d06 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -9,8 +10,6 @@ #include #include -#include - namespace xrpl { bool diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp index 8e15351dd7..e73dbc0cf0 100644 --- a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp @@ -1,11 +1,10 @@ #include #include #include +#include #include #include -#include - namespace xrpl { TxConsequences diff --git a/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp b/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp index c0c172707c..490e0406c4 100644 --- a/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp +++ b/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp @@ -1,9 +1,9 @@ #include #include +#include #include #include #include -#include #include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp index 99729132ca..d0b87a91ff 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp @@ -132,32 +132,6 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx) return tesSUCCESS; } -TER -MPTokenAuthorize::createMPToken( - ApplyView& view, - MPTID const& mptIssuanceID, - AccountID const& account, - std::uint32_t const flags) -{ - auto const mptokenKey = keylet::mptoken(mptIssuanceID, account); - - auto const ownerNode = - view.dirInsert(keylet::ownerDir(account), mptokenKey, describeOwnerDir(account)); - - if (!ownerNode) - return tecDIR_FULL; // LCOV_EXCL_LINE - - auto mptoken = std::make_shared(mptokenKey); - (*mptoken)[sfAccount] = account; - (*mptoken)[sfMPTokenIssuanceID] = mptIssuanceID; - (*mptoken)[sfFlags] = flags; - (*mptoken)[sfOwnerNode] = *ownerNode; - - view.insert(mptoken); - - return tesSUCCESS; -} - TER MPTokenAuthorize::doApply() { diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp index fc09a53ae1..67e0d9077d 100644 --- a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp @@ -1,7 +1,7 @@ +#include #include #include #include -#include #include namespace xrpl { diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp b/src/libxrpl/tx/transactors/token/TrustSet.cpp index 5761c020d2..73e427074f 100644 --- a/src/libxrpl/tx/transactors/token/TrustSet.cpp +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -7,7 +8,6 @@ #include #include #include -#include #include namespace { diff --git a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp index 9602300c23..001ee984a5 100644 --- a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp +++ b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp @@ -14,6 +14,7 @@ #include #include +#include namespace xrpl { NotTEC @@ -227,7 +228,11 @@ VaultClawback::assetsToClawback( auto const mptIssuanceID = *vault->at(sfShareMPTID); MPTIssue const share{mptIssuanceID}; - if (clawbackAmount == beast::zero) + // Pre-fixSecurity3_1_3: zero-amount clawback returned early without + // clamping to assetsAvailable, allowing more assets to be recovered + // than available when there was an outstanding loan. Retained for + // ledger replay compatibility. + if (!ctx_.view().rules().enabled(fixSecurity3_1_3) && clawbackAmount == beast::zero) { auto const sharesDestroyed = accountHolds( view(), @@ -244,22 +249,40 @@ VaultClawback::assetsToClawback( } STAmount sharesDestroyed; - STAmount assetsRecovered = clawbackAmount; + STAmount assetsRecovered; + try { + if (clawbackAmount == beast::zero) + { + sharesDestroyed = accountHolds( + view(), + holder, + share, + FreezeHandling::fhIGNORE_FREEZE, + AuthHandling::ahIGNORE_AUTH, + j_); + auto const maybeAssets = + sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed); + if (!maybeAssets) + return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE + + assetsRecovered = *maybeAssets; + } + else { auto const maybeShares = - assetsToSharesWithdraw(vault, sleShareIssuance, assetsRecovered); + assetsToSharesWithdraw(vault, sleShareIssuance, clawbackAmount); if (!maybeShares) return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE sharesDestroyed = *maybeShares; + + auto const maybeAssets = + sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed); + if (!maybeAssets) + return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE + assetsRecovered = *maybeAssets; } - - auto const maybeAssets = sharesToAssetsWithdraw(vault, sleShareIssuance, sharesDestroyed); - if (!maybeAssets) - return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE - assetsRecovered = *maybeAssets; - // Clamp to maximum. if (assetsRecovered > *assetsAvailable) { diff --git a/src/test/app/AMMCalc_test.cpp b/src/test/app/AMMCalc_test.cpp index c3091a166d..021cf4bf46 100644 --- a/src/test/app/AMMCalc_test.cpp +++ b/src/test/app/AMMCalc_test.cpp @@ -1,7 +1,7 @@ #include +#include #include -#include #include diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index 245ee38ac2..a0def59c92 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -2,8 +2,8 @@ #include #include +#include #include -#include namespace xrpl { namespace test { diff --git a/src/test/app/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index cc63f3d124..281ceafd19 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -6,13 +6,13 @@ #include #include +#include #include #include #include #include #include #include -#include #include #include diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index c19eb971a7..75b29fb79d 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -7,14 +7,14 @@ #include #include +#include +#include #include #include #include #include #include #include -#include -#include #include diff --git a/src/test/app/FixNFTokenPageLinks_test.cpp b/src/test/app/FixNFTokenPageLinks_test.cpp index 25366534cd..e5ecdf2639 100644 --- a/src/test/app/FixNFTokenPageLinks_test.cpp +++ b/src/test/app/FixNFTokenPageLinks_test.cpp @@ -1,9 +1,9 @@ #include +#include #include #include #include -#include namespace xrpl { diff --git a/src/test/app/Invariants_test.cpp b/src/test/app/Invariants_test.cpp index 1927f3bf2c..e2c2a811b2 100644 --- a/src/test/app/Invariants_test.cpp +++ b/src/test/app/Invariants_test.cpp @@ -2046,36 +2046,36 @@ class Invariants_test : public beast::unit_test::suite { // Initialize with a placeholder value because there's no default // ctor + auto const setupAsset = + [&](Account const& alice, Account const& issuer, Env& env) -> PrettyAsset { + switch (assetType) + { + case Asset::IOU: { + PrettyAsset const iouAsset = issuer["IOU"]; + env(trust(alice, iouAsset(1000))); + env(pay(issuer, alice, iouAsset(1000))); + env.close(); + return iouAsset; + } + case Asset::MPT: { + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset const mptAsset = mptt.issuanceID(); + mptt.authorize({.account = alice}); + env(pay(issuer, alice, mptAsset(1000))); + env.close(); + return mptAsset; + } + case Asset::XRP: + default: + return PrettyAsset{xrpIssue(), 1'000'000}; + } + }; + Keylet loanBrokerKeylet = keylet::amendments(); Preclose const createLoanBroker = [&, this](Account const& alice, Account const& issuer, Env& env) { - PrettyAsset const asset = [&]() { - switch (assetType) - { - case Asset::IOU: { - PrettyAsset const iouAsset = issuer["IOU"]; - env(trust(alice, iouAsset(1000))); - env(pay(issuer, alice, iouAsset(1000))); - env.close(); - return iouAsset; - } - - case Asset::MPT: { - MPTTester mptt{env, issuer, mptInitNoFund}; - mptt.create( - {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); - PrettyAsset const mptAsset = mptt.issuanceID(); - mptt.authorize({.account = alice}); - env(pay(issuer, alice, mptAsset(1000))); - env.close(); - return mptAsset; - } - - case Asset::XRP: - default: - return PrettyAsset{xrpIssue(), 1'000'000}; - } - }(); + auto const asset = setupAsset(alice, issuer, env); loanBrokerKeylet = this->createLoanBroker(alice, env, asset); return BEAST_EXPECT(env.le(loanBrokerKeylet)); }; @@ -2249,6 +2249,56 @@ class Invariants_test : public beast::unit_test::suite STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, createLoanBroker); + + // Test: cover available less than pseudo-account asset balance + { + Keylet brokerKeylet = keylet::amendments(); + Preclose const createBrokerWithCover = + [&, this](Account const& alice, Account const& issuer, Env& env) { + auto const asset = setupAsset(alice, issuer, env); + brokerKeylet = this->createLoanBroker(alice, env, asset); + if (!BEAST_EXPECT(env.le(brokerKeylet))) + return false; + env(loanBroker::coverDeposit(alice, brokerKeylet.key, asset(10))); + env.close(); + return BEAST_EXPECT(env.le(brokerKeylet)); + }; + + doInvariantCheck( + {{"Loan Broker cover available is less than pseudo-account asset balance"}}, + [&](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(brokerKeylet); + if (!BEAST_EXPECT(sle)) + return false; + // Pseudo-account holds 10 units, set cover to 5 + sle->at(sfCoverAvailable) = Number(5); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createBrokerWithCover); + } + + // Test: cover available greater than pseudo-account asset balance + // (requires fixSecurity3_1_3) + doInvariantCheck( + {{"Loan Broker cover available is greater than pseudo-account asset balance"}}, + [&](Account const&, Account const&, ApplyContext& ac) { + auto sle = ac.view().peek(loanBrokerKeylet); + if (!BEAST_EXPECT(sle)) + return false; + // Pseudo-account has no cover deposited; set cover + // higher than any incidental balance + sle->at(sfCoverAvailable) = Number(1'000'000); + ac.view().update(sle); + return true; + }, + XRPAmount{}, + STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}}, + {tecINVARIANT_FAILED, tefINVARIANT_FAILED}, + createLoanBroker); } } diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index a63d31f030..05123c11c0 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -2071,7 +2071,19 @@ protected: STAmount{broker.asset, state.periodicPayment * Number{15, -1}}, tfLoanOverpayment), fee(XRPAmount{baseFee * (Number{15, -1} / loanPaymentsPerFeeIncrement + 1)}), - ter(temINVALID_FLAG)); + ter(tecNO_PERMISSION)); + + { + env.disableFeature(fixSecurity3_1_3); + env(pay(borrower, + loanKeylet.key, + STAmount{broker.asset, state.periodicPayment * Number{15, -1}}, + tfLoanOverpayment), + fee(XRPAmount{ + baseFee * (Number{15, -1} / loanPaymentsPerFeeIncrement + 1)}), + ter(temINVALID_FLAG)); + env.enableFeature(fixSecurity3_1_3); + } } // Try to send a payment marked as multiple mutually exclusive // payment types. Do not include `txFlags`, so we don't duplicate diff --git a/src/test/app/NFTokenAuth_test.cpp b/src/test/app/NFTokenAuth_test.cpp index 0e3fb24305..0c044fc009 100644 --- a/src/test/app/NFTokenAuth_test.cpp +++ b/src/test/app/NFTokenAuth_test.cpp @@ -1,6 +1,6 @@ #include -#include +#include namespace xrpl { diff --git a/src/test/app/NFTokenBurn_test.cpp b/src/test/app/NFTokenBurn_test.cpp index cd0df42c03..99b1832466 100644 --- a/src/test/app/NFTokenBurn_test.cpp +++ b/src/test/app/NFTokenBurn_test.cpp @@ -1,8 +1,9 @@ #include +#include #include #include -#include +#include #include diff --git a/src/test/app/NFTokenDir_test.cpp b/src/test/app/NFTokenDir_test.cpp index 78765cb6c0..6ed5912034 100644 --- a/src/test/app/NFTokenDir_test.cpp +++ b/src/test/app/NFTokenDir_test.cpp @@ -1,9 +1,9 @@ #include +#include #include #include #include -#include #include diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 0d391147a8..abbd5ba8e1 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -1,9 +1,9 @@ #include #include +#include #include #include -#include #include diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 823cf7aafd..fd4cbd5334 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -3370,10 +3371,10 @@ class Vault_test : public beast::unit_test::suite [&](OpenView& view, beast::Journal j) -> bool { Sandbox sb(&view, tapNONE); auto vault = sb.peek(keylet::vault(keylet.key)); - if (!BEAST_EXPECT(vault != nullptr)) + if (!BEAST_EXPECT(vault)) return false; auto shares = sb.peek(keylet::mptIssuance(vault->at(sfShareMPTID))); - if (!BEAST_EXPECT(shares != nullptr)) + if (!BEAST_EXPECT(shares)) return false; if (fn(*vault, *shares)) { @@ -4102,6 +4103,66 @@ class Vault_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(d.vaultAccount, d.shares).number() == 0); } }); + + // Non-1:1 ratio (scale=1, 10:1 shares:assets) with an outstanding loan. + // Deposit 100 IOU → 1000 shares. Borrow 40 → assetsAvailable=60. + // Clawback 80 IOU → clamped to 60, then share math uses truncation. + testCase(1, [&, this](Env& env, Data d) { + using namespace loanBroker; + using namespace loan; + + testcase("Scale clawback clamped with outstanding loan"); + + auto tx = d.vault.deposit( + {.depositor = d.depositor, + .id = d.keylet.key, + .amount = STAmount(d.asset, Number(100, 0))}); + env(tx); + env.close(); + BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000)); + + // Create a loan broker backed by this vault + auto const brokerKeylet = keylet::loanbroker(d.owner.id(), env.seq(d.owner)); + env(set(d.owner, d.keylet.key)); + env.close(); + + // Borrow 40: assetsAvailable=60, assetsTotal=100 + env(set(d.depositor, brokerKeylet.key, STAmount(d.asset, Number(40, 0))), + loan::interestRate(TenthBips32(0)), + gracePeriod(60), + paymentInterval(120), + paymentTotal(10), + sig(sfCounterpartySignature, d.owner), + fee(env.current()->fees().base * 2), + ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(d.keylet); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == STAmount(d.asset, Number(60, 0))); + BEAST_EXPECT(sle->at(sfAssetsTotal) == STAmount(d.asset, Number(100, 0))); + } + + // Request 80 IOU clawback — clamped to assetsAvailable (60) + // With scale=1 (10:1), 60 assets = 600 shares destroyed + tx = d.vault.clawback( + {.issuer = d.issuer, + .id = d.keylet.key, + .holder = d.depositor, + .amount = STAmount(d.asset, Number(80, 0))}); + env(tx, ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(d.keylet); + BEAST_EXPECT(sle != nullptr); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == STAmount(d.asset, Number(0, 0))); + BEAST_EXPECT(sle->at(sfAssetsTotal) == STAmount(d.asset, Number(40, 0))); + + // 600 of 1000 shares destroyed, 400 remain + BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(400)); + } + }); } void @@ -4648,8 +4709,7 @@ class Vault_test : public beast::unit_test::suite "VaultClawback (share) - " + prefix + " owner incomplete share clawback fails"); auto [vault, vaultKeylet] = setupVault(asset, owner, depositor); auto const& vaultSle = env.le(vaultKeylet); - BEAST_EXPECT(vaultSle != nullptr); - if (!vaultSle) + if (!BEAST_EXPECT(vaultSle)) return; Asset const share = vaultSle->at(sfShareMPTID); env(vault.clawback({ @@ -4684,8 +4744,7 @@ class Vault_test : public beast::unit_test::suite " owner explicit complete share clawback succeeds"); auto [vault, vaultKeylet] = setupVault(asset, owner, depositor); auto const& vaultSle = env.le(vaultKeylet); - BEAST_EXPECT(vaultSle != nullptr); - if (!vaultSle) + if (!BEAST_EXPECT(vaultSle)) return; Asset const share = vaultSle->at(sfShareMPTID); env(vault.clawback({ @@ -4701,8 +4760,7 @@ class Vault_test : public beast::unit_test::suite testcase("VaultClawback (share) - " + prefix + " owner can clawback own shares"); auto [vault, vaultKeylet] = setupVault(asset, owner, owner); auto const& vaultSle = env.le(vaultKeylet); - BEAST_EXPECT(vaultSle != nullptr); - if (!vaultSle) + if (!BEAST_EXPECT(vaultSle)) return; Asset const share = vaultSle->at(sfShareMPTID); env(vault.clawback({ @@ -4719,7 +4777,7 @@ class Vault_test : public beast::unit_test::suite testcase("VaultClawback (share) - " + prefix + " empty vault share clawback fails"); auto [vault, vaultKeylet] = setupVault(asset, owner, owner); auto const& vaultSle = env.le(vaultKeylet); - if (BEAST_EXPECT(vaultSle != nullptr)) + if (!BEAST_EXPECT(vaultSle)) return; Asset const share = vaultSle->at(sfShareMPTID); env(vault.clawback({ @@ -4735,6 +4793,7 @@ class Vault_test : public beast::unit_test::suite .issuer = owner, .id = vaultKeylet.key, .holder = owner, + .amount = share(vaultShareBalance(vaultKeylet)).value(), }), ter(tecNO_PERMISSION)); env.close(); @@ -4786,6 +4845,7 @@ class Vault_test : public beast::unit_test::suite using namespace loanBroker; using namespace loan; Env env(*this); + env.enableFeature(fixSecurity3_1_3); auto const setupVault = [&](PrettyAsset const& asset, Account const& owner, @@ -4799,6 +4859,7 @@ class Vault_test : public beast::unit_test::suite auto const& vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle != nullptr); + env.memoize(Account("vault", vaultSle->at(sfAccount))); env(vault.deposit( {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}), ter(tesSUCCESS)); @@ -4899,8 +4960,7 @@ class Vault_test : public beast::unit_test::suite testcase("VaultClawback (asset) - " + prefix + " issuer share clawback fails"); auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer); auto const& vaultSle = env.le(vaultKeylet); - BEAST_EXPECT(vaultSle != nullptr); - if (!vaultSle) + if (!BEAST_EXPECT(vaultSle)) return; Asset const share = vaultSle->at(sfShareMPTID); @@ -4955,6 +5015,288 @@ class Vault_test : public beast::unit_test::suite }), ter(tesSUCCESS)); } + + { + testcase( + "VaultClawback (asset) - " + prefix + + " zero-amount clawback clamped with outstanding loan"); + auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer); + + auto const vaultSle = env.le(vaultKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + + PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); + + // Create a loan broker backed by this vault + auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner)); + env(set(owner, vaultKeylet.key)); + env.close(); + + // Depositor borrows 40 units, reducing assetsAvailable to 60 + // while assetsTotal stays at 100 + env(set(depositor, brokerKeylet.key, asset(40).value()), + loan::interestRate(TenthBips32(0)), + gracePeriod(60), + paymentInterval(120), + paymentTotal(10), + sig(sfCounterpartySignature, owner), + fee(env.current()->fees().base * 2), + ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value()); + BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value()); + } + + // Zero-amount clawback (= "clawback all") should succeed, + // clamped to assetsAvailable (60) rather than the full + // share value (100). + env(vault.clawback({ + .issuer = issuer, + .id = vaultKeylet.key, + .holder = depositor, + }), + ter(tesSUCCESS)); + env.close(); + + // Only 60 assets clawed back; loan's 40 still outstanding + { + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle != nullptr); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value()); + BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value()); + + // 60 of 100 shares destroyed (1:1 ratio), 40 remain + auto const sharesAfter = env.balance(depositor, shares); + BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1})); + } + } + + { + testcase( + "VaultClawback (asset) - " + prefix + + " non-zero clawback clamped with outstanding loan"); + auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer); + + auto const vaultSle = env.le(vaultKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); + + // Create a loan broker backed by this vault + auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner)); + env(set(owner, vaultKeylet.key)); + env.close(); + + // Depositor borrows 40 units + env(set(depositor, brokerKeylet.key, asset(40).value()), + loan::interestRate(TenthBips32(0)), + gracePeriod(60), + paymentInterval(120), + paymentTotal(10), + sig(sfCounterpartySignature, owner), + fee(env.current()->fees().base * 2), + ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value()); + BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value()); + } + + // Request 100 but only 60 available — clamped to 60 + env(vault.clawback({ + .issuer = issuer, + .id = vaultKeylet.key, + .holder = depositor, + .amount = asset(100).value(), + }), + ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle != nullptr); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value()); + BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value()); + + // 60 of 100 shares destroyed (1:1 ratio), 40 remain + auto const sharesAfter = env.balance(depositor, shares); + BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1})); + } + } + + { + testcase( + "VaultClawback (asset) - " + prefix + + " partial clawback below available with outstanding loan"); + auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer); + + auto const vaultSle = env.le(vaultKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); + + // Create a loan broker backed by this vault + auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner)); + env(set(owner, vaultKeylet.key)); + env.close(); + + // Depositor borrows 40 units: assetsAvailable=60, assetsTotal=100 + env(set(depositor, brokerKeylet.key, asset(40).value()), + loan::interestRate(TenthBips32(0)), + gracePeriod(60), + paymentInterval(120), + paymentTotal(10), + sig(sfCounterpartySignature, owner), + fee(env.current()->fees().base * 2), + ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value()); + BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value()); + } + + // Clawback 30 — well under available (60), no clamping needed + env(vault.clawback({ + .issuer = issuer, + .id = vaultKeylet.key, + .holder = depositor, + .amount = asset(30).value(), + }), + ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle != nullptr); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(30).value()); + BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(70).value()); + + // 30 of 100 shares destroyed (1:1 ratio), 70 remain + auto const sharesAfter = env.balance(depositor, shares); + BEAST_EXPECT(sharesAfter == shares(Number{7, sle->at(sfScale) + 1})); + } + } + + { + testcase( + "VaultClawback (asset) - " + prefix + + " clawback exactly equal to available with outstanding loan"); + auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer); + + auto const vaultSle = env.le(vaultKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); + + auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner)); + env(set(owner, vaultKeylet.key)); + env.close(); + + // Depositor borrows 40 units: assetsAvailable=60, assetsTotal=100 + env(set(depositor, brokerKeylet.key, asset(40).value()), + loan::interestRate(TenthBips32(0)), + gracePeriod(60), + paymentInterval(120), + paymentTotal(10), + sig(sfCounterpartySignature, owner), + fee(env.current()->fees().base * 2), + ter(tesSUCCESS)); + env.close(); + + // Clawback exactly 60 — at the boundary, no clamping needed + env(vault.clawback({ + .issuer = issuer, + .id = vaultKeylet.key, + .holder = depositor, + .amount = asset(60).value(), + }), + ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle != nullptr); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value()); + BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value()); + + // 60 of 100 shares destroyed (1:1 ratio), 40 remain + auto const sharesAfter = env.balance(depositor, shares); + BEAST_EXPECT(sharesAfter == shares(Number{4, sle->at(sfScale) + 1})); + } + } + + { + testcase( + "VaultClawback (asset) - " + prefix + + " clawback with zero available (fully borrowed)"); + auto [vault, vaultKeylet] = setupVault(asset, owner, depositor, issuer); + + auto const vaultSle = env.le(vaultKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); + + auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner)); + env(set(owner, vaultKeylet.key)); + env.close(); + + // Depositor borrows all 100 units: assetsAvailable=0, assetsTotal=100 + env(set(depositor, brokerKeylet.key, asset(100).value()), + loan::interestRate(TenthBips32(0)), + gracePeriod(60), + paymentInterval(120), + paymentTotal(10), + sig(sfCounterpartySignature, owner), + fee(env.current()->fees().base * 2), + ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value()); + BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value()); + } + + auto const sharesBefore = env.balance(depositor, shares); + + // Zero-amount clawback — nothing available, clamped to 0, + // resulting in zero shares destroyed → tecPRECISION_LOSS + env(vault.clawback({ + .issuer = issuer, + .id = vaultKeylet.key, + .holder = depositor, + }), + ter(tecPRECISION_LOSS)); + env.close(); + + // Explicit amount clawback — also nothing available + env(vault.clawback({ + .issuer = issuer, + .id = vaultKeylet.key, + .holder = depositor, + .amount = asset(50).value(), + }), + ter(tecPRECISION_LOSS)); + env.close(); + + { + // Nothing changed — vault and shares unchanged + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle != nullptr); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(0).value()); + BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(100).value()); + auto const sharesAfter = env.balance(depositor, shares); + BEAST_EXPECT(sharesAfter == sharesBefore); + } + } }; Account owner{"alice"}; @@ -4972,10 +5314,10 @@ class Vault_test : public beast::unit_test::suite PrettyAsset const IOU = issuer["IOU"]; env(fset(issuer, asfAllowTrustLineClawback)); env.close(); - env.trust(IOU(1000), owner); - env.trust(IOU(1000), depositor); - env(pay(issuer, owner, IOU(1000))); - env(pay(issuer, depositor, IOU(1000))); + env.trust(IOU(2000), owner); + env.trust(IOU(2000), depositor); + env(pay(issuer, owner, IOU(2000))); + env(pay(issuer, depositor, IOU(2000))); env.close(); testCase(IOU, "IOU", owner, depositor, issuer); @@ -4985,9 +5327,77 @@ class Vault_test : public beast::unit_test::suite PrettyAsset const MPT = mptt.issuanceID(); mptt.authorize({.account = owner}); mptt.authorize({.account = depositor}); - env(pay(issuer, depositor, MPT(1000))); + env(pay(issuer, depositor, MPT(2000))); env.close(); testCase(MPT, "MPT", owner, depositor, issuer); + + // Test pre-fixSecurity3_1_3 legacy path: zero-amount clawback + // returns early without clamping to assetsAvailable. + { + testcase( + "VaultClawback (asset) - IOU pre-fixSecurity3_1_3" + " zero-amount clawback unclamped with outstanding loan"); + + env.disableFeature(fixSecurity3_1_3); + + auto [vault, vaultKeylet] = setupVault(IOU, owner, depositor, issuer); + + auto const vaultSle = env.le(vaultKeylet); + BEAST_EXPECT(vaultSle != nullptr); + if (!vaultSle) + return; + + PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); + + // Create a loan broker backed by this vault + auto const brokerKeylet = keylet::loanbroker(owner.id(), env.seq(owner)); + env(set(owner, vaultKeylet.key)); + env.close(); + + // Depositor borrows 40 units, reducing assetsAvailable to 60 + // while assetsTotal stays at 100 + env(set(depositor, brokerKeylet.key, IOU(40).value()), + loan::interestRate(TenthBips32(0)), + gracePeriod(60), + paymentInterval(120), + paymentTotal(10), + sig(sfCounterpartySignature, owner), + fee(env.current()->fees().base * 2), + ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == IOU(60).value()); + BEAST_EXPECT(sle->at(sfAssetsTotal) == IOU(100).value()); + } + + auto const sharesBefore = env.balance(depositor, shares); + + // Legacy: zero-amount clawback tries to recover the full + // share value (100) without clamping to assetsAvailable (60). + // This causes the vault balance to go negative, triggering + // the sanity check in doApply → tefINTERNAL. + env(vault.clawback({ + .issuer = issuer, + .id = vaultKeylet.key, + .holder = depositor, + }), + ter(tefINTERNAL)); + env.close(); + + { + // Transaction rolled back — vault and shares unchanged + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle != nullptr); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == IOU(60).value()); + BEAST_EXPECT(sle->at(sfAssetsTotal) == IOU(100).value()); + auto const sharesAfter = env.balance(depositor, shares); + BEAST_EXPECT(sharesAfter == sharesBefore); + } + + env.enableFeature(fixSecurity3_1_3); + } } void @@ -5231,6 +5641,240 @@ class Vault_test : public beast::unit_test::suite } } + void + testVaultEscrowedMPT() + { + using namespace test::jtx; + using namespace std::literals; + + // Verify vault deposit/withdraw/clawback respect sfLockedAmount. + // When MPT tokens are escrowed, sfMPTAmount is reduced and + // sfLockedAmount is increased. Vault operations go through + // accountSend/accountHolds which read sfMPTAmount, so escrowed + // tokens are naturally excluded. + + { + testcase("Vault deposit fails when MPT asset is escrowed"); + + Env env{*this, testable_amendments()}; + auto const baseFee = env.current()->fees().base; + Account const owner{"owner"}; + Account const depositor{"depositor"}; + Account const issuer{"issuer"}; + Account const bob{"bob"}; + + env.fund(XRP(10000), issuer, owner, depositor, bob); + env.close(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow}); + mptt.authorize({.account = owner}); + mptt.authorize({.account = depositor}); + mptt.authorize({.account = bob}); + PrettyAsset const asset = mptt.issuanceID(); + env(pay(issuer, depositor, asset(100))); + env.close(); + + // Escrow 60 of 100 MPT tokens: sfMPTAmount drops to 40 + auto const escrowSeq = env.seq(depositor); + env(escrow::create(depositor, bob, asset(60)), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + Vault const vault{env}; + auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset}); + env(tx, ter(tesSUCCESS)); + env.close(); + + // Deposit 100 should fail — only 40 spendable + env(vault.deposit( + {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + + // Deposit 40 (the unlocked balance) should succeed + env(vault.deposit({.depositor = depositor, .id = vaultKeylet.key, .amount = asset(40)}), + ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(40).value()); + } + + // Clean up escrow + env(escrow::finish(bob, depositor, escrowSeq), + escrow::condition(escrow::cb1), + escrow::fulfillment(escrow::fb1), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + } + + { + testcase("Vault withdraw respects escrowed shares"); + + Env env{*this, testable_amendments()}; + auto const baseFee = env.current()->fees().base; + Account const owner{"owner"}; + Account const depositor{"depositor"}; + Account const issuer{"issuer"}; + Account const bob{"bob"}; + + env.fund(XRP(10000), issuer, owner, depositor, bob); + env.close(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow}); + mptt.authorize({.account = owner}); + mptt.authorize({.account = depositor}); + PrettyAsset const asset = mptt.issuanceID(); + env(pay(issuer, depositor, asset(100))); + env.close(); + + Vault const vault{env}; + auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset}); + env(tx, ter(tesSUCCESS)); + env.close(); + + // Deposit 100 → get shares + env(vault.deposit( + {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}), + ter(tesSUCCESS)); + env.close(); + + auto const vaultSle = env.le(vaultKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + env.memoize(Account("vault", vaultSle->at(sfAccount))); + PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); + + // Authorize bob for share MPT so he can receive escrowed shares + auto const shareMPTID = vaultSle->at(sfShareMPTID); + { + Json::Value jv; + jv[jss::Account] = bob.human(); + jv[sfMPTokenIssuanceID] = to_string(shareMPTID); + jv[jss::TransactionType] = jss::MPTokenAuthorize; + env(jv, ter(tesSUCCESS)); + env.close(); + } + + // Escrow 60% of shares + auto const escrowAmount = shares(Number{6, vaultSle->at(sfScale) + 1}); + env(escrow::create(depositor, bob, escrowAmount), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // Withdraw all 100 should fail — only 40% of shares are unlocked + env(vault.withdraw( + {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + + // Withdraw 40 (matching unlocked shares) should succeed + env(vault.withdraw( + {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(40)}), + ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(60).value()); + } + } + + { + testcase("Vault clawback only recovers unlocked shares"); + + Env env{*this, testable_amendments() | fixSecurity3_1_3}; + auto const baseFee = env.current()->fees().base; + Account const owner{"owner"}; + Account const depositor{"depositor"}; + Account const issuer{"issuer"}; + Account const bob{"bob"}; + + env.fund(XRP(10000), issuer, owner, depositor, bob); + env.close(); + + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create( + {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock | tfMPTCanEscrow}); + mptt.authorize({.account = owner}); + mptt.authorize({.account = depositor}); + PrettyAsset const asset = mptt.issuanceID(); + env(pay(issuer, depositor, asset(100))); + env.close(); + + Vault const vault{env}; + auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset}); + env(tx, ter(tesSUCCESS)); + env.close(); + + // Deposit 100 → get shares + env(vault.deposit( + {.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)}), + ter(tesSUCCESS)); + env.close(); + + auto const vaultSle = env.le(vaultKeylet); + if (!BEAST_EXPECT(vaultSle)) + return; + env.memoize(Account("vault", vaultSle->at(sfAccount))); + PrettyAsset const shares = MPTIssue(vaultSle->at(sfShareMPTID)); + + // Authorize bob for share MPT so he can receive escrowed shares + auto const shareMPTID = vaultSle->at(sfShareMPTID); + { + Json::Value jv; + jv[jss::Account] = bob.human(); + jv[sfMPTokenIssuanceID] = to_string(shareMPTID); + jv[jss::TransactionType] = jss::MPTokenAuthorize; + env(jv, ter(tesSUCCESS)); + env.close(); + } + + // Escrow 60% of shares + auto const escrowAmount = shares(Number{6, vaultSle->at(sfScale) + 1}); + env(escrow::create(depositor, bob, escrowAmount), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // Zero-amount clawback ("all") — should only recover assets + // corresponding to unlocked shares (40%) + env(vault.clawback({ + .issuer = issuer, + .id = vaultKeylet.key, + .holder = depositor, + }), + ter(tesSUCCESS)); + env.close(); + + { + auto const sle = env.le(vaultKeylet); + BEAST_EXPECT(sle != nullptr); + // Only 40 of 100 assets recovered (matching 40% unlocked shares) + BEAST_EXPECT(sle->at(sfAssetsTotal) == asset(60).value()); + BEAST_EXPECT(sle->at(sfAssetsAvailable) == asset(60).value()); + + // Depositor's unlocked shares are now 0 + auto const sharesAfter = env.balance(depositor, shares); + BEAST_EXPECT(sharesAfter == shares(0)); + } + } + } + // Reproduction: canWithdraw IOU limit check bypassed when // withdrawal amount is specified in shares (MPT) rather than in assets. void @@ -5327,6 +5971,104 @@ class Vault_test : public beast::unit_test::suite } } + void + testRemoveEmptyHoldingLockedAmount() + { + testcase("removeEmptyHolding deletes MPToken with sfLockedAmount"); + using namespace test::jtx; + using namespace std::literals; + + auto const amendments = testable_amendments(); + auto runTest = [&](FeatureBitset f) { + Env env{*this, f}; + auto const baseFee = env.current()->fees().base; + + Account const issuer{"issuer"}; + Account const owner{"owner"}; + Account const depositor{"depositor"}; + Account const bob{"bob"}; + + env.fund(XRP(100000), issuer, owner, depositor, bob); + env.close(); + + Vault const vault{env}; + + // Create an MPT asset for the vault + MPTTester mptt{env, issuer, mptInitNoFund}; + mptt.create({.flags = tfMPTCanTransfer | tfMPTCanLock}); + PrettyAsset const asset = mptt.issuanceID(); + mptt.authorize({.account = owner}); + mptt.authorize({.account = depositor}); + env(pay(issuer, depositor, asset(1000))); + env.close(); + + // Create vault + auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); + env(tx); + env.close(); + + auto const vaultSle = env.le(keylet); + BEAST_EXPECT(vaultSle != nullptr); + auto const shareMptID = vaultSle->at(sfShareMPTID); + MPTIssue const shareIssue{shareMptID}; + + // Depositor deposits 1000 asset units into vault, receiving shares + env(vault.deposit({.depositor = depositor, .id = keylet.key, .amount = asset(1000)})); + env.close(); + + // Check depositor has shares + { + auto const sleMpt = env.le(keylet::mptoken(shareMptID, depositor)); + BEAST_EXPECT(sleMpt != nullptr); + BEAST_EXPECT(sleMpt->at(sfMPTAmount) == 1000); + } + + // Escrow 500 of those shares + env(escrow::create(depositor, bob, STAmount{shareIssue, 500}), + escrow::condition(escrow::cb1), + escrow::finish_time(env.now() + 1s), + fee(baseFee * 150), + ter(tesSUCCESS)); + env.close(); + + // Verify: sfMPTAmount=500, sfLockedAmount=500 + { + auto const sleMpt = env.le(keylet::mptoken(shareMptID, depositor)); + BEAST_EXPECT(sleMpt != nullptr); + BEAST_EXPECT(sleMpt->at(sfLockedAmount) == 500); + BEAST_EXPECT(sleMpt->at(sfMPTAmount) == 500); + } + + // Withdraw remaining spendable shares — triggers removeEmptyHolding + env(vault.withdraw({.depositor = depositor, .id = keylet.key, .amount = asset(500)}), + ter(tesSUCCESS)); + env.close(); + + auto const sleMptAfter = env.le(keylet::mptoken(shareMptID, depositor)); + if (!f[fixSecurity3_1_3]) + { + // Without the fix, removeEmptyHolding deletes the MPToken + // even though sfLockedAmount > 0, leaving the escrow's locked + // amount untracked. + BEAST_EXPECT(sleMptAfter == nullptr); + } + else + { + // With the fix, MPToken must still exist with sfLockedAmount > 0 + // and sfMPTAmount == 0 (all spendable shares withdrawn). + BEAST_EXPECT(sleMptAfter != nullptr); + if (sleMptAfter) + { + BEAST_EXPECT(sleMptAfter->at(sfLockedAmount) == 500); + BEAST_EXPECT(sleMptAfter->at(sfMPTAmount) == 0); + } + } + }; + + runTest(amendments - fixSecurity3_1_3); + runTest(amendments); + } + public: void run() override @@ -5346,8 +6088,10 @@ public: testRPC(); testVaultClawbackBurnShares(); testVaultClawbackAssets(); + testVaultEscrowedMPT(); testAssetsMaximum(); testBug6_LimitBypassWithShares(); + testRemoveEmptyHoldingLockedAmount(); } }; diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index fe8fb8c443..b06353a30d 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -2,12 +2,12 @@ #include #include +#include +#include #include #include #include #include -#include -#include namespace xrpl { namespace test { diff --git a/src/test/rpc/Roles_test.cpp b/src/test/rpc/Roles_test.cpp index e3d90a9c56..314d0972d2 100644 --- a/src/test/rpc/Roles_test.cpp +++ b/src/test/rpc/Roles_test.cpp @@ -43,6 +43,7 @@ class Roles_test : public beast::unit_test::suite Env env{*this, envconfig(secure_gateway)}; BEAST_EXPECT(env.rpc("ping")["result"]["role"] == "proxied"); + BEAST_EXPECT(!env.rpc("ping")["result"].isMember("ip")); auto wsRes = makeWSClient(env.app().config())->invoke("ping")["result"]; BEAST_EXPECT(!wsRes.isMember("unlimited") || !wsRes["unlimited"].asBool()); diff --git a/src/xrpld/app/ledger/OrderBookDBImpl.cpp b/src/xrpld/app/ledger/OrderBookDBImpl.cpp index ffd8499aba..add9c7eea9 100644 --- a/src/xrpld/app/ledger/OrderBookDBImpl.cpp +++ b/src/xrpld/app/ledger/OrderBookDBImpl.cpp @@ -2,9 +2,9 @@ #include #include +#include #include #include -#include namespace xrpl { diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp b/src/xrpld/app/misc/detail/ValidatorList.cpp index bed91afc44..0b203114b3 100644 --- a/src/xrpld/app/misc/detail/ValidatorList.cpp +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp @@ -1710,7 +1710,7 @@ ValidatorList::for_each_available( if (plCollection.status != PublisherStatus::available) continue; XRPL_ASSERT( - plCollection.maxSequence != 0, + plCollection.maxSequence.value_or(0) != 0, "xrpl::ValidatorList::for_each_available : nonzero maxSequence"); func( plCollection.rawManifest, diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp b/src/xrpld/rpc/detail/RPCHelpers.cpp index b4a0685bd6..120479ded8 100644 --- a/src/xrpld/rpc/detail/RPCHelpers.cpp +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp @@ -5,12 +5,12 @@ #include #include +#include #include #include #include #include #include -#include #include #include diff --git a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp index b879968e4e..e1ead76e85 100644 --- a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp +++ b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp @@ -4,13 +4,13 @@ #include #include +#include #include #include #include #include #include #include -#include namespace xrpl { diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp b/src/xrpld/rpc/handlers/account/AccountObjects.cpp index c09920f4a6..2e8462de2d 100644 --- a/src/xrpld/rpc/handlers/account/AccountObjects.cpp +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include diff --git a/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp b/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp index 56a4d97b94..97c4efcc7a 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp @@ -36,7 +36,7 @@ doLedgerDiffGrpc(RPC::GRPCContext& con std::dynamic_pointer_cast(desiredLedgerRv); if (!desiredLedger) { - grpc::Status const errorStatus{grpc::StatusCode::NOT_FOUND, "base ledger not validated"}; + grpc::Status const errorStatus{grpc::StatusCode::NOT_FOUND, "desired ledger not validated"}; return {response, errorStatus}; } diff --git a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp index ff25f55e6a..008b77eb81 100644 --- a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp +++ b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp @@ -5,10 +5,10 @@ #include #include #include +#include #include #include #include -#include #include diff --git a/src/xrpld/rpc/handlers/utility/Ping.cpp b/src/xrpld/rpc/handlers/utility/Ping.cpp index 4e9b18c4c9..695d90b964 100644 --- a/src/xrpld/rpc/handlers/utility/Ping.cpp +++ b/src/xrpld/rpc/handlers/utility/Ping.cpp @@ -27,7 +27,9 @@ doPing(RPC::JsonContext& context) break; case Role::PROXY: ret[jss::role] = "proxied"; - ret[jss::ip] = std::string{context.headers.forwardedFor}; + if (!context.headers.forwardedFor.empty()) + ret[jss::ip] = std::string{context.headers.forwardedFor}; + break; default:; }