Rippled Audit

Payments

Payments are the mechanism through which value is transferred between accounts on the XRP Ledger. When a Payment is initialized the source account specifies both the source and destination currencies and amounts (along with the issuers of the currencies if not XRP) as well as the destination account which to send the payment to. Various flags can also be set on the transaction facilitating different behaviour in the code base.

Assuming all generic fee, sequence, and signing requirements are met, the following conditions are imposed on Payment transactions:

preflight conditions

47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
NotTEC
Payment::preflight (PreflightContext const& ctx)
{
    auto const ret = preflight1 (ctx);
    if (!isTesSuccess (ret))
        return ret;

    auto& tx = ctx.tx;
    auto& j = ctx.j;

    std::uint32_t const uTxFlags = tx.getFlags ();

    if (uTxFlags & tfPaymentMask)
    {
        JLOG(j.trace()) << "Malformed transaction: " <<
            "Invalid flags set.";
        return temINVALID_FLAG;
    }

    bool const partialPaymentAllowed = uTxFlags & tfPartialPayment;
    bool const limitQuality = uTxFlags & tfLimitQuality;
    bool const defaultPathsAllowed = !(uTxFlags & tfNoRippleDirect);
    bool const bPaths = tx.isFieldPresent (sfPaths);
    bool const bMax = tx.isFieldPresent (sfSendMax);

    STAmount const saDstAmount (tx.getFieldAmount (sfAmount));

    STAmount maxSourceAmount;
    auto const account = tx.getAccountID(sfAccount);

    if (bMax)
        maxSourceAmount = tx.getFieldAmount (sfSendMax);
    else if (saDstAmount.native ())
        maxSourceAmount = saDstAmount;
    else
        maxSourceAmount = STAmount (
            { saDstAmount.getCurrency (), account },
            saDstAmount.mantissa(), saDstAmount.exponent (),
            saDstAmount < beast::zero);

    auto const& uSrcCurrency = maxSourceAmount.getCurrency ();
    auto const& uDstCurrency = saDstAmount.getCurrency ();

    // isZero() is XRP.  FIX!
    bool const bXRPDirect = uSrcCurrency.isZero () && uDstCurrency.isZero ();
  1. source and destination amounts must be valid

    93
    94
    
           if (!isLegalNet (saDstAmount) || !isLegalNet (maxSourceAmount))
               return temBAD_AMOUNT;
    
  2. destination account must be specified and valid

    96
    97
    98
    99
    100
    101
    102
    103
    
            auto const uDstAccountID = tx.getAccountID (sfDestination);
    
            if (!uDstAccountID)
            {
                JLOG(j.trace()) << "Malformed transaction: " <<
                    "Payment destination account not specified.";
                return temDST_NEEDED;
            }
    
  3. source and destination amounts must be > 0

    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    
            if (bMax && maxSourceAmount <= beast::zero)
            {
                JLOG(j.trace()) << "Malformed transaction: " <<
                    "bad max amount: " << maxSourceAmount.getFullText ();
                return temBAD_AMOUNT;
            }
            if (saDstAmount <= beast::zero)
            {
                JLOG(j.trace()) << "Malformed transaction: "<<
                    "bad dst amount: " << saDstAmount.getFullText ();
                return temBAD_AMOUNT;
            }
    
  4. source and destination currencies must be valid

    116
    117
    118
    119
    120
    121
    
            if (badCurrency() == uSrcCurrency || badCurrency() == uDstCurrency)
            {
                JLOG(j.trace()) <<"Malformed transaction: " <<
                    "Bad currency.";
                return temBAD_CURRENCY;
            }
    
  5. source and destination cannot be same if sending the same currency from the same issuer

    122
    123
    124
    125
    126
    127
    128
    129
    130
    
            if (account == uDstAccountID && uSrcCurrency == uDstCurrency && !bPaths)
            {
                // You're signing yourself a payment.
                // If bPaths is true, you might be trying some arbitrage.
                JLOG(j.trace()) << "Malformed transaction: " <<
                    "Redundant payment from " << to_string (account) <<
                    " to self without path for " << to_string (uDstCurrency);
                return temREDUNDANT;
            }
    
  6. cannot send 'max' direct XRP payments

    131
    132
    133
    134
    135
    136
    137
    
            if (bXRPDirect && bMax)
            {
                // Consistent but redundant transaction.
                JLOG(j.trace()) << "Malformed transaction: " <<
                    "SendMax specified for XRP to XRP.";
                return temBAD_SEND_XRP_MAX;
            }
    
  7. cannot specify payment paths if sending/receiving XRP

    138
    139
    140
    141
    142
    143
    144
    
            if (bXRPDirect && bPaths)
            {
                // XRP is sent without paths.
                JLOG(j.trace()) << "Malformed transaction: " <<
                    "Paths specified for XRP to XRP.";
                return temBAD_SEND_XRP_PATHS;
            }
    
  8. cannot send 'partial' direct XRP payments

    145
    146
    147
    148
    149
    150
    151
    
            if (bXRPDirect && partialPaymentAllowed)
            {
                // Consistent but redundant transaction.
                JLOG(j.trace()) << "Malformed transaction: " <<
                    "Partial payment specified for XRP to XRP.";
                return temBAD_SEND_XRP_PARTIAL;
            }
    
  9. cannot specificy quality limit on direct XRP payments

    152
    153
    154
    155
    156
    157
    158
    
            if (bXRPDirect && limitQuality)
            {
                // Consistent but redundant transaction.
                JLOG(j.trace()) << "Malformed transaction: " <<
                    "Limit quality specified for XRP to XRP.";
                return temBAD_SEND_XRP_LIMIT;
            }
    
  10. cannot disable default paths on direct XRP payments

    159
    160
    161
    162
    163
    164
    165
    166
    
            if (bXRPDirect && !defaultPathsAllowed)
            {
                // Consistent but redundant transaction.
                JLOG(j.trace()) << "Malformed transaction: " <<
                    "No ripple direct specified for XRP to XRP.";
                return temBAD_SEND_XRP_NO_DIRECT;
            }
    
    
  11. cannot receive minimum payment if 'partial' payment is disabled

    167
    168
    169
    170
    171
    172
    173
    174
    175
    
            auto const deliverMin = tx[~sfDeliverMin];
            if (deliverMin)
            {
                if (! partialPaymentAllowed)
                {
                    JLOG(j.trace()) << "Malformed transaction: Partial payment not "
                        "specified for " << jss::DeliverMin.c_str() << ".";
                    return temBAD_AMOUNT;
                }
    
  12. if minimum payment amount is specified it must be valid and > 0

    178
    179
    180
    181
    182
    183
    184
    185
    
            auto const dMin = *deliverMin;
            if (!isLegalNet(dMin) || dMin <= beast::zero)
            {
                JLOG(j.trace()) << "Malformed transaction: Invalid " <<
                    jss::DeliverMin.c_str() << " amount. " <<
                        dMin.getFullText();
                return temBAD_AMOUNT;
            }
    
  13. minimum payment issuer must be same as destination amount issuer

    185
    186
    187
    188
    189
    190
    191
    
            if (dMin.issue() != saDstAmount.issue())
            {
                JLOG(j.trace()) <<  "Malformed transaction: Dst issue differs "
                    "from " << jss::DeliverMin.c_str() << ". " <<
                        dMin.getFullText();
                return temBAD_AMOUNT;
            }
    
  14. minimum payment amount must be < destination amount

    192
    193
    194
    195
    196
    197
    198
    
            if (dMin > saDstAmount)
            {
                JLOG(j.trace()) << "Malformed transaction: Dst amount less than " <<
                    jss::DeliverMin.c_str() << ". " <<
                        dMin.getFullText();
                return temBAD_AMOUNT;
            }
    

preclaim conditions

205
206
207
208
209
210
211
212
213
214
215
216
217
218
TER
Payment::preclaim(PreclaimContext const& ctx)
{
    // Ripple if source or destination is non-native or if there are paths.
    std::uint32_t const uTxFlags = ctx.tx.getFlags();
    bool const partialPaymentAllowed = uTxFlags & tfPartialPayment;
    auto const paths = ctx.tx.isFieldPresent(sfPaths);
    auto const sendMax = ctx.tx[~sfSendMax];

    AccountID const uDstAccountID(ctx.tx[sfDestination]);
    STAmount const saDstAmount(ctx.tx[sfAmount]);

    auto const k = keylet::account(uDstAccountID);
    auto const sleDst = ctx.view.read(k);
  1. Destination account exists if not sending XRP

    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    
            if (!sleDst)
            {
                // Destination account does not exist.
                if (!saDstAmount.native())
                {
                    JLOG(ctx.j.trace()) <<
                        "Delay transaction: Destination account does not exist.";
    
                    // Another transaction could create the account and then this
                    // transaction would succeed.
                    return tecNO_DST;
                }
    
  2. Destination account exists if sending partial payment

    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    
            else if (ctx.view.open()
                && partialPaymentAllowed)
            {
                // You cannot fund an account with a partial payment.
                // Make retry work smaller, by rejecting this.
                JLOG(ctx.j.trace()) <<
                    "Delay transaction: Partial payment not allowed to create account.";
    
    
                // Another transaction could create the account and then this
                // transaction would succeed.
                return telNO_DST_PARTIAL;
            }
    
  3. If destination account doesn't exist, sending at least the reserve XRP

    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    
            else if (saDstAmount < STAmount(ctx.view.fees().accountReserve(0)))
            {
                // accountReserve is the minimum amount that an account can have.
                // Reserve is not scaled by load.
                JLOG(ctx.j.trace()) <<
                    "Delay transaction: Destination account does not exist. " <<
                    "Insufficent payment to create account.";
    
                // TODO: dedupe
                // Another transaction could create the account and then this
                // transaction would succeed.
                return tecNO_DST_INSUF_XRP;
            }
        }
    
  4. The destination tag is specified if the destination account indicates it is needed

    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    
            else if ((sleDst->getFlags() & lsfRequireDestTag) &&
                !ctx.tx.isFieldPresent(sfDestinationTag))
            {
                // The tag is basically account-specific information we don't
                // understand, but we can require someone to fill it in.
    
                // We didn't make this test for a newly-formed account because there's
                // no way for this field to be set.
                JLOG(ctx.j.trace()) << "Malformed transaction: DestinationTag required.";
    
                return tecDST_TAG_NEEDED;
            }
    
  5. If paths are specified, we have fewer paths than the max and no single path's steps are larger than then max

    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    
            if (paths || sendMax || !saDstAmount.native())
            {
                // Ripple payment with at least one intermediate step and uses
                // transitive balances.
    
                // Copy paths into an editable class.
                STPathSet const spsPaths = ctx.tx.getFieldPathSet(sfPaths);
    
                auto pathTooBig = spsPaths.size() > MaxPathSize;
    
                if(!pathTooBig)
                    for (auto const& path : spsPaths)
                        if (path.size() > MaxPathLength)
                        {
                            pathTooBig = true;
                            break;
                        }
    
                if (ctx.view.open() && pathTooBig)
                {
                    return telBAD_PATH_COUNT; // Too many paths for proposed ledger.
                }
            }
    

doApply - applying the transaction

Assuming all these checks pass, the Payment can be applied to the Open Ledger. This is accomplished via the following sequence of steps:

  1. Create the destination account if it does not exist
  2. Update the destination account if it exists
  3. Verify that the transaction's preauth rules are consistent with the server's (fail to apply transaction otherwise)
  4. If path-based rippling is enabled/required for this transaction, calculate the ripple path to use and apply it to the ledger. (more on rippling in a later section)
  5. Report on partial payment if full amount cannot be delivered
  6. Charge fee to source account
  7. Perform final validation that source has sufficient XRP to send payment w/ fees
  8. Verify source account is authorized to deposit to destination (if preauth enabled)
  9. Credit destination account, and debit source account for amount being transferred
299
300
301
TER
Payment::doApply ()
{

Payment::doApply - creating / updating dst account - src/ripple/app/tx/impl/Payment.cpp

329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
    // Open a ledger for editing.
    auto const k = keylet::account(uDstAccountID);
    SLE::pointer sleDst = view().peek (k);

    if (!sleDst)
    {
        // Create the account.
        sleDst = std::make_shared<SLE>(k);
        sleDst->setAccountID(sfAccount, uDstAccountID);
        sleDst->setFieldU32(sfSequence, 1);
        view().insert(sleDst);
    }
    else
    {
        // Tell the engine that we are intending to change the destination
        // account.  The source account gets always charged a fee so it's always
        // marked as modified.
        view().update (sleDst);
    }

Payment::doApply - rippling - src/ripple/app/tx/impl/Payment.cpp

355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
    bool const bRipple = paths || sendMax || !saDstAmount.native ();

    // If the destination has lsfDepositAuth set, then only direct XRP
    // payments (no intermediate steps) are allowed to the destination.
    if (!depositPreauth && bRipple && reqDepositAuth)
        return tecNO_PERMISSION;

    if (bRipple)
    {
        // Ripple payment with at least one intermediate step and uses
        // transitive balances.

        if (depositPreauth && reqDepositAuth)
        {
            // If depositPreauth is enabled, then an account that requires
            // authorization has two ways to get an IOU Payment in:
            //  1. If Account == Destination, or
            //  2. If Account is deposit preauthorized by destination.
            if (uDstAccountID != account_)
            {
                if (! view().exists (
                    keylet::depositPreauth (uDstAccountID, account_)))
                    return tecNO_PERMISSION;
            }
        }

        // Copy paths into an editable class.
        STPathSet spsPaths = ctx_.tx.getFieldPathSet (sfPaths);

        path::RippleCalc::Input rcInput;
        rcInput.partialPaymentAllowed = partialPaymentAllowed;
        rcInput.defaultPathsAllowed = defaultPathsAllowed;
        rcInput.limitQuality = limitQuality;
        rcInput.isLedgerOpen = view().open();

        path::RippleCalc::Output rc;
        {
            PaymentSandbox pv(&view());
            JLOG(j_.debug())
                << "Entering RippleCalc in payment: "
                << ctx_.tx.getTransactionID();
            rc = path::RippleCalc::rippleCalculate (
                pv,
                maxSourceAmount,
                saDstAmount,
                uDstAccountID,
                account_,
                spsPaths,
                ctx_.app.logs(),
                &rcInput);
            // VFALCO NOTE We might not need to apply, depending
            //             on the TER. But always applying *should*
            //             be safe.
            pv.apply(ctx_.rawView());
        }

Payment::doApply - direct payment - src/ripple/app/tx/impl/Payment.cpp

501
502
503
504
505
506
507
508
509
510
511
512
    // Do the arithmetic for the transfer and make the ledger change.
    view()
        .peek(keylet::account(account_))
        ->setFieldAmount(sfBalance, mSourceBalance - saDstAmount);
    sleDst->setFieldAmount(
        sfBalance, sleDst->getFieldAmount(sfBalance) + saDstAmount);

    // Re-arm the password change fee if we can and need to.
    if ((sleDst->getFlags() & lsfPasswordSpent))
        sleDst->clearFlag(lsfPasswordSpent);

    return tesSUCCESS;

In the next section we will look at the Order Book and how Order transactions modify it.