Rippled Audit

The Order Book

Orders (aka Offers) represent an intent to exchange one currency for another on the XRP ledger. An Order may be created to exchange:

  • XRP for a currency issued by a gateway
  • A currency issued by a gateway for another currency issued by either the same gateway or a different one
  • The same currency issued by different gateways

In whichever case, new Orders are applied to the open ledger by issuing an OfferCreate transaction and cancelled by issuing OfferCancel transactions. Upon creation, Offers may specify various optional fields which affect ledger behaviour including:

  • An Offer previously created to replace (in effect cancelling it)
  • A 'Fill or Kill' flag forcing the offer to be rejected if it cannot be completely filled immediately
  • A 'Immediate or Cancel' flag which attempts to fill as much of the order as possible immediately without persisting storing afterwards
  • A 'Sell' flag which attempts to sell all of the currency being offered (as opposed to the default case where the offer is completed after all the currency being requested is obtained)
  • An expiration time after which the offer is no longer valid

Much of the preflight and preclaim involves verifying the validity of the fields specified by the offer as well as what is being offered for exchange. Actual Order execution involve matching the Offer against existing OrderBooks, updating ledger account balances and trust lines, and persistently storing the outstanding Offer which could not immediately be filled

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

preflight conditions

41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
  NotTEC
  CreateOffer::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 & tfOfferCreateMask)
    {
        JLOG(j.debug()) <<
            "Malformed transaction: Invalid flags set.";
        return temINVALID_FLAG;
    }

    bool const bImmediateOrCancel (uTxFlags & tfImmediateOrCancel);
    bool const bFillOrKill (uTxFlags & tfFillOrKill);
  1. 'Fill Or Kill' and 'Immediate Or Cancel' flags are mutually exclusive

    63
    64
    65
    66
    67
    68
    
            if (bImmediateOrCancel && bFillOrKill)
            {
                JLOG(j.debug()) <<
                    "Malformed transaction: both IoC and FoK set.";
                return temINVALID_FLAG;
            }
    
  2. Expiration and Offer-to-Cancel ('OfferSequence') field are valid (if specified)

    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    
            bool const bHaveExpiration (tx.isFieldPresent (sfExpiration));
    
            if (bHaveExpiration && (tx.getFieldU32 (sfExpiration) == 0))
            {
                JLOG(j.debug()) <<
                    "Malformed offer: bad expiration";
                return temBAD_EXPIRATION;
            }
    
            bool const bHaveCancel (tx.isFieldPresent (sfOfferSequence));
    
            if (bHaveCancel && (tx.getFieldU32 (sfOfferSequence) == 0))
            {
                JLOG(j.debug()) <<
                    "Malformed offer: bad cancel sequence";
                return temBAD_SEQUENCE;
            }
    
  3. Finally we perform sanity checks the currencies being traded and their issuers to ensure they are valid. For example it is illegal to trade XRP for XRP, to trade the same currency issued by the same issuer, etc

    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    
        STAmount saTakerPays = tx[sfTakerPays];
        STAmount saTakerGets = tx[sfTakerGets];
    
        if (!isLegalNet (saTakerPays) || !isLegalNet (saTakerGets))
            return temBAD_AMOUNT;
    
        if (saTakerPays.native () && saTakerGets.native ())
        {
            JLOG(j.debug()) <<
                "Malformed offer: redundant (XRP for XRP)";
            return temBAD_OFFER;
        }
        if (saTakerPays <= beast::zero || saTakerGets <= beast::zero)
        {
            JLOG(j.debug()) <<
                "Malformed offer: bad amount";
            return temBAD_OFFER;
        }
    
        auto const& uPaysIssuerID = saTakerPays.getIssuer ();
        auto const& uPaysCurrency = saTakerPays.getCurrency ();
    
        auto const& uGetsIssuerID = saTakerGets.getIssuer ();
        auto const& uGetsCurrency = saTakerGets.getCurrency ();
    
        if (uPaysCurrency == uGetsCurrency && uPaysIssuerID == uGetsIssuerID)
        {
            JLOG(j.debug()) <<
                "Malformed offer: redundant (IOU for IOU)";
            return temREDUNDANT;
        }
        // We don't allow a non-native currency to use the currency code XRP.
        if (badCurrency() == uPaysCurrency || badCurrency() == uGetsCurrency)
        {
            JLOG(j.debug()) <<
                "Malformed offer: bad currency";
            return temBAD_CURRENCY;
        }
    
        if (saTakerPays.native () != !uPaysIssuerID ||
            saTakerGets.native () != !uGetsIssuerID)
        {
            JLOG(j.warn()) <<
                "Malformed offer: bad issuer";
            return temBAD_ISSUER;
        }
    

