Files
rippled/src/libxrpl/tx/transactors/check/CheckCash.cpp

624 lines
25 KiB
C++

#include <xrpl/tx/transactors/check/CheckCash.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/scope.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/paths/Flow.h>
#include <xrpl/tx/paths/detail/Steps.h>
#include <algorithm>
#include <memory>
#include <optional>
namespace xrpl {
bool
CheckCash::checkExtraFeatures(xrpl::PreflightContext const& ctx)
{
auto const optAmount = ctx.tx[~sfAmount];
auto const optDeliverMin = ctx.tx[~sfDeliverMin];
return ctx.rules.enabled(featureMPTokensV2) ||
(!(optAmount && optAmount->holds<MPTIssue>()) &&
!(optDeliverMin && optDeliverMin->holds<MPTIssue>()));
}
NotTEC
CheckCash::preflight(PreflightContext const& ctx)
{
// Exactly one of Amount or DeliverMin must be present.
auto const optAmount = ctx.tx[~sfAmount];
auto const optDeliverMin = ctx.tx[~sfDeliverMin];
if (static_cast<bool>(optAmount) == static_cast<bool>(optDeliverMin))
{
JLOG(ctx.j.warn()) << "Malformed transaction: "
"does not specify exactly one of Amount and DeliverMin.";
return temMALFORMED;
}
// Make sure the amount is valid.
STAmount const value{optAmount ? *optAmount : *optDeliverMin};
if (!isLegalNet(value) || value.signum() <= 0)
{
JLOG(ctx.j.warn()) << "Malformed transaction: bad amount: " << value.getFullText();
return temBAD_AMOUNT;
}
if (badAsset() == value.asset())
{
JLOG(ctx.j.warn()) << "Malformed transaction: Bad currency.";
return temBAD_CURRENCY;
}
return tesSUCCESS;
}
TER
CheckCash::preclaim(PreclaimContext const& ctx)
{
auto const sleCheck = ctx.view.read(keylet::check(ctx.tx[sfCheckID]));
if (!sleCheck)
{
JLOG(ctx.j.warn()) << "Check does not exist.";
return tecNO_ENTRY;
}
// Only cash a check with this account as the destination.
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->at(sfAccount);
if (srcId == dstId)
{
// They wrote a check to themselves. This should be caught when
// the check is created, but better late than never.
// LCOV_EXCL_START
JLOG(ctx.j.error()) << "Malformed transaction: Cashing check to self.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
{
auto const sleSrc = ctx.view.read(keylet::account(srcId));
auto const sleDst = ctx.view.read(keylet::account(dstId));
if (!sleSrc || !sleDst)
{
// If the check exists this should never occur.
JLOG(ctx.j.warn()) << "Malformed transaction: source or destination not in ledger";
return tecNO_ENTRY;
}
if (sleDst->isFlag(lsfRequireDestTag) && !sleCheck->isFieldPresent(sfDestinationTag))
{
// The tag is basically account-specific information we don't
// understand, but we can require someone to fill it in.
JLOG(ctx.j.warn()) << "Malformed transaction: DestinationTag required in check.";
return tecDST_TAG_NEEDED;
}
}
if (hasExpired(ctx.view, sleCheck->at(~sfExpiration)))
{
JLOG(ctx.j.warn()) << "Cashing a check that has already expired.";
return tecEXPIRED;
}
{
// Preflight verified exactly one of Amount or DeliverMin is present.
// Make sure the requested amount is reasonable.
STAmount const value{[](STTx const& tx) {
auto const optAmount = tx[~sfAmount];
return optAmount ? *optAmount : tx[sfDeliverMin];
}(ctx.tx)};
STAmount const sendMax = sleCheck->at(sfSendMax);
// A legacy Check may contain a non-canonical MPT sfSendMax. Universal
// preflight only validates the CheckCash transaction, not the stored Check.
if (ctx.view.rules().enabled(fixCleanup3_2_0) && !isLegalMPT(sendMax))
return tefBAD_LEDGER;
if (!equalTokens(value.asset(), sendMax.asset()))
{
JLOG(ctx.j.warn()) << "Check cash does not match check currency.";
return temMALFORMED;
}
AccountID const issuerId{value.getIssuer()};
if (issuerId != sendMax.getIssuer())
{
JLOG(ctx.j.warn()) << "Check cash does not match check issuer.";
return temMALFORMED;
}
if (value > sendMax)
{
JLOG(ctx.j.warn()) << "Check cashed for more than check sendMax.";
return tecPATH_PARTIAL;
}
// Make sure the check owner holds at least value. If they have
// less than value the check cannot be cashed.
{
STAmount availableFunds{accountFunds(
ctx.view,
sleCheck->at(sfAccount),
value,
FreezeHandling::ZeroIfFrozen,
AuthHandling::ZeroIfUnauthorized,
ctx.j)};
// Note that src will have one reserve's worth of additional XRP
// once the check is cashed, since the check's reserve will no
// longer be required. So, if we're dealing in XRP, we add one
// reserve's worth to the available funds.
if (value.native() && !sleCheck->isFieldPresent(sfSponsor))
availableFunds += XRPAmount{ctx.view.fees().increment};
if (value > availableFunds)
{
JLOG(ctx.j.warn()) << "Check cashed for more than owner's balance.";
return tecPATH_PARTIAL;
}
}
// An issuer can always accept their own currency.
if (!value.native() && (value.getIssuer() != dstId))
{
return value.asset().visit(
[&](Issue const& issue) -> TER {
Currency const currency{issue.currency};
auto const sleTrustLine =
ctx.view.read(keylet::trustLine(dstId, issuerId, currency));
auto const sleIssuer = ctx.view.read(keylet::account(issuerId));
if (!sleIssuer)
{
JLOG(ctx.j.warn()) << "Can't receive IOUs from "
"non-existent issuer: "
<< to_string(issuerId);
return tecNO_ISSUER;
}
if (sleIssuer->isFlag(lsfRequireAuth))
{
if (!sleTrustLine)
{
// We can only create a trust line if the issuer
// does not have lsfRequireAuth 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 canonicalGt(dstId > issuerId);
bool const isAuthorized(
(sleTrustLine->at(sfFlags) &
(canonicalGt ? lsfLowAuth : lsfHighAuth)) != 0u);
if (!isAuthorized)
{
JLOG(ctx.j.warn()) << "Can't receive IOUs from "
"issuer without auth.";
return tecNO_AUTH;
}
}
// The trustline from source to issuer does not need to
// be checked for freezing, since we already verified
// that the source has sufficient non-frozen funds
// available.
// However, the trustline from destination to issuer may
// not be frozen.
if (isFrozen(ctx.view, dstId, currency, issuerId))
{
JLOG(ctx.j.warn()) << "Cashing a check to a frozen trustline.";
return tecFROZEN;
}
return tesSUCCESS;
},
[&](MPTIssue const& issue) -> TER {
auto const sleIssuer = ctx.view.read(keylet::account(issuerId));
if (!sleIssuer)
{
JLOG(ctx.j.warn()) << "Can't receive MPTs from "
"non-existent issuer: "
<< to_string(issuerId);
return tecNO_ISSUER;
}
if (auto const err = requireAuth(ctx.view, issue, dstId, AuthType::WeakAuth);
!isTesSuccess(err))
{
JLOG(ctx.j.warn()) << "Cashing a check to a MPT requiring auth.";
return err;
}
if (isFrozen(ctx.view, dstId, issue))
{
JLOG(ctx.j.warn()) << "Cashing a check to a frozen MPT.";
return tecLOCKED;
}
if (auto const err = canTransfer(ctx.view, issue, srcId, dstId);
!isTesSuccess(err))
{
JLOG(ctx.j.warn()) << "MPT transfer is disabled.";
return err;
}
return tesSUCCESS;
});
}
}
return tesSUCCESS;
}
TER
CheckCash::doApply()
{
// Flow requires that we operate on a PaymentSandbox, rather than
// directly on a View.
PaymentSandbox psb(&ctx_.view());
auto sleCheck = psb.peek(keylet::check(ctx_.tx[sfCheckID]));
if (!sleCheck)
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Precheck did not verify check's existence.";
return tecFAILED_PROCESSING;
// LCOV_EXCL_STOP
}
AccountID const srcId{sleCheck->getAccountID(sfAccount)};
if (!psb.exists(keylet::account(srcId)) || !psb.exists(keylet::account(accountID_)))
{
// LCOV_EXCL_START
JLOG(ctx_.journal.fatal()) << "Precheck did not verify source or destination's existence.";
return tecFAILED_PROCESSING;
// LCOV_EXCL_STOP
}
auto const sponsorCheckSle = getLedgerEntryReserveSponsor(psb, sleCheck);
// Preclaim already checked that source has at least the requested
// funds.
//
// Therefore, if this is a check written to self, (and it shouldn't be)
// we know they have sufficient funds to pay the check. Since they are
// taking the funds from their own pocket and putting it back in their
// pocket no balance will change.
//
// If it is not a check to self (as should be the case), then there's
// work to do...
auto viewJ = ctx_.registry.get().getJournal("View");
auto const optDeliverMin = ctx_.tx[~sfDeliverMin];
if (srcId != accountID_)
{
STAmount const sendMax = sleCheck->at(sfSendMax);
// Flow() doesn't do XRP to XRP transfers.
if (sendMax.native())
{
// Here we need to calculate the amount of XRP src can send.
// The amount they have available is their balance minus their
// reserve.
//
// Since (if we're successful) we're about to remove an entry
// from src's directory, we allow them to send that additional
// incremental reserve amount in the transfer. Hence the -1
// argument.
STAmount const srcLiquid{xrpLiquid(psb, srcId, sponsorCheckSle ? 0 : -1, viewJ)};
// Now, how much do they need in order to be successful?
STAmount const xrpDeliver{
optDeliverMin ? std::max(*optDeliverMin, std::min(sendMax, srcLiquid))
: ctx_.tx.getFieldAmount(sfAmount)};
if (srcLiquid < xrpDeliver)
{
// Vote no. However the transaction might succeed if applied
// in a different order.
JLOG(j_.trace()) << "Cash Check: Insufficient XRP: " << srcLiquid.getFullText()
<< " < " << xrpDeliver.getFullText();
return tecUNFUNDED_PAYMENT;
}
if (optDeliverMin)
{
// Set the DeliveredAmount metadata.
ctx_.deliver(xrpDeliver);
}
// The source account has enough XRP so make the ledger change.
if (TER const ter{transferXRP(psb, srcId, accountID_, xrpDeliver, viewJ)};
!isTesSuccess(ter))
{
// The transfer failed. Return the error code.
return ter;
}
}
else
{
// 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.
auto const maxDeliverMin = [&]() {
return optDeliverMin->asset().visit(
[&](Issue const&) {
return STAmount(
optDeliverMin->asset(), STAmount::kMaxValue / 2, STAmount::kMaxOffset);
},
[&](MPTIssue const&) {
return STAmount(optDeliverMin->asset(), kMaxMpTokenAmount / 2);
});
};
STAmount const flowDeliver{
optDeliverMin ? maxDeliverMin() : ctx_.tx.getFieldAmount(sfAmount)};
auto applyViewContext = ApplyViewContext({.view = psb, .tx = ctx_.tx});
auto const sponsorSle = getTxReserveSponsor(applyViewContext);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
// Check reserve. Return destination account SLE if enough reserve,
// otherwise return nullptr.
auto checkReserve = [&]() -> SLE::pointer {
auto sleDst = psb.peek(keylet::account(accountID_));
// Can the account cover the trust line's or MPT reserve?
if (auto const ret = checkInsufficientReserve(
applyViewContext,
sleDst,
preFeeBalance_,
*sponsorSle,
{.ownerCountDelta = 1},
j_);
!isTesSuccess(ret))
{
JLOG(j_.trace()) << "Trust line does not exist. "
"Insufficient reserve to create line.";
return nullptr;
}
return sleDst;
};
std::optional<Keylet> trustLineKey;
STAmount savedLimit;
bool destLow = false;
AccountID const& deliverIssuer = flowDeliver.getIssuer();
auto const err = flowDeliver.asset().visit(
[&](Issue const& issue) -> std::optional<TER> {
// If a trust line does not exist yet create one.
Issue const& trustLineIssue = issue;
AccountID const truster = deliverIssuer == accountID_ ? srcId : accountID_;
trustLineKey = keylet::trustLine(truster, trustLineIssue);
destLow = deliverIssuer > accountID_;
if (!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).
auto const sleDst = checkReserve();
if (sleDst == nullptr)
return tecNO_LINE_INSUF_RESERVE;
Currency const& currency = issue.currency;
STAmount initialBalance(flowDeliver.asset());
initialBalance.get<Issue>().account = noAccount();
if (TER const ter = trustCreate(
psb, // payment sandbox
destLow, // is dest low?
deliverIssuer, // source
accountID_, // destination
trustLineKey->key, // ledger index
sleDst, // Account to add to
false, // authorize account
!sleDst->isFlag(lsfDefaultRipple), //
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, accountID_), // limit of zero
0, // quality in
0, // quality out
*sponsorSle, // sponsor
viewJ); // journal
!isTesSuccess(ter))
{
return ter;
}
psb.update(sleDst);
// 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;
savedLimit = sleTrustLine->at(tweakedLimit);
// Set the trust line limit to the highest possible
// value while flow runs.
STAmount const bigAmount(
trustLineIssue, STAmount::kMaxValue, STAmount::kMaxOffset);
sleTrustLine->at(tweakedLimit) = bigAmount;
return std::nullopt;
},
[&](MPTIssue const& issue) -> std::optional<TER> {
if (accountID_ != deliverIssuer)
{
auto const& mptID = issue.getMptID();
// Create MPT if it doesn't exist
auto const mptokenKey = keylet::mptoken(mptID, accountID_);
if (!psb.exists(mptokenKey))
{
auto sleDst = checkReserve();
if (sleDst == nullptr)
return tecINSUFFICIENT_RESERVE;
if (auto const err =
checkCreateMPT(psb, mptID, accountID_, *sponsorSle, j_);
!isTesSuccess(err))
{
return err;
}
}
}
return std::nullopt;
});
if (err)
return *err;
// Make sure the tweaked limits are restored when we leave
// scope.
ScopeExit const fixup([&psb, &trustLineKey, destLow, &savedLimit]() {
if (trustLineKey)
{
SF_AMOUNT const& tweakedLimit = destLow ? sfLowLimit : sfHighLimit;
if (auto const sleTrustLine = psb.peek(*trustLineKey))
sleTrustLine->at(tweakedLimit) = savedLimit;
}
});
// Let flow() do the heavy lifting on a check for an IOU.
auto const result = flow(
psb,
flowDeliver,
srcId,
accountID_,
STPathSet{},
true, // default path
static_cast<bool>(optDeliverMin), // partial payment
true, // owner pays transfer fee
OfferCrossing::No,
std::nullopt,
sleCheck->getFieldAmount(sfSendMax),
std::nullopt, // check does not support domain
viewJ);
if (!isTesSuccess(result.result()))
{
JLOG(ctx_.journal.warn()) << "flow failed when cashing check.";
return result.result();
}
// Make sure that deliverMin was satisfied.
if (optDeliverMin)
{
if (result.actualAmountOut < *optDeliverMin)
{
JLOG(ctx_.journal.warn()) << "flow did not produce DeliverMin.";
return tecPATH_PARTIAL;
}
ctx_.deliver(result.actualAmountOut);
}
// Set the delivered amount metadata in all cases, not just
// for DeliverMin.
ctx_.deliver(result.actualAmountOut);
sleCheck = psb.peek(keylet::check(ctx_.tx[sfCheckID]));
}
}
// Check was cashed. If not a self send (and it shouldn't be), remove
// check link from destination directory.
if (srcId != accountID_ &&
!psb.dirRemove(
keylet::ownerDir(accountID_), sleCheck->at(sfDestinationNode), sleCheck->key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete check from destination.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
// Remove check from check owner's directory.
if (!psb.dirRemove(keylet::ownerDir(srcId), sleCheck->at(sfOwnerNode), sleCheck->key(), true))
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Unable to delete check from owner.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
// If we succeeded, update the check owner's reserve.
decreaseOwnerCountForObject(psb, srcId, sleCheck, 1, viewJ);
// Remove check from ledger.
psb.erase(sleCheck);
psb.apply(ctx_.rawView());
return tesSUCCESS;
}
void
CheckCash::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
{
// No transaction-specific invariants yet (future work).
}
bool
CheckCash::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
{
// No transaction-specific invariants yet (future work).
return true;
}
} // namespace xrpl