fix rebase issues

This commit is contained in:
Mayukha Vadari
2026-03-23 09:44:30 -07:00
parent 1eef4554e2
commit 370a420aa3
4 changed files with 125 additions and 1801 deletions

View File

@@ -1,811 +0,0 @@
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
//
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TxFlags.h>
namespace xrpl {
// Forward declarations for functions that remain in View.h/cpp
bool
isVaultPseudoAccountFrozen(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptShare,
int depth);
[[nodiscard]] TER
dirLink(
ApplyView& view,
AccountID const& owner,
std::shared_ptr<SLE>& object,
SF_UINT64 const& node = sfOwnerNode);
bool
MPTokenIssuance::isGlobalFrozen() const
{
if (sle_)
return sle_->isFlag(lsfMPTLocked);
return false;
}
bool
MPTokenIssuance::isIndividualFrozen(AccountID const& account) const
{
if (auto const sle = readView_.read(keylet::mptoken(mptID_, account)))
return sle->isFlag(lsfMPTLocked);
return false;
}
bool
MPTokenIssuance::isFrozen(AccountID const& account, int depth) const
{
return isGlobalFrozen() || isIndividualFrozen(account) ||
isVaultPseudoAccountFrozen(readView_, account, mptIssue_, depth);
}
[[nodiscard]] bool
MPTokenIssuance::isAnyFrozen(std::initializer_list<AccountID> const& accounts, int depth) const
{
if (isGlobalFrozen())
return true;
for (auto const& account : accounts)
{
if (isIndividualFrozen(account))
return true;
}
for (auto const& account : accounts)
{
if (isVaultPseudoAccountFrozen(readView_, account, mptIssue_, depth))
return true;
}
return false;
}
TER
MPTokenIssuance::checkFrozen(AccountID const& account) const
{
return isFrozen(account) ? TER{tecLOCKED} : TER{tesSUCCESS};
}
bool
MPTokenIssuance::isDeepFrozen(AccountID const& account, int depth) const
{
return isFrozen(account, depth);
}
TER
MPTokenIssuance::checkDeepFrozen(AccountID const& account) const
{
return isDeepFrozen(account) ? TER{tecLOCKED} : TER{tesSUCCESS};
}
Rate
MPTokenIssuance::transferRate() const
{
// fee is 0-50,000 (0-50%), rate is 1,000,000,000-2,000,000,000
// For example, if transfer fee is 50% then 10,000 * 50,000 = 500,000
// which represents 50% of 1,000,000,000
if (sle_ && sle_->isFieldPresent(sfTransferFee))
return Rate{1'000'000'000u + 10'000 * sle_->getFieldU16(sfTransferFee)};
return parityRate;
}
[[nodiscard]] TER
MPTokenIssuance::canAddHolding() const
{
if (!sle_)
{
return tecOBJECT_NOT_FOUND;
}
if (!sle_->isFlag(lsfMPTCanTransfer))
{
return tecNO_AUTH;
}
return tesSUCCESS;
}
[[nodiscard]] TER
WritableMPTokenIssuance::addEmptyHolding(
AccountID const& accountID,
XRPAmount priorBalance,
beast::Journal journal)
{
if (!mutableSle_)
return tefINTERNAL; // LCOV_EXCL_LINE
if (mutableSle_->isFlag(lsfMPTLocked))
return tefINTERNAL; // LCOV_EXCL_LINE
if (applyView_.peek(keylet::mptoken(mptID_, accountID)))
return tecDUPLICATE;
if (accountID == mptIssue_.getIssuer())
return tesSUCCESS;
return authorizeMPToken(priorBalance, accountID, journal);
}
[[nodiscard]] TER
WritableMPTokenIssuance::authorizeMPToken(
XRPAmount const& priorBalance,
AccountID const& account,
beast::Journal journal,
std::uint32_t flags,
std::optional<AccountID> holderID)
{
WritableAccountRoot wrappedAcct(account, applyView_);
if (!wrappedAcct)
return tecINTERNAL; // LCOV_EXCL_LINE
// If the account that submitted the tx is a holder
// Note: `account_` is holder's account
// `holderID` is NOT used
if (!holderID)
{
// When a holder wants to unauthorize/delete a MPT, the ledger must
// - delete mptokenKey from owner directory
// - delete the MPToken
if (flags & tfMPTUnauthorize)
{
auto const mptokenKey = keylet::mptoken(mptID_, account);
auto const sleMpt = applyView_.peek(mptokenKey);
if (!sleMpt || (*sleMpt)[sfMPTAmount] != 0)
return tecINTERNAL; // LCOV_EXCL_LINE
if (!applyView_.dirRemove(
keylet::ownerDir(account), (*sleMpt)[sfOwnerNode], sleMpt->key(), false))
return tecINTERNAL; // LCOV_EXCL_LINE
wrappedAcct.adjustOwnerCount(-1, journal);
applyView_.erase(sleMpt);
return tesSUCCESS;
}
// A potential holder wants to authorize/hold a mpt, the ledger must:
// - add the new mptokenKey to the owner directory
// - create the MPToken object for the holder
// The reserve that is required to create the MPToken. Note
// that although the reserve increases with every item
// an account owns, in the case of MPTokens we only
// *enforce* a reserve if the user owns more than two
// items. This is similar to the reserve requirements of trust lines.
std::uint32_t const uOwnerCount = wrappedAcct->getFieldU32(sfOwnerCount);
XRPAmount const reserveCreate(
(uOwnerCount < 2) ? XRPAmount(beast::zero)
: applyView_.fees().accountReserve(uOwnerCount + 1));
if (priorBalance < reserveCreate)
return tecINSUFFICIENT_RESERVE;
// Defensive check before we attempt to create MPToken for the issuer
if (!mutableSle_ || mutableSle_->getAccountID(sfIssuer) == account)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::authorizeMPToken : invalid issuance or issuers token");
if (applyView_.rules().enabled(featureLendingProtocol))
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const mptokenKey = keylet::mptoken(mptID_, account);
auto mptoken = std::make_shared<SLE>(mptokenKey);
if (auto ter = dirLink(applyView_, account, mptoken))
return ter; // LCOV_EXCL_LINE
(*mptoken)[sfAccount] = account;
(*mptoken)[sfMPTokenIssuanceID] = mptID_;
(*mptoken)[sfFlags] = 0;
applyView_.insert(mptoken);
// Update owner count.
wrappedAcct.adjustOwnerCount(1, journal);
return tesSUCCESS;
}
if (!mutableSle_)
return tecINTERNAL; // LCOV_EXCL_LINE
// If the account that submitted this tx is the issuer of the MPT
// Note: `account_` is issuer's account
// `holderID` is holder's account
if (account != (*mutableSle_)[sfIssuer])
return tecINTERNAL; // LCOV_EXCL_LINE
auto const sleMpt = applyView_.peek(keylet::mptoken(mptID_, *holderID));
if (!sleMpt)
return tecINTERNAL; // LCOV_EXCL_LINE
std::uint32_t const flagsIn = sleMpt->getFieldU32(sfFlags);
std::uint32_t flagsOut = flagsIn;
// Issuer wants to unauthorize the holder, unset lsfMPTAuthorized on
// their MPToken
if (flags & tfMPTUnauthorize)
{
flagsOut &= ~lsfMPTAuthorized;
}
// Issuer wants to authorize a holder, set lsfMPTAuthorized on their
// MPToken
else
{
flagsOut |= lsfMPTAuthorized;
}
if (flagsIn != flagsOut)
sleMpt->setFieldU32(sfFlags, flagsOut);
applyView_.update(sleMpt);
return tesSUCCESS;
}
[[nodiscard]] TER
WritableMPTokenIssuance::removeEmptyHolding(AccountID const& accountID, beast::Journal journal)
{
// If the account is the issuer, then no token should exist. MPTs do not
// have the legacy ability to create such a situation, but check anyway. If
// a token does exist, it will get deleted. If not, return success.
bool const accountIsIssuer = accountID == mptIssue_.getIssuer();
auto const mptoken = applyView_.peek(keylet::mptoken(mptID_, accountID));
if (!mptoken)
return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND;
// Unlike a trust line, if the account is the issuer, and the token has a
// balance, it can not just be deleted, because that will throw the issuance
// accounting out of balance, so fail. Since this should be impossible
// anyway, I'm not going to put any effort into it.
if (mptoken->at(sfMPTAmount) != 0)
return tecHAS_OBLIGATIONS;
return authorizeMPToken(
{}, // priorBalance
accountID,
journal,
tfMPTUnauthorize // flags
);
}
[[nodiscard]] TER
MPTokenIssuance::requireAuth(AccountID const& account, AuthType authType, int depth) const
{
if (!sle_)
return tecOBJECT_NOT_FOUND;
auto const mptIssuer = AccountRoot(sle_->getAccountID(sfIssuer), readView_);
// issuer is always "authorized"
if (mptIssuer == account) // Issuer won't have MPToken
return tesSUCCESS;
bool const featureSAVEnabled = readView_.rules().enabled(featureSingleAssetVault);
if (featureSAVEnabled)
{
if (depth >= maxAssetCheckDepth)
return tecINTERNAL; // LCOV_EXCL_LINE
// requireAuth is recursive if the issuer is a vault pseudo-account
if (!mptIssuer.exists())
return tefINTERNAL; // LCOV_EXCL_LINE
if (mptIssuer->isFieldPresent(sfVaultID))
{
auto const sleVault = readView_.read(keylet::vault(mptIssuer->getFieldH256(sfVaultID)));
if (!sleVault)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const asset = sleVault->at(sfAsset);
if (auto const err =
makeTokenBase(readView_, asset)->requireAuth(account, authType, depth + 1);
!isTesSuccess(err))
return err;
}
}
auto const sleToken = readView_.read(keylet::mptoken(mptID_, account));
// if account has no MPToken, fail
if (!sleToken && (authType == AuthType::StrongAuth || authType == AuthType::Legacy))
return tecNO_AUTH;
// Note, this check is not amendment-gated because DomainID will be always
// empty **unless** writing to it has been enabled by an amendment
auto const maybeDomainID = sle_->at(~sfDomainID);
if (maybeDomainID)
{
XRPL_ASSERT(
sle_->getFieldU32(sfFlags) & lsfMPTRequireAuth,
"xrpl::requireAuth : issuance requires authorization");
// ter = tefINTERNAL | tecOBJECT_NOT_FOUND | tecNO_AUTH | tecEXPIRED
auto const ter = credentials::validDomain(readView_, *maybeDomainID, account);
if (isTesSuccess(ter))
{
return ter; // Note: sleToken might be null
}
if (!sleToken)
{
return ter;
}
// We ignore error from validDomain if we found sleToken, as it could
// belong to someone who is explicitly authorized e.g. a vault owner.
}
if (featureSAVEnabled)
{
// Implicitly authorize Vault and LoanBroker pseudo-accounts
if (isPseudoAccount(readView_, account, {&sfVaultID, &sfLoanBrokerID}))
return tesSUCCESS;
}
// mptoken must be authorized if issuance enabled requireAuth
if (sle_->isFlag(lsfMPTRequireAuth) && (!sleToken || !sleToken->isFlag(lsfMPTAuthorized)))
return tecNO_AUTH;
return tesSUCCESS; // Note: sleToken might be null
}
[[nodiscard]] TER
WritableMPTokenIssuance::enforceMPTokenAuthorization(
AccountID const& account,
XRPAmount const& priorBalance, // for MPToken authorization
beast::Journal j)
{
if (!mutableSle_)
return tefINTERNAL; // LCOV_EXCL_LINE
XRPL_ASSERT(
mutableSle_->isFlag(lsfMPTRequireAuth),
"xrpl::enforceMPTokenAuthorization : authorization required");
if (account == mutableSle_->at(sfIssuer))
return tefINTERNAL; // LCOV_EXCL_LINE
auto const keylet = keylet::mptoken(mptID_, account);
auto const sleToken = readView_.read(keylet); // NOTE: might be null
auto const maybeDomainID = mutableSle_->at(~sfDomainID);
bool expired = false;
bool const authorizedByDomain = [&]() -> bool {
// NOTE: defensive here, should be checked in preclaim
if (!maybeDomainID)
return false; // LCOV_EXCL_LINE
auto const ter = verifyValidDomain(applyView(), account, *maybeDomainID, j);
if (isTesSuccess(ter))
return true;
if (ter == tecEXPIRED)
expired = true;
return false;
}();
if (!authorizedByDomain && sleToken == nullptr)
{
// Could not find MPToken and won't create one, could be either of:
//
// 1. Field sfDomainID not set in MPTokenIssuance or
// 2. Account has no matching and accepted credentials or
// 3. Account has all expired credentials (deleted in verifyValidDomain)
//
// Either way, return tecNO_AUTH and there is nothing else to do
return expired ? tecEXPIRED : tecNO_AUTH;
}
if (!authorizedByDomain && maybeDomainID)
{
// Found an MPToken but the account is not authorized and we expect
// it to have been authorized by the domain. This could be because the
// credentials used to create the MPToken have expired or been deleted.
return expired ? tecEXPIRED : tecNO_AUTH;
}
if (!authorizedByDomain)
{
// We found an MPToken, but sfDomainID is not set, so this is a classic
// MPToken which requires authorization by the token issuer.
XRPL_ASSERT(
sleToken != nullptr && !maybeDomainID,
"xrpl::enforceMPTokenAuthorization : found MPToken");
if (sleToken->isFlag(lsfMPTAuthorized))
return tesSUCCESS;
return tecNO_AUTH;
}
if (authorizedByDomain && sleToken != nullptr)
{
// Found an MPToken, authorized by the domain. Ignore authorization flag
// lsfMPTAuthorized because it is meaningless. Return tesSUCCESS
XRPL_ASSERT(maybeDomainID, "xrpl::enforceMPTokenAuthorization : found MPToken for domain");
return tesSUCCESS;
}
if (authorizedByDomain)
{
// Could not find MPToken but there should be one because we are
// authorized by domain. Proceed to create it, then return tesSUCCESS
XRPL_ASSERT(
maybeDomainID && sleToken == nullptr,
"xrpl::enforceMPTokenAuthorization : new MPToken for domain");
if (auto const err = authorizeMPToken(
priorBalance, // priorBalance
account, // account
j);
!isTesSuccess(err))
return err;
return tesSUCCESS;
}
// LCOV_EXCL_START
UNREACHABLE("xrpl::enforceMPTokenAuthorization : condition list is incomplete");
return tefINTERNAL;
// LCOV_EXCL_STOP
}
TER
MPTokenIssuance::canTransfer(AccountID const& from, AccountID const& to) const
{
if (!sle_)
return tecOBJECT_NOT_FOUND;
if (!(sle_->getFieldU32(sfFlags) & lsfMPTCanTransfer))
{
if (from != (*sle_)[sfIssuer] && to != (*sle_)[sfIssuer])
return TER{tecNO_AUTH};
}
return tesSUCCESS;
}
//------------------------------------------------------------------------------
//
// Token capability checks (MPT-specific)
//
//------------------------------------------------------------------------------
bool
MPTokenIssuance::canClawback() const
{
if (!sle_)
return false;
return sle_->isFlag(lsfMPTCanClawback);
}
bool
MPTokenIssuance::requiresAuth() const
{
if (!sle_)
return false;
return sle_->isFlag(lsfMPTRequireAuth);
}
TER
rippleLockEscrowMPT(
ApplyView& view,
AccountID const& sender,
STAmount const& amount,
beast::Journal j)
{
auto const mptIssue = amount.get<MPTIssue>();
auto mptIssuance = WritableMPTokenIssuance(view, mptIssue);
if (!mptIssuance.exists())
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleLockEscrowMPT: MPT issuance not found for "
<< mptIssue.getMptID();
return tecOBJECT_NOT_FOUND;
} // LCOV_EXCL_STOP
if (mptIssuance.getIssuer() == sender)
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleLockEscrowMPT: sender is the issuer, cannot lock MPTs.";
return tecINTERNAL;
} // LCOV_EXCL_STOP
// 1. Decrease the MPT Holder MPTAmount
// 2. Increase the MPT Holder EscrowedAmount
{
auto const mptokenID = keylet::mptoken(mptIssuance.getMptID(), sender);
auto sle = view.peek(mptokenID);
if (!sle)
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleLockEscrowMPT: MPToken not found for " << sender;
return tecOBJECT_NOT_FOUND;
} // LCOV_EXCL_STOP
auto const amt = sle->getFieldU64(sfMPTAmount);
auto const pay = amount.mpt().value();
// Underflow check for subtraction
if (!canSubtract(STAmount(mptIssue, amt), STAmount(mptIssue, pay)))
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleLockEscrowMPT: insufficient MPTAmount for "
<< to_string(sender) << ": " << amt << " < " << pay;
return tecINTERNAL;
} // LCOV_EXCL_STOP
(*sle)[sfMPTAmount] = amt - pay;
// Overflow check for addition
uint64_t const locked = (*sle)[~sfLockedAmount].value_or(0);
if (!canAdd(STAmount(mptIssue, locked), STAmount(mptIssue, pay)))
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleLockEscrowMPT: overflow on locked amount for "
<< to_string(sender) << ": " << locked << " + " << pay;
return tecINTERNAL;
} // LCOV_EXCL_STOP
if (sle->isFieldPresent(sfLockedAmount))
{
(*sle)[sfLockedAmount] += pay;
}
else
{
sle->setFieldU64(sfLockedAmount, pay);
}
view.update(sle);
}
// 1. Increase the Issuance EscrowedAmount
// 2. DO NOT change the Issuance OutstandingAmount
{
uint64_t const issuanceEscrowed = (*mptIssuance)[~sfLockedAmount].value_or(0);
auto const pay = amount.mpt().value();
// Overflow check for addition
if (!canAdd(STAmount(mptIssue, issuanceEscrowed), STAmount(mptIssue, pay)))
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleLockEscrowMPT: overflow on issuance "
"locked amount for "
<< mptIssue.getMptID() << ": " << issuanceEscrowed << " + " << pay;
return tecINTERNAL;
} // LCOV_EXCL_STOP
if (mptIssuance->isFieldPresent(sfLockedAmount))
{
(*mptIssuance)[sfLockedAmount] += pay;
}
else
{
mptIssuance->setFieldU64(sfLockedAmount, pay);
}
mptIssuance.update();
}
return tesSUCCESS;
}
TER
rippleUnlockEscrowMPT(
ApplyView& view,
AccountID const& sender,
AccountID const& receiver,
STAmount const& netAmount,
STAmount const& grossAmount,
beast::Journal j)
{
if (!view.rules().enabled(fixTokenEscrowV1))
{
XRPL_ASSERT(
netAmount == grossAmount, "xrpl::rippleUnlockEscrowMPT : netAmount == grossAmount");
}
auto const& issuer = netAmount.getIssuer();
auto const& mptIssue = netAmount.get<MPTIssue>();
auto mptIssuance = WritableMPTokenIssuance(view, mptIssue);
if (!mptIssuance)
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleUnlockEscrowMPT: MPT issuance not found for "
<< mptIssue.getMptID();
return tecOBJECT_NOT_FOUND;
} // LCOV_EXCL_STOP
// Decrease the Issuance EscrowedAmount
{
if (!mptIssuance->isFieldPresent(sfLockedAmount))
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleUnlockEscrowMPT: no locked amount in issuance for "
<< mptIssue.getMptID();
return tecINTERNAL;
} // LCOV_EXCL_STOP
auto const locked = mptIssuance->getFieldU64(sfLockedAmount);
auto const redeem = grossAmount.mpt().value();
// Underflow check for subtraction
if (!canSubtract(STAmount(mptIssue, locked), STAmount(mptIssue, redeem)))
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleUnlockEscrowMPT: insufficient locked amount for "
<< mptIssue.getMptID() << ": " << locked << " < " << redeem;
return tecINTERNAL;
} // LCOV_EXCL_STOP
auto const newLocked = locked - redeem;
if (newLocked == 0)
{
mptIssuance->makeFieldAbsent(sfLockedAmount);
}
else
{
mptIssuance->setFieldU64(sfLockedAmount, newLocked);
}
mptIssuance.update();
}
if (issuer != receiver)
{
// Increase the MPT Holder MPTAmount
auto const mptokenID = keylet::mptoken(mptIssue.getMptID(), receiver);
auto sle = view.peek(mptokenID);
if (!sle)
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleUnlockEscrowMPT: MPToken not found for " << receiver;
return tecOBJECT_NOT_FOUND;
} // LCOV_EXCL_STOP
auto current = sle->getFieldU64(sfMPTAmount);
auto delta = netAmount.mpt().value();
// Overflow check for addition
if (!canAdd(STAmount(mptIssue, current), STAmount(mptIssue, delta)))
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleUnlockEscrowMPT: overflow on MPTAmount for "
<< to_string(receiver) << ": " << current << " + " << delta;
return tecINTERNAL;
} // LCOV_EXCL_STOP
(*sle)[sfMPTAmount] += delta;
view.update(sle);
}
else
{
// Decrease the Issuance OutstandingAmount
auto const outstanding = mptIssuance->getFieldU64(sfOutstandingAmount);
auto const redeem = netAmount.mpt().value();
// Underflow check for subtraction
if (!canSubtract(STAmount(mptIssue, outstanding), STAmount(mptIssue, redeem)))
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleUnlockEscrowMPT: insufficient outstanding amount for "
<< mptIssue.getMptID() << ": " << outstanding << " < " << redeem;
return tecINTERNAL;
} // LCOV_EXCL_STOP
mptIssuance->setFieldU64(sfOutstandingAmount, outstanding - redeem);
mptIssuance.update();
}
if (issuer == sender)
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleUnlockEscrowMPT: sender is the issuer, "
"cannot unlock MPTs.";
return tecINTERNAL;
} // LCOV_EXCL_STOP
// Decrease the MPT Holder EscrowedAmount
auto const mptokenID = keylet::mptoken(mptIssue.getMptID(), sender);
auto sle = view.peek(mptokenID);
if (!sle)
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleUnlockEscrowMPT: MPToken not found for " << sender;
return tecOBJECT_NOT_FOUND;
} // LCOV_EXCL_STOP
if (!sle->isFieldPresent(sfLockedAmount))
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleUnlockEscrowMPT: no locked amount in MPToken for "
<< to_string(sender);
return tecINTERNAL;
} // LCOV_EXCL_STOP
auto const locked = sle->getFieldU64(sfLockedAmount);
auto const delta = grossAmount.mpt().value();
// Underflow check for subtraction
if (!canSubtract(STAmount(mptIssue, locked), STAmount(mptIssue, delta)))
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleUnlockEscrowMPT: insufficient locked amount for "
<< to_string(sender) << ": " << locked << " < " << delta;
return tecINTERNAL;
} // LCOV_EXCL_STOP
auto const newLocked = locked - delta;
if (newLocked == 0)
sle->makeFieldAbsent(sfLockedAmount);
else
sle->setFieldU64(sfLockedAmount, newLocked);
view.update(sle);
// Note: The gross amount is the amount that was locked, the net
// amount is the amount that is being unlocked. The difference is the fee
// that was charged for the transfer. If this difference is greater than
// zero, we need to update the outstanding amount.
auto const diff = grossAmount.mpt().value() - netAmount.mpt().value();
if (diff != 0)
{
auto const outstanding = mptIssuance->getFieldU64(sfOutstandingAmount);
// Underflow check for subtraction
if (!canSubtract(STAmount(mptIssue, outstanding), STAmount(mptIssue, diff)))
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleUnlockEscrowMPT: insufficient outstanding amount for "
<< mptIssue.getMptID() << ": " << outstanding << " < " << diff;
return tecINTERNAL;
} // LCOV_EXCL_STOP
mptIssuance->setFieldU64(sfOutstandingAmount, outstanding - diff);
mptIssuance.update();
}
return tesSUCCESS;
}
STAmount
MPTokenIssuance::accountHolds(
AccountID const& account,
FreezeHandling zeroIfFrozen,
beast::Journal j,
SpendableHandling includeFullBalance) const
{
return accountHolds(account, zeroIfFrozen, ahIGNORE_AUTH, j, includeFullBalance);
}
STAmount
MPTokenIssuance::accountHolds(
AccountID const& account,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j,
SpendableHandling includeFullBalance) const
{
bool const returnSpendable = (includeFullBalance == shFULL_BALANCE);
if (returnSpendable && account == mptIssue_.getIssuer())
{
// if the account is the issuer, and the issuance exists, their limit is
// the issuance limit minus the outstanding value
if (!sle_)
{
return STAmount{mptIssue_};
}
return STAmount{
mptIssue_,
sle_->at(~sfMaximumAmount).value_or(maxMPTokenAmount) - sle_->at(sfOutstandingAmount)};
}
STAmount amount;
auto const sleMpt = readView_.read(keylet::mptoken(mptID_, account));
if (!sleMpt)
amount.clear(mptIssue_);
else if (zeroIfFrozen == fhZERO_IF_FROZEN && isFrozen(account))
amount.clear(mptIssue_);
else
{
amount = STAmount{mptIssue_, sleMpt->getFieldU64(sfMPTAmount)};
// Only if auth check is needed, as it needs to do an additional read
// operation. Note featureSingleAssetVault will affect error codes.
if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED &&
readView_.rules().enabled(featureSingleAssetVault))
{
if (auto const err = requireAuth(account, AuthType::StrongAuth); !isTesSuccess(err))
amount.clear(mptIssue_);
}
else if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED)
{
// if auth is enabled on the issuance and mpt is not authorized,
// clear amount
if (sle_ && sle_->isFlag(lsfMPTRequireAuth) && !sleMpt->isFlag(lsfMPTAuthorized))
amount.clear(mptIssue_);
}
}
return amount;
}
} // namespace xrpl