preclaim conditions

138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
    TER
    CreateOffer::preclaim(PreclaimContext const& ctx)
    {
      auto const id = ctx.tx[sfAccount];

      auto saTakerPays = ctx.tx[sfTakerPays];
      auto saTakerGets = ctx.tx[sfTakerGets];

      auto const& uPaysIssuerID = saTakerPays.getIssuer();
      auto const& uPaysCurrency = saTakerPays.getCurrency();

      auto const& uGetsIssuerID = saTakerGets.getIssuer();

      auto const cancelSequence = ctx.tx[~sfOfferSequence];

      auto const sleCreator = ctx.view.read(keylet::account(id));

      std::uint32_t const uAccountSequence = sleCreator->getFieldU32(sfSequence);

      auto viewJ = ctx.app.journal("View");
  1. The preclaim logic first ensures issuer assets are not frozen.

    159
    160
    161
    162
    163
    164
    165
    166
    
            if (isGlobalFrozen(ctx.view, uPaysIssuerID) ||
            isGlobalFrozen(ctx.view, uGetsIssuerID))
            {
              JLOG(ctx.j.info()) <<
                  "Offer involves frozen asset";
    
              return tecFROZEN;
            }
    
  2. Minimum account funds are verified

    167
    168
    169
    170
    171
    172
    173
    174
    
            else if (accountFunds(ctx.view, id, saTakerGets,
            fhZERO_IF_FROZEN, viewJ) <= beast::zero)
            {
                JLOG(ctx.j.debug()) <<
                    "delay: Offers must be at least partially funded.";
    
                return tecUNFUNDED_OFFER;
            }
    
  3. Sanity checks are performed on the cancel sequence and expiration fields

    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    
            else if (cancelSequence && (uAccountSequence <= *cancelSequence))
            {
                JLOG(ctx.j.debug()) <<
                    "uAccountSequenceNext=" << uAccountSequence <<
                    " uOfferSequence=" << *cancelSequence;
    
                return temBAD_SEQUENCE;
            }
    
            using d = NetClock::duration;
            using tp = NetClock::time_point;
            auto const expiration = ctx.tx[~sfExpiration];
    
            // Expiration is defined in terms of the close time of the parent ledger,
            // because we definitively know the time that it closed but we do not
            // know the closing time of the ledger that is under construction.
            if (expiration &&
                (ctx.view.parentCloseTime() >= tp{d{*expiration}}))
            {
                // Note that this will get checked again in applyGuts, but it saves
                // us a call to checkAcceptAsset and possible false negative.
                //
                // The return code change is attached to featureChecks as a convenience.
                // The change is not big enough to deserve its own amendment.
                return ctx.view.rules().enabled(featureDepositPreauth)
                    ? TER {tecEXPIRED}
                    : TER {tesSUCCESS};
            }
    
  4. Finally we validate the source's trust line accept the currency/issuer they are requesting.

    207
    208
    209
    210
    211
    212
    213
    
            if (!saTakerPays.native())
            {
                auto result = checkAcceptAsset(ctx.view, ctx.flags,
                    id, ctx.j, Issue(uPaysCurrency, uPaysIssuerID));
                if (result != tesSUCCESS)
                    return result;
            }
    

doApply - applying the transaction

Assuming all these checks pass, the Offer will be applied to the Open Ledger. This is accomplished by the following logic which matches and executes the Offer against existing order via one of two logic paths.Which path is selected depends on in the FlowCross amendment is currently in effect (which upon the writing of this section is still in the planned phase (unratified)).

1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
    TER
    CreateOffer::doApply()
    {
        // This is the ledger view that we work against. Transactions are applied
        // as we go on processing transactions.
        Sandbox sb (&ctx_.view());

        // This is a ledger with just the fees paid and any unfunded or expired
        // offers we encounter removed. It's used when handling Fill-or-Kill offers,
        // if the order isn't going to be placed, to avoid wasting the work we did.
        Sandbox sbCancel (&ctx_.view());

        auto const result = applyGuts(sb, sbCancel);
        if (result.second)
            sb.apply(ctx_.rawView());
        else
            sbCancel.apply(ctx_.rawView());
        return result.first;
    }

As seen above doApply simply dispatches to applyGuts

1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
    std::pair<TER, bool>
    CreateOffer::applyGuts (Sandbox& sb, Sandbox& sbCancel)
    {
    using beast::zero;

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

    bool const bPassive (uTxFlags & tfPassive);
    bool const bImmediateOrCancel (uTxFlags & tfImmediateOrCancel);
    bool const bFillOrKill (uTxFlags & tfFillOrKill);
    bool const bSell (uTxFlags & tfSell);

    auto saTakerPays = ctx_.tx[sfTakerPays];
    auto saTakerGets = ctx_.tx[sfTakerGets];

    auto const cancelSequence = ctx_.tx[~sfOfferSequence];

    // FIXME understand why we use SequenceNext instead of current transaction
    //       sequence to determine the transaction. Why is the offer sequence
    //       number insufficient?
    auto const uSequence = ctx_.tx.getSequence ();

    // This is the original rate of the offer, and is the rate at which
    // it will be placed, even if crossing offers change the amounts that
    // end up on the books.
    auto uRate = getRate (saTakerGets, saTakerPays);

    auto viewJ = ctx_.app.journal("View");

    TER result = tesSUCCESS;
  1. We start off by deleting the cancelled offer, if specified

    1118
    1119
    1120
    1121
    1122
    1123
    1124
    1125
    1126
    1127
    1128
    1129
    1130
    1131
    
            if (cancelSequence)
            {
              auto const sleCancel = sb.peek(
                  keylet::offer(account_, *cancelSequence));
    
              // It's not an error to not find the offer to cancel: it might have
              // been consumed or removed. If it is found, however, it's an error
              // to fail to delete it.
              if (sleCancel)
              {
                  JLOG(j_.debug()) << "Create cancels order " << *cancelSequence;
                  result = offerDelete (sb, sleCancel, viewJ);
              }
            }
    

    The offerDelete method removes the specified offer from the owner account's directory in the ledger as well as from the corresponding order book.

    1057
    1058
    1059
    1060
    1061
    1062
    1063
    1064
    1065
    1066
    1067
    1068
    1069
    1070
    1071
    1072
    1073
    1074
    1075
    1076
    1077
    1078
    1079
    1080
    1081
    1082
    1083
    1084
    1085
    1086
    1087
    1088
    1089
    1090
    1091
    1092
    1093
    1094
    
            TER
            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);
    
                // Detect legacy directories.
                uint256 uDirectory = sle->getFieldH256 (sfBookDirectory);
    
                if (! view.dirRemove(
                    keylet::ownerDir(owner),
                    sle->getFieldU64(sfOwnerNode),
                    offerIndex,
                    false))
                {
                    return tefBAD_LEDGER;
                }
    
                if (! view.dirRemove(
                    keylet::page(uDirectory),
                    sle->getFieldU64(sfBookNode),
                    offerIndex,
                    false))
                {
                    return tefBAD_LEDGER;
                }
    
                adjustOwnerCount(view, view.peek(
                    keylet::account(owner)), -1, j);
    
                view.erase(sle);
    
                return tesSUCCESS;
            }
    
  2. Next we validate the expiration time

    1133
    1134
    1135
    1136
    1137
    1138
    1139
    1140
    1141
    1142
    1143
    1144
    1145
    1146
    1147
    1148
    1149
    1150
    1151
    
            auto const expiration = ctx_.tx[~sfExpiration];
            using d = NetClock::duration;
            using tp = NetClock::time_point;
    
            // Expiration is defined in terms of the close time of the parent ledger,
            // because we definitively know the time that it closed but we do not
            // know the closing time of the ledger that is under construction.
            if (expiration &&
                (ctx_.view().parentCloseTime() >= tp{d{*expiration}}))
            {
                // If the offer has expired, the transaction has successfully
                // done nothing, so short circuit from here.
                //
                // The return code change is attached to featureDepositPreauth as a
                // convenience.  The change is not big enough to deserve a fix code.
                TER const ter {ctx_.view().rules().enabled(
                    featureDepositPreauth) ? TER {tecEXPIRED} : TER {tesSUCCESS}};
                return{ ter, true };
            }
    
  3. Next the open ledger is retrieved and the offer is adjusted to incorporate the minimum tick size. The order's buy/sell rate is calculated.

  4. The offer is then compared against corresponding open order books for matching offers which can be fullfilled immediately. The offer matching analysis can be invoked via one of two mechanisms, again depending on the FlowCross amendment (more below).

    1210
    1211
    1212
    1213
    1214
    1215
    1216
    1217
    1218
    1219
    1220
    1221
    1222
    1223
    1224
    1225
    1226
    1227
    1228
    1229
    1230
    1231
    
             // We reverse pays and gets because during crossing we are taking.
            Amounts const takerAmount (saTakerGets, saTakerPays);
    
            // The amount of the offer that is unfilled after crossing has been
            // performed. It may be equal to the original amount (didn't cross),
            // empty (fully crossed), or something in-between.
            Amounts place_offer;
    
            JLOG(j_.debug()) << "Attempting cross: " <<
                to_string (takerAmount.in.issue ()) << " -> " <<
                to_string (takerAmount.out.issue ());
    
            if (auto stream = j_.trace())
            {
                stream << "   mode: " <<
                    (bPassive ? "passive " : "") <<
                    (bSell ? "sell" : "buy");
                stream <<"     in: " << format_amount (takerAmount.in);
                stream << "    out: " << format_amount (takerAmount.out);
            }
    
            std::tie(result, place_offer) = cross (sb, sbCancel, takerAmount);
    
  5. The 'fill or kill' and 'immediate of cancel' flags are then processed. In the case of the former, the transaction will fail unless the order is completely filled by outstanding order book offers. In the case of the later, the transaction immediately returns a 'success' status code, as a new persistant order should not be created.

    1297
    1298
    1299
    1300
    1301
    1302
    1303
    1304
    1305
    1306
    1307
    1308
    1309
    1310
    1311
    1312
    1313
    
            // For 'fill or kill' offers, failure to fully cross means that the
            // entire operation should be aborted, with only fees paid.
            if (bFillOrKill)
            {
                JLOG (j_.trace()) << "Fill or Kill: offer killed";
                if (sb.rules().enabled (fix1578))
                    return { tecKILLED, false };
                return { tesSUCCESS, false };
            }
    
            // For 'immediate or cancel' offers, the amount remaining doesn't get
            // placed - it gets canceled and the operation succeeds.
            if (bImmediateOrCancel)
            {
                JLOG (j_.trace()) << "Immediate or cancel: offer canceled";
                return { tesSUCCESS, true };
            }
    
  6. The creating account is checked to validate reserve requirements

    1315
    1316
    1317
    1318
    1319
    1320
    1321
    1322
    1323
    1324
    1325
    1326
    1327
    1328
    1329
    1330
    1331
    1332
    1333
    1334
    1335
    1336
    
            auto const sleCreator = sb.peek (keylet::account(account_));
            {
                XRPAmount reserve = ctx_.view().fees().accountReserve(
                    sleCreator->getFieldU32 (sfOwnerCount) + 1);
    
                if (mPriorBalance < reserve)
                {
                    // If we are here, the signing account had an insufficient reserve
                    // *prior* to our processing. If something actually crossed, then
                    // we allow this; otherwise, we just claim a fee.
                    if (!crossed)
                        result = tecINSUF_RESERVE_OFFER;
    
                    if (result != tesSUCCESS)
                    {
                        JLOG (j_.debug()) <<
                            "final result: " << transToken (result);
                    }
    
                    return { result, true };
                }
            }
    
  7. The order is then added to the order book which it belows

    1359
    1360
    1361
    1362
    1363
    1364
    1365
    1366
    1367
    1368
    1369
    1370
    1371
    1372
    1373
    1374
    1375
    1376
    1377
    1378
    
            Book const book { saTakerPays.issue(), saTakerGets.issue() };
    
            // Add offer to order book, using the original rate
            // before any crossing occured.
            auto dir = keylet::quality (keylet::book (book), uRate);
            bool const bookExisted = static_cast<bool>(sb.peek (dir));
    
            auto const bookNode = dirAdd (sb, dir, offer_index, true,
                [&](SLE::ref sle)
                {
                    sle->setFieldH160 (sfTakerPaysCurrency,
                        saTakerPays.issue().currency);
                    sle->setFieldH160 (sfTakerPaysIssuer,
                        saTakerPays.issue().account);
                    sle->setFieldH160 (sfTakerGetsCurrency,
                        saTakerGets.issue().currency);
                    sle->setFieldH160 (sfTakerGetsIssuer,
                        saTakerGets.issue().account);
                    sle->setFieldU64 (sfExchangeRate, uRate);
                }, viewJ);
    
  8. Finally the order is added to the ledger (the corresponding order book is created if it does not exist)

    1387
    1388
    1389
    1390
    1391
    1392
    1393
    1394
    1395
    1396
    1397
    1398
    1399
    1400
    1401
    1402
    1403
    1404
    
            auto sleOffer = std::make_shared<SLE>(ltOFFER, offer_index);
            sleOffer->setAccountID (sfAccount, account_);
            sleOffer->setFieldU32 (sfSequence, uSequence);
            sleOffer->setFieldH256 (sfBookDirectory, dir.key);
            sleOffer->setFieldAmount (sfTakerPays, saTakerPays);
            sleOffer->setFieldAmount (sfTakerGets, saTakerGets);
            sleOffer->setFieldU64 (sfOwnerNode, *ownerNode);
            sleOffer->setFieldU64 (sfBookNode, *bookNode);
            if (expiration)
                sleOffer->setFieldU32 (sfExpiration, *expiration);
            if (bPassive)
                sleOffer->setFlag (lsfPassive);
            if (bSell)
                sleOffer->setFlag (lsfSell);
            sb.insert(sleOffer);
    
            if (!bookExisted)
                ctx_.app.getOrderBookDB().addOrderBook(book);
    

