Introduce CheckCashMakesTrustLine amendment:

With this amendment, the CheckCash transaction creates a TrustLine
if needed.  The change is modeled after offer crossing.  And,
similar to offer crossing, cashing a check allows an account to
exceed its trust line limit.
This commit is contained in:
Scott Schurr
2021-04-07 18:11:10 -07:00
committed by manojsdoshi
parent 8d59c7dd40
commit bf75094224
5 changed files with 954 additions and 87 deletions

View File

@@ -21,6 +21,7 @@
#include <ripple/app/paths/Flow.h>
#include <ripple/app/tx/impl/CashCheck.h>
#include <ripple/basics/Log.h>
#include <ripple/basics/scope.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Indexes.h>
#include <ripple/protocol/STAccount.h>
@@ -89,13 +90,13 @@ CashCheck::preclaim(PreclaimContext const& ctx)
}
// Only cash a check with this account as the destination.
AccountID const dstId{(*sleCheck)[sfDestination]};
AccountID const dstId = sleCheck->at(sfDestination);
if (ctx.tx[sfAccount] != dstId)
{
JLOG(ctx.j.warn()) << "Cashing a check with wrong Destination.";
return tecNO_PERMISSION;
}
AccountID const srcId{(*sleCheck)[sfAccount]};
AccountID const srcId = sleCheck->at(sfAccount);
if (srcId == dstId)
{
// They wrote a check to themselves. This should be caught when
@@ -127,7 +128,7 @@ CashCheck::preclaim(PreclaimContext const& ctx)
{
using duration = NetClock::duration;
using timepoint = NetClock::time_point;
auto const optExpiry = (*sleCheck)[~sfExpiration];
auto const optExpiry = sleCheck->at(~sfExpiration);
// Expiration is defined in terms of the close time of the parent
// ledger, because we definitively know the time that it closed but
@@ -148,7 +149,7 @@ CashCheck::preclaim(PreclaimContext const& ctx)
return optAmount ? *optAmount : tx[sfDeliverMin];
}(ctx.tx)};
STAmount const sendMax{(*sleCheck)[sfSendMax]};
STAmount const sendMax = sleCheck->at(sfSendMax);
Currency const currency{value.getCurrency()};
if (currency != sendMax.getCurrency())
{
@@ -172,7 +173,7 @@ CashCheck::preclaim(PreclaimContext const& ctx)
{
STAmount availableFunds{accountFunds(
ctx.view,
(*sleCheck)[sfAccount],
sleCheck->at(sfAccount),
value,
fhZERO_IF_FROZEN,
ctx.j)};
@@ -197,7 +198,9 @@ CashCheck::preclaim(PreclaimContext const& ctx)
{
auto const sleTrustLine =
ctx.view.read(keylet::line(dstId, issuerId, currency));
if (!sleTrustLine)
if (!sleTrustLine &&
!ctx.view.rules().enabled(featureCheckCashMakesTrustLine))
{
JLOG(ctx.j.warn())
<< "Cannot cash check for IOU without trustline.";
@@ -213,15 +216,22 @@ CashCheck::preclaim(PreclaimContext const& ctx)
return tecNO_ISSUER;
}
if ((*sleIssuer)[sfFlags] & lsfRequireAuth)
if (sleIssuer->at(sfFlags) & lsfRequireAuth)
{
if (!sleTrustLine)
{
// We can only create a trust line if the issuer does not
// have requireAuth set.
return tecNO_AUTH;
}
// Entries have a canonical representation, determined by a
// lexicographical "greater than" comparison employing strict
// weak ordering. Determine which entry we need to access.
bool const canonical_gt(dstId > issuerId);
bool const is_authorized(
(*sleTrustLine)[sfFlags] &
sleTrustLine->at(sfFlags) &
(canonical_gt ? lsfLowAuth : lsfHighAuth));
if (!is_authorized)
@@ -286,9 +296,10 @@ CashCheck::doApply()
auto viewJ = ctx_.app.journal("View");
auto const optDeliverMin = ctx_.tx[~sfDeliverMin];
bool const doFix1623{ctx_.view().rules().enabled(fix1623)};
if (srcId != account_)
{
STAmount const sendMax{sleCheck->getFieldAmount(sfSendMax)};
STAmount const sendMax = sleCheck->at(sfSendMax);
// Flow() doesn't do XRP to XRP transfers.
if (sendMax.native())
@@ -334,19 +345,106 @@ CashCheck::doApply()
}
else
{
// Let flow() do the heavy lifting on a check for an IOU.
//
// Note that for DeliverMin we don't know exactly how much
// currency we want flow to deliver. We can't ask for the
// maximum possible currency because there might be a gateway
// transfer rate to account for. Since the transfer rate cannot
// exceed 200%, we use 1/2 maxValue as our limit.
STAmount const flowDeliver{
optDeliverMin
? STAmount{optDeliverMin->issue(), STAmount::cMaxValue / 2, STAmount::cMaxOffset}
: static_cast<STAmount>(ctx_.tx[sfAmount])};
optDeliverMin ? STAmount(
optDeliverMin->issue(),
STAmount::cMaxValue / 2,
STAmount::cMaxOffset)
: ctx_.tx.getFieldAmount(sfAmount)};
// Call the payment engine's flow() to do the actual work.
// If a trust line does not exist yet create one.
Issue const& trustLineIssue = flowDeliver.issue();
AccountID const issuer = flowDeliver.getIssuer();
AccountID const truster = issuer == account_ ? srcId : account_;
Keylet const trustLineKey = keylet::line(truster, trustLineIssue);
bool const destLow = issuer > account_;
bool const checkCashMakesTrustLine =
psb.rules().enabled(featureCheckCashMakesTrustLine);
if (checkCashMakesTrustLine && !psb.exists(trustLineKey))
{
// 1. Can the check casher meet the reserve for the trust line?
// 2. Create trust line between destination (this) account
// and the issuer.
// 3. Apply correct noRipple settings on trust line. Use...
// a. this (destination) account and
// b. issuing account (not sending account).
// Can the account cover the trust line's reserve?
if (std::uint32_t const ownerCount = {sleDst->at(sfOwnerCount)};
mPriorBalance < psb.fees().accountReserve(ownerCount + 1))
{
JLOG(j_.trace()) << "Trust line does not exist. "
"Insufficent reserve to create line.";
return tecNO_LINE_INSUF_RESERVE;
}
Currency const currency = flowDeliver.getCurrency();
STAmount initialBalance(flowDeliver.issue());
initialBalance.setIssuer(noAccount());
// clang-format off
if (TER const ter = trustCreate(
psb, // payment sandbox
destLow, // is dest low?
issuer, // source
account_, // destination
trustLineKey.key, // ledger index
sleDst, // Account to add to
false, // authorize account
(sleDst->getFlags() & lsfDefaultRipple) == 0,
false, // freeze trust line
initialBalance, // zero initial balance
Issue(currency, account_), // limit of zero
0, // quality in
0, // quality out
viewJ); // journal
!isTesSuccess(ter))
{
return ter;
}
// clang-format on
// Note that we _don't_ need to be careful about destroying
// the trust line if the check cashing fails. The transaction
// machinery will automatically clean it up.
}
// Since the destination is signing the check, they clearly want
// the funds even if their new total funds would exceed the limit
// on their trust line. So we tweak the trust line limits before
// calling flow and then restore the trust line limits afterwards.
auto const sleTrustLine = psb.peek(trustLineKey);
if (!sleTrustLine)
return tecNO_LINE;
SF_AMOUNT const& tweakedLimit = destLow ? sfLowLimit : sfHighLimit;
STAmount const savedLimit = sleTrustLine->at(tweakedLimit);
// Make sure the tweaked limits are restored when we leave scope.
scope_exit fixup(
[&psb, &trustLineKey, &tweakedLimit, &savedLimit]() {
if (auto const sleTrustLine = psb.peek(trustLineKey))
sleTrustLine->at(tweakedLimit) = savedLimit;
});
if (checkCashMakesTrustLine)
{
// Set the trust line limit to the highest possible value
// while flow runs.
STAmount const bigAmount(
trustLineIssue, STAmount::cMaxValue, STAmount::cMaxOffset);
sleTrustLine->at(tweakedLimit) = bigAmount;
}
// Let flow() do the heavy lifting on a check for an IOU.
auto const result = flow(
psb,
flowDeliver,
@@ -376,10 +474,14 @@ CashCheck::doApply()
<< "flow did not produce DeliverMin.";
return tecPATH_PARTIAL;
}
if (doFix1623)
if (doFix1623 && !checkCashMakesTrustLine)
// Set the delivered_amount metadata.
ctx_.deliver(result.actualAmountOut);
}
// Set the delivered amount metadata in all cases, not just
// for DeliverMin.
if (checkCashMakesTrustLine)
ctx_.deliver(result.actualAmountOut);
}
}
@@ -387,7 +489,7 @@ CashCheck::doApply()
// check link from destination directory.
if (srcId != account_)
{
std::uint64_t const page{(*sleCheck)[sfDestinationNode]};
std::uint64_t const page = {sleCheck->at(sfDestinationNode)};
if (!ctx_.view().dirRemove(
keylet::ownerDir(account_), page, sleCheck->key(), true))
{
@@ -397,7 +499,7 @@ CashCheck::doApply()
}
// Remove check from check owner's directory.
{
std::uint64_t const page{(*sleCheck)[sfOwnerNode]};
std::uint64_t const page = {sleCheck->at(sfOwnerNode)};
if (!ctx_.view().dirRemove(
keylet::ownerDir(srcId), page, sleCheck->key(), true))
{

View File

@@ -117,6 +117,7 @@ class FeatureCollections
"FlowSortStrands",
"fixSTAmountCanonicalize",
"fixRmSmallIncreasedQOffers",
"CheckCashMakesTrustLine",
};
std::vector<uint256> features;
@@ -378,6 +379,7 @@ extern uint256 const featureTicketBatch;
extern uint256 const featureFlowSortStrands;
extern uint256 const fixSTAmountCanonicalize;
extern uint256 const fixRmSmallIncreasedQOffers;
extern uint256 const featureCheckCashMakesTrustLine;
} // namespace ripple

View File

@@ -136,6 +136,7 @@ detail::supportedAmendments()
"FlowSortStrands",
"fixSTAmountCanonicalize",
"fixRmSmallIncreasedQOffers",
"CheckCashMakesTrustLine",
};
return supported;
}
@@ -192,7 +193,8 @@ uint256 const
featureTicketBatch = *getRegisteredFeature("TicketBatch"),
featureFlowSortStrands = *getRegisteredFeature("FlowSortStrands"),
fixSTAmountCanonicalize = *getRegisteredFeature("fixSTAmountCanonicalize"),
fixRmSmallIncreasedQOffers = *getRegisteredFeature("fixRmSmallIncreasedQOffers");
fixRmSmallIncreasedQOffers = *getRegisteredFeature("fixRmSmallIncreasedQOffers"),
featureCheckCashMakesTrustLine = *getRegisteredFeature("CheckCashMakesTrustLine");
// The following amendments have been active for at least two years. Their
// pre-amendment code has been removed and the identifiers are deprecated.

File diff suppressed because it is too large Load Diff

View File

@@ -348,8 +348,9 @@ Env::postconditions(JTx const& jt, TER ter, bool didApply)
if (jt.ter &&
!test.expect(
ter == *jt.ter,
"apply: " + transToken(ter) + " (" + transHuman(ter) + ") != " +
transToken(*jt.ter) + " (" + transHuman(*jt.ter) + ")"))
"apply: Got " + transToken(ter) + " (" + transHuman(ter) +
"); Expected " + transToken(*jt.ter) + " (" +
transHuman(*jt.ter) + ")"))
{
test.log << pretty(jt.jv) << std::endl;
// Don't check postconditions if