merged IOUEscrow amendment

This commit is contained in:
Richard Holland
2022-06-14 09:04:43 +00:00
16 changed files with 1353 additions and 81 deletions

View File

@@ -33,6 +33,8 @@
#include <ripple/protocol/TxFlags.h>
#include <ripple/protocol/digest.h>
#include <ripple/protocol/st.h>
#include <ripple/protocol/Rate.h>
// During an EscrowFinish, the transaction must specify both
// a condition and a fulfillment. We track whether that
@@ -93,7 +95,8 @@ after(NetClock::time_point now, std::uint32_t mark)
TxConsequences
EscrowCreate::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
return TxConsequences{ctx.tx,
isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero};
}
NotTEC
@@ -105,8 +108,25 @@ EscrowCreate::preflight(PreflightContext const& ctx)
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
if (!isXRP(ctx.tx[sfAmount]))
return temBAD_AMOUNT;
STAmount const amount {ctx.tx[sfAmount]};
if (!isXRP(amount))
{
if (!ctx.rules.enabled(featurePaychanAndEscrowForTokens))
return temBAD_AMOUNT;
if (!isLegalNet(amount))
return temBAD_AMOUNT;
if (isFakeXRP(amount))
return temBAD_CURRENCY;
if (ctx.tx[sfAccount] == amount.getIssuer())
{
JLOG(ctx.j.trace())
<< "Malformed transaction: Cannot escrow own tokens to self.";
return temDST_IS_SRC;
}
}
if (ctx.tx[sfAmount] <= beast::zero)
return temBAD_AMOUNT;
@@ -199,17 +219,66 @@ EscrowCreate::doApply()
if (!sle)
return tefINTERNAL;
STAmount const amount {ctx_.tx[sfAmount]};
std::shared_ptr<SLE> sleLine;
auto const balance = STAmount((*sle)[sfBalance]).xrp();
auto const reserve =
ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
// Check reserve and funds availability
if (isXRP(amount) && balance < reserve + STAmount(ctx_.tx[sfAmount]).xrp())
return tecUNFUNDED;
else
{
auto const balance = STAmount((*sle)[sfBalance]).xrp();
auto const reserve =
ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1);
// preflight will prevent this ever firing, included
// defensively for completeness
if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens))
return tefINTERNAL;
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
// check if the escrow is capable of being
// finished before we allow it to be created
{
TER result =
trustTransferAllowed(
ctx_.view(),
{account, ctx_.tx[sfDestination]},
amount.issue(),
ctx_.journal);
if (balance < reserve + STAmount(ctx_.tx[sfAmount]).xrp())
return tecUNFUNDED;
JLOG(ctx_.journal.trace())
<< "EscrowCreate::doApply trustTransferAllowed result="
<< result;
if (!isTesSuccess(result))
return result;
}
// perform the lock as a dry run before
// we modify anything on-ledger
sleLine = ctx_.view().peek(keylet::line(account, amount.getIssuer(), amount.getCurrency()));
{
TER result =
trustAdjustLockedBalance(
ctx_.view(),
sleLine,
amount,
1,
ctx_.journal,
DryRun);
JLOG(ctx_.journal.trace())
<< "EscrowCreate::doApply trustAdjustLockedBalance (dry) result="
<< result;
if (!isTesSuccess(result))
return result;
}
}
// Check destination account
@@ -274,7 +343,31 @@ EscrowCreate::doApply()
}
// Deduct owner's balance, increment owner count
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
if (isXRP(amount))
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
else
{
if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens) || !sleLine)
return tefINTERNAL;
// do the lock-up for real now
TER result =
trustAdjustLockedBalance(
ctx_.view(),
sleLine,
amount,
1,
ctx_.journal,
WetRun);
JLOG(ctx_.journal.trace())
<< "EscrowCreate::doApply trustAdjustLockedBalance (wet) result="
<< result;
if (!isTesSuccess(result))
return result;
}
adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal);
ctx_.view().update(sle);
@@ -384,6 +477,10 @@ EscrowFinish::doApply()
if (!slep)
return tecNO_TARGET;
AccountID const account = (*slep)[sfAccount];
auto const sle = ctx_.view().peek(keylet::account(account));
auto amount = slep->getFieldAmount(sfAmount);
// If a cancel time is present, a finish operation should only succeed prior
// to that time. fix1571 corrects a logic error in the check that would make
// a finish only succeed strictly after the cancel time.
@@ -484,7 +581,33 @@ EscrowFinish::doApply()
}
}
AccountID const account = (*slep)[sfAccount];
if (!isXRP(amount))
{
if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens))
return tefINTERNAL;
// perform a dry run of the transfer before we
// change anything on-ledger
TER result =
trustTransferLockedBalance(
ctx_.view(),
account_, // txn signing account
sle, // src account
sled, // dst account
amount, // xfer amount
-1,
j_,
DryRun // dry run
);
JLOG(j_.trace())
<< "EscrowFinish::doApply trustTransferLockedBalance (dry) result="
<< result;
if (!isTesSuccess(result))
return result;
}
// Remove escrow from owner directory
{
@@ -508,12 +631,38 @@ EscrowFinish::doApply()
}
}
// Transfer amount to destination
(*sled)[sfBalance] = (*sled)[sfBalance] + (*slep)[sfAmount];
if (isXRP(amount))
(*sled)[sfBalance] = (*sled)[sfBalance] + (*slep)[sfAmount];
else
{
// all the significant complexity of checking the validity of this
// transfer and ensuring the lines exist etc is hidden away in this
// function, all we need to do is call it and return if unsuccessful.
TER result =
trustTransferLockedBalance(
ctx_.view(),
account_, // txn signing account
sle, // src account
sled, // dst account
amount, // xfer amount
-1,
j_,
WetRun // wet run;
);
JLOG(j_.trace())
<< "EscrowFinish::doApply trustTransferLockedBalance (wet) result="
<< result;
if (!isTesSuccess(result))
return result;
}
ctx_.view().update(sled);
// Adjust source owner count
auto const sle = ctx_.view().peek(keylet::account(account));
adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal);
ctx_.view().update(sle);
@@ -581,6 +730,32 @@ EscrowCancel::doApply()
}
AccountID const account = (*slep)[sfAccount];
auto const sle = ctx_.view().peek(keylet::account(account));
auto amount = slep->getFieldAmount(sfAmount);
std::shared_ptr<SLE> sleLine;
if (!isXRP(amount))
{
if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens))
return tefINTERNAL;
sleLine =
ctx_.view().peek(
keylet::line(account, amount.getIssuer(), amount.getCurrency()));
// dry run before we make any changes to ledger
if (TER result =
trustAdjustLockedBalance(
ctx_.view(),
sleLine,
-amount,
-1,
ctx_.journal,
DryRun);
result != tesSUCCESS)
return result;
}
// Remove escrow from owner directory
{
@@ -607,9 +782,33 @@ EscrowCancel::doApply()
}
}
// Transfer amount back to owner, decrement owner count
auto const sle = ctx_.view().peek(keylet::account(account));
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount];
// Transfer amount back to the owner (or unlock it in TL case)
if (isXRP(amount))
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount];
else
{
if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens))
return tefINTERNAL;
// unlock previously locked tokens from source line
TER result =
trustAdjustLockedBalance(
ctx_.view(),
sleLine,
-amount,
-1,
ctx_.journal,
WetRun);
JLOG(ctx_.journal.trace())
<< "EscrowCancel::doApply trustAdjustLockedBalance (wet) result="
<< result;
if (!isTesSuccess(result))
return result;
}
// Decrement owner count
adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal);
ctx_.view().update(sle);