Offer matching and Execution

CreateOffer::cross, invoked from CreateOffer#doApply above, is used to match newly created offers with existing ones for immediate fullfillment. It dispatches to one of two methods takerCross or flowCross depending on if the FlowCross ammendment is in effect before comparing the results and returning the result.

927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
    std::pair<TER, Amounts>
    CreateOffer::cross (
        Sandbox& sb,
        Sandbox& sbCancel,
        Amounts const& takerAmount)
    {
        using beast::zero;

        // There are features for Flow offer crossing and for comparing results
        // between Taker and Flow offer crossing.  Turn those into bools.
        bool const useFlowCross { sb.rules().enabled (featureFlowCross) };
        bool const doCompare { sb.rules().enabled (featureCompareTakerFlowCross) };

        Sandbox sbTaker { &sb };
        Sandbox sbCancelTaker { &sbCancel };
        auto const takerR = (!useFlowCross || doCompare)
            ? takerCross (sbTaker, sbCancelTaker, takerAmount)
            : std::make_pair (tecINTERNAL, takerAmount);

        PaymentSandbox psbFlow { &sb };
        PaymentSandbox psbCancelFlow { &sbCancel };
        auto const flowR = (useFlowCross || doCompare)
            ? flowCross (psbFlow, psbCancelFlow, takerAmount)
            : std::make_pair (tecINTERNAL, takerAmount);

        if (doCompare)
        {
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
      // Return one result or the other based on amendment.
      if (useFlowCross)
      {
          psbFlow.apply (sb);
          psbCancelFlow.apply (sbCancel);
          return flowR;
      }

      sbTaker.apply (sb);
      sbCancelTaker.apply (sbCancel);
      return takerR;
    }

The primary difference between the traditional behaviour, as implemented in takerCross vs the new flow behaviour implemented through flowCross is that in the former the order execution logic is implemented in the OfferCreate transaction class directly while in the later the common logic used by the payment engine's flow model is dispatched to.

We see this in OfferCreate::flowCross

657
658
659
660
661
662
663
    std::pair<TER, Amounts>
    CreateOffer::flowCross (
        PaymentSandbox& psb,
        PaymentSandbox& psbCancel,
        Amounts const& takerAmount)
    {
      // ...
739
740
741
742
743
744
745
746
747
        // Call the payment engine's flow() to do the actual work.
        auto const result = flow (psb, deliver, account_, account_,
            paths,
            true,                       // default path
            ! (txFlags & tfFillOrKill), // partial payment
            true,                       // owner pays transfer fee
            true,                       // offer crossing
            threshold,
            sendMax, j_);

takerCross dispatches to one of two methods depending on if the offer constitutes a XRP/IOU trade or an IOU/IOU trade. In the case of the former direct_cross is invoked whereas in the case of the later brided_cross is called.

612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
    // Fill as much of the offer as possible by consuming offers
    // already on the books. Return the status and the amount of
    // the offer to left unfilled.
    std::pair<TER, Amounts>
    CreateOffer::takerCross (
        Sandbox& sb,
        Sandbox& sbCancel,
        Amounts const& takerAmount)
    {
        NetClock::time_point const when{ctx_.view().parentCloseTime()};

        beast::WrappedSink takerSink (j_, "Taker ");

        Taker taker (cross_type_, sb, account_, takerAmount,
            ctx_.tx.getFlags(), beast::Journal (takerSink));

        // If the taker is unfunded before we begin crossing
        // there's nothing to do - just return an error.
        //
        // We check this in preclaim, but when selling XRP
        // charged fees can cause a user's available balance
        // to go to 0 (by causing it to dip below the reserve)
        // so we check this case again.
        if (taker.unfunded ())
        {
            JLOG (j_.debug()) <<
                "Not crossing: taker is unfunded.";
            return { tecUNFUNDED_OFFER, takerAmount };
        }

        try
        {
            if (cross_type_ == CrossType::IouToIou)
                return bridged_cross (taker, sb, sbCancel, when);

            return direct_cross (taker, sb, sbCancel, when);
        }
        catch (std::exception const& e)
        {
            JLOG (j_.error()) <<
                "Exception during offer crossing: " << e.what ();
            return { tecINTERNAL, taker.remaining_offer () };
        }
    }

direct_cross being the simpler case, constructs an OfferStream from the offers in the corresponding order book, and invokes Taker#cross on each, which is used to evaluate if it can be used to satisfy the offer. Either way, the result of the operation is returned with the outstanding offer balance.

507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
    std::pair<TER, Amounts>
    CreateOffer::direct_cross (
        Taker& taker,
        ApplyView& view,
        ApplyView& view_cancel,
        NetClock::time_point const when)
    {
        OfferStream offers (
            view, view_cancel,
            Book (taker.issue_in (), taker.issue_out ()),
            when, stepCounter_, j_);

        TER cross_result (tesSUCCESS);
        int count = 0;

        bool have_offer = step_account (offers, taker);

        // Modifying the order or logic of the operations in the loop will cause
        // a protocol breaking change.
        while (have_offer)
        {
            bool direct_consumed = false;
            auto& offer (offers.tip());
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
            cross_result = taker.cross (offer);

            JLOG (j_.debug()) << "Direct Result: " << transToken (cross_result);

            if (dry_offer (view, offer))
            {
                direct_consumed = true;
                have_offer = step_account (offers, taker);
            }

            if (cross_result != tesSUCCESS)
            {
                cross_result = tecFAILED_PROCESSING;
                break;
            }
586
587
588
    }

    return std::make_pair(cross_result, taker.remaining_offer ());

bridged_cross works in a similar manner, with the added complexity of XRP being used as the bridging asset, and the XRP/IOU order books being incorporated into the matching offer analysis.

333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
    std::pair<TER, Amounts>
    CreateOffer::bridged_cross (
        Taker& taker,
        ApplyView& view,
        ApplyView& view_cancel,
        NetClock::time_point const when)
    {
        auto const& takerAmount = taker.original_offer ();

        assert (!isXRP (takerAmount.in) && !isXRP (takerAmount.out));

        if (isXRP (takerAmount.in) || isXRP (takerAmount.out))
            Throw<std::logic_error> ("Bridging with XRP and an endpoint.");

        OfferStream offers_direct (view, view_cancel,
            Book (taker.issue_in (), taker.issue_out ()),
                when, stepCounter_, j_);

        OfferStream offers_leg1 (view, view_cancel,
            Book (taker.issue_in (), xrpIssue ()),
            when, stepCounter_, j_);

        OfferStream offers_leg2 (view, view_cancel,
            Book (xrpIssue (), taker.issue_out ()),
            when, stepCounter_, j_);

Supporting Classes

Finally we highlight the helper classes implementing the business logic used to represent offers, order books, order sequences, and currency swaps.

  • TOffer is used to represent an account's offer in memory, providing read/write access to the Serializable Ledger Entry written to the database

  • The rippled server instantiates Book instances with currency-issuer pairs to represent OrderBooks for comparison

  • Actual sorting of offers by quality (buy/sell ratio) and iteration logic is implemented in OfferStream which makes use of BookTip internally

  • Taker (subclass of BasicTaker) is used to implement the offer transaction logic, actually moving funds to/from accounts.