mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-25 16:10:57 +00:00
462 lines
18 KiB
C++
462 lines
18 KiB
C++
#include <xrpl/basics/Log.h>
|
|
#include <xrpl/basics/scope.h>
|
|
#include <xrpl/ledger/View.h>
|
|
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
|
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
|
|
#include <xrpl/ledger/helpers/TokenHelpers.h>
|
|
#include <xrpl/protocol/Feature.h>
|
|
#include <xrpl/protocol/Indexes.h>
|
|
#include <xrpl/protocol/TER.h>
|
|
#include <xrpl/protocol/TxFlags.h>
|
|
#include <xrpl/tx/paths/Flow.h>
|
|
#include <xrpl/tx/transactors/check/CheckCash.h>
|
|
|
|
#include <algorithm>
|
|
|
|
namespace xrpl {
|
|
|
|
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 (badCurrency() == value.getCurrency())
|
|
{
|
|
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
|
|
}
|
|
{
|
|
AccountRoot const acctSrc(srcId, ctx.view);
|
|
AccountRoot const acctDst(dstId, ctx.view);
|
|
if (!acctSrc || !acctDst)
|
|
{
|
|
// If the check exists this should never occur.
|
|
JLOG(ctx.j.warn()) << "Malformed transaction: source or destination not in ledger";
|
|
return tecNO_ENTRY;
|
|
}
|
|
|
|
if ((acctDst->getFlags() & 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);
|
|
Currency const currency{value.getCurrency()};
|
|
if (currency != sendMax.getCurrency())
|
|
{
|
|
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.
|
|
{
|
|
auto const checkAccount = sleCheck->at(sfAccount);
|
|
STAmount availableFunds = [&]() -> STAmount {
|
|
if (!value.native() && value.getIssuer() == checkAccount)
|
|
return value;
|
|
return makeTokenBase(ctx.view, value.asset())
|
|
->accountHolds(checkAccount, fhZERO_IF_FROZEN, 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())
|
|
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))
|
|
{
|
|
IOUToken const iouToken(ctx.view, issuerId, currency);
|
|
if (!iouToken)
|
|
{
|
|
JLOG(ctx.j.warn())
|
|
<< "Can't receive IOUs from non-existent issuer: " << to_string(issuerId);
|
|
return tecNO_ISSUER;
|
|
}
|
|
|
|
if (iouToken.requiresAuth())
|
|
{
|
|
auto const sleTrustLine = ctx.view.read(keylet::line(dstId, issuerId, currency));
|
|
|
|
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 canonical_gt(dstId > issuerId);
|
|
|
|
bool const is_authorized(
|
|
sleTrustLine->at(sfFlags) & (canonical_gt ? lsfLowAuth : lsfHighAuth));
|
|
|
|
if (!is_authorized)
|
|
{
|
|
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.
|
|
IOUToken wrapped(ctx.view, Issue{currency, issuerId});
|
|
if (wrapped.isFrozen(dstId))
|
|
{
|
|
JLOG(ctx.j.warn()) << "Cashing a check to a frozen trustline.";
|
|
return tecFROZEN;
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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.journal("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.
|
|
WritableAccountRoot wrappedSrc(srcId, psb);
|
|
STAmount const srcLiquid{wrappedSrc.xrpLiquid(-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.
|
|
STAmount const flowDeliver{
|
|
optDeliverMin
|
|
? STAmount(
|
|
optDeliverMin->issue(), STAmount::cMaxValue / 2, STAmount::cMaxOffset)
|
|
: ctx_.tx.getFieldAmount(sfAmount)};
|
|
|
|
// If a trust line does not exist yet create one.
|
|
Issue const& trustLineIssue = flowDeliver.issue();
|
|
AccountID const issuer = flowDeliver.getIssuer();
|
|
AccountID const truster = issuer == accountID_ ? srcId : accountID_;
|
|
Keylet const trustLineKey = keylet::line(truster, trustLineIssue);
|
|
bool const destLow = issuer > 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).
|
|
|
|
WritableAccountRoot wrappedDst(accountID_, psb);
|
|
|
|
// Can the account cover the trust line's reserve?
|
|
if (std::uint32_t const ownerCount = {wrappedDst->at(sfOwnerCount)};
|
|
preFeeBalance_ < 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
|
|
accountID_, // destination
|
|
trustLineKey.key, // ledger index
|
|
wrappedDst, // Account to add to
|
|
false, // authorize account
|
|
(wrappedDst->getFlags() & lsfDefaultRipple) == 0,
|
|
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
|
|
viewJ); // journal
|
|
!isTesSuccess(ter))
|
|
{
|
|
return ter;
|
|
}
|
|
// clang-format on
|
|
|
|
wrappedDst.update();
|
|
|
|
// 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;
|
|
});
|
|
|
|
// 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,
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
WritableAccountRoot wrappedSrc(srcId, psb);
|
|
wrappedSrc.adjustOwnerCount(-1, viewJ);
|
|
|
|
// Remove check from ledger.
|
|
psb.erase(sleCheck);
|
|
|
|
psb.apply(ctx_.rawView());
|
|
return tesSUCCESS;
|
|
}
|
|
|
|
} // namespace xrpl
|