View File

@@ -1,925 +0,0 @@
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
//
#include <xrpl/basics/Log.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/protocol/AmountConversions.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Rules.h>
namespace xrpl {
//------------------------------------------------------------------------------
//
// Credit functions (from Credit.cpp)
//
//------------------------------------------------------------------------------
STAmount
creditLimit(
ReadView const& readView_,
AccountID const& account,
AccountID const& issuer,
Currency const& currency)
{
STAmount result(Issue{currency, account});
auto sleRippleState = readView_.read(keylet::line(account, issuer, currency));
if (sleRippleState)
{
result = sleRippleState->getFieldAmount(account < issuer ? sfLowLimit : sfHighLimit);
result.setIssuer(account);
}
XRPL_ASSERT(result.getIssuer() == account, "xrpl::creditLimit : result issuer match");
XRPL_ASSERT(result.getCurrency() == currency, "xrpl::creditLimit : result currency match");
return result;
}
IOUAmount
creditLimit2(ReadView const& v, AccountID const& acc, AccountID const& iss, Currency const& cur)
{
return toAmount<IOUAmount>(creditLimit(v, acc, iss, cur));
}
STAmount
creditBalance(
ReadView const& readView_,
AccountID const& account,
AccountID const& issuer,
Currency const& currency)
{
STAmount result(Issue{currency, account});
auto sleRippleState = readView_.read(keylet::line(account, issuer, currency));
if (sleRippleState)
{
result = sleRippleState->getFieldAmount(sfBalance);
if (account < issuer)
result.negate();
result.setIssuer(account);
}
XRPL_ASSERT(result.getIssuer() == account, "xrpl::creditBalance : result issuer match");
XRPL_ASSERT(result.getCurrency() == currency, "xrpl::creditBalance : result currency match");
return result;
}
//------------------------------------------------------------------------------
//
// Freeze checking (IOU-specific)
//
//------------------------------------------------------------------------------
bool
IOUToken::isIndividualFrozen(AccountID const& account) const
{
if (isXRP(currency_))
return false;
if (issuer_ != account)
{
// Check if the issuer froze the line
auto const sle = readView_.read(keylet::line(account, issuer_, currency_));
if (sle && sle->isFlag((issuer_ > account) ? lsfHighFreeze : lsfLowFreeze))
return true;
}
return false;
}
// Can the specified account spend the specified currency issued by
// the specified issuer or does the freeze flag prohibit it?
bool
IOUToken::isFrozen(AccountID const& account, int depth) const
{
// NOTE: depth is ignored here because it's only relevant for MPTs
if (isXRP(currency_))
return false;
if (issuerAccount_.exists() && issuerAccount_->isFlag(lsfGlobalFreeze))
return true;
if (issuer_ != account)
{
// Check if the issuer froze the line
auto const sleLine = readView_.read(keylet::line(account, issuer_, currency_));
if (sleLine && sleLine->isFlag((issuer_ > account) ? lsfHighFreeze : lsfLowFreeze))
return true;
}
return false;
}
bool
IOUToken::isDeepFrozen(AccountID const& account, int depth) const
{
// NOTE: depth is ignored here because it's only relevant for MPTs
if (isXRP(currency_))
{
return false;
}
if (issuer_ == account)
{
return false;
}
auto const sle = readView_.read(keylet::line(account, issuer_, currency_));
if (!sle)
{
return false;
}
return sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze);
}
TER
IOUToken::checkFrozen(AccountID const& account) const
{
return isFrozen(account) ? TER{tecFROZEN} : TER{tesSUCCESS};
}
TER
IOUToken::checkDeepFrozen(AccountID const& account) const
{
return isDeepFrozen(account) ? TER{tecFROZEN} : TER{tesSUCCESS};
}
bool
IOUToken::isAnyFrozen(std::initializer_list<AccountID> const& accounts, int depth) const
{
// NOTE: depth is ignored here because it's only relevant for MPTs
if (isGlobalFrozen())
return true;
for (auto const& account : accounts)
{
if (isFrozen(account, depth))
return true;
}
return false;
}
STAmount
IOUToken::accountFunds(
AccountID const& id,
STAmount const& saDefault,
FreezeHandling freezeHandling,
beast::Journal j) const
{
if (!saDefault.native() && saDefault.getIssuer() == id)
return saDefault;
return accountHolds(id, freezeHandling, j);
}
STAmount
IOUToken::accountHolds(
AccountID const& account,
FreezeHandling zeroIfFrozen,
beast::Journal j,
SpendableHandling includeFullBalance) const
{
return accountHolds(account, zeroIfFrozen, ahIGNORE_AUTH, j, includeFullBalance);
}
STAmount
IOUToken::accountHolds(
AccountID const& account,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j,
SpendableHandling includeFullBalance) const
{
if (isXRP(currency_))
{
AccountRoot accountRoot(account, readView_);
return {accountRoot.xrpLiquid(0, j)};
}
bool const returnSpendable = (includeFullBalance == shFULL_BALANCE);
if (returnSpendable && account == issuer_)
// If the account is the issuer, then their limit is effectively
// infinite
return STAmount{issue_, STAmount::cMaxValue, STAmount::cMaxOffset};
// IOU: Return balance on trust line modulo freeze
// Check if line exists and is usable (mirrors old getLineIfUsable)
SLE::const_pointer sle = readView_.read(keylet::line(account, issuer_, currency_));
if (sle && zeroIfFrozen == fhZERO_IF_FROZEN)
{
if (isFrozen(account) || isDeepFrozen(account))
{
sle = nullptr;
}
// when fixFrozenLPTokenTransfer is enabled, if currency is lptoken,
// we need to check if the associated assets have been frozen
if (sle && readView_.rules().enabled(fixFrozenLPTokenTransfer))
{
auto const sleIssuer = readView_.read(keylet::account(issuer_));
if (!sleIssuer)
{
sle = nullptr; // LCOV_EXCL_LINE
}
else if (sleIssuer->isFieldPresent(sfAMMID))
{
auto const sleAmm = readView_.read(keylet::amm((*sleIssuer)[sfAMMID]));
if (!sleAmm ||
isLPTokenFrozen(
readView_,
account,
(*sleAmm)[sfAsset].get<Issue>(),
(*sleAmm)[sfAsset2].get<Issue>()))
{
sle = nullptr;
}
}
}
}
// Extract balance (mirrors old getTrustLineBalance)
STAmount amount;
if (sle)
{
amount = sle->getFieldAmount(sfBalance);
bool const accountHigh = account > issuer_;
auto const& oppositeField = accountHigh ? sfLowLimit : sfHighLimit;
if (accountHigh)
{
// Put balance in account terms.
amount.negate();
}
if (returnSpendable)
{
amount += sle->getFieldAmount(oppositeField);
}
amount.setIssuer(issuer_);
}
else
{
amount.clear(Issue{currency_, issuer_});
}
JLOG(j.trace()) << "IOUToken::accountHolds:" << " account=" << to_string(account)
<< " amount=" << amount.getFullText();
return readView_.balanceHook(account, issuer_, amount);
}
TER
IOUToken::canAddHolding() const
{
if (isXRP(issue_))
return tesSUCCESS;
if (!issuerAccount_.exists())
return terNO_ACCOUNT;
if (!issuerAccount_->isFlag(lsfDefaultRipple))
return terNO_RIPPLE;
return tesSUCCESS;
}
Rate
IOUToken::transferRate() const
{
return issuerAccount_.transferRate();
}
//------------------------------------------------------------------------------
//
// Trust line operations
//
//------------------------------------------------------------------------------
TER
trustCreate(
ApplyView& view,
bool const bSrcHigh,
AccountID const& uSrcAccountID,
AccountID const& uDstAccountID,
uint256 const& uIndex, // ripple state entry
WritableAccountRoot& wrappedAcct, // the account being set.
bool const bAuth, // authorize account.
bool const bNoRipple, // others cannot ripple through
bool const bFreeze, // funds cannot leave
bool bDeepFreeze, // can neither receive nor send funds
STAmount const& saBalance, // balance of account being set.
// Issuer should be noAccount()
STAmount const& saLimit, // limit for account being set.
// Issuer should be the account being set.
std::uint32_t uQualityIn,
std::uint32_t uQualityOut,
beast::Journal j)
{
JLOG(j.trace()) << "trustCreate: " << to_string(uSrcAccountID) << ", "
<< to_string(uDstAccountID) << ", " << saBalance.getFullText();
auto const& uLowAccountID = !bSrcHigh ? uSrcAccountID : uDstAccountID;
auto const& uHighAccountID = bSrcHigh ? uSrcAccountID : uDstAccountID;
if (uLowAccountID == uHighAccountID)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::trustCreate : trust line to self");
if (view.rules().enabled(featureLendingProtocol))
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const sleRippleState = std::make_shared<SLE>(ltRIPPLE_STATE, uIndex);
view.insert(sleRippleState);
auto lowNode = view.dirInsert(
keylet::ownerDir(uLowAccountID), sleRippleState->key(), describeOwnerDir(uLowAccountID));
if (!lowNode)
return tecDIR_FULL; // LCOV_EXCL_LINE
auto highNode = view.dirInsert(
keylet::ownerDir(uHighAccountID), sleRippleState->key(), describeOwnerDir(uHighAccountID));
if (!highNode)
return tecDIR_FULL; // LCOV_EXCL_LINE
bool const bSetDst = saLimit.getIssuer() == uDstAccountID;
bool const bSetHigh = bSrcHigh ^ bSetDst;
XRPL_ASSERT(wrappedAcct, "xrpl::trustCreate : non-null SLE");
if (!wrappedAcct)
return tefINTERNAL; // LCOV_EXCL_LINE
XRPL_ASSERT(
wrappedAcct->getAccountID(sfAccount) == (bSetHigh ? uHighAccountID : uLowAccountID),
"xrpl::trustCreate : matching account ID");
auto const peer = AccountRoot(bSetHigh ? uLowAccountID : uHighAccountID, view);
if (!peer.exists())
return tecNO_TARGET;
// Remember deletion hints.
sleRippleState->setFieldU64(sfLowNode, *lowNode);
sleRippleState->setFieldU64(sfHighNode, *highNode);
sleRippleState->setFieldAmount(bSetHigh ? sfHighLimit : sfLowLimit, saLimit);
sleRippleState->setFieldAmount(
bSetHigh ? sfLowLimit : sfHighLimit,
STAmount(Issue{saBalance.getCurrency(), bSetDst ? uSrcAccountID : uDstAccountID}));
if (uQualityIn)
sleRippleState->setFieldU32(bSetHigh ? sfHighQualityIn : sfLowQualityIn, uQualityIn);
if (uQualityOut)
sleRippleState->setFieldU32(bSetHigh ? sfHighQualityOut : sfLowQualityOut, uQualityOut);
std::uint32_t uFlags = bSetHigh ? lsfHighReserve : lsfLowReserve;
if (bAuth)
{
uFlags |= (bSetHigh ? lsfHighAuth : lsfLowAuth);
}
if (bNoRipple)
{
uFlags |= (bSetHigh ? lsfHighNoRipple : lsfLowNoRipple);
}
if (bFreeze)
{
uFlags |= (bSetHigh ? lsfHighFreeze : lsfLowFreeze);
}
if (bDeepFreeze)
{
uFlags |= (bSetHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze);
}
if ((peer->getFlags() & lsfDefaultRipple) == 0)
{
// The other side's default is no rippling
uFlags |= (bSetHigh ? lsfLowNoRipple : lsfHighNoRipple);
}
sleRippleState->setFieldU32(sfFlags, uFlags);
wrappedAcct.adjustOwnerCount(1, j);
// ONLY: Create ripple balance.
sleRippleState->setFieldAmount(sfBalance, bSetHigh ? -saBalance : saBalance);
view.creditHook(uSrcAccountID, uDstAccountID, saBalance, saBalance.zeroed());
return tesSUCCESS;
}
TER
trustDelete(
ApplyView& view,
std::shared_ptr<SLE> const& sleRippleState,
AccountID const& uLowAccountID,
AccountID const& uHighAccountID,
beast::Journal j)
{
// Detect legacy dirs.
std::uint64_t uLowNode = sleRippleState->getFieldU64(sfLowNode);
std::uint64_t uHighNode = sleRippleState->getFieldU64(sfHighNode);
JLOG(j.trace()) << "trustDelete: Deleting ripple line: low";
if (!view.dirRemove(keylet::ownerDir(uLowAccountID), uLowNode, sleRippleState->key(), false))
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
JLOG(j.trace()) << "trustDelete: Deleting ripple line: high";
if (!view.dirRemove(keylet::ownerDir(uHighAccountID), uHighNode, sleRippleState->key(), false))
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
JLOG(j.trace()) << "trustDelete: Deleting ripple line: state";
view.erase(sleRippleState);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
//
// IOU issuance/redemption
//
//------------------------------------------------------------------------------
static bool
updateTrustLine(
ApplyView& view,
SLE::pointer state,
bool bSenderHigh,
AccountID const& sender,
STAmount const& before,
STAmount const& after,
beast::Journal j)
{
if (!state)
return false;
std::uint32_t const flags(state->getFieldU32(sfFlags));
WritableAccountRoot wrappedAcct(sender, view);
if (!wrappedAcct)
return false;
// YYY Could skip this if rippling in reverse.
if (before > beast::zero
// Sender balance was positive.
&& after <= beast::zero
// Sender is zero or negative.
&& (flags & (!bSenderHigh ? lsfLowReserve : lsfHighReserve))
// Sender reserve is set.
&& static_cast<bool>(flags & (!bSenderHigh ? lsfLowNoRipple : lsfHighNoRipple)) !=
static_cast<bool>(wrappedAcct->getFlags() & lsfDefaultRipple) &&
!(flags & (!bSenderHigh ? lsfLowFreeze : lsfHighFreeze)) &&
!state->getFieldAmount(!bSenderHigh ? sfLowLimit : sfHighLimit)
// Sender trust limit is 0.
&& !state->getFieldU32(!bSenderHigh ? sfLowQualityIn : sfHighQualityIn)
// Sender quality in is 0.
&& !state->getFieldU32(!bSenderHigh ? sfLowQualityOut : sfHighQualityOut))
// Sender quality out is 0.
{
// VFALCO Where is the line being deleted?
// Clear the reserve of the sender, possibly delete the line!
wrappedAcct.adjustOwnerCount(-1, j);
// Clear reserve flag.
state->setFieldU32(sfFlags, flags & (!bSenderHigh ? ~lsfLowReserve : ~lsfHighReserve));
// Balance is zero, receiver reserve is clear.
if (!after // Balance is zero.
&& !(flags & (bSenderHigh ? lsfLowReserve : lsfHighReserve)))
return true;
}
return false;
}
TER
issueIOU(
ApplyView& view,
AccountID const& account,
STAmount const& amount,
Issue const& issue,
beast::Journal j)
{
XRPL_ASSERT(
!isXRP(account) && !isXRP(issue.account),
"xrpl::issueIOU : neither account nor issuer is XRP");
// Consistency check
XRPL_ASSERT(issue == amount.issue(), "xrpl::issueIOU : matching issue");
// Can't send to self!
XRPL_ASSERT(issue.account != account, "xrpl::issueIOU : not issuer account");
JLOG(j.trace()) << "issueIOU: " << to_string(account) << ": " << amount.getFullText();
bool bSenderHigh = issue.account > account;
auto const index = keylet::line(issue.account, account, issue.currency);
if (auto state = view.peek(index))
{
STAmount final_balance = state->getFieldAmount(sfBalance);
if (bSenderHigh)
final_balance.negate(); // Put balance in sender terms.
STAmount const start_balance = final_balance;
final_balance -= amount;
auto const must_delete = updateTrustLine(
view, state, bSenderHigh, issue.account, start_balance, final_balance, j);
view.creditHook(issue.account, account, amount, start_balance);
if (bSenderHigh)
final_balance.negate();
// Adjust the balance on the trust line if necessary. We do this even
// if we are going to delete the line to reflect the correct balance
// at the time of deletion.
state->setFieldAmount(sfBalance, final_balance);
if (must_delete)
{
return trustDelete(
view,
state,
bSenderHigh ? account : issue.account,
bSenderHigh ? issue.account : account,
j);
}
view.update(state);
return tesSUCCESS;
}
// NIKB TODO: The limit uses the receiver's account as the issuer and
// this is unnecessarily inefficient as copying which could be avoided
// is now required. Consider available options.
STAmount const limit(Issue{issue.currency, account});
STAmount final_balance = amount;
final_balance.setIssuer(noAccount());
WritableAccountRoot receiverAccount(account, view);
if (!receiverAccount)
return tefINTERNAL; // LCOV_EXCL_LINE
bool noRipple = (receiverAccount->getFlags() & lsfDefaultRipple) == 0;
return trustCreate(
view,
bSenderHigh,
issue.account,
account,
index.key,
receiverAccount,
false,
noRipple,
false,
false,
final_balance,
limit,
0,
0,
j);
}
TER
redeemIOU(
ApplyView& applyView,
AccountID const& account,
STAmount const& amount,
Issue const& issue,
beast::Journal j)
{
XRPL_ASSERT(
!isXRP(account) && !isXRP(issue.account),
"xrpl::redeemIOU : neither account nor issuer is XRP");
// Consistency check
XRPL_ASSERT(issue == amount.issue(), "xrpl::redeemIOU : matching issue");
// Can't send to self!
XRPL_ASSERT(issue.account != account, "xrpl::redeemIOU : not issuer account");
JLOG(j.trace()) << "redeemIOU: " << to_string(account) << ": " << amount.getFullText();
bool bSenderHigh = account > issue.account;
if (auto state = applyView.peek(keylet::line(account, issue.account, issue.currency)))
{
STAmount final_balance = state->getFieldAmount(sfBalance);
if (bSenderHigh)
final_balance.negate(); // Put balance in sender terms.
STAmount const start_balance = final_balance;
final_balance -= amount;
auto const must_delete = updateTrustLine(
applyView, state, bSenderHigh, account, start_balance, final_balance, j);
applyView.creditHook(account, issue.account, amount, start_balance);
if (bSenderHigh)
final_balance.negate();
// Adjust the balance on the trust line if necessary. We do this even
// if we are going to delete the line to reflect the correct balance
// at the time of deletion.
state->setFieldAmount(sfBalance, final_balance);
if (must_delete)
{
return trustDelete(
applyView,
state,
bSenderHigh ? issue.account : account,
bSenderHigh ? account : issue.account,
j);
}
applyView.update(state);
return tesSUCCESS;
}
// In order to hold an IOU, a trust line *MUST* exist to track the
// balance. If it doesn't, then something is very wrong. Don't try
// to continue.
// LCOV_EXCL_START
JLOG(j.fatal()) << "redeemIOU: " << to_string(account) << " attempts to "
<< "redeem " << amount.getFullText() << " but no trust line exists!";
return tefINTERNAL;
// LCOV_EXCL_STOP
}
//------------------------------------------------------------------------------
//
// Authorization and transfer checks (IOU-specific)
//
//------------------------------------------------------------------------------
TER
IOUToken::requireAuth(AccountID const& account, AuthType authType, int depth) const
{
// NOTE: depth is ignored here because it's only relevant for MPTs
if (isXRP(issue_) || issuer_ == account)
return tesSUCCESS;
auto const trustLine = readView_.read(keylet::line(account, issuer_, issue_.currency));
// If account has no line, and this is a strong check, fail
if (!trustLine && authType == AuthType::StrongAuth)
return tecNO_LINE;
// If this is a weak or legacy check, or if the account has a line, fail if
// auth is required and not set on the line
if (issuerAccount_.exists() && (*issuerAccount_)[sfFlags] & lsfRequireAuth)
{
if (trustLine)
{
return ((*trustLine)[sfFlags] & ((account > issuer_) ? lsfLowAuth : lsfHighAuth))
? tesSUCCESS
: TER{tecNO_AUTH};
}
return TER{tecNO_LINE};
}
return tesSUCCESS;
}
TER
IOUToken::canTransfer(AccountID const& from, AccountID const& to) const
{
if (issue_.native())
return tesSUCCESS;
if (issuer_ == from || issuer_ == to)
return tesSUCCESS;
if (!issuerAccount_.exists())
return tefINTERNAL; // LCOV_EXCL_LINE
auto const isRippleDisabled = [&](AccountID account) -> bool {
// Line might not exist, but some transfers can create it. If this
// is the case, just check the default ripple on the issuer account.
auto const line = readView_.read(keylet::line(account, issue_));
if (line)
{
bool const issuerHigh = issuer_ > account;
return line->isFlag(issuerHigh ? lsfHighNoRipple : lsfLowNoRipple);
}
return issuerAccount_->isFlag(lsfDefaultRipple) == false;
};
// Fail if rippling disabled on both trust lines
if (isRippleDisabled(from) && isRippleDisabled(to))
return terNO_RIPPLE;
return tesSUCCESS;
}
//------------------------------------------------------------------------------
//
// Token capability checks (IOU-specific)
//
//------------------------------------------------------------------------------
bool
IOUToken::canClawback() const
{
if (!issuerAccount_.exists())
return false;
return issuerAccount_->isFlag(lsfAllowTrustLineClawback) &&
!issuerAccount_->isFlag(lsfNoFreeze);
}
bool
IOUToken::requiresAuth() const
{
if (!issuerAccount_.exists())
return false;
return issuerAccount_->isFlag(lsfRequireAuth);
}
//------------------------------------------------------------------------------
//
// Empty holding operations (IOU-specific)
//
//------------------------------------------------------------------------------
TER
WritableIOUToken::addEmptyHolding(
AccountID const& accountID,
XRPAmount priorBalance,
beast::Journal journal)
{
// Every account can hold XRP. An issuer can issue directly.
if (issue_.native() || accountID == issuer_)
return tesSUCCESS;
if (issuerAccount_.isGlobalFrozen())
return tecFROZEN; // LCOV_EXCL_LINE
auto const& srcId = issuer_;
auto const& dstId = accountID;
auto const high = srcId > dstId;
auto const index = keylet::line(srcId, dstId, currency_);
WritableAccountRoot wrappedSrc(srcId, applyView_);
WritableAccountRoot wrappedDst(dstId, applyView_);
if (!wrappedDst || !wrappedSrc)
return tefINTERNAL; // LCOV_EXCL_LINE
if (!wrappedSrc->isFlag(lsfDefaultRipple))
return tecINTERNAL; // LCOV_EXCL_LINE
// If the line already exists, don't create it again.
if (applyView_.read(index))
return tecDUPLICATE;
// Can the account cover the trust line reserve ?
std::uint32_t const ownerCount = wrappedDst->at(sfOwnerCount);
if (priorBalance < readView_.fees().accountReserve(ownerCount + 1))
return tecNO_LINE_INSUF_RESERVE;
return trustCreate(
applyView_,
high,
srcId,
dstId,
index.key,
wrappedDst,
/*bAuth=*/false,
/*bNoRipple=*/true,
/*bFreeze=*/false,
/*deepFreeze*/ false,
/*saBalance=*/STAmount{Issue{currency_, noAccount()}},
/*saLimit=*/STAmount{Issue{currency_, dstId}},
/*uQualityIn=*/0,
/*uQualityOut=*/0,
journal);
}
TER
WritableIOUToken::removeEmptyHolding(AccountID const& accountID, beast::Journal journal)
{
if (issue_.native())
{
auto const account = AccountRoot(accountID, applyView_);
if (!account.exists())
return tecINTERNAL; // LCOV_EXCL_LINE
auto const balance = account->getFieldAmount(sfBalance);
if (balance.xrp() != 0)
return tecHAS_OBLIGATIONS;
return tesSUCCESS;
}
// `asset` is an IOU.
// If the account is the issuer, then no line should exist. Check anyway.
// If a line does exist, it will get deleted. If not, return success.
bool const accountIsIssuer = accountID == issue_.account;
auto const line = applyView_.peek(keylet::line(accountID, issue_));
if (!line)
return accountIsIssuer ? (TER)tesSUCCESS : (TER)tecOBJECT_NOT_FOUND;
if (!accountIsIssuer && line->at(sfBalance)->iou() != beast::zero)
return tecHAS_OBLIGATIONS;
// Adjust the owner count(s)
if (line->isFlag(lsfLowReserve))
{
// Clear reserve for low account.
WritableAccountRoot wrappedLow(line->at(sfLowLimit)->getIssuer(), applyView_);
if (!wrappedLow)
return tecINTERNAL; // LCOV_EXCL_LINE
wrappedLow.adjustOwnerCount(-1, journal);
// It's not really necessary to clear the reserve flag, since the line
// is about to be deleted, but this will make the metadata reflect an
// accurate state at the time of deletion.
line->clearFlag(lsfLowReserve);
}
if (line->isFlag(lsfHighReserve))
{
// Clear reserve for high account.
WritableAccountRoot wrappedHigh(line->at(sfHighLimit)->getIssuer(), applyView_);
if (!wrappedHigh)
return tecINTERNAL; // LCOV_EXCL_LINE
wrappedHigh.adjustOwnerCount(-1, journal);
// It's not really necessary to clear the reserve flag, since the line
// is about to be deleted, but this will make the metadata reflect an
// accurate state at the time of deletion.
line->clearFlag(lsfHighReserve);
}
return trustDelete(
applyView_,
line,
line->at(sfLowLimit)->getIssuer(),
line->at(sfHighLimit)->getIssuer(),
journal);
}
TER
deleteAMMTrustLine(
ApplyView& view,
std::shared_ptr<SLE> sleState,
std::optional<AccountID> const& ammAccountID,
beast::Journal j)
{
if (!sleState || sleState->getType() != ltRIPPLE_STATE)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const& [low, high] = std::minmax(
sleState->getFieldAmount(sfLowLimit).getIssuer(),
sleState->getFieldAmount(sfHighLimit).getIssuer());
WritableAccountRoot wrappedLow(low, view);
WritableAccountRoot wrappedHigh(high, view);
if (!wrappedLow || !wrappedHigh)
return tecINTERNAL; // LCOV_EXCL_LINE
bool const ammLow = wrappedLow->isFieldPresent(sfAMMID);
bool const ammHigh = wrappedHigh->isFieldPresent(sfAMMID);
// can't both be AMM
if (ammLow && ammHigh)
return tecINTERNAL; // LCOV_EXCL_LINE
// at least one must be
if (!ammLow && !ammHigh)
return terNO_AMM;
// one must be the target amm
if (ammAccountID && (low != *ammAccountID && high != *ammAccountID))
return terNO_AMM;
if (auto const ter = trustDelete(view, sleState, low, high, j); !isTesSuccess(ter))
{
JLOG(j.error()) << "deleteAMMTrustLine: failed to delete the trustline.";
return ter;
}
auto const uFlags = !ammLow ? lsfLowReserve : lsfHighReserve;
if (!(sleState->getFlags() & uFlags))
return tecINTERNAL; // LCOV_EXCL_LINE
WritableAccountRoot wrappedHolder = !ammLow ? wrappedLow : wrappedHigh;
wrappedHolder.adjustOwnerCount(-1, j);
return tesSUCCESS;
}
} // namespace xrpl

View File

@@ -28,7 +28,7 @@ dirLink(
SF_UINT64 const& node = sfOwnerNode);
bool
MPToken::isGlobalFrozen() const
MPTokenIssuance::isGlobalFrozen() const
{
if (sle_)
return sle_->isFlag(lsfMPTLocked);
@@ -36,7 +36,7 @@ MPToken::isGlobalFrozen() const
}
bool
MPToken::isIndividualFrozen(AccountID const& account) const
MPTokenIssuance::isIndividualFrozen(AccountID const& account) const
{
if (auto const sle = readView_.read(keylet::mptoken(mptID_, account)))
return sle->isFlag(lsfMPTLocked);
@@ -44,14 +44,14 @@ MPToken::isIndividualFrozen(AccountID const& account) const
}
bool
MPToken::isFrozen(AccountID const& account, int depth) const
MPTokenIssuance::isFrozen(AccountID const& account, int depth) const
{
return isGlobalFrozen() || isIndividualFrozen(account) ||
isVaultPseudoAccountFrozen(readView_, account, mptIssue_, depth);
}
[[nodiscard]] bool
MPToken::isAnyFrozen(std::initializer_list<AccountID> const& accounts, int depth) const
MPTokenIssuance::isAnyFrozen(std::initializer_list<AccountID> const& accounts, int depth) const
{
if (isGlobalFrozen())
return true;
@@ -72,26 +72,25 @@ MPToken::isAnyFrozen(std::initializer_list<AccountID> const& accounts, int depth
}
TER
MPToken::checkFrozen(AccountID const& account) const
MPTokenIssuance::checkFrozen(AccountID const& account) const
{
return isFrozen(account) ? TER{tecFROZEN} : TER{tesSUCCESS};
return isFrozen(account) ? TER{tecLOCKED} : TER{tesSUCCESS};
}
bool
MPToken::isDeepFrozen(AccountID const& account, int depth) const
MPTokenIssuance::isDeepFrozen(AccountID const& account, int depth) const
{
// MPTs don't have deep freeze, so this always returns false
return false;
return isFrozen(account, depth);
}
TER
MPToken::checkDeepFrozen(AccountID const& account) const
MPTokenIssuance::checkDeepFrozen(AccountID const& account) const
{
return isDeepFrozen(account) ? TER{tecLOCKED} : TER{tesSUCCESS};
}
Rate
MPToken::transferRate() const
MPTokenIssuance::transferRate() const
{
// fee is 0-50,000 (0-50%), rate is 1,000,000,000-2,000,000,000
// For example, if transfer fee is 50% then 10,000 * 50,000 = 500,000
@@ -103,7 +102,7 @@ MPToken::transferRate() const
}
[[nodiscard]] TER
MPToken::canAddHolding() const
MPTokenIssuance::canAddHolding() const
{
if (!sle_)
{
@@ -118,7 +117,7 @@ MPToken::canAddHolding() const
}
[[nodiscard]] TER
WritableMPToken::addEmptyHolding(
WritableMPTokenIssuance::addEmptyHolding(
AccountID const& accountID,
XRPAmount priorBalance,
beast::Journal journal)
@@ -136,7 +135,7 @@ WritableMPToken::addEmptyHolding(
}
[[nodiscard]] TER
WritableMPToken::authorizeMPToken(
WritableMPTokenIssuance::authorizeMPToken(
XRPAmount const& priorBalance,
AccountID const& account,
beast::Journal journal,
@@ -252,7 +251,7 @@ WritableMPToken::authorizeMPToken(
}
[[nodiscard]] TER
WritableMPToken::removeEmptyHolding(AccountID const& accountID, beast::Journal journal)
WritableMPTokenIssuance::removeEmptyHolding(AccountID const& accountID, beast::Journal journal)
{
// If the account is the issuer, then no token should exist. MPTs do not
// have the legacy ability to create such a situation, but check anyway. If
@@ -277,7 +276,7 @@ WritableMPToken::removeEmptyHolding(AccountID const& accountID, beast::Journal j
}
[[nodiscard]] TER
MPToken::requireAuth(AccountID const& account, AuthType authType, int depth) const
MPTokenIssuance::requireAuth(AccountID const& account, AuthType authType, int depth) const
{
if (!sle_)
return tecOBJECT_NOT_FOUND;
@@ -356,7 +355,7 @@ MPToken::requireAuth(AccountID const& account, AuthType authType, int depth) con
}
[[nodiscard]] TER
WritableMPToken::enforceMPTokenAuthorization(
WritableMPTokenIssuance::enforceMPTokenAuthorization(
AccountID const& account,
XRPAmount const& priorBalance, // for MPToken authorization
beast::Journal j)
@@ -449,7 +448,7 @@ WritableMPToken::enforceMPTokenAuthorization(
}
TER
MPToken::canTransfer(AccountID const& from, AccountID const& to) const
MPTokenIssuance::canTransfer(AccountID const& from, AccountID const& to) const
{
if (!sle_)
return tecOBJECT_NOT_FOUND;
@@ -462,6 +461,28 @@ MPToken::canTransfer(AccountID const& from, AccountID const& to) const
return tesSUCCESS;
}
//------------------------------------------------------------------------------
//
// Token capability checks (MPT-specific)
//
//------------------------------------------------------------------------------
bool
MPTokenIssuance::canClawback() const
{
if (!sle_)
return false;
return sle_->isFlag(lsfMPTCanClawback);
}
bool
MPTokenIssuance::requiresAuth() const
{
if (!sle_)
return false;
return sle_->isFlag(lsfMPTRequireAuth);
}
TER
rippleLockEscrowMPT(
ApplyView& view,
@@ -470,7 +491,7 @@ rippleLockEscrowMPT(
beast::Journal j)
{
auto const mptIssue = amount.get<MPTIssue>();
auto mptIssuance = WritableMPToken(view, mptIssue);
auto mptIssuance = WritableMPTokenIssuance(view, mptIssue);
if (!mptIssuance.exists())
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleLockEscrowMPT: MPT issuance not found for "
@@ -576,7 +597,7 @@ rippleUnlockEscrowMPT(
auto const& issuer = netAmount.getIssuer();
auto const& mptIssue = netAmount.get<MPTIssue>();
auto mptIssuance = WritableMPToken(view, mptIssue);
auto mptIssuance = WritableMPTokenIssuance(view, mptIssue);
if (!mptIssuance)
{ // LCOV_EXCL_START
JLOG(j.error()) << "rippleUnlockEscrowMPT: MPT issuance not found for "
@@ -722,7 +743,7 @@ rippleUnlockEscrowMPT(
}
STAmount
MPToken::accountHolds(
MPTokenIssuance::accountHolds(
AccountID const& account,
FreezeHandling zeroIfFrozen,
beast::Journal j,
@@ -732,7 +753,7 @@ MPToken::accountHolds(
}
STAmount
MPToken::accountHolds(
MPTokenIssuance::accountHolds(
AccountID const& account,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,

View File

@@ -98,7 +98,7 @@ IOUToken::isIndividualFrozen(AccountID const& account) const
bool
IOUToken::isFrozen(AccountID const& account, int depth) const
{
XRPL_ASSERT(depth == 0, "IOUToken::isFrozen : depth is 0");
// NOTE: depth is ignored here because it's only relevant for MPTs
if (isXRP(currency_))
return false;
if (issuerAccount_.exists() && issuerAccount_->isFlag(lsfGlobalFreeze))
@@ -116,7 +116,7 @@ IOUToken::isFrozen(AccountID const& account, int depth) const
bool
IOUToken::isDeepFrozen(AccountID const& account, int depth) const
{
XRPL_ASSERT(depth == 0, "IOUToken::isDeepFrozen : depth is 0");
// NOTE: depth is ignored here because it's only relevant for MPTs
if (isXRP(currency_))
{
return false;
@@ -151,7 +151,7 @@ IOUToken::checkDeepFrozen(AccountID const& account) const
bool
IOUToken::isAnyFrozen(std::initializer_list<AccountID> const& accounts, int depth) const
{
XRPL_ASSERT(depth == 0, "IOUToken::isAnyFrozen : depth is 0");
// NOTE: depth is ignored here because it's only relevant for MPTs
if (isGlobalFrozen())
return true;
@@ -164,6 +164,19 @@ IOUToken::isAnyFrozen(std::initializer_list<AccountID> const& accounts, int dept
return false;
}
STAmount
IOUToken::accountFunds(
AccountID const& id,
STAmount const& saDefault,
FreezeHandling freezeHandling,
beast::Journal j) const
{
if (!saDefault.native() && saDefault.getIssuer() == id)
return saDefault;
return accountHolds(id, freezeHandling, j);
}
STAmount
IOUToken::accountHolds(
AccountID const& account,
@@ -184,7 +197,8 @@ IOUToken::accountHolds(
{
if (isXRP(currency_))
{
return {issuerAccount_.xrpLiquid(0, j)};
AccountRoot accountRoot(account, readView_);
return {accountRoot.xrpLiquid(0, j)};
}
bool const returnSpendable = (includeFullBalance == shFULL_BALANCE);
@@ -194,35 +208,24 @@ IOUToken::accountHolds(
return STAmount{issue_, STAmount::cMaxValue, STAmount::cMaxOffset};
// IOU: Return balance on trust line modulo freeze
// Check if line exists and is usable
auto const sle = readView_.read(keylet::line(account, issuer_, currency_));
if (!sle)
{
STAmount result;
result.clear(Issue{currency_, issuer_});
return result;
}
// Check if line exists and is usable (mirrors old getLineIfUsable)
SLE::const_pointer sle = readView_.read(keylet::line(account, issuer_, currency_));
// Check freeze status
if (zeroIfFrozen == fhZERO_IF_FROZEN)
if (sle && zeroIfFrozen == fhZERO_IF_FROZEN)
{
if (isFrozen(account) || isDeepFrozen(account))
{
STAmount result;
result.clear(Issue{currency_, issuer_});
return result;
sle = nullptr;
}
// when fixFrozenLPTokenTransfer is enabled, if currency is lptoken,
// we need to check if the associated assets have been frozen
if (readView_.rules().enabled(fixFrozenLPTokenTransfer))
if (sle && readView_.rules().enabled(fixFrozenLPTokenTransfer))
{
auto const sleIssuer = readView_.read(keylet::account(issuer_));
if (!sleIssuer)
{
STAmount result;
result.clear(Issue{currency_, issuer_});
return result;
sle = nullptr; // LCOV_EXCL_LINE
}
else if (sleIssuer->isFieldPresent(sfAMMID))
{
@@ -235,39 +238,54 @@ IOUToken::accountHolds(
(*sleAmm)[sfAsset].get<Issue>(),
(*sleAmm)[sfAsset2].get<Issue>()))
{
STAmount result;
result.clear(Issue{currency_, issuer_});
return result;
sle = nullptr;
}
}
}
}
// Extract balance from SLE
STAmount amount = sle->getFieldAmount(sfBalance);
bool const accountHigh = account > issuer_;
auto const& oppositeField = accountHigh ? sfLowLimit : sfHighLimit;
if (accountHigh)
// Extract balance (mirrors old getTrustLineBalance)
STAmount amount;
if (sle)
{
// Put balance in account terms.
amount.negate();
amount = sle->getFieldAmount(sfBalance);
bool const accountHigh = account > issuer_;
auto const& oppositeField = accountHigh ? sfLowLimit : sfHighLimit;
if (accountHigh)
{
// Put balance in account terms.
amount.negate();
}
if (returnSpendable)
{
amount += sle->getFieldAmount(oppositeField);
}
amount.setIssuer(issuer_);
}
if (returnSpendable)
else
{
amount += sle->getFieldAmount(oppositeField);
amount.clear(Issue{currency_, issuer_});
}
amount.setIssuer(issuer_);
JLOG(j.trace()) << "IOUToken::accountHolds:" << " account=" << to_string(account)
<< " amount=" << amount.getFullText();
return amount;
return readView_.balanceHook(account, issuer_, amount);
}
TER
IOUToken::canAddHolding() const
{
return tesSUCCESS; // IOUs don't have restrictions on adding holdings
if (isXRP(issue_))
return tesSUCCESS;
if (!issuerAccount_.exists())
return terNO_ACCOUNT;
if (!issuerAccount_->isFlag(lsfDefaultRipple))
return terNO_RIPPLE;
return tesSUCCESS;
}
Rate
@@ -398,7 +416,7 @@ trustCreate(
TER
trustDelete(
ApplyView& readView_,
ApplyView& view,
std::shared_ptr<SLE> const& sleRippleState,
AccountID const& uLowAccountID,
AccountID const& uHighAccountID,
@@ -410,22 +428,20 @@ trustDelete(
JLOG(j.trace()) << "trustDelete: Deleting ripple line: low";
if (!readView_.dirRemove(
keylet::ownerDir(uLowAccountID), uLowNode, sleRippleState->key(), false))
if (!view.dirRemove(keylet::ownerDir(uLowAccountID), uLowNode, sleRippleState->key(), false))
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
JLOG(j.trace()) << "trustDelete: Deleting ripple line: high";
if (!readView_.dirRemove(
keylet::ownerDir(uHighAccountID), uHighNode, sleRippleState->key(), false))
if (!view.dirRemove(keylet::ownerDir(uHighAccountID), uHighNode, sleRippleState->key(), false))
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
JLOG(j.trace()) << "trustDelete: Deleting ripple line: state";
readView_.erase(sleRippleState);
view.erase(sleRippleState);
return tesSUCCESS;
}
@@ -660,7 +676,7 @@ redeemIOU(
TER
IOUToken::requireAuth(AccountID const& account, AuthType authType, int depth) const
{
XRPL_ASSERT(depth == 0, "IOUToken::requireAuth : depth is 0");
// NOTE: depth is ignored here because it's only relevant for MPTs
if (isXRP(issue_) || issuer_ == account)
return tesSUCCESS;
@@ -715,6 +731,29 @@ IOUToken::canTransfer(AccountID const& from, AccountID const& to) const
return tesSUCCESS;
}
//------------------------------------------------------------------------------
//
// Token capability checks (IOU-specific)
//
//------------------------------------------------------------------------------
bool
IOUToken::canClawback() const
{
if (!issuerAccount_.exists())
return false;
return issuerAccount_->isFlag(lsfAllowTrustLineClawback) &&
!issuerAccount_->isFlag(lsfNoFreeze);
}
bool
IOUToken::requiresAuth() const
{
if (!issuerAccount_.exists())
return false;
return issuerAccount_->isFlag(lsfRequireAuth);
}
//------------------------------------------------------------------------------
//
// Empty holding operations (IOU-specific)