Compare commits

...

53 Commits

Author SHA1 Message Date
Ed Hennis
4bf649e9ef Merge branch 'develop' into ximinez/lending-sendmulti 2026-05-19 17:14:29 -04:00
Vito Tumas
93ac1aa7aa fix: Disable unnecessary sanity-check in VaultDeposit (#7288) 2026-05-19 16:38:50 +00:00
dependabot[bot]
d9a3af8207 ci: [DEPENDABOT] bump actions/upload-artifact from 7.0.0 to 7.0.1 (#7286)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 16:35:38 +00:00
Ayaz Salikhov
8d1083e5ea ci: Only run reusable package in public repos (#7293) 2026-05-19 13:15:11 +00:00
Fomo
1e45d363c5 fix: Set default peering port to 2459 (#6848)
Co-authored-by: Bart <bthomee@users.noreply.github.com>
2026-05-19 06:05:47 +00:00
Ed Hennis
4d7788ff4a Merge branch 'develop' into ximinez/lending-sendmulti 2026-05-14 20:39:46 -04:00
Ed Hennis
6f93cbc011 Merge branch 'develop' into ximinez/lending-sendmulti 2026-05-14 10:49:03 -04:00
Ed Hennis
fbed72cedb Merge branch 'develop' into ximinez/lending-sendmulti 2026-05-13 12:04:23 -04:00
Ed Hennis
f1f4418d59 Merge branch 'develop' into ximinez/lending-sendmulti 2026-05-12 20:11:52 -04:00
Ed Hennis
b64da840c8 Merge branch 'develop' into ximinez/lending-sendmulti 2026-05-07 18:10:44 -04:00
Ed Hennis
9869d6e75d Merge branch 'develop' into ximinez/lending-sendmulti 2026-05-07 14:19:52 -04:00
Ed Hennis
e079186a2f Merge branch 'develop' into ximinez/lending-sendmulti 2026-05-07 13:29:13 -04:00
Ed Hennis
29d8f1d18b Merge branch 'develop' into ximinez/lending-sendmulti 2026-05-06 22:35:02 -04:00
Ed Hennis
92225d2838 Merge branch 'develop' into ximinez/lending-sendmulti 2026-05-06 14:18:42 -04:00
Ed Hennis
4f7188dc98 Merge branch 'develop' into ximinez/lending-sendmulti 2026-05-05 20:08:43 -04:00
Ed Hennis
b12dd38d5c Merge branch 'develop' into ximinez/lending-sendmulti 2026-05-01 14:01:22 -04:00
Ed Hennis
70cad8850d Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-28 16:31:56 -04:00
Ed Hennis
4a1e0cc7fe Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-26 23:26:49 -04:00
Ed Hennis
404be5b054 Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-23 15:56:45 -04:00
Ed Hennis
922885a4f8 Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-22 23:53:24 -04:00
Ed Hennis
012926ead9 Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-22 14:49:42 -04:00
Ed Hennis
9a0b70b45d Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-22 13:11:15 -04:00
Ed Hennis
6311292c2b Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-21 19:35:16 -04:00
Ed Hennis
4f98a09b65 Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-20 17:50:16 -04:00
Ed Hennis
eabfa77c77 Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-20 15:45:33 -04:00
Ed Hennis
d689047950 Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-20 11:39:41 -04:00
Ed Hennis
4d4ac414ae Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-17 18:21:13 -04:00
Ed Hennis
6e751091b1 Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-16 13:45:04 -04:00
Ed Hennis
d6459c8ac7 Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-15 19:09:48 -04:00
Ed Hennis
40cb63f423 Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-15 14:29:25 -04:00
Ed Hennis
98206e6514 Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-13 20:45:27 -04:00
Ed Hennis
a047d1bc9b Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-10 12:13:23 -04:00
Ed Hennis
2ca91e701e Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-10 09:14:24 -04:00
Ed Hennis
3db7e84e06 Merge branch 'develop' into ximinez/lending-sendmulti 2026-04-09 17:00:43 -04:00
Ed Hennis
75d0960e1d Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-sendmulti
* XRPLF/develop: (105 commits)
  fix: Handle WSClient write failure when server closes WebSocket (6671)
  ci: Change conditions for uploading artifacts in public/private/org repos (6734)
  refactor: Rename non-functional uses of `ripple(d)` to `xrpl(d)` (6676)
  refactor: Move more helper files into `libxrpl/ledger/helpers` (6731)
  fix: Minor RPC fixes (6730)
  fix: Prevent deletion of MPTokens with active escrow (6635)
  fix: Clamp VaultClawback to assetsAvailable for zero-amount clawback (6646)
  fix: Add assorted Lending Protocol fixes (6678)
  fix: Change variable signedness and correctly handle `std::optional` (6657)
  refactor: Reorganize RPC handler files (6628)
  chore: Update XRPLF/actions (6713)
  docs: Add explanatory comment to checkFee (6631)
  fix: Decouple reserve from fee in delegate payment (6568)
  fix: Check trustline limits for share-denominated vault withdrawals (6645)
  fix: Remove fatal assertion on Linux thread name truncation  (6690)
  chore: Enable clang-tidy `coreguidelines` checks (6698)
  ci: Allow uploading artifacts for XRPLF org (6702)
  fix: Enforce aggregate MaximumAmount in multi-send MPT (6644)
  chore: Use nudb recipe from the upstream (6701)
  fix: Fix previous ledger size typo in RCLConsensus (6696)
  ...
2026-04-07 18:11:51 -04:00
Ed Hennis
091709e7d7 Merge branch 'develop' into ximinez/lending-sendmulti 2026-03-12 15:04:57 -04:00
Ed Hennis
b5756c44bc Merge branch 'develop' into ximinez/lending-sendmulti 2026-03-10 13:39:33 -04:00
Ed Hennis
613a94645d Merge branch 'develop' into ximinez/lending-sendmulti 2026-03-06 13:55:14 -04:00
Ed Hennis
c0323540f9 Merge branch 'develop' into ximinez/lending-sendmulti 2026-03-04 17:12:17 -04:00
Ed Hennis
e0fd480ae7 Merge branch 'develop' into ximinez/lending-sendmulti 2026-03-03 18:38:33 -04:00
Ed Hennis
fe80f0e895 Merge branch 'develop' into ximinez/lending-sendmulti 2026-02-24 17:43:52 -04:00
Ed Hennis
9988e596e9 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-sendmulti
* XRPLF/develop:
  ci: [DEPENDABOT] bump actions/upload-artifact from 4.6.2 to 6.0.0 (6396)
  ci: [DEPENDABOT] bump actions/checkout from 4.3.0 to 6.0.2 (6397)
  ci: [DEPENDABOT] bump actions/setup-python from 5.6.0 to 6.2.0 (6395)
  ci: [DEPENDABOT] bump tj-actions/changed-files from 46.0.5 to 47.0.4 (6394)
  ci: [DEPENDABOT] bump codecov/codecov-action from 5.4.3 to 5.5.2 (6398)
  ci: Build docs in PRs and in private repos (6400)
  ci: Add dependabot config (6379)
  Fix tautological assertion (6393)
2026-02-20 17:56:41 -05:00
Ed Hennis
3523c437a8 Merge commit '2c1fad1023' into ximinez/lending-sendmulti
* commit '2c1fad1023':
  chore: Apply clang-format width 100 (6387)
2026-02-20 17:56:19 -05:00
Ed Hennis
0f38b4b541 Update formatting 2026-02-20 17:52:11 -05:00
Ed Hennis
f84350c61c Merge commit '25cca465538a56cce501477f9e5e2c1c7ea2d84c' into ximinez/lending-sendmulti
* commit '25cca465538a56cce501477f9e5e2c1c7ea2d84c':
  chore: Set clang-format width to 100 in config file (6387)
2026-02-20 17:51:51 -05:00
Ed Hennis
7a118245f7 Merge branch 'develop' into ximinez/lending-sendmulti 2026-02-19 16:25:29 -05:00
Ed Hennis
47ddc34fda Merge branch 'develop' into ximinez/lending-sendmulti 2026-02-18 21:15:30 -04:00
Ed Hennis
1f579efc2f Merge branch 'develop' into ximinez/lending-sendmulti 2026-02-04 17:18:43 -04:00
Ed Hennis
e464e101be Fix formatting 2026-01-28 19:23:26 -05:00
Ed Hennis
4dfa6db32a Merge branch 'develop' into ximinez/lending-sendmulti 2026-01-28 19:33:14 -04:00
Ed Hennis
766124ed6d Merge commit '5f638f55536def0d88b970d1018a465a238e55f4' into ximinez/lending-sendmulti
* commit '5f638f55536def0d88b970d1018a465a238e55f4':
  chore: Set ColumnLimit to 120 in clang-format (6288)
2026-01-28 18:32:07 -05:00
Ed Hennis
5c34a7b8fb Merge commit '92046785d1fea5f9efe5a770d636792ea6cab78b' into ximinez/lending-sendmulti
* commit '92046785d1fea5f9efe5a770d636792ea6cab78b':
  test: Fix the `xrpl.net` unit test using async read (6241)
  ci: Upload Conan recipes for develop, release candidates, and releases (6286)
  fix: Stop embedded tests from hanging on ARM by using `atomic_flag` (6248)
  fix:  Remove DEFAULT fields that change to the default in associateAsset (6259) (6273)
  refactor: Update Boost to 1.90 (6280)
  refactor: clean up uses of `std::source_location` (6272)
  ci: Pass missing sanitizers input to actions (6266)
  ci: Properly propagate Conan credentials (6265)
  ci: Explicitly set version when exporting the Conan recipe (6264)
  ci: Use plus instead of hyphen for Conan recipe version suffix (6261)
  chore: Detect uninitialized variables in CMake files (6247)
  ci: Run on-trigger and on-pr when generate-version is modified (6257)
  refactor: Enforce 15-char limit and simplify labels for thread naming (6212)
  docs: Update Ripple Bug Bounty public key (6258)
  ci: Add missing commit hash to Conan recipe version (6256)
  fix: Include `<functional>` header in `Number.h` (6254)
  ci: Upload Conan recipe for merges into develop and commits to release (6235)
  Limit reply size on `TMGetObjectByHash` queries (6110)
  ci: remove 'master' branch as a trigger (6234)
  Improve ledger_entry lookups for fee, amendments, NUNL, and hashes (5644)
2026-01-28 18:31:27 -05:00
Ed Hennis
f6f3542b7e Refactor the sendMulti functions into a common method with callbacks 2026-01-15 12:01:23 -05:00
8 changed files with 669 additions and 22 deletions

View File

@@ -58,6 +58,7 @@ jobs:
package:
needs: [generate-matrix, generate-version]
if: ${{ github.event.repository.visibility == 'public' }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
@@ -88,8 +89,7 @@ jobs:
run: ./package/build_pkg.sh
- name: Upload package artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: ${{ github.event.repository.visibility == 'public' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.artifact_name }}-pkg-${{ needs.generate-version.outputs.version }}
path: |

View File

@@ -1466,10 +1466,7 @@ admin = 127.0.0.1
protocol = http
[port_peer]
# Many servers still use the legacy port of 51235, so for backward-compatibility
# we maintain that port number here. However, for new servers we recommend
# changing this to the default port of 2459.
port = 51235
port = 2459
ip = 0.0.0.0
# alternatively, to accept connections on IPv4 + IPv6, use:
#ip = ::

View File

@@ -452,6 +452,489 @@ doWithdraw(
return accountSend(view, sourceAcct, dstAcct, amount, j, WaiveTransferFee::Yes);
}
// Direct send w/o fees:
// - Redeeming IOUs and/or sending sender's own IOUs.
// - Create trust line if needed.
// --> bCheckIssuer : normally require issuer to be involved.
static TER
rippleCreditIOU(
ApplyView& view,
AccountID const& uSenderID,
AccountID const& uReceiverID,
STAmount const& saAmount,
bool bCheckIssuer,
beast::Journal j)
{
AccountID const& issuer = saAmount.getIssuer();
Currency const& currency = saAmount.getCurrency();
// Make sure issuer is involved.
XRPL_ASSERT(
!bCheckIssuer || uSenderID == issuer || uReceiverID == issuer,
"xrpl::rippleCreditIOU : matching issuer or don't care");
(void)issuer;
// Disallow sending to self.
XRPL_ASSERT(uSenderID != uReceiverID, "xrpl::rippleCreditIOU : sender is not receiver");
bool const bSenderHigh = uSenderID > uReceiverID;
auto const index = keylet::line(uSenderID, uReceiverID, currency);
XRPL_ASSERT(
!isXRP(uSenderID) && uSenderID != noAccount(), "xrpl::rippleCreditIOU : sender is not XRP");
XRPL_ASSERT(
!isXRP(uReceiverID) && uReceiverID != noAccount(),
"xrpl::rippleCreditIOU : receiver is not XRP");
// If the line exists, modify it accordingly.
if (auto const sleRippleState = view.peek(index))
{
STAmount saBalance = sleRippleState->getFieldAmount(sfBalance);
if (bSenderHigh)
saBalance.negate(); // Put balance in sender terms.
view.creditHook(uSenderID, uReceiverID, saAmount, saBalance);
STAmount const saBefore = saBalance;
saBalance -= saAmount;
JLOG(j.trace()) << "rippleCreditIOU: " << to_string(uSenderID) << " -> "
<< to_string(uReceiverID) << " : before=" << saBefore.getFullText()
<< " amount=" << saAmount.getFullText()
<< " after=" << saBalance.getFullText();
std::uint32_t const uFlags(sleRippleState->getFieldU32(sfFlags));
bool bDelete = false;
// FIXME This NEEDS to be cleaned up and simplified. It's impossible
// for anyone to understand.
if (saBefore > beast::zero
// Sender balance was positive.
&& saBalance <= beast::zero
// Sender is zero or negative.
&& (uFlags & (!bSenderHigh ? lsfLowReserve : lsfHighReserve))
// Sender reserve is set.
&& static_cast<bool>(uFlags & (!bSenderHigh ? lsfLowNoRipple : lsfHighNoRipple)) !=
static_cast<bool>(
view.read(keylet::account(uSenderID))->getFlags() & lsfDefaultRipple) &&
!(uFlags & (!bSenderHigh ? lsfLowFreeze : lsfHighFreeze)) &&
!sleRippleState->getFieldAmount(!bSenderHigh ? sfLowLimit : sfHighLimit)
// Sender trust limit is 0.
&& !sleRippleState->getFieldU32(!bSenderHigh ? sfLowQualityIn : sfHighQualityIn)
// Sender quality in is 0.
&& !sleRippleState->getFieldU32(!bSenderHigh ? sfLowQualityOut : sfHighQualityOut))
// Sender quality out is 0.
{
// Clear the reserve of the sender, possibly delete the line!
adjustOwnerCount(view, view.peek(keylet::account(uSenderID)), -1, j);
// Clear reserve flag.
sleRippleState->setFieldU32(
sfFlags, uFlags & (!bSenderHigh ? ~lsfLowReserve : ~lsfHighReserve));
// Balance is zero, receiver reserve is clear.
bDelete = !saBalance // Balance is zero.
&& !(uFlags & (bSenderHigh ? lsfLowReserve : lsfHighReserve));
// Receiver reserve is clear.
}
if (bSenderHigh)
saBalance.negate();
// Want to reflect balance to zero even if we are deleting line.
sleRippleState->setFieldAmount(sfBalance, saBalance);
// ONLY: Adjust ripple balance.
if (bDelete)
{
return trustDelete(
view,
sleRippleState,
bSenderHigh ? uReceiverID : uSenderID,
!bSenderHigh ? uReceiverID : uSenderID,
j);
}
view.update(sleRippleState);
return tesSUCCESS;
}
STAmount const saReceiverLimit(Issue{currency, uReceiverID});
STAmount saBalance{saAmount};
saBalance.setIssuer(noAccount());
JLOG(j.debug()) << "rippleCreditIOU: "
"create line: "
<< to_string(uSenderID) << " -> " << to_string(uReceiverID) << " : "
<< saAmount.getFullText();
auto const sleAccount = view.peek(keylet::account(uReceiverID));
if (!sleAccount)
return tefINTERNAL; // LCOV_EXCL_LINE
bool const noRipple = (sleAccount->getFlags() & lsfDefaultRipple) == 0;
return trustCreate(
view,
bSenderHigh,
uSenderID,
uReceiverID,
index.key,
sleAccount,
false,
noRipple,
false,
false,
saBalance,
saReceiverLimit,
0,
0,
j);
}
// Send regardless of limits.
// --> saAmount: Amount/currency/issuer to deliver to receiver.
// <-- saActual: Amount actually cost. Sender pays fees.
static TER
rippleSendIOU(
ApplyView& view,
AccountID const& uSenderID,
AccountID const& uReceiverID,
STAmount const& saAmount,
STAmount& saActual,
beast::Journal j,
WaiveTransferFee waiveFee)
{
auto const& issuer = saAmount.getIssuer();
XRPL_ASSERT(
!isXRP(uSenderID) && !isXRP(uReceiverID),
"xrpl::rippleSendIOU : neither sender nor receiver is XRP");
XRPL_ASSERT(uSenderID != uReceiverID, "xrpl::rippleSendIOU : sender is not receiver");
if (uSenderID == issuer || uReceiverID == issuer || issuer == noAccount())
{
// Direct send: redeeming IOUs and/or sending own IOUs.
auto const ter = rippleCreditIOU(view, uSenderID, uReceiverID, saAmount, false, j);
if (ter != tesSUCCESS)
return ter;
saActual = saAmount;
return tesSUCCESS;
}
// Sending 3rd party IOUs: transit.
// Calculate the amount to transfer accounting
// for any transfer fees if the fee is not waived:
saActual = (waiveFee == WaiveTransferFee::Yes) ? saAmount
: multiply(saAmount, transferRate(view, issuer));
JLOG(j.debug()) << "rippleSendIOU> " << to_string(uSenderID) << " - > "
<< to_string(uReceiverID) << " : deliver=" << saAmount.getFullText()
<< " cost=" << saActual.getFullText();
TER terResult = rippleCreditIOU(view, issuer, uReceiverID, saAmount, true, j);
if (tesSUCCESS == terResult)
terResult = rippleCreditIOU(view, uSenderID, issuer, saActual, true, j);
return terResult;
}
template <class TAsset>
static TER
doSendMulti(
std::string const& name,
ApplyView& view,
AccountID const& senderID,
TAsset const& issue,
MultiplePaymentDestinations const& receivers,
STAmount& actual,
beast::Journal j,
WaiveTransferFee waiveFee,
// Don't pass back parameters that the caller already has
std::function<
TER(AccountID const& senderID,
AccountID const& receiverID,
STAmount const& amount,
bool checkIssuer)> doCredit,
std::function<
TER(AccountID const& issuer, STAmount const& takeFromSender, STAmount const& amount)>
preMint = {})
{
// Use the same pattern for all the SendMulti functions to help avoid
// divergence and copy/paste errors.
auto const& issuer = issue.getIssuer();
// These values may not stay in sync
STAmount takeFromSender{issue};
actual = takeFromSender;
// Failures return immediately.
for (auto const& r : receivers)
{
auto const& receiverID = r.first;
STAmount amount{issue, r.second};
if (amount < beast::zero)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
/* If we aren't sending anything or if the sender is the same as the
* receiver then we don't need to do anything.
*/
if (!amount || (senderID == receiverID))
continue;
using namespace std::string_literals;
XRPL_ASSERT(!isXRP(receiverID), ("xrpl::"s + name + " : receiver is not XRP").c_str());
if (senderID == issuer || receiverID == issuer || issuer == noAccount())
{
if (preMint)
{
if (auto const ter = preMint(issuer, takeFromSender, amount))
return ter;
}
// Direct send: redeeming IOUs and/or sending own IOUs.
if (auto const ter = doCredit(senderID, receiverID, amount, false))
return ter;
actual += amount;
// Do not add amount to takeFromSender, because doCredit took
// it.
continue;
}
// Sending 3rd party: transit.
// Calculate the amount to transfer accounting
// for any transfer fees if the fee is not waived:
STAmount actualSend = (waiveFee == WaiveTransferFee::Yes || issue.native())
? amount
: multiply(amount, transferRate(view, amount));
actual += actualSend;
takeFromSender += actualSend;
JLOG(j.debug()) << name << "> " << to_string(senderID) << " - > " << to_string(receiverID)
<< " : deliver=" << amount.getFullText()
<< " cost=" << actualSend.getFullText();
if (TER const terResult = doCredit(issuer, receiverID, amount, true))
return terResult;
}
if (senderID != issuer && takeFromSender)
{
if (TER const terResult = doCredit(senderID, issuer, takeFromSender, true))
return terResult;
}
return tesSUCCESS;
}
// Send regardless of limits.
// --> receivers: Amount/currency/issuer to deliver to receivers.
// <-- saActual: Amount actually cost to sender. Sender pays fees.
static TER
rippleSendMultiIOU(
ApplyView& view,
AccountID const& senderID,
Issue const& issue,
MultiplePaymentDestinations const& receivers,
STAmount& actual,
beast::Journal j,
WaiveTransferFee waiveFee)
{
XRPL_ASSERT(!isXRP(senderID), "xrpl::rippleSendMultiIOU : sender is not XRP");
auto doCredit = [&view, j](
AccountID const& senderID,
AccountID const& receiverID,
STAmount const& amount,
bool checkIssuer) {
return rippleCreditIOU(view, senderID, receiverID, amount, checkIssuer, j);
};
return doSendMulti(
"rippleSendMultiIOU", view, senderID, issue, receivers, actual, j, waiveFee, doCredit);
}
static TER
rippleCreditMPT(
ApplyView& view,
AccountID const& uSenderID,
AccountID const& uReceiverID,
STAmount const& saAmount,
beast::Journal j)
{
// Do not check MPT authorization here - it must have been checked earlier
auto const mptID = keylet::mptIssuance(saAmount.get<MPTIssue>().getMptID());
auto const& issuer = saAmount.getIssuer();
auto sleIssuance = view.peek(mptID);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (uSenderID == issuer)
{
(*sleIssuance)[sfOutstandingAmount] += saAmount.mpt().value();
view.update(sleIssuance);
}
else
{
auto const mptokenID = keylet::mptoken(mptID.key, uSenderID);
if (auto sle = view.peek(mptokenID))
{
auto const amt = sle->getFieldU64(sfMPTAmount);
auto const pay = saAmount.mpt().value();
if (amt < pay)
return tecINSUFFICIENT_FUNDS;
(*sle)[sfMPTAmount] = amt - pay;
view.update(sle);
}
else
return tecNO_AUTH;
}
if (uReceiverID == issuer)
{
auto const outstanding = sleIssuance->getFieldU64(sfOutstandingAmount);
auto const redeem = saAmount.mpt().value();
if (outstanding >= redeem)
{
sleIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem);
view.update(sleIssuance);
}
else
return tecINTERNAL; // LCOV_EXCL_LINE
}
else
{
auto const mptokenID = keylet::mptoken(mptID.key, uReceiverID);
if (auto sle = view.peek(mptokenID))
{
(*sle)[sfMPTAmount] += saAmount.mpt().value();
view.update(sle);
}
else
return tecNO_AUTH;
}
return tesSUCCESS;
}
static TER
rippleSendMPT(
ApplyView& view,
AccountID const& uSenderID,
AccountID const& uReceiverID,
STAmount const& saAmount,
STAmount& saActual,
beast::Journal j,
WaiveTransferFee waiveFee)
{
XRPL_ASSERT(uSenderID != uReceiverID, "xrpl::rippleSendMPT : sender is not receiver");
// Safe to get MPT since rippleSendMPT is only called by accountSendMPT
auto const& issuer = saAmount.getIssuer();
auto const sle = view.read(keylet::mptIssuance(saAmount.get<MPTIssue>().getMptID()));
if (!sle)
return tecOBJECT_NOT_FOUND;
if (uSenderID == issuer || uReceiverID == issuer)
{
// if sender is issuer, check that the new OutstandingAmount will not
// exceed MaximumAmount
if (uSenderID == issuer)
{
auto const sendAmount = saAmount.mpt().value();
auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
if (sendAmount > maximumAmount ||
sle->getFieldU64(sfOutstandingAmount) > maximumAmount - sendAmount)
return tecPATH_DRY;
}
// Direct send: redeeming MPTs and/or sending own MPTs.
auto const ter = rippleCreditMPT(view, uSenderID, uReceiverID, saAmount, j);
if (ter != tesSUCCESS)
return ter;
saActual = saAmount;
return tesSUCCESS;
}
// Sending 3rd party MPTs: transit.
saActual = (waiveFee == WaiveTransferFee::Yes)
? saAmount
: multiply(saAmount, transferRate(view, saAmount.get<MPTIssue>().getMptID()));
JLOG(j.debug()) << "rippleSendMPT> " << to_string(uSenderID) << " - > "
<< to_string(uReceiverID) << " : deliver=" << saAmount.getFullText()
<< " cost=" << saActual.getFullText();
if (auto const terResult = rippleCreditMPT(view, issuer, uReceiverID, saAmount, j);
terResult != tesSUCCESS)
return terResult;
return rippleCreditMPT(view, uSenderID, issuer, saActual, j);
}
static TER
rippleSendMultiMPT(
ApplyView& view,
AccountID const& senderID,
MPTIssue const& mptIssue,
MultiplePaymentDestinations const& receivers,
STAmount& actual,
beast::Journal j,
WaiveTransferFee waiveFee)
{
auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID()));
if (!sle)
return tecOBJECT_NOT_FOUND;
auto preMint = [&](AccountID const& issuer,
STAmount const& takeFromSender,
STAmount const& amount) -> TER {
// if sender is issuer, check that the new OutstandingAmount will
// not exceed MaximumAmount
if (senderID == issuer)
{
XRPL_ASSERT_PARTS(
takeFromSender == beast::zero,
"rippler::rippleSendMultiMPT",
"sender == issuer, takeFromSender == zero");
auto const sendAmount = amount.mpt().value();
auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
if (sendAmount > maximumAmount ||
sle->getFieldU64(sfOutstandingAmount) > maximumAmount - sendAmount)
return tecPATH_DRY;
}
return tesSUCCESS;
};
auto doCredit =
[&view, j](
AccountID const& senderID, AccountID const& receiverID, STAmount const& amount, bool) {
return rippleCreditMPT(view, senderID, receiverID, amount, j);
};
return doSendMulti(
"rippleSendMultiMPT",
view,
senderID,
mptIssue,
receivers,
actual,
j,
waiveFee,
doCredit,
preMint);
}
TER
cleanupOnAccountDelete(
ApplyView& view,

View File

@@ -22,7 +22,7 @@ offerDelete(ApplyView& view, std::shared_ptr<SLE> const& sle, beast::Journal j)
if (!sle)
return tesSUCCESS;
auto offerIndex = sle->key();
auto owner = sle->getAccountID(sfAccount);
auto const owner = sle->getAccountID(sfAccount);
// Detect legacy directories.
uint256 const uDirectory = sle->getFieldH256(sfBookDirectory);

View File

@@ -580,7 +580,7 @@ requireAuth(ReadView const& view, Issue const& issue, AccountID const& account,
return tesSUCCESS;
}
TER
[[nodiscard]] TER
canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, AccountID const& to)
{
if (issue.native())
@@ -618,7 +618,7 @@ canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, Acc
//
//------------------------------------------------------------------------------
TER
[[nodiscard]] TER
addEmptyHolding(
ApplyView& view,
AccountID const& accountID,
@@ -672,7 +672,7 @@ addEmptyHolding(
journal);
}
TER
[[nodiscard]] TER
removeEmptyHolding(
ApplyView& view,
AccountID const& accountID,

View File

@@ -944,6 +944,85 @@ accountSendMultiIOU(
<< receivers.size() << " receivers.";
}
auto doCredit = [&view, &sender, &receivers, j](
AccountID const& senderID,
AccountID const& receiverID,
STAmount const& amount,
bool /*checkIssuer*/) -> TER {
if (!senderID)
{
SLE::pointer receiver =
receiverID != beast::zero ? view.peek(keylet::account(receiverID)) : SLE::pointer();
if (auto stream = j.trace())
{
std::string receiver_bal("-");
if (receiver)
receiver_bal = receiver->getFieldAmount(sfBalance).getFullText();
stream << "accountSendMultiIOU> " << to_string(senderID) << " -> "
<< to_string(receiverID) << " (" << receiver_bal
<< ") : " << amount.getFullText();
}
if (receiver)
{
// Increment XRP balance.
auto const rcvBal = receiver->getFieldAmount(sfBalance);
receiver->setFieldAmount(sfBalance, rcvBal + amount);
view.creditHook(xrpAccount(), receiverID, amount, -rcvBal);
view.update(receiver);
}
if (auto stream = j.trace())
{
std::string receiver_bal("-");
if (receiver)
receiver_bal = receiver->getFieldAmount(sfBalance).getFullText();
stream << "accountSendMultiIOU< " << to_string(senderID) << " -> "
<< to_string(receiverID) << " (" << receiver_bal
<< ") : " << amount.getFullText();
}
return tesSUCCESS;
}
// Sender
if (sender)
{
if (sender->getFieldAmount(sfBalance) < amount)
{
return TER{tecFAILED_PROCESSING};
}
else
{
auto const sndBal = sender->getFieldAmount(sfBalance);
view.creditHook(senderID, xrpAccount(), amount, sndBal);
// Decrement XRP balance.
sender->setFieldAmount(sfBalance, sndBal - amount);
view.update(sender);
}
}
if (auto stream = j.trace())
{
std::string sender_bal("-");
if (sender)
sender_bal = sender->getFieldAmount(sfBalance).getFullText();
stream << "accountSendMultiIOU< " << to_string(senderID) << " (" << sender_bal
<< ") -> " << receivers.size() << " receivers.";
}
return tesSUCCESS;
};
return doSendMulti(
"accountSendMultiIOU", view, senderID, issue, receivers, actual, j, waiveFee, doCredit);
// Failures return immediately.
STAmount takeFromSender{issue};
for (auto const& r : receivers)

View File

@@ -7,6 +7,7 @@
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/ledger/helpers/VaultHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
@@ -252,19 +253,26 @@ VaultDeposit::doApply()
!isTesSuccess(ter))
return ter;
// Sanity check
if (accountHolds(
view(),
accountID_,
assetsDeposited.asset(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j_) < beast::kZero)
// This check is wrong. Disable it with fixCleanup3_2_0.
// For XRP and MPT the predicate is structurally unsatisfiable: xrpLiquid clamps at zero, and
// MPT balances are unsigned. For IOUs it only fires when the deposit drove the depositor's
// trust line into debt the exact case preclaim authorizes via SpendableHandling::FullBalance.
// The check thus converts a preclaim- authorized deposit into tefINTERNAL after the asset
// transfer.
if (!view().rules().enabled(fixCleanup3_2_0))
{
// LCOV_EXCL_START
JLOG(j_.error()) << "VaultDeposit: negative balance of account assets.";
return tefINTERNAL;
// LCOV_EXCL_STOP
// Sanity check
if (accountHolds(
view(),
accountID_,
assetsDeposited.asset(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j_) < beast::kZero)
{
JLOG(j_.error()) << "VaultDeposit: negative balance of account assets.";
return tefINTERNAL;
}
}
// Transfer shares from vault to depositor.

View File

@@ -6140,10 +6140,90 @@ class Vault_test : public beast::unit_test::Suite
runTest(amendments);
}
// VaultDeposit::preclaim uses accountHolds(..., SpendableHandling::
// shFULL_BALANCE), which for an IOU asset adds the counterparty's
// LowLimit/HighLimit to the depositor's raw balance (TokenHelpers.cpp:
// getTrustLineBalance with includeOppositeLimit=true). When the
// depositor's raw balance < deposit amount but raw + opposite limit >=
// amount, preclaim is satisfied. doApply then calls
// directSendNoFeeIOU, which unconditionally subtracts saAmount from
// saBalance — driving the trust line negative — and returns tesSUCCESS.
// The post-send sanity check uses the default shSIMPLE_BALANCE (no
// opposite-limit add), sees a negative balance, and returns tefINTERNAL.
void
testVaultDepositNegativeBalanceFromOppositeLimit()
{
auto runTest = [&](FeatureBitset f, TER expected) {
using namespace test::jtx;
using namespace std::literals;
Env env{*this, f};
Account const gw{"gateway"};
Account const owner{"owner"};
Account const depositor{"depositor"};
env.fund(XRP(10000), gw, owner, depositor);
env.close();
// Gateway with DefaultRipple so vault creation on its IOU works.
env(fset(gw, asfDefaultRipple));
env.close();
// Depositor opens a trust line to gateway and receives a small
// balance.
PrettyAsset const usd = gw["USD"];
env.trust(usd(1000), depositor);
env(pay(gw, depositor, usd(100))); // raw trust-line balance: 100
env.close();
// Key precondition: gateway sets a non-zero limit on the same
// RippleState — the "opposite field" from depositor's perspective.
// This is what inflates shFULL_BALANCE in preclaim above the raw
// balance.
env(trust(gw, depositor["USD"](1000)));
env.close();
// Create the IOU vault.
Vault const vault{env};
auto [vaultTx, keylet] = vault.create({.owner = owner, .asset = usd});
env(vaultTx);
env.close();
// Submit a deposit of 500 USD:
// - raw balance: 100 USD
// - opposite limit (gw's side): 1000 USD
// - preclaim sees 100 + 1000 = 1100, passes (>= 500)
// - doApply transfers 500, depositor's trust-line balance
// becomes -400
// - sanity check at VaultDeposit.cpp:256 fires
// - tx returns tefINTERNAL (BUG — should be tesSUCCESS.
auto depositTx =
vault.deposit({.depositor = depositor, .id = keylet.key, .amount = usd(500)});
env(depositTx, Ter(expected));
env.close();
};
{
testcase(
"IOU vault deposit exceeding depositor's balance but "
"within counterparty's trust limit, pre-fixCleanup3_2_0 "
"(tefINTERNAL)");
runTest(test::jtx::testableAmendments() - fixCleanup3_2_0, tefINTERNAL);
}
{
testcase(
"IOU vault deposit exceeding depositor's balance but "
"within counterparty's trust limit, post-fixCleanup3_2_0 "
"(tesSUCCESS)");
runTest(test::jtx::testableAmendments(), tesSUCCESS);
}
}
public:
void
run() override
{
testVaultDepositNegativeBalanceFromOppositeLimit();
testSequences();
testPreflight();
testCreateFailXRP();