Compare commits

...

14 Commits

Author SHA1 Message Date
Denis Angell
0b58c3a644 [fold] clang-format 2025-01-28 11:16:44 +01:00
Denis Angell
0c4f5680e6 check tem failures to preflight 2025-01-28 11:12:02 +01:00
Richard Holland
f0222d9fa6 ensure inclusion of ServiceFee field continues to return temMALFORMED until amendment is enabled 2025-01-28 14:04:40 +11:00
Richard Holland
baffb7c2ae move service fee block above invariant checks, add trustTransferAllowed logic, change trace to info for servicefee logging 2025-01-28 13:56:28 +11:00
Denis Angell
891a9c0b81 [fold] clang-format 2025-01-27 14:04:30 +01:00
Denis Angell
efa5800384 add tests 2025-01-27 14:02:57 +01:00
Denis Angell
96b78b1c8a add iou checks
- RequireAuth
- Freeze
- Trustline Balance / Limit
2025-01-27 14:02:51 +01:00
Denis Angell
f19e03842b add requireAuth helper 2025-01-27 14:02:15 +01:00
Richard Holland
e57160913e change both types of xfer to use accountSend 2025-01-27 09:26:55 +11:00
Denis Angell
515c4c14bd [fold] clang-format 2025-01-26 15:15:54 +01:00
Denis Angell
2b8efc398d add test framework 2025-01-26 15:12:22 +01:00
Denis Angell
01bd0cc4ac [fold] clang-format 2025-01-26 15:10:50 +01:00
Richard Holland
6fe06d857c compiling, not tested 2025-01-26 22:39:44 +11:00
Richard Holland
51764e1b4e initial version of feature service fee, uncompiled untested 2025-01-26 21:42:19 +11:00
14 changed files with 929 additions and 7 deletions

View File

@@ -749,6 +749,7 @@ if (tests)
src/test/app/Regression_test.cpp
src/test/app/Remit_test.cpp
src/test/app/SHAMapStore_test.cpp
src/test/app/ServiceFee_test.cpp
src/test/app/SetAuth_test.cpp
src/test/app/SetRegularKey_test.cpp
src/test/app/SetTrust_test.cpp

View File

@@ -88,6 +88,69 @@ preflight0(PreflightContext const& ctx)
return tesSUCCESS;
}
NotTEC
checkServiceFee(PreflightContext const& ctx)
{
if (!ctx.tx.isFieldPresent(sfServiceFee))
return tesSUCCESS;
if (!ctx.rules.enabled(featureServiceFee))
{
JLOG(ctx.j.debug())
<< "ServiceFee: Service fee feature is not enabled.";
return temDISABLED;
}
STObject const& obj = const_cast<ripple::STTx&>(ctx.tx)
.getField(sfServiceFee)
.downcast<STObject>();
// 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(ctx.j.debug()) << "ServiceFee: Malformed: Destination and Amount "
"fields are required.";
return temINVALID;
}
if (ctx.tx.getAccountID(sfAccount) == obj.getAccountID(sfDestination))
{
JLOG(ctx.j.debug()) << "ServiceFee: Malformed: Destination may not be "
"the same as the source account.";
return temDST_IS_SRC;
}
auto const amount = obj.getFieldAmount(sfAmount);
if (!isXRP(amount))
{
if (!isLegalNet(amount))
{
JLOG(ctx.j.debug())
<< "ServiceFee: Malformed: Amount must be a valid net amount.";
return temBAD_AMOUNT;
}
if (isBadCurrency(amount))
{
JLOG(ctx.j.debug())
<< "ServiceFee: Malformed: Currency is not allowed.";
return temBAD_CURRENCY;
}
}
if (amount <= beast::zero)
{
JLOG(ctx.j.debug())
<< "ServiceFee: Malformed: Amount must be a positive value.";
return temBAD_AMOUNT;
}
return tesSUCCESS;
}
/** Performs early sanity checks on the account and fee fields */
NotTEC
preflight1(PreflightContext const& ctx)
@@ -165,6 +228,9 @@ preflight1(PreflightContext const& ctx)
ctx.tx.isFieldPresent(sfAccountTxnID))
return temINVALID;
if (auto const ret = checkServiceFee(ctx); !isTesSuccess(ret))
return ret;
return tesSUCCESS;
}
@@ -1894,6 +1960,178 @@ Transactor::operator()()
applied = isTecClaim(result);
}
if (applied && view().rules().enabled(featureServiceFee) &&
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.
STObject const& obj = const_cast<ripple::STTx&>(ctx_.tx)
.getField(sfServiceFee)
.downcast<STObject>();
auto const src = ctx_.tx.getAccountID(sfAccount);
auto const dst = obj.getAccountID(sfDestination);
auto const amt = obj.getFieldAmount(sfAmount);
// 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_.debug()) << "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_.debug())
<< "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 - fee;
if (after < view().fees().accountReserve(
sleSrc->getFieldU32(sfOwnerCount)))
{
JLOG(j_.debug())
<< "service fee not applied because source " << src
<< " cannot pay it (native).";
break;
}
PaymentSandbox pv(&view());
auto res = accountSend(pv, src, dst, amt, j_, false);
if (isTesSuccess(res))
{
pv.apply(ctx_.rawView());
break;
}
JLOG(j_.warn()) << "service fee (native) not applied because "
"accountSend failed.";
break;
}
// issued currency
// 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 issuer = amt.getIssuer();
if (issuer != src && !view().exists(keylet::line(src, amt.issue())))
{
JLOG(j_.debug())
<< "service fee not applied because source " << src
<< " has no trustline for currency: " << amt.getCurrency()
<< " issued by: " << toBase58(issuer) << ".";
break;
}
if (issuer != dst && !view().exists(keylet::line(dst, amt.issue())))
{
JLOG(j_.debug())
<< "service fee not applied because destination " << dst
<< " has no trustline for currency: " << amt.getCurrency()
<< " issued by: " << toBase58(issuer) << ".";
break;
}
if (dst != issuer && src != issuer)
{
TER const res =
trustTransferAllowed(view(), {src, dst}, amt.issue(), j_);
if (!isTesSuccess(res))
{
JLOG(j_.debug())
<< "service fee not applied because destination " << dst
<< " trust transfer not allowed for currency: "
<< amt.getCurrency()
<< " issued by: " << toBase58(issuer) << ".";
break;
}
}
if (src != issuer)
{
Keylet const srcLine =
keylet::line(src, issuer, amt.getCurrency());
auto const sleSrcLine = view().read(srcLine);
STAmount srcBalance = src < amt.issue().account
? (*sleSrcLine)[sfBalance]
: -(*sleSrcLine)[sfBalance];
// TODO: xferRate
if (srcBalance < amt)
{
JLOG(j_.debug())
<< "service fee not applied because source " << src
<< " has insufficient funds for currency: "
<< amt.getCurrency()
<< " issued by: " << toBase58(issuer) << ".";
break;
}
}
if (dst != issuer)
{
Keylet const dstLine =
keylet::line(dst, issuer, amt.getCurrency());
auto const sleDstLine = view().read(dstLine);
STAmount dstLimit = dst < amt.issue().account
? (*sleDstLine)[sfLowLimit]
: (*sleDstLine)[sfHighLimit];
if (accountFunds(view(), dst, amt, fhZERO_IF_FROZEN, j_) + amt >
dstLimit)
{
JLOG(j_.debug())
<< "service fee not applied because destination " << dst
<< " has insufficient trustline limit for "
"currency: "
<< amt.getCurrency()
<< " issued by: " << toBase58(issuer) << ".";
break;
}
}
// action the transfer
{
PaymentSandbox pv(&view());
STAmount saActual;
auto res = accountSend(pv, src, dst, amt, j_, false);
if (isTesSuccess(res))
{
pv.apply(ctx_.rawView());
break;
}
JLOG(j_.warn())
<< "service fee not sent from " << src << " to " << dst
<< " for " << amt.getCurrency() << " issued by "
<< toBase58(issuer) << " because "
<< "accountSend() failed with code " << res << ".";
break;
}
} while (0);
if (applied)
{
// Check invariants: if `tecINVARIANT_FAILED` is not returned, we can
@@ -2042,12 +2280,12 @@ Transactor::operator()()
if (applied)
{
// Transaction succeeded fully or (retries are not allowed and the
// transaction could claim a fee)
// 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.
// 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<std::logic_error>("fee charged is negative!");
@@ -2058,7 +2296,8 @@ Transactor::operator()()
if (!view().open() && fee != beast::zero)
ctx_.destroyXRP(fee);
// Once we call apply, we will no longer be able to look at view()
// Once we call apply, we will no longer be able to look at
// view()
ctx_.apply(result);
}

View File

@@ -422,6 +422,13 @@ transferXRP(
STAmount const& amount,
beast::Journal j);
/** Check if the account lacks required authorization.
* Return tecNO_AUTH or tecNO_LINE if it does
* and tesSUCCESS otherwise.
*/
[[nodiscard]] TER
requireAuth(ReadView const& view, Issue const& issue, AccountID const& account);
//------------------------------------------------------------------------------
//

View File

@@ -1583,4 +1583,24 @@ transferXRP(
return tesSUCCESS;
}
TER
requireAuth(ReadView const& view, Issue const& issue, AccountID const& account)
{
if (isXRP(issue) || issue.account == account)
return tesSUCCESS;
if (auto const issuerAccount = view.read(keylet::account(issue.account));
issuerAccount && (*issuerAccount)[sfFlags] & lsfRequireAuth)
{
if (auto const trustLine =
view.read(keylet::line(account, issue.account, issue.currency)))
return ((*trustLine)[sfFlags] &
((account > issue.account) ? lsfLowAuth : lsfHighAuth))
? tesSUCCESS
: TER{tecNO_AUTH};
return TER{tecNO_LINE};
}
return tesSUCCESS;
}
} // namespace ripple

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 = 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

View File

@@ -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;

View File

@@ -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.

View File

@@ -157,6 +157,13 @@ InnerObjectFormats::InnerObjectFormats()
{sfDigest, soeOPTIONAL},
{sfFlags, soeOPTIONAL},
});
add(sfServiceFee.jsonName.c_str(),
sfServiceFee.getCode(),
{
{sfAmount, soeREQUIRED},
{sfDestination, soeREQUIRED},
});
}
InnerObjectFormats const&

View File

@@ -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

View File

@@ -44,6 +44,7 @@ TxFormats::TxFormats()
{sfNetworkID, soeOPTIONAL},
{sfHookParameters, soeOPTIONAL},
{sfOperationLimit, soeOPTIONAL},
{sfServiceFee, soeOPTIONAL},
};
add(jss::AccountSet,

View File

@@ -126,6 +126,7 @@ JSS(UNLReport); // transaction type.
JSS(SettleDelay); // in: TransactionSign
JSS(SendMax); // in: TransactionSign
JSS(Sequence); // in/out: TransactionSign; field.
JSS(ServiceFee); // field.
JSS(SetFlag); // field.
JSS(SetRegularKey); // transaction type.
JSS(SetHook); // transaction type.

View File

@@ -0,0 +1,617 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/basics/chrono.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/jss.h>
#include <test/jtx.h>
#include <chrono>
namespace ripple {
namespace test {
struct ServiceFee_test : public beast::unit_test::suite
{
static STAmount
lineBalance(
jtx::Env const& env,
jtx::Account const& account,
jtx::Account const& gw,
jtx::IOU const& iou)
{
auto const sle = env.le(keylet::line(account, gw, iou.currency));
if (sle && sle->isFieldPresent(sfBalance))
return (*sle)[sfBalance];
return STAmount(iou, 0);
}
void
testEnabled(FeatureBitset features)
{
testcase("enabled");
using namespace jtx;
using namespace std::literals::chrono_literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
for (bool const withSFee : {true, false})
{
auto const amend =
withSFee ? features : features - featureServiceFee;
Env env{*this, amend};
auto const feeDrops = env.current()->fees().base;
env.fund(XRP(1000), alice, bob, carol);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const preCarol = env.balance(carol);
auto const result = withSFee ? ter(tesSUCCESS) : ter(temDISABLED);
env(pay(alice, bob, XRP(10)),
fee(feeDrops),
sfee(XRP(1), carol),
result);
env.close();
auto const postAlice =
withSFee ? preAlice - feeDrops - XRP(10) - XRP(1) : preAlice;
auto const postBob = withSFee ? preBob + XRP(10) : preBob;
auto const postCarol = withSFee ? preCarol + XRP(1) : preCarol;
BEAST_EXPECT(env.balance(alice) == postAlice);
BEAST_EXPECT(env.balance(bob) == postBob);
BEAST_EXPECT(env.balance(carol) == postCarol);
}
}
void
testInvalid(FeatureBitset features)
{
testcase("invalid");
using namespace jtx;
using namespace std::literals::chrono_literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account("gw");
auto const USD = gw["USD"];
// malformed inner object. No Amount // TEMPLATE ERROR
// malformed inner object. No Destination // TEMPLATE ERROR
// skipping self service-fee
{
Env env{*this, features};
env.fund(XRP(1000), alice, bob);
env.close();
auto const amt = XRP(10);
auto const sfeeAmt = XRP(1);
env(pay(alice, bob, amt), sfee(sfeeAmt, alice), ter(temDST_IS_SRC));
env.close();
}
// skipping non-positive service-fee
{
Env env{*this, features};
env.fund(XRP(1000), alice, bob, carol);
env.close();
auto const amt = XRP(10);
auto const sfeeAmt = XRP(-1);
env(pay(alice, bob, amt), sfee(sfeeAmt, carol), ter(temBAD_AMOUNT));
env.close();
}
// source does not exist.
{
// TODO
} // destination does not exist.
{
Env env{*this, features};
auto const baseFee = env.current()->fees().base;
env.fund(XRP(1000), alice, bob);
env.close();
auto const preAlice = env.balance(alice);
auto const amt = XRP(10);
auto const sfeeAmt = XRP(1);
env(pay(alice, bob, amt), sfee(sfeeAmt, carol));
env.close();
BEAST_EXPECT(env.balance(alice) == preAlice - amt - baseFee);
}
// insufficient reserve
{
// TODO
} // no trustline (source)
{
Env env{*this, features};
env.fund(XRP(1000), alice, bob, carol, gw);
env.close();
env.trust(USD(100000), carol);
env.close();
env(pay(gw, carol, USD(10000)));
env.close();
auto const preAlice = env.balance(alice, USD);
auto const preCarol = env.balance(carol, USD);
BEAST_EXPECT(preAlice == USD(0));
auto const sfeeAmt = USD(1);
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
env.close();
BEAST_EXPECT(env.balance(alice, USD) == preAlice);
BEAST_EXPECT(env.balance(carol, USD) == preCarol);
}
// insufficient trustline balance (source)
{
Env env{*this, features};
env.fund(XRP(1000), alice, bob, carol, gw);
env.close();
env.trust(USD(10), alice, carol);
env.close();
env(pay(gw, alice, USD(1)));
env(pay(gw, carol, USD(1)));
env.close();
auto const preAlice = env.balance(alice, USD);
auto const preCarol = env.balance(carol, USD);
auto const sfeeAmt = USD(10);
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
env.close();
BEAST_EXPECT(env.balance(alice, USD) == preAlice);
BEAST_EXPECT(env.balance(carol, USD) == preCarol);
}
// no trustline (destination)
{
Env env{*this, features};
env.fund(XRP(1000), alice, bob, carol, gw);
env.close();
env.trust(USD(100000), alice);
env.close();
env(pay(gw, alice, USD(10000)));
env.close();
auto const preAlice = env.balance(alice, USD);
auto const preCarol = env.balance(carol, USD);
auto const sfeeAmt = USD(1);
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
env.close();
BEAST_EXPECT(env.balance(alice, USD) == preAlice);
BEAST_EXPECT(env.balance(carol, USD) == preCarol);
}
// insufficient trustline limit (destination)
{
Env env{*this, features};
env.fund(XRP(1000), alice, bob, carol, gw);
env.close();
env.trust(USD(10), alice);
env.trust(USD(1), carol);
env.close();
env(pay(gw, alice, USD(10)));
env.close();
auto const preAlice = env.balance(alice, USD);
auto const preCarol = env.balance(carol, USD);
auto const sfeeAmt = USD(10);
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
env.close();
BEAST_EXPECT(env.balance(alice, USD) == preAlice);
BEAST_EXPECT(env.balance(carol, USD) == preCarol);
}
// accountSend() failed // INTERNAL ERROR
}
void
testRippleState(FeatureBitset features)
{
testcase("ripple_state");
using namespace test::jtx;
using namespace std::literals;
struct TestAccountData
{
Account src;
Account dst;
Account gw;
bool hasTrustline;
bool negative;
};
std::array<TestAccountData, 8> tests = {{
// src > dst && src > issuer && dst no trustline
{Account("alice2"), Account("bob0"), Account{"gw0"}, false, true},
// src < dst && src < issuer && dst no trustline
{Account("carol0"), Account("dan1"), Account{"gw1"}, false, false},
// dst > src && dst > issuer && dst no trustline
{Account("dan1"), Account("alice2"), Account{"gw0"}, false, true},
// dst < src && dst < issuer && dst no trustline
{Account("bob0"), Account("carol0"), Account{"gw1"}, false, false},
// src > dst && src > issuer && dst has trustline
{Account("alice2"), Account("bob0"), Account{"gw0"}, true, true},
// src < dst && src < issuer && dst has trustline
{Account("carol0"), Account("dan1"), Account{"gw1"}, true, false},
// dst > src && dst > issuer && dst has trustline
{Account("dan1"), Account("alice2"), Account{"gw0"}, true, true},
// dst < src && dst < issuer && dst has trustline
{Account("bob0"), Account("carol0"), Account{"gw1"}, true, false},
}};
for (auto const& t : tests)
{
Env env{*this, features};
auto const carol = Account("carol");
auto const USD = t.gw["USD"];
env.fund(XRP(5000), t.src, t.dst, t.gw, carol);
env.close();
if (t.hasTrustline)
env.trust(USD(100000), t.src, t.dst, carol);
else
env.trust(USD(100000), t.src, t.dst);
env.close();
env(pay(t.gw, t.src, USD(10000)));
env(pay(t.gw, t.dst, USD(10000)));
if (t.hasTrustline)
env(pay(t.gw, carol, USD(10000)));
env.close();
auto const delta = USD(100);
auto const sfeeAmt = USD(1);
auto const preSrc = lineBalance(env, t.src, t.gw, USD);
auto const preDst = lineBalance(env, t.dst, t.gw, USD);
auto const preCarol = lineBalance(env, carol, t.gw, USD);
env(pay(t.src, t.dst, delta), sfee(sfeeAmt, carol));
env.close();
auto const appliedSFee = t.hasTrustline ? sfeeAmt : USD(0);
auto const postSrc = t.negative ? (preSrc + delta + appliedSFee)
: (preSrc - delta - appliedSFee);
auto const postDst =
t.negative ? (preDst - delta) : (preDst + delta);
auto const postCarol = t.negative ? (preCarol - appliedSFee)
: (preCarol + appliedSFee);
BEAST_EXPECT(lineBalance(env, t.src, t.gw, USD) == postSrc);
BEAST_EXPECT(lineBalance(env, t.dst, t.gw, USD) == postDst);
BEAST_EXPECT(lineBalance(env, carol, t.gw, USD) == postCarol);
}
}
void
testGateway(FeatureBitset features)
{
testcase("gateway");
using namespace test::jtx;
using namespace std::literals;
struct TestAccountData
{
Account acct;
Account gw;
bool hasTrustline;
bool negative;
};
std::array<TestAccountData, 4> tests = {{
// acct no trustline
// acct > issuer
{Account("alice2"), Account{"gw0"}, false, true},
// acct < issuer
{Account("carol0"), Account{"gw1"}, false, false},
// acct has trustline
// acct > issuer
{Account("alice2"), Account{"gw0"}, true, true},
// acct < issuer
{Account("carol0"), Account{"gw1"}, true, false},
}};
// test gateway is source
for (auto const& t : tests)
{
Env env{*this, features};
auto const carol = Account("carol");
auto const USD = t.gw["USD"];
env.fund(XRP(5000), t.acct, t.gw, carol);
env.trust(USD(100000), carol);
env(pay(t.gw, carol, USD(10000)));
env.close();
if (t.hasTrustline)
{
env.trust(USD(100000), t.acct);
env(pay(t.gw, t.acct, USD(10000)));
env.close();
}
auto const preAcct = lineBalance(env, t.acct, t.gw, USD);
auto const preCarol = lineBalance(env, carol, t.gw, USD);
auto const delta = USD(100);
auto const sfeeAmt = USD(1);
env(pay(t.gw, carol, delta), sfee(sfeeAmt, t.acct));
env.close();
auto const appliedSFee = t.hasTrustline ? sfeeAmt : USD(0);
// Receiver of service fee
auto const postAcct =
t.negative ? (preAcct - appliedSFee) : (preAcct + appliedSFee);
// Receiver of payment
auto const postCarol =
t.negative ? (preCarol - delta) : (preCarol + delta);
BEAST_EXPECT(lineBalance(env, t.acct, t.gw, USD) == postAcct);
BEAST_EXPECT(lineBalance(env, carol, t.gw, USD) == postCarol);
BEAST_EXPECT(lineBalance(env, t.gw, t.acct, USD) == postAcct);
BEAST_EXPECT(lineBalance(env, t.gw, carol, USD) == postCarol);
}
// test gateway is destination
for (auto const& t : tests)
{
Env env{*this, features};
auto const USD = t.gw["USD"];
env.fund(XRP(5000), t.acct, t.gw);
env.trust(USD(100000), t.acct);
env(pay(t.gw, t.acct, USD(10000)));
env.close();
auto const preAcct = lineBalance(env, t.acct, t.gw, USD);
auto const delta = USD(100);
auto const sfeeAmt = USD(1);
env(pay(t.acct, t.gw, delta), sfee(sfeeAmt, t.gw));
env.close();
// Sender of Payment & Fee
auto const postAcct = t.negative ? (preAcct + delta + sfeeAmt)
: (preAcct - delta - sfeeAmt);
BEAST_EXPECT(lineBalance(env, t.acct, t.gw, USD) == postAcct);
BEAST_EXPECT(lineBalance(env, t.gw, t.acct, USD) == postAcct);
}
}
void
testRequireAuth(FeatureBitset features)
{
testcase("require_auth");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account{"gateway"};
auto const USD = gw["USD"];
auto const aliceUSD = alice["USD"];
auto const bobUSD = bob["USD"];
auto const carolUSD = carol["USD"];
// test asfRequireAuth
{
Env env{*this, features};
env.fund(XRP(1000), alice, bob, carol, gw);
env(fset(gw, asfRequireAuth));
env.close();
env(trust(gw, carolUSD(10000)), txflags(tfSetfAuth));
env(trust(carol, USD(10000)));
env(trust(gw, bobUSD(10000)), txflags(tfSetfAuth));
env(trust(bob, USD(10000)));
env.close();
env(pay(gw, carol, USD(1000)));
env(pay(gw, bob, USD(1000)));
env.close();
{
// Alice does not receive service fee because she is not
// authorized
auto const preAlice = env.balance(alice, USD);
auto const sfeeAmt = USD(1);
env(pay(bob, carol, XRP(100)), sfee(sfeeAmt, alice));
env.close();
BEAST_EXPECT(env.balance(alice, USD) == preAlice);
}
{
env(trust(gw, aliceUSD(10000)), txflags(tfSetfAuth));
env(trust(alice, USD(10000)));
env.close();
env(pay(gw, alice, USD(1000)));
env.close();
// Alice now receives service fee because she is authorized
auto const preAlice = env.balance(alice, USD);
auto const sfeeAmt = USD(1);
env(pay(bob, carol, XRP(100)), sfee(sfeeAmt, alice));
env.close();
BEAST_EXPECT(env.balance(alice, USD) == preAlice + sfeeAmt);
}
}
}
void
testFreeze(FeatureBitset features)
{
testcase("freeze");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account{"gateway"};
auto const USD = gw["USD"];
// test Global Freeze
{
// setup env
Env env{*this, features};
env.fund(XRP(1000), alice, bob, carol, gw);
env.close();
env.trust(USD(100000), alice, bob, carol);
env.close();
env(pay(gw, alice, USD(1000)));
env(pay(gw, bob, USD(1000)));
env(pay(gw, carol, USD(1000)));
env.close();
env(fset(gw, asfGlobalFreeze));
env.close();
{
// carol cannot receive because of global freeze
auto const preCarol = env.balance(carol, USD);
auto const sfeeAmt = USD(1);
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
env.close();
BEAST_EXPECT(env.balance(carol, USD) == preCarol);
}
{
// clear global freeze
env(fclear(gw, asfGlobalFreeze));
env.close();
// carol can receive because global freeze is cleared
auto const preCarol = env.balance(carol, USD);
auto const sfeeAmt = USD(1);
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
env.close();
BEAST_EXPECT(env.balance(carol, USD) == preCarol + sfeeAmt);
}
}
// test Individual Freeze
{
// Env Setup
Env env{*this, features};
env.fund(XRP(1000), alice, bob, carol, gw);
env.close();
env.trust(USD(100000), alice, bob, carol);
env.close();
env(pay(gw, alice, USD(1000)));
env(pay(gw, bob, USD(1000)));
env(pay(gw, carol, USD(1000)));
env.close();
// set freeze on carol trustline
env(trust(gw, USD(10000), carol, tfSetFreeze));
env.close();
{
auto const preCarol = env.balance(carol, USD);
auto const sfeeAmt = USD(1);
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
env.close();
BEAST_EXPECT(env.balance(carol, USD) == preCarol);
}
{
// clear freeze on carol trustline
env(trust(gw, USD(10000), carol, tfClearFreeze));
env.close();
auto const preCarol = env.balance(carol, USD);
auto const sfeeAmt = USD(1);
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
env.close();
BEAST_EXPECT(env.balance(carol, USD) == preCarol + sfeeAmt);
}
}
}
void
testTransferRate(FeatureBitset features)
{
testcase("transfer_rate");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const carol = Account("carol");
auto const gw = Account{"gateway"};
auto const USD = gw["USD"];
// test rate
{
Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol, gw);
env(rate(gw, 1.25));
env.close();
env.trust(USD(100000), alice, carol);
env.close();
env(pay(gw, alice, USD(10000)));
env(pay(gw, carol, USD(10000)));
env.close();
auto const preAlice = env.balance(alice, USD);
auto const preCarol = env.balance(carol, USD);
auto const sfeeAmt = USD(1);
env(pay(alice, bob, XRP(100)), sfee(sfeeAmt, carol));
env.close();
BEAST_EXPECT(env.balance(alice, USD) == preAlice - sfeeAmt);
BEAST_EXPECT(
env.balance(carol, USD) == preCarol + sfeeAmt - USD(0.20));
}
// test issuer doesnt pay own rate
{
Env env{*this, features};
env.fund(XRP(10000), alice, carol, gw);
env(rate(gw, 1.25));
env.close();
env.trust(USD(100000), carol);
env.close();
env(pay(gw, carol, USD(10000)));
env.close();
auto const preGwC = -lineBalance(env, carol, gw, USD);
auto const preCarol = env.balance(carol, USD);
auto const sfeeAmt = USD(1);
env(pay(gw, alice, XRP(100)), sfee(sfeeAmt, carol));
env.close();
BEAST_EXPECT(-lineBalance(env, carol, gw, USD) == preGwC + sfeeAmt);
BEAST_EXPECT(env.balance(carol, USD) == preCarol + sfeeAmt);
}
}
void
testWithFeats(FeatureBitset features)
{
testEnabled(features);
testInvalid(features);
testRippleState(features);
testGateway(features);
testRequireAuth(features);
testFreeze(features);
testTransferRate(features);
}
public:
void
run() override
{
using namespace test::jtx;
auto const sa = supported_amendments();
testWithFeats(sa);
}
};
BEAST_DEFINE_TESTSUITE(ServiceFee, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -61,6 +61,23 @@ public:
operator()(Env&, JTx& jt) const;
};
/** Set the service fee on a JTx. */
class sfee
{
private:
STAmount amount_;
Account dest_;
public:
explicit sfee(STAmount const& amount, Account const& destination)
: amount_(amount), dest_(destination)
{
}
void
operator()(Env&, JTx& jtx) const;
};
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -34,6 +34,14 @@ fee::operator()(Env&, JTx& jt) const
jt[jss::Fee] = amount_->getJson(JsonOptions::none);
}
void
sfee::operator()(Env&, JTx& jt) const
{
jt.jv[jss::ServiceFee] = Json::objectValue;
jt.jv[jss::ServiceFee][jss::Amount] = amount_.getJson(JsonOptions::none);
jt.jv[jss::ServiceFee][jss::Destination] = dest_.human();
}
} // namespace jtx
} // namespace test
} // namespace ripple