diff --git a/src/ripple/app/tx/impl/Escrow.cpp b/src/ripple/app/tx/impl/Escrow.cpp index 609b8f6a4..56eeb1c7b 100644 --- a/src/ripple/app/tx/impl/Escrow.cpp +++ b/src/ripple/app/tx/impl/Escrow.cpp @@ -33,6 +33,8 @@ #include #include #include +#include + // 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 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 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); diff --git a/src/ripple/app/tx/impl/InvariantCheck.cpp b/src/ripple/app/tx/impl/InvariantCheck.cpp index 554dfdb0d..f3a5a438d 100644 --- a/src/ripple/app/tx/impl/InvariantCheck.cpp +++ b/src/ripple/app/tx/impl/InvariantCheck.cpp @@ -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"; diff --git a/src/ripple/app/tx/impl/PayChan.cpp b/src/ripple/app/tx/impl/PayChan.cpp index e9306ac9e..846cc706d 100644 --- a/src/ripple/app/tx/impl/PayChan.cpp +++ b/src/ripple/app/tx/impl/PayChan.cpp @@ -120,6 +120,36 @@ closeChannel( beast::Journal j) { AccountID const src = (*slep)[sfAccount]; + auto const amount = (*slep)[sfAmount] - (*slep)[sfBalance]; + + std::shared_ptr 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 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); } diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index ee9171155..2b97b2d61 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -33,11 +33,13 @@ #include #include #include +#include +#include #include #include #include #include - +#include #include 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() +#define WetRun RunType() +template +struct RunType +{ + //see: + //http://alumni.media.mit.edu/~rahimi/compile-time-flags/ + constexpr operator T() const + { + static_assert(std::is_same::value); + return V; + } + + constexpr T operator!() const + { + static_assert(std::is_same::value); + return !(V); + } +}; + +// allow party lists to be logged easily +template +std::ostream& operator<< (std::ostream& lhs, std::vector 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 const& acc, // side to check + std::shared_ptr const& line); // line to check + +/** Lock or unlock a TrustLine balance. + If positive deltaAmt lock the amount. + If negative deltaAmt unlock the amount. +*/ +template +[[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::value && + std::is_same>::value) || + (std::is_same::value && + std::is_same>::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::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 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::value && std::is_same>::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 +[[nodiscard]]TER +trustTransferAllowed( + V& view, + std::vector const& parties, + Issue const& issue, + beast::Journal const& j) +{ + static_assert( + std::is_same::value || + std::is_same::value); + + typedef typename std::conditional< + std::is_same::value, + std::shared_ptr, + std::shared_ptr>::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 +[[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::value && !dryRun, + std::shared_ptr, + std::shared_ptr>::type SLEPtr; + + auto peek = [&](Keylet& k) + { + if constexpr (std::is_same::value && !dryRun) + return const_cast(view).peek(k); + else + return view.read(k); + }; + + static_assert(std::is_same::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::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 diff --git a/src/ripple/ledger/impl/View.cpp b/src/ripple/ledger/impl/View.cpp index 54e78ecc9..8923b451d 100644 --- a/src/ripple/ledger/impl/View.cpp +++ b/src/ripple/ledger/impl/View.cpp @@ -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 const& acc, + std::shared_ptr 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 const& sle, beast::Journal j) { diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 35384d13b..e930c0f0c 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -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 diff --git a/src/ripple/protocol/PayChan.h b/src/ripple/protocol/PayChan.h index 216835de2..beae88883 100644 --- a/src/ripple/protocol/PayChan.h +++ b/src/ripple/protocol/PayChan.h @@ -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(amt.exponent() + 512 + 97) << (64 - 10))); + else // 256 = positive + msg.add64( + amt.mantissa() | + (static_cast(amt.exponent() + 512 + 256 + 97) + << (64 - 10))); + msg.addBitString(cur); + msg.addBitString(iss); +} + } // namespace ripple #endif diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index d02bb2ecf..21825d525 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -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; diff --git a/src/ripple/protocol/STAmount.h b/src/ripple/protocol/STAmount.h index d0add30db..82dd45604 100644 --- a/src/ripple/protocol/STAmount.h +++ b/src/ripple/protocol/STAmount.h @@ -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 diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 8bc3fbdac..164c7ecce 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -293,6 +293,7 @@ enum TECcodes : TERUnderlyingType { tecOBJECT_NOT_FOUND = 160, tecINSUFFICIENT_PAYMENT = 161, tecREQUIRES_FLAG = 162, + tecPRECISION_LOSS = 163, }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/UintTypes.h b/src/ripple/protocol/UintTypes.h index 6fb685551..895da7d9e 100644 --- a/src/ripple/protocol/UintTypes.h +++ b/src/ripple/protocol/UintTypes.h @@ -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); diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index c766152f8..fc59512c1 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -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. diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index 5a4545a0c..9695122d6 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -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 diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index b2a7e31bd..d549850ee 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -108,6 +108,8 @@ LedgerFormats::LedgerFormats() {sfHighNode, soeOPTIONAL}, {sfHighQualityIn, soeOPTIONAL}, {sfHighQualityOut, soeOPTIONAL}, + {sfLockedBalance, soeOPTIONAL}, + {sfLockCount, soeOPTIONAL}, }, commonFields); diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 0430f4eca..866fc4663 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -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); diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index 76ce209ec..ed8915571 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -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."),