View File

@@ -99,11 +99,14 @@ XRPNotCreated::visitEntry(
drops_ -= (*before)[sfBalance].xrp().drops();
break;
case ltPAYCHAN:
drops_ -=
((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
if (isXRP((*before)[sfAmount]))
drops_ -=
((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
break;
case ltESCROW:
drops_ -= (*before)[sfAmount].xrp().drops();
if (isXRP((*before)[sfAmount]))
drops_ -=
(*before)[sfAmount].xrp().drops();
break;
default:
break;
@@ -118,14 +121,14 @@ XRPNotCreated::visitEntry(
drops_ += (*after)[sfBalance].xrp().drops();
break;
case ltPAYCHAN:
if (!isDelete)
drops_ += ((*after)[sfAmount] - (*after)[sfBalance])
.xrp()
.drops();
if (!isDelete && isXRP((*after)[sfAmount]))
drops_ +=
((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops();
break;
case ltESCROW:
if (!isDelete)
drops_ += (*after)[sfAmount].xrp().drops();
if (!isDelete && isXRP((*after)[sfAmount]))
drops_ +=
(*after)[sfAmount].xrp().drops();
break;
default:
break;
@@ -285,12 +288,25 @@ NoZeroEscrow::visitEntry(
bool
NoZeroEscrow::finalize(
STTx const&,
STTx const& txn,
TER const,
XRPAmount const,
ReadView const&,
ReadView const& rv,
beast::Journal const& j)
{
// bypass this invariant check for IOU escrows
if (bad_ &&
rv.rules().enabled(featurePaychanAndEscrowForTokens) &&
txn.isFieldPresent(sfTransactionType))
{
uint16_t tt = txn.getFieldU16(sfTransactionType);
if (tt == ttESCROW_CANCEL || tt == ttESCROW_FINISH)
return true;
if (txn.isFieldPresent(sfAmount) && !isXRP(txn.getFieldAmount(sfAmount)))
return true;
}
if (bad_)
{
JLOG(j.fatal()) << "Invariant failed: escrow specifies invalid amount";

View File

@@ -120,6 +120,36 @@ closeChannel(
beast::Journal j)
{
AccountID const src = (*slep)[sfAccount];
auto const amount = (*slep)[sfAmount] - (*slep)[sfBalance];
std::shared_ptr<SLE> sleLine;
if (!isXRP(amount))
{
if (!view.rules().enabled(featurePaychanAndEscrowForTokens))
return tefINTERNAL;
sleLine =
view.peek(keylet::line(src, amount.getIssuer(), amount.getCurrency()));
// dry run
TER result =
trustAdjustLockedBalance(
view,
sleLine,
-amount,
-1,
j,
DryRun);
JLOG(j.trace())
<< "closeChannel: trustAdjustLockedBalance(dry) result="
<< result;
if (!isTesSuccess(result))
return result;
}
// Remove PayChan from owner directory
{
auto const page = (*slep)[sfOwnerNode];
@@ -150,8 +180,28 @@ closeChannel(
return tefINTERNAL;
assert((*slep)[sfAmount] >= (*slep)[sfBalance]);
(*sle)[sfBalance] =
(*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
if (isXRP(amount))
(*sle)[sfBalance] = (*sle)[sfBalance] + amount;
else
{
TER result =
trustAdjustLockedBalance(
view,
sleLine,
-amount,
-1,
j,
WetRun);
JLOG(j.trace())
<< "closeChannel: trustAdjustLockedBalance(wet) result="
<< result;
if (!isTesSuccess(result))
return result;
}
adjustOwnerCount(view, sle, -1, j);
view.update(sle);
@@ -165,7 +215,8 @@ closeChannel(
TxConsequences
PayChanCreate::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
return TxConsequences{ctx.tx,
isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero};
}
NotTEC
@@ -177,7 +228,27 @@ PayChanCreate::preflight(PreflightContext const& ctx)
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero))
STAmount const amount {ctx.tx[sfAmount]};
if (!isXRP(amount))
{
if (!ctx.rules.enabled(featurePaychanAndEscrowForTokens))
return temBAD_AMOUNT;
if (!isLegalNet(amount))
return temBAD_AMOUNT;
if (isFakeXRP(amount))
return temBAD_CURRENCY;
if (ctx.tx[sfAccount] == amount.getIssuer())
{
JLOG(ctx.j.trace())
<< "Malformed transaction: Cannot paychan own tokens to self.";
return temDST_IS_SRC;
}
}
if (ctx.tx[sfAmount] <= beast::zero)
return temBAD_AMOUNT;
if (ctx.tx[sfAccount] == ctx.tx[sfDestination])
@@ -197,21 +268,65 @@ PayChanCreate::preclaim(PreclaimContext const& ctx)
if (!sle)
return terNO_ACCOUNT;
// Check reserve and funds availability
{
auto const balance = (*sle)[sfBalance];
auto const reserve =
ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1);
STAmount const amount {ctx.tx[sfAmount]};
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
auto const balance = (*sle)[sfBalance];
auto const reserve =
ctx.view.fees().accountReserve((*sle)[sfOwnerCount] + 1);
if (balance < reserve + ctx.tx[sfAmount])
return tecUNFUNDED;
}
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
auto const dst = ctx.tx[sfDestination];
// Check reserve and funds availability
if (isXRP(amount) && balance < reserve + ctx.tx[sfAmount])
return tecUNFUNDED;
else
{
if (!ctx.view.rules().enabled(featurePaychanAndEscrowForTokens))
return tecINTERNAL;
// check for any possible bars to a channel existing
// between these accounts for this asset
{
TER result =
trustTransferAllowed(
ctx.view,
{account, dst},
amount.issue(),
ctx.j);
JLOG(ctx.j.trace())
<< "PayChanCreate::preclaim trustTransferAllowed result="
<< result;
if (!isTesSuccess(result))
return result;
}
// check if the amount can be locked
{
auto sleLine =
ctx.view.read(
keylet::line(account, amount.getIssuer(), amount.getCurrency()));
TER result =
trustAdjustLockedBalance(
ctx.view,
sleLine,
amount,
1,
ctx.j,
DryRun);
JLOG(ctx.j.trace())
<< "PayChanCreate::preclaim trustAdjustLockedBalance(dry) result="
<< result;
if (!isTesSuccess(result))
return result;
}
}
{
// Check destination account
auto const sled = ctx.view.read(keylet::account(dst));
@@ -241,6 +356,8 @@ PayChanCreate::doApply()
auto const dst = ctx_.tx[sfDestination];
STAmount const amount {ctx_.tx[sfAmount]};
// Create PayChan in ledger.
//
// Note that we we use the value from the sequence or ticket as the
@@ -293,7 +410,36 @@ PayChanCreate::doApply()
}
// Deduct owner's balance, increment owner count
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
if (isXRP(amount))
(*sle)[sfBalance] = (*sle)[sfBalance] - amount;
else
{
if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens))
return tefINTERNAL;
auto sleLine =
ctx_.view().peek(keylet::line(account, amount.getIssuer(), amount.getCurrency()));
if (!sleLine)
return tecUNFUNDED_PAYMENT;
TER result =
trustAdjustLockedBalance(
ctx_.view(),
sleLine,
amount,
1,
ctx_.journal,
WetRun);
JLOG(ctx_.journal.trace())
<< "PayChanCreate::doApply trustAdjustLockedBalance(wet) result="
<< result;
if (!isTesSuccess(result))
return tefINTERNAL;
}
adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal);
ctx_.view().update(sle);
@@ -305,7 +451,8 @@ PayChanCreate::doApply()
TxConsequences
PayChanFund::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
return TxConsequences{ctx.tx,
isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::zero};
}
NotTEC
@@ -317,7 +464,27 @@ PayChanFund::preflight(PreflightContext const& ctx)
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::zero))
STAmount const amount {ctx.tx[sfAmount]};
if (!isXRP(amount))
{
if (!ctx.rules.enabled(featurePaychanAndEscrowForTokens))
return temBAD_AMOUNT;
if (!isLegalNet(amount))
return temBAD_AMOUNT;
if (isFakeXRP(amount))
return temBAD_CURRENCY;
if (ctx.tx[sfAccount] == amount.getIssuer())
{
JLOG(ctx.j.trace())
<< "Malformed transaction: Cannot escrow own tokens to self.";
return temDST_IS_SRC;
}
}
if (ctx.tx[sfAmount] <= beast::zero)
return temBAD_AMOUNT;
return preflight2(ctx);
@@ -330,11 +497,42 @@ PayChanFund::doApply()
auto const slep = ctx_.view().peek(k);
if (!slep)
return tecNO_ENTRY;
STAmount const amount {ctx_.tx[sfAmount]};
std::shared_ptr<SLE> sleLine; // if XRP or featurePaychanAndEscrowForTokens
// not enabled this remains null
// if this is a Fund operation on an IOU then perform a dry run here
if (!isXRP(amount) &&
ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens))
{
sleLine = ctx_.view().peek(
keylet::line(
(*slep)[sfAccount],
amount.getIssuer(),
amount.getCurrency()));
TER result =
trustAdjustLockedBalance(
ctx_.view(),
sleLine,
amount,
1,
ctx_.journal,
DryRun);
JLOG(ctx_.journal.trace())
<< "PayChanFund::doApply trustAdjustLockedBalance(dry) result="
<< result;
if (!isTesSuccess(result))
return result;
}
AccountID const src = (*slep)[sfAccount];
auto const txAccount = ctx_.tx[sfAccount];
auto const expiration = (*slep)[~sfExpiration];
{
auto const cancelAfter = (*slep)[~sfCancelAfter];
auto const closeTime =
@@ -367,19 +565,6 @@ PayChanFund::doApply()
if (!sle)
return tefINTERNAL;
{
// Check reserve and funds availability
auto const balance = (*sle)[sfBalance];
auto const reserve =
ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
if (balance < reserve + ctx_.tx[sfAmount])
return tecUNFUNDED;
}
// do not allow adding funds if dst does not exist
if (AccountID const dst = (*slep)[sfDestination];
!ctx_.view().read(keylet::account(dst)))
@@ -387,12 +572,49 @@ PayChanFund::doApply()
return tecNO_DST;
}
// Check reserve and funds availability
auto const balance = (*sle)[sfBalance];
auto const reserve =
ctx_.view().fees().accountReserve((*sle)[sfOwnerCount]);
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
if (isXRP(amount))
{
if (balance < reserve + amount)
return tecUNFUNDED;
(*sle)[sfBalance] = (*sle)[sfBalance] - amount;
ctx_.view().update(sle);
}
else
{
if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens))
return tefINTERNAL;
TER result =
trustAdjustLockedBalance(
ctx_.view(),
sleLine,
amount,
1,
ctx_.journal,
WetRun);
JLOG(ctx_.journal.trace())
<< "PayChanFund::doApply trustAdjustLockedBalance(wet) result="
<< result;
if (!isTesSuccess(result))
return tefINTERNAL;
}
(*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount];
ctx_.view().update(slep);
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
ctx_.view().update(sle);
return tesSUCCESS;
}
@@ -405,12 +627,25 @@ PayChanClaim::preflight(PreflightContext const& ctx)
return ret;
auto const bal = ctx.tx[~sfBalance];
if (bal && (!isXRP(*bal) || *bal <= beast::zero))
return temBAD_AMOUNT;
if (bal)
{
if (!isXRP(*bal) && !ctx.rules.enabled(featurePaychanAndEscrowForTokens))
return temBAD_AMOUNT;
if (*bal <= beast::zero)
return temBAD_AMOUNT;
}
auto const amt = ctx.tx[~sfAmount];
if (amt && (!isXRP(*amt) || *amt <= beast::zero))
return temBAD_AMOUNT;
if (amt)
{
if (!isXRP(*amt) && !ctx.rules.enabled(featurePaychanAndEscrowForTokens))
return temBAD_AMOUNT;
if (*amt <= beast::zero)
return temBAD_AMOUNT;
}
if (bal && amt && *bal > *amt)
return temBAD_AMOUNT;
@@ -434,8 +669,8 @@ PayChanClaim::preflight(PreflightContext const& ctx)
// The signature isn't needed if txAccount == src, but if it's
// present, check it
auto const reqBalance = bal->xrp();
auto const authAmt = amt ? amt->xrp() : reqBalance;
auto const reqBalance = *bal;
auto const authAmt = *amt ? *amt : reqBalance;
if (reqBalance > authAmt)
return temBAD_AMOUNT;
@@ -446,7 +681,12 @@ PayChanClaim::preflight(PreflightContext const& ctx)
PublicKey const pk(ctx.tx[sfPublicKey]);
Serializer msg;
serializePayChanAuthorization(msg, k.key, authAmt);
if (isXRP(authAmt))
serializePayChanAuthorization(msg, k.key, authAmt.xrp());
else
serializePayChanAuthorization(msg, k.key, authAmt.iou(), authAmt.getCurrency(), authAmt.getIssuer());
if (!verify(pk, msg.slice(), *sig, /*canonical*/ true))
return temBAD_SIGNATURE;
}
@@ -482,9 +722,9 @@ PayChanClaim::doApply()
if (ctx_.tx[~sfBalance])
{
auto const chanBalance = slep->getFieldAmount(sfBalance).xrp();
auto const chanFunds = slep->getFieldAmount(sfAmount).xrp();
auto const reqBalance = ctx_.tx[sfBalance].xrp();
auto const chanBalance = slep->getFieldAmount(sfBalance);
auto const chanFunds = slep->getFieldAmount(sfAmount);
auto const reqBalance = ctx_.tx[sfBalance];
if (txAccount == dst && !ctx_.tx[~sfSignature])
return temBAD_SIGNATURE;
@@ -503,7 +743,7 @@ PayChanClaim::doApply()
// nothing requested
return tecUNFUNDED_PAYMENT;
auto const sled = ctx_.view().peek(keylet::account(dst));
auto sled = ctx_.view().peek(keylet::account(dst));
if (!sled)
return tecNO_DST;
@@ -511,6 +751,7 @@ PayChanClaim::doApply()
// featureDepositAuth to remove the bug.
bool const depositAuth{ctx_.view().rules().enabled(featureDepositAuth)};
if (!depositAuth &&
// RH TODO: does this condition need to be changed for IOU paychans?
(txAccount == src && (sled->getFlags() & lsfDisallowXRP)))
return tecNO_TARGET;
@@ -529,9 +770,38 @@ PayChanClaim::doApply()
}
(*slep)[sfBalance] = ctx_.tx[sfBalance];
XRPAmount const reqDelta = reqBalance - chanBalance;
STAmount const reqDelta = reqBalance - chanBalance;
assert(reqDelta >= beast::zero);
(*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta;
if (isXRP(reqDelta))
(*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta;
else
{
// xfer locked tokens to satisfy claim
// RH NOTE: there's no ledger modification before this point so
// no reason to do a dry run first
if (!ctx_.view().rules().enabled(featurePaychanAndEscrowForTokens))
return tefINTERNAL;
auto sleSrcAcc = ctx_.view().peek(keylet::account(src));
TER result =
trustTransferLockedBalance(
ctx_.view(),
txAccount,
sleSrcAcc,
sled,
reqDelta,
0,
ctx_.journal,
WetRun);
JLOG(ctx_.journal.trace())
<< "PayChanClaim::doApply trustTransferLockedBalance(wet) result="
<< result;
if (!isTesSuccess(result))
return result;
}
ctx_.view().update(sled);
ctx_.view().update(slep);
}

View File

@@ -33,11 +33,13 @@
#include <ripple/protocol/STTx.h>
#include <ripple/protocol/Serializer.h>
#include <ripple/protocol/TER.h>
#include <ripple/protocol/Feature.h>
#include <ripple/basics/Log.h>
#include <functional>
#include <map>
#include <memory>
#include <utility>
#include <type_traits>
#include <vector>
namespace ripple {
@@ -351,6 +353,7 @@ trustDelete(
AccountID const& uHighAccountID,
beast::Journal j);
/** Delete an offer.
Requirements:
@@ -413,6 +416,628 @@ transferXRP(
STAmount const& amount,
beast::Journal j);
//------------------------------------------------------------------------------
//
// Trustline Locking and Transfer (PaychanAndEscrowForTokens)
//
// In functions white require a `RunType`
// pass DryRun (don't apply changes) or WetRun (do apply changes)
// to allow compile time evaluation of which types and calls to use
// For all functions below that take a Dry/Wet run parameter
// View may be ReadView const or ApplyView for DryRuns.
// View *must* be ApplyView for a WetRun.
// Passed SLEs must be non-const for WetRun.
#define DryRun RunType<bool, true>()
#define WetRun RunType<bool, false>()
template <class T, T V>
struct RunType
{
//see:
//http://alumni.media.mit.edu/~rahimi/compile-time-flags/
constexpr operator T() const
{
static_assert(std::is_same<bool, T>::value);
return V;
}
constexpr T operator!() const
{
static_assert(std::is_same<bool, T>::value);
return !(V);
}
};
// allow party lists to be logged easily
template <class T>
std::ostream& operator<< (std::ostream& lhs, std::vector<T> const& rhs)
{
lhs << "{";
for (int i = 0; i < rhs.size(); ++i)
lhs << rhs[i] << (i < rhs.size() - 1 ? ", " : "");
lhs << "}";
return lhs;
}
// Return true iff the acc side of line is in default state
bool isTrustDefault(
std::shared_ptr<SLE> const& acc, // side to check
std::shared_ptr<SLE> const& line); // line to check
/** Lock or unlock a TrustLine balance.
If positive deltaAmt lock the amount.
If negative deltaAmt unlock the amount.
*/
template<class V, class S, class R>
[[nodiscard]] TER
trustAdjustLockedBalance(
V& view,
S& sleLine,
STAmount const& deltaAmt,
int deltaLockCount, // if +1 lockCount is increased, -1 is decreased, 0 unchanged
beast::Journal const& j,
R dryRun)
{
static_assert(
(std::is_same<V, ReadView const>::value &&
std::is_same<S, std::shared_ptr<SLE const>>::value) ||
(std::is_same<V, ApplyView>::value &&
std::is_same<S, std::shared_ptr<SLE>>::value));
// dry runs are explicit in code, but really the view type determines
// what occurs here, so this combination is invalid.
static_assert(!(std::is_same<V, ReadView const>::value && !dryRun));
if (!view.rules().enabled(featurePaychanAndEscrowForTokens))
return tefINTERNAL;
auto const currency = deltaAmt.getCurrency();
auto const issuer = deltaAmt.getIssuer();
STAmount lowLimit = sleLine->getFieldAmount(sfLowLimit);
// the account which is modifying the LockedBalance is always
// the side that isn't the issuer, so if the low side is the
// issuer then the high side is the account.
bool high = lowLimit.getIssuer() == issuer;
std::vector<AccountID> parties
{high ? sleLine->getFieldAmount(sfHighLimit).getIssuer(): lowLimit.getIssuer()};
// check for freezes & auth
{
TER result =
trustTransferAllowed(
view,
parties,
deltaAmt.issue(),
j);
JLOG(j.trace())
<< "trustAdjustLockedBalance: trustTransferAllowed result="
<< result;
if (!isTesSuccess(result))
return result;
}
// pull the TL balance from the account's perspective
STAmount balance =
high ? -(*sleLine)[sfBalance] : (*sleLine)[sfBalance];
// this would mean somehow the issuer is trying to lock balance
if (balance < beast::zero)
return tecINTERNAL;
if (deltaAmt == beast::zero)
return tesSUCCESS;
// can't lock or unlock a zero balance
if (balance == beast::zero)
{
JLOG(j.trace())
<< "trustAdjustLockedBalance failed, zero balance";
return tecUNFUNDED_PAYMENT;
}
STAmount priorLockedBalance {sfLockedBalance, deltaAmt.issue()};
if (sleLine->isFieldPresent(sfLockedBalance))
priorLockedBalance =
high ? -(*sleLine)[sfLockedBalance] : (*sleLine)[sfLockedBalance];
uint32_t priorLockCount = 0;
if (sleLine->isFieldPresent(sfLockCount))
priorLockCount = sleLine->getFieldU32(sfLockCount);
uint32_t finalLockCount = priorLockCount + deltaLockCount;
STAmount finalLockedBalance = priorLockedBalance + deltaAmt;
if (finalLockedBalance > balance)
{
JLOG(j.trace())
<< "trustAdjustLockedBalance: "
<< "lockedBalance("
<< finalLockedBalance
<< ") > balance("
<< balance
<< ") = true\n";
return tecUNFUNDED_PAYMENT;
}
if (finalLockedBalance < beast::zero)
return tecINTERNAL;
// check if there is significant precision loss
if (!isAddable(balance, deltaAmt) ||
!isAddable(priorLockedBalance, deltaAmt) ||
!isAddable(finalLockedBalance, balance))
return tecPRECISION_LOSS;
// sanity check possible overflows on the lock counter
if ((deltaLockCount > 0 && priorLockCount > finalLockCount) ||
(deltaLockCount < 0 && priorLockCount < finalLockCount) ||
(deltaLockCount == 0 && priorLockCount != finalLockCount))
return tecINTERNAL;
// we won't update any SLEs if it is a dry run
if (dryRun)
return tesSUCCESS;
if constexpr(std::is_same<V, ApplyView>::value && std::is_same<S, std::shared_ptr<SLE>>::value)
{
if (finalLockedBalance == beast::zero || finalLockCount == 0)
{
sleLine->makeFieldAbsent(sfLockedBalance);
sleLine->makeFieldAbsent(sfLockCount);
}
else
{
sleLine->
setFieldAmount(sfLockedBalance, high ? -finalLockedBalance : finalLockedBalance);
sleLine->setFieldU32(sfLockCount, finalLockCount);
}
view.update(sleLine);
}
return tesSUCCESS;
}
/** Check if a set of accounts can freely exchange the specified token.
Read only, does not change any ledger object.
May be called with ApplyView or ReadView.
(including unlocking) is forbidden by any flag or condition.
If parties contains 1 entry then noRipple is not a bar to xfer.
If parties contains more than 1 entry then any party with noRipple
on issuer side is a bar to xfer.
*/
template<class V>
[[nodiscard]]TER
trustTransferAllowed(
V& view,
std::vector<AccountID> const& parties,
Issue const& issue,
beast::Journal const& j)
{
static_assert(
std::is_same<V, ReadView const>::value ||
std::is_same<V, ApplyView>::value);
typedef typename std::conditional<
std::is_same<V, ApplyView>::value,
std::shared_ptr<SLE>,
std::shared_ptr<SLE const>>::type SLEPtr;
if (isFakeXRP(issue.currency))
return tecNO_PERMISSION;
auto const sleIssuerAcc = view.read(keylet::account(issue.account));
bool lockedBalanceAllowed =
view.rules().enabled(featurePaychanAndEscrowForTokens);
// missing issuer is always a bar to xfer
if (!sleIssuerAcc)
return tecNO_ISSUER;
// issuer global freeze is always a bar to xfer
if (isGlobalFrozen(view, issue.account))
return tecFROZEN;
uint32_t issuerFlags = sleIssuerAcc->getFieldU32(sfFlags);
bool requireAuth = issuerFlags & lsfRequireAuth;
for (AccountID const& p: parties)
{
if (p == issue.account)
continue;
auto const line = view.read(keylet::line(p, issue.account, issue.currency));
if (!line)
{
if (requireAuth)
{
// the line doesn't exist, i.e. it is in default state
// default state means the line has not been authed
// therefore if auth is required by issuer then
// this is now a bar to xfer
return tecNO_AUTH;
}
// missing line is a line in default state, this is not
// a general bar to xfer, however additional conditions
// do attach to completing an xfer into a default line
// but these are checked in trustTransferLockedBalance at
// the point of transfer.
continue;
}
// sanity check the line, insane lines are a bar to xfer
{
// these "strange" old lines, if they even exist anymore are
// always a bar to xfer
if (line->getFieldAmount(sfLowLimit).getIssuer() ==
line->getFieldAmount(sfHighLimit).getIssuer())
return tecINTERNAL;
if (line->isFieldPresent(sfLockedBalance))
{
if (!lockedBalanceAllowed)
{
JLOG(j.warn())
<< "trustTransferAllowed: "
<< "sfLockedBalance found on line when amendment not enabled";
return tecINTERNAL;
}
STAmount lockedBalance = line->getFieldAmount(sfLockedBalance);
STAmount balance = line->getFieldAmount(sfBalance);
if (lockedBalance.getCurrency() != balance.getCurrency())
{
JLOG(j.warn())
<< "trustTansferAllowed: "
<< "lockedBalance currency did not match balance currency";
return tecINTERNAL;
}
}
}
// check the bars to xfer ... these are:
// any TL in the set has noRipple on the issuer's side
// any TL in the set has a freeze on the issuer's side
// any TL in the set has RequireAuth and the TL lacks lsf*Auth
{
bool pHigh = p > issue.account;
auto const flagIssuerNoRipple { pHigh ? lsfLowNoRipple : lsfHighNoRipple };
auto const flagIssuerFreeze { pHigh ? lsfLowFreeze : lsfHighFreeze };
auto const flagIssuerAuth { pHigh ? lsfLowAuth : lsfHighAuth };
uint32_t flags = line->getFieldU32(sfFlags);
if (flags & flagIssuerFreeze)
{
JLOG(j.trace())
<< "trustTransferAllowed: "
<< "parties=[" << parties << "], "
<< "issuer: " << issue.account << " "
<< "has freeze on party: " << p;
return tecFROZEN;
}
// if called with more than one party then any party
// that has a noripple on the issuer side of their tl
// blocks any possible xfer
if (parties.size() > 1 && (flags & flagIssuerNoRipple))
{
JLOG(j.trace())
<< "trustTransferAllowed: "
<< "parties=[" << parties << "], "
<< "issuer: " << issue.account << " "
<< "has noRipple on party: " << p;
return tecPATH_DRY;
}
// every party involved must be on an authed trustline if
// the issuer has specified lsfRequireAuth
if (requireAuth && !(flags & flagIssuerAuth))
{
JLOG(j.trace())
<< "trustTransferAllowed: "
<< "parties=[" << parties << "], "
<< "issuer: " << issue.account << " "
<< "requires TL auth which "
<< "party: " << p << " "
<< "does not possess.";
return tecNO_AUTH;
}
}
}
return tesSUCCESS;
}
/** Transfer a locked balance from one TL to an unlocked balance on another
or create a line at the destination if the actingAcc has permission to.
Used for resolving payment instruments that use locked TL balances.
*/
template <class V, class S, class R>
[[nodiscard]] TER
trustTransferLockedBalance(
V& view,
AccountID const& actingAccID, // the account whose tx is actioning xfer
S& sleSrcAcc,
S& sleDstAcc,
STAmount const& amount, // issuer, currency are in this field
int deltaLockCount, // -1 decrement, +1 increment, 0 unchanged
beast::Journal const& j,
R dryRun)
{
typedef typename std::conditional<
std::is_same<V, ApplyView>::value && !dryRun,
std::shared_ptr<SLE>,
std::shared_ptr<SLE const>>::type SLEPtr;
auto peek = [&](Keylet& k)
{
if constexpr (std::is_same<V, ApplyView>::value && !dryRun)
return const_cast<ApplyView&>(view).peek(k);
else
return view.read(k);
};
static_assert(std::is_same<V, ApplyView>::value || dryRun);
if (!view.rules().enabled(featurePaychanAndEscrowForTokens))
return tefINTERNAL;
if (!sleSrcAcc || !sleDstAcc)
{
JLOG(j.warn())
<< "trustTransferLockedBalance without sleSrc/sleDst";
return tecINTERNAL;
}
if (amount <= beast::zero)
{
JLOG(j.warn())
<< "trustTransferLockedBalance with non-positive amount";
return tecINTERNAL;
}
auto issuerAccID = amount.getIssuer();
auto currency = amount.getCurrency();
auto srcAccID = sleSrcAcc->getAccountID(sfAccount);
auto dstAccID = sleDstAcc->getAccountID(sfAccount);
bool srcHigh = srcAccID > issuerAccID;
bool dstHigh = dstAccID > issuerAccID;
// check for freezing, auth, no ripple and TL sanity
{
TER result =
trustTransferAllowed(
view,
{srcAccID, dstAccID},
{currency, issuerAccID},
j);
JLOG(j.trace())
<< "trustTransferLockedBalance: trustTransferAlowed result="
<< result;
if (!isTesSuccess(result))
return result;
}
// ensure source line exists
Keylet klSrcLine { keylet::line(srcAccID, issuerAccID, currency)};
SLEPtr sleSrcLine = peek(klSrcLine);
if (!sleSrcLine)
return tecNO_LINE;
// can't transfer a locked balance that does not exist
if (!sleSrcLine->isFieldPresent(sfLockedBalance) || !sleSrcLine->isFieldPresent(sfLockCount))
{
JLOG(j.trace())
<< "trustTransferLockedBalance could not find sfLockedBalance/sfLockCount on source line";
return tecUNFUNDED_PAYMENT;
}
// decrement source balance
{
STAmount priorBalance =
srcHigh ? -((*sleSrcLine)[sfBalance]) : (*sleSrcLine)[sfBalance];
STAmount priorLockedBalance =
srcHigh ? -((*sleSrcLine)[sfLockedBalance]) : (*sleSrcLine)[sfLockedBalance];
uint32_t priorLockCount = (*sleSrcLine)[sfLockCount];
AccountID srcIssuerAccID =
sleSrcLine->getFieldAmount(srcHigh ? sfLowLimit : sfHighLimit).getIssuer();
// check they have sufficient funds
if (amount > priorLockedBalance)
{
JLOG(j.trace())
<< "trustTransferLockedBalance amount > lockedBalance: "
<< "amount=" << amount << " lockedBalance="
<< priorLockedBalance;
return tecUNFUNDED_PAYMENT;
}
STAmount finalBalance = priorBalance - amount;
STAmount finalLockedBalance = priorLockedBalance - amount;
uint32_t finalLockCount = priorLockCount + deltaLockCount;
// check if there is significant precision loss
if (!isAddable(priorBalance, amount) ||
!isAddable(priorLockedBalance, amount))
return tecPRECISION_LOSS;
// sanity check possible overflows on the lock counter
if ((deltaLockCount > 0 && priorLockCount > finalLockCount) ||
(deltaLockCount < 0 && priorLockCount < finalLockCount) ||
(deltaLockCount == 0 && priorLockCount != finalLockCount))
return tecINTERNAL;
// this should never happen but defensively check it here before updating sle
if (finalBalance < beast::zero || finalLockedBalance < beast::zero)
{
JLOG(j.warn())
<< "trustTransferLockedBalance results in a negative balance on source line";
return tecINTERNAL;
}
if constexpr(!dryRun)
{
sleSrcLine->setFieldAmount(sfBalance, srcHigh ? -finalBalance : finalBalance);
if (finalLockedBalance == beast::zero || finalLockCount == 0)
{
sleSrcLine->makeFieldAbsent(sfLockedBalance);
sleSrcLine->makeFieldAbsent(sfLockCount);
}
else
{
sleSrcLine->setFieldAmount(sfLockedBalance, srcHigh ? -finalLockedBalance : finalLockedBalance);
sleSrcLine->setFieldU32(sfLockCount, finalLockCount);
}
}
}
// dstLow XNOR srcLow tells us if we need to flip the balance amount
// on the destination line
bool flipDstAmt = !((dstHigh && srcHigh) || (!dstHigh && !srcHigh));
// compute transfer fee, if any
auto xferRate = transferRate(view, issuerAccID);
// the destination will sometimes get less depending on xfer rate
// with any difference in tokens burned
auto dstAmt =
xferRate == parityRate
? amount
: multiplyRound(amount, xferRate, amount.issue(), true);
// check for a destination line
Keylet klDstLine = keylet::line(dstAccID, issuerAccID, currency);
SLEPtr sleDstLine = peek(klDstLine);
if (!sleDstLine)
{
// in most circumstances a missing destination line is a deal breaker
if (actingAccID != dstAccID && srcAccID != dstAccID)
return tecNO_PERMISSION;
STAmount dstBalanceDrops = sleDstAcc->getFieldAmount(sfBalance);
// no dst line exists, we might be able to create one...
if (std::uint32_t const ownerCount = {sleDstAcc->at(sfOwnerCount)};
dstBalanceDrops < view.fees().accountReserve(ownerCount + 1))
return tecNO_LINE_INSUF_RESERVE;
// yes we can... we will
if constexpr(!dryRun)
{
// clang-format off
if (TER const ter = trustCreate(
view,
!dstHigh, // is dest low?
issuerAccID, // source
dstAccID, // destination
klDstLine.key, // ledger index
sleDstAcc, // Account to add to
false, // authorize account
(sleDstAcc->getFlags() & lsfDefaultRipple) == 0,
false, // freeze trust line
flipDstAmt ? -dstAmt : dstAmt, // initial balance
Issue(currency, dstAccID), // limit of zero
0, // quality in
0, // quality out
j); // journal
!isTesSuccess(ter))
{
return ter;
}
}
// clang-format on
}
else
{
// the dst line does exist, and it would have been checked above
// in trustTransferAllowed for NoRipple and Freeze flags
// check the limit
STAmount dstLimit =
dstHigh ? (*sleDstLine)[sfHighLimit] : (*sleDstLine)[sfLowLimit];
STAmount priorBalance =
dstHigh ? -((*sleDstLine)[sfBalance]) : (*sleDstLine)[sfBalance];
STAmount finalBalance = priorBalance + (flipDstAmt ? -dstAmt : dstAmt);
if (finalBalance < priorBalance)
{
JLOG(j.warn())
<< "trustTransferLockedBalance resulted in a lower final balance on dest line";
return tecINTERNAL;
}
if (finalBalance > dstLimit && actingAccID != dstAccID)
{
JLOG(j.trace())
<< "trustTransferLockedBalance would increase dest line above limit without permission";
return tecPATH_DRY;
}
// check if there is significant precision loss
if (!isAddable(priorBalance, dstAmt))
return tecPRECISION_LOSS;
if constexpr(!dryRun)
sleDstLine->setFieldAmount(sfBalance, dstHigh ? -finalBalance : finalBalance);
}
if constexpr (!dryRun)
{
static_assert(std::is_same<V, ApplyView>::value);
// check if source line ended up in default state and adjust owner count if it did
if (isTrustDefault(sleSrcAcc, sleSrcLine))
{
uint32_t flags = sleSrcLine->getFieldU32(sfFlags);
uint32_t fReserve { srcHigh ? lsfHighReserve : lsfLowReserve };
if (flags & fReserve)
{
sleSrcLine->setFieldU32(sfFlags, flags & ~fReserve);
adjustOwnerCount(view, sleSrcAcc, -1, j);
view.update(sleSrcAcc);
}
}
view.update(sleSrcLine);
// a destination line already existed and was updated
if (sleDstLine)
view.update(sleDstLine);
}
return tesSUCCESS;
}
} // namespace ripple
#endif

View File

@@ -254,6 +254,29 @@ accountHolds(
// Put balance in account terms.
amount.negate();
}
// If tokens can be escrowed then they can be locked in the trustline
// which means we must never spend them until the escrow is released.
if (view.rules().enabled(featurePaychanAndEscrowForTokens)
&& sle->isFieldPresent(sfLockedBalance))
{
STAmount lockedBalance = sle->getFieldAmount(sfLockedBalance);
STAmount spendableBalance = amount -
(account > issuer ? -lockedBalance : lockedBalance);
// RH NOTE: this is defensively programmed, it should never fire
// if something bad does happen the trustline acts as a frozen line.
if (spendableBalance < beast::zero || spendableBalance > amount)
{
JLOG(j.error())
<< "SpendableBalance has illegal value in accountHolds "
<< spendableBalance;
amount.clear(Issue{currency, issuer});
}
else
amount = spendableBalance;
}
amount.setIssuer(issuer);
}
JLOG(j.trace()) << "accountHolds:"
@@ -889,6 +912,55 @@ trustDelete(
return tesSUCCESS;
}
bool isTrustDefault(
std::shared_ptr<SLE> const& acc,
std::shared_ptr<SLE> const& line)
{
assert(acc && line);
uint32_t tlFlags = line->getFieldU32(sfFlags);
AccountID highAccID = line->getFieldAmount(sfHighLimit).issue().account;
AccountID lowAccID = line->getFieldAmount(sfLowLimit ).issue().account;
AccountID accID = acc->getAccountID(sfAccount);
assert(accID == highAccID || accID == lowAccID);
bool high = accID == highAccID;
uint32_t acFlags = line->getFieldU32(sfFlags);
const auto fNoRipple {high ? lsfHighNoRipple : lsfLowNoRipple};
const auto fFreeze {high ? lsfHighFreeze : lsfLowFreeze};
if (tlFlags & fFreeze)
return false;
if ((acFlags & lsfDefaultRipple) && (tlFlags & fNoRipple))
return false;
if (line->getFieldAmount(sfBalance) != beast::zero)
return false;
if (line->isFieldPresent(sfLockedBalance))
return false;
if (line->getFieldAmount(high ? sfHighLimit : sfLowLimit) != beast::zero)
return false;
uint32_t qualityIn = line->getFieldU32(high ? sfHighQualityIn : sfLowQualityIn);
uint32_t qualityOut = line->getFieldU32(high ? sfHighQualityOut : sfLowQualityOut);
if (qualityIn && qualityIn != QUALITY_ONE)
return false;
if (qualityOut && qualityOut != QUALITY_ONE)
return false;
return true;
}
TER
offerDelete(ApplyView& view, std::shared_ptr<SLE> const& sle, beast::Journal j)
{

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 49;
static constexpr std::size_t numFeatures = 50;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
@@ -337,6 +337,7 @@ extern uint256 const featureHooks;
extern uint256 const featureExpandedSignerList;
extern uint256 const featureNonFungibleTokensV1;
extern uint256 const fixNFTokenDirV1;
extern uint256 const featurePaychanAndEscrowForTokens;
} // namespace ripple

View File

@@ -38,6 +38,31 @@ serializePayChanAuthorization(
msg.add64(amt.drops());
}
inline void
serializePayChanAuthorization(
Serializer& msg,
uint256 const& key,
IOUAmount const& amt,
Currency const& cur,
AccountID const& iss)
{
msg.add32(HashPrefix::paymentChannelClaim);
msg.addBitString(key);
if (amt == beast::zero)
msg.add64(STAmount::cNotNative);
else if (amt.signum() == -1) // 512 = not native
msg.add64(
amt.mantissa() |
(static_cast<std::uint64_t>(amt.exponent() + 512 + 97) << (64 - 10)));
else // 256 = positive
msg.add64(
amt.mantissa() |
(static_cast<std::uint64_t>(amt.exponent() + 512 + 256 + 97)
<< (64 - 10)));
msg.addBitString(cur);
msg.addBitString(iss);
}
} // namespace ripple
#endif

View File

@@ -400,6 +400,7 @@ extern SF_UINT32 const sfMintedNFTokens;
extern SF_UINT32 const sfBurnedNFTokens;
extern SF_UINT32 const sfHookStateCount;
extern SF_UINT32 const sfEmitGeneration;
extern SF_UINT32 const sfLockCount;
// 64-bit integers (common)
extern SF_UINT64 const sfIndexNext;
@@ -479,6 +480,7 @@ extern SF_AMOUNT const sfHighLimit;
extern SF_AMOUNT const sfFee;
extern SF_AMOUNT const sfSendMax;
extern SF_AMOUNT const sfDeliverMin;
extern SF_AMOUNT const sfLockedBalance;
// currency amount (uncommon)
extern SF_AMOUNT const sfMinimumOffer;

View File

@@ -517,6 +517,56 @@ isXRP(STAmount const& amount)
return isXRP(amount.issue().currency);
}
inline bool
isFakeXRP(STAmount const& amount)
{
if (amount.native())
return false;
return isFakeXRP(amount.issue().currency);
}
/** returns true iff adding or subtracting results in less than or equal to 0.01% precision loss **/
inline bool
isAddable(STAmount const& amt1, STAmount const& amt2)
{
// special case: adding anything to zero is always fine
if (amt1 == beast::zero || amt2 == beast::zero)
return true;
// special case: adding two xrp amounts together.
// this is just an overflow check
if (isXRP(amt1) && isXRP(amt2))
{
XRPAmount A = (amt1.signum() == -1 ? -(amt1.xrp()) : amt1.xrp());
XRPAmount B = (amt2.signum() == -1 ? -(amt2.xrp()) : amt2.xrp());
XRPAmount finalAmt = A + B;
return (finalAmt >= A && finalAmt >= B);
}
static const STAmount one {IOUAmount{1, 0}, noIssue()};
static const STAmount maxLoss {IOUAmount{1, -4}, noIssue()};
STAmount A = amt1;
STAmount B = amt2;
if (isXRP(A))
A = STAmount{IOUAmount{A.xrp().drops(), -6}, noIssue()};
if (isXRP(B))
B = STAmount{IOUAmount{B.xrp().drops(), -6}, noIssue()};
A.setIssue(noIssue());
B.setIssue(noIssue());
STAmount lhs = divide((A - B) + B, A, noIssue()) - one;
STAmount rhs = divide((B - A) + A, B, noIssue()) - one;
return ((rhs.negative() ? -rhs : rhs) + (lhs.negative() ? -lhs : lhs)) <= maxLoss;
}
// Since `canonicalize` does not have access to a ledger, this is needed to put
// the low-level routine stAmountCanonicalize on an amendment switch. Only
// transactions need to use this switchover. Outside of a transaction it's safe

View File

@@ -293,6 +293,7 @@ enum TECcodes : TERUnderlyingType {
tecOBJECT_NOT_FOUND = 160,
tecINSUFFICIENT_PAYMENT = 161,
tecREQUIRES_FLAG = 162,
tecPRECISION_LOSS = 163,
};
//------------------------------------------------------------------------------

View File

@@ -77,6 +77,12 @@ isXRP(Currency const& c)
return c == beast::zero;
}
inline bool
isFakeXRP(Currency const& c)
{
return c == badCurrency();
}
/** Returns "", "XRP", or three letter ISO code. */
std::string
to_string(Currency const& c);

View File

@@ -441,6 +441,7 @@ REGISTER_FEATURE(Hooks, Supported::yes, DefaultVote::no)
REGISTER_FEATURE(NonFungibleTokensV1, Supported::yes, DefaultVote::no);
REGISTER_FEATURE(ExpandedSignerList, Supported::yes, DefaultVote::no);
REGISTER_FIX (fixNFTokenDirV1, Supported::yes, DefaultVote::no);
REGISTER_FEATURE(PaychanAndEscrowForTokens, Supported::yes, DefaultVote::no);
// The following amendments have been active for at least two years. Their
// pre-amendment code has been removed and the identifiers are deprecated.

View File

@@ -56,7 +56,7 @@ enum class LedgerNameSpace : std::uint16_t {
FEE_SETTINGS = 'e',
TICKET = 'T',
SIGNER_LIST = 'S',
XRP_PAYMENT_CHANNEL = 'x',
PAYMENT_CHANNEL = 'x',
CHECK = 'C',
DEPOSIT_PREAUTH = 'p',
NEGATIVE_UNL = 'N',
@@ -369,7 +369,7 @@ payChan(AccountID const& src, AccountID const& dst, UInt32or256 const& seq) noex
{
return {
ltPAYCHAN,
indexHash(LedgerNameSpace::XRP_PAYMENT_CHANNEL, src, dst, seq)};
indexHash(LedgerNameSpace::PAYMENT_CHANNEL, src, dst, seq)};
}
Keylet

View File

@@ -108,6 +108,8 @@ LedgerFormats::LedgerFormats()
{sfHighNode, soeOPTIONAL},
{sfHighQualityIn, soeOPTIONAL},
{sfHighQualityOut, soeOPTIONAL},
{sfLockedBalance, soeOPTIONAL},
{sfLockCount, soeOPTIONAL},
},
commonFields);

View File

@@ -150,6 +150,7 @@ CONSTRUCT_TYPED_SFIELD(sfMintedNFTokens, "MintedNFTokens", UINT32,
CONSTRUCT_TYPED_SFIELD(sfBurnedNFTokens, "BurnedNFTokens", UINT32, 44);
CONSTRUCT_TYPED_SFIELD(sfHookStateCount, "HookStateCount", UINT32, 45);
CONSTRUCT_TYPED_SFIELD(sfEmitGeneration, "EmitGeneration", UINT32, 46);
CONSTRUCT_TYPED_SFIELD(sfLockCount, "LockCount", UINT32, 47);
// 64-bit integers (common)
CONSTRUCT_TYPED_SFIELD(sfIndexNext, "IndexNext", UINT64, 1);
@@ -236,6 +237,7 @@ CONSTRUCT_TYPED_SFIELD(sfRippleEscrow, "RippleEscrow", AMOUNT,
CONSTRUCT_TYPED_SFIELD(sfDeliveredAmount, "DeliveredAmount", AMOUNT, 18);
CONSTRUCT_TYPED_SFIELD(sfNFTokenBrokerFee, "NFTokenBrokerFee", AMOUNT, 19);
CONSTRUCT_TYPED_SFIELD(sfHookCallbackFee, "HookCallbackFee", AMOUNT, 20);
CONSTRUCT_TYPED_SFIELD(sfLockedBalance, "LockedBalance", AMOUNT, 21);
// variable length (common)
CONSTRUCT_TYPED_SFIELD(sfPublicKey, "PublicKey", VL, 1);

View File

@@ -89,7 +89,7 @@ transResults()
MAKE_ERROR(tecOBJECT_NOT_FOUND, "A requested object could not be located."),
MAKE_ERROR(tecINSUFFICIENT_PAYMENT, "The payment is not sufficient."),
MAKE_ERROR(tecHOOK_REJECTED, "Rejected by hook on sending or receiving account."),
MAKE_ERROR(tecPRECISION_LOSS, "The IOU amounts used by the transaction cannot interact."),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
MAKE_ERROR(tefBAD_AUTH, "Transaction's public key is not authorized."),