diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 180bf64a8..372bbdc52 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -2040,32 +2040,167 @@ Transactor::operator()() result = tecOVERSIZE; } - if (applied) - { - // Transaction succeeded fully or (retries are not allowed and the - // transaction could claim a fee) + if (view().rules().enabled(featureServiceFee) && applied && + ctx_.tx.isFieldPresent(sfServiceFee)) + do + { + // Service fee is processed on a best-effort basis without affecting + // tx application. The reason is that the client completely controls + // the service fee that it submits with the user's txn, and + // therefore is already completely aware of the user's capacity to + // pay the fee and therefore enforcement logic is unnecessary + // chain-side. - // The transactor and invariant checkers guarantee that this will - // *never* trigger but if it, somehow, happens, don't allow a tx - // that charges a negative fee. - if (fee < beast::zero) - Throw("fee charged is negative!"); + STObject const& obj = const_cast(ctx_.tx) + .getField(sfServiceFee) + .downcast(); - // Charge whatever fee they specified. The fee has already been - // deducted from the balance of the account that issued the - // transaction. We just need to account for it in the ledger - // header. - if (!view().open() && fee != beast::zero) - ctx_.destroyXRP(fee); + // This should be enforced by template but doesn't hurt to + // defensively check it here. + if (!obj.isFieldPresent(sfDestination) || + !obj.isFieldPresent(sfAmount) || obj.getCount() != 2) + { + JLOG(j_.warn()) + << "service fee not applied - malformed inner object."; + break; + } - // Once we call apply, we will no longer be able to look at view() - ctx_.apply(result); - } + auto const src = ctx_.tx.getAccountID(sfAccount); + auto const dst = obj.getAccountID(sfDestination); - JLOG(j_.trace()) << (applied ? "applied" : "not applied") - << transToken(result); + auto const amt = obj.getFieldAmount(sfAmount); - return {result, applied}; -} + // sanity check fields + if (src == dst) + { + JLOG(j_.trace()) + << "skipping self service-fee on " << src << "."; + break; + } + + if (amt <= beast::zero) + { + JLOG(j_.trace()) + << "skipping non-positive service-fee from " << src << "."; + break; + } + + // check if the source exists + auto const& sleSrc = view().read(keylet::account(src)); + if (!sleSrc) + { + // this can happen if the account was just deleted + JLOG(j_.trace()) << "service fee not applied because source " + << src << " does not exist."; + break; + } + + // check if the destination exists + // service fee cannot be used to create accounts. + if (!view().exists(keylet::account(dst))) + { + JLOG(j_.trace()) + << "service fee not applied because destination " << dst + << " does not exist."; + break; + } + + if (isXRP(amt)) + { + // check if there's enough left in the sender's account + auto srcBal = sleSrc.getFieldAmount(sfBalance); + + // service fee will only be delivered if the account + // contains adequate balance to cover reserves, otherwise + // it is disregarded + auto after = srcBal - amt; + if (after < view().fees().accountReserve( + sleSrc->getFieldU32(sfOwnerCount))) + { + JLOG(j_.trace()) + << "service fee not applied because source " << src + << " cannot pay it (native)."; + break; + } + + // action the transfer + if (TER const ter{ + view().transferXRP(view(), src, dst, amt, j_)) + !isTesSuccess(ter)) + { + JLOG(j_.warn()) + << "service fee error transferring " << amt << " from " + << src << " to " << dst << " error: " << ter << "."; + } + break; + } + + // execution to here means issued currency service fee + + // service fee cannot be used to create trustlines, + // so a line must already exist and the currency must + // be able to be xfer'd to it + + auto const& sleLine = view().peek(keylet::line(dst, amt.getIssuer(), amt.getCurrency())); + + if (!sleLine && amt.getIssuer() != dst) + { + JLOG(j_.trace()) + << "service fee not applied because destination " << dst + << " has no trustline for currency: " + << amt.getCurrency() + << " issued by: " << amt.getIssuer() << "."; + break; + } + + // action the transfer + { + PaymentSandbox pv(&view()); + auto res = accountSend(pv, src, dst, amt, j_); + + if (res == tesSUCCESS) + { + pv.apply(ctx_.rawView()); + break; + } + + JLOG(j_.trace()) + << "service fee not sent from " << src << " to " << dst + << " for " << amt.getCurrency() " issued by " + << amt.getIssuer() " because " + << "accountSend() failed with code " << res << "."; + } + } + while (0) + ; + + if (applied) + { + // Transaction succeeded fully or (retries are not allowed and + // the transaction could claim a fee) + + // The transactor and invariant checkers guarantee that this + // will *never* trigger but if it, somehow, happens, don't allow + // a tx that charges a negative fee. + if (fee < beast::zero) + Throw("fee charged is negative!"); + + // Charge whatever fee they specified. The fee has already been + // deducted from the balance of the account that issued the + // transaction. We just need to account for it in the ledger + // header. + if (!view().open() && fee != beast::zero) + ctx_.destroyXRP(fee); + + // Once we call apply, we will no longer be able to look at + // view() + ctx_.apply(result); + } + + JLOG(j_.trace()) + << (applied ? "applied" : "not applied") << transToken(result); + + return {result, applied}; + } } // namespace ripple diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index f479ecba7..2941ce9d5 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 = 76; +static constexpr std::size_t numFeatures = 77; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -364,6 +364,7 @@ extern uint256 const fix240911; extern uint256 const fixFloatDivide; extern uint256 const fixReduceImport; extern uint256 const fixXahauV3; +extern uint256 const featureServiceFee; } // namespace ripple diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index 1f9d15368..a1a506795 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -608,6 +608,7 @@ extern SField const sfMemos; extern SField const sfNFTokens; extern SField const sfHooks; extern SField const sfGenesisMint; +extern SField const sfServiceFee; // array of objects (uncommon) extern SField const sfMajorities; diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 12c7b66c8..45eceb787 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -470,6 +470,7 @@ REGISTER_FIX (fix240911, Supported::yes, VoteBehavior::De REGISTER_FIX (fixFloatDivide, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixReduceImport, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixXahauV3, Supported::yes, VoteBehavior::DefaultNo); +REGISTER_FEATURE(ServiceFee, Supported::yes, VoteBehavior::DefaultNo); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/ripple/protocol/impl/InnerObjectFormats.cpp b/src/ripple/protocol/impl/InnerObjectFormats.cpp index e52d6ff8f..0b48ad979 100644 --- a/src/ripple/protocol/impl/InnerObjectFormats.cpp +++ b/src/ripple/protocol/impl/InnerObjectFormats.cpp @@ -157,6 +157,13 @@ InnerObjectFormats::InnerObjectFormats() {sfDigest, soeOPTIONAL}, {sfFlags, soeOPTIONAL}, }); + + add(sfServiceFee.jsonName.c_str(), + sfServiceFee.getCode(), + { + {sfAmount, soeREQUIRED}, + {sfDestination, soeREQUIRED}, + }); } InnerObjectFormats const& diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index a72208607..f72d7127c 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -350,6 +350,7 @@ CONSTRUCT_UNTYPED_SFIELD(sfImportVLKey, "ImportVLKey", OBJECT, CONSTRUCT_UNTYPED_SFIELD(sfHookEmission, "HookEmission", OBJECT, 93); CONSTRUCT_UNTYPED_SFIELD(sfMintURIToken, "MintURIToken", OBJECT, 92); CONSTRUCT_UNTYPED_SFIELD(sfAmountEntry, "AmountEntry", OBJECT, 91); +CONSTRUCT_UNTYPED_SFIELD(sfServiceFee, "ServiceFee", OBJECT, 90); // array of objects // ARRAY/1 is reserved for end of array diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 6c38711ad..1b4b360b1 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -44,6 +44,7 @@ TxFormats::TxFormats() {sfNetworkID, soeOPTIONAL}, {sfHookParameters, soeOPTIONAL}, {sfOperationLimit, soeOPTIONAL}, + {sfServiceFee, soeOPTIONAL}, }; add(jss::AccountSet,