diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 036fce206..d408b44e8 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -703,6 +703,7 @@ if (tests) src/test/app/AccountTxPaging_test.cpp src/test/app/AmendmentTable_test.cpp src/test/app/Check_test.cpp + src/test/app/ClaimReward_test.cpp src/test/app/CrossingLimits_test.cpp src/test/app/DeliverMin_test.cpp src/test/app/DepositAuth_test.cpp diff --git a/src/ripple/app/tx/impl/ClaimReward.cpp b/src/ripple/app/tx/impl/ClaimReward.cpp index 6bfd7bdb5..9c9669677 100644 --- a/src/ripple/app/tx/impl/ClaimReward.cpp +++ b/src/ripple/app/tx/impl/ClaimReward.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. + Copyright (c) 2023 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 @@ -24,7 +24,6 @@ #include #include #include -#include #include namespace ripple { @@ -38,13 +37,15 @@ ClaimReward::makeTxConsequences(PreflightContext const& ctx) NotTEC ClaimReward::preflight(PreflightContext const& ctx) { + if (!ctx.rules.enabled(featureBalanceRewards)) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; // can have flag 1 set to opt-out of rewards - if (ctx.tx.isFieldPresent(sfFlags) && - ctx.tx.getFieldU32(sfFlags) > 1) - return temMALFORMED; + if (ctx.tx.isFieldPresent(sfFlags) && ctx.tx.getFieldU32(sfFlags) > 1) + return temINVALID_FLAG; return preflight2(ctx); } @@ -65,7 +66,6 @@ ClaimReward::preclaim(PreclaimContext const& ctx) std::optional issuer = ctx.tx[~sfIssuer]; bool isOptOut = flags && *flags == 1; - if ((issuer && isOptOut) || (!issuer && !isOptOut)) return temMALFORMED; @@ -81,12 +81,10 @@ ClaimReward::doApply() auto const sle = view().peek(keylet::account(account_)); if (!sle) return tefINTERNAL; - + std::optional flags = ctx_.tx[~sfFlags]; - std::optional issuer = ctx_.tx[~sfIssuer]; bool isOptOut = flags && *flags == 1; - if (isOptOut) { if (sle->isFieldPresent(sfRewardLgrFirst)) @@ -106,15 +104,14 @@ ClaimReward::doApply() sle->setFieldU32(sfRewardLgrFirst, lgrCur); sle->setFieldU32(sfRewardLgrLast, lgrCur); sle->setFieldU64(sfRewardAccumulator, 0ULL); - sle->setFieldU32(sfRewardTime, - std::chrono::duration_cast - ( + sle->setFieldU32( + sfRewardTime, + std::chrono::duration_cast( ctx_.app.getLedgerMaster() - .getValidatedLedger()->info() - .parentCloseTime - .time_since_epoch() - ).count()); - + .getValidatedLedger() + ->info() + .parentCloseTime.time_since_epoch()) + .count()); } view().update(sle); diff --git a/src/test/app/ClaimReward_test.cpp b/src/test/app/ClaimReward_test.cpp new file mode 100644 index 000000000..f701e3051 --- /dev/null +++ b/src/test/app/ClaimReward_test.cpp @@ -0,0 +1,361 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 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 +#include + +namespace ripple { +namespace test { +struct ClaimReward_test : public beast::unit_test::suite +{ + static Json::Value + claim(jtx::Account const& account) + { + using namespace jtx; + Json::Value jv; + jv[jss::TransactionType] = jss::ClaimReward; + jv[jss::Account] = account.human(); + return jv; + } + + std::unique_ptr + makeNetworkConfig(uint32_t networkID) + { + using namespace jtx; + return envconfig([&](std::unique_ptr cfg) { + cfg->NETWORK_ID = networkID; + Section config; + config.append( + {"reference_fee = 10", + "account_reserve = 1000000", + "owner_reserve = 200000"}); + auto setup = setup_FeeVote(config); + cfg->FEES = setup; + return cfg; + }); + } + + bool + expectRewards( + jtx::Env const& env, + jtx::Account const& acct, + std::uint32_t ledgerFirst, + std::uint32_t ledgerLast, + std::uint64_t accumulator, + std::uint32_t time) + { + auto const sle = env.le(keylet::account(acct)); + if (!sle->isFieldPresent(sfRewardLgrFirst) || + sle->getFieldU32(sfRewardLgrFirst) != ledgerFirst) + { + return false; + } + if (!sle->isFieldPresent(sfRewardLgrLast) || + sle->getFieldU32(sfRewardLgrLast) != ledgerLast) + { + return false; + } + if (!sle->isFieldPresent(sfRewardAccumulator) || + sle->getFieldU64(sfRewardAccumulator) != accumulator) + { + return false; + } + if (!sle->isFieldPresent(sfRewardTime) || + sle->getFieldU32(sfRewardTime) != time) + { + return false; + } + return true; + } + + bool + expectNoRewards(jtx::Env const& env, jtx::Account const& acct) + { + auto const sle = env.le(keylet::account(acct)); + if (sle->isFieldPresent(sfRewardLgrFirst)) + { + return false; + } + if (sle->isFieldPresent(sfRewardLgrLast)) + { + return false; + } + if (sle->isFieldPresent(sfRewardAccumulator)) + { + return false; + } + if (sle->isFieldPresent(sfRewardTime)) + { + return false; + } + return true; + } + + void + testEnabled(FeatureBitset features) + { + testcase("enabled"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + // setup env + auto const alice = Account("alice"); + auto const issuer = Account("issuer"); + + for (bool const withClaimReward : {false, true}) + { + // If the BalanceRewards amendment is not enabled, you should not be + // able to claim rewards. + auto const amend = + withClaimReward ? features : features - featureBalanceRewards; + Env env{*this, amend}; + + env.fund(XRP(1000), alice, issuer); + env.close(); + + auto const txResult = + withClaimReward ? ter(tesSUCCESS) : ter(temDISABLED); + + auto const currentLedger = env.current()->seq(); + auto const currentTime = + std::chrono::duration_cast( + env.app() + .getLedgerMaster() + .getValidatedLedger() + ->info() + .parentCloseTime.time_since_epoch()) + .count(); + + // CLAIM + auto tx = claim(alice); + tx[sfIssuer.jsonName] = issuer.human(); + env(tx, txResult); + env.close(); + + if (withClaimReward) + { + BEAST_EXPECT( + expectRewards( + env, + alice, + currentLedger, + currentLedger, + 0, + currentTime) == true); + } + else + { + BEAST_EXPECT(expectNoRewards(env, alice) == true); + } + } + } + + void + testInvalidPreflight(FeatureBitset features) + { + testcase("invalid preflight"); + using namespace test::jtx; + using namespace std::literals; + + //---------------------------------------------------------------------- + // preflight + + // temDISABLED + // amendment is disabled + { + test::jtx::Env env{ + *this, + makeNetworkConfig(21337), + features - featureBalanceRewards}; + + auto const alice = Account("alice"); + auto const issuer = Account("issuer"); + + env.fund(XRP(1000), alice, issuer); + env.close(); + + auto tx = claim(alice); + tx[sfIssuer.jsonName] = issuer.human(); + env(tx, ter(temDISABLED)); + env.close(); + } + + // temINVALID_FLAG + { + test::jtx::Env env{*this, makeNetworkConfig(21337)}; + + auto const alice = Account("alice"); + auto const issuer = Account("issuer"); + + env.fund(XRP(1000), alice, issuer); + env.close(); + + auto tx = claim(alice); + tx[sfIssuer.jsonName] = issuer.human(); + env(tx, txflags(tfClose), ter(temINVALID_FLAG)); + env.close(); + } + } + + void + testInvalidPreclaim(FeatureBitset features) + { + testcase("invalid preclaim"); + using namespace test::jtx; + using namespace std::literals; + + //---------------------------------------------------------------------- + // preclaim + + // temDISABLED: DA tested in testEnabled() & testInvalidPreflight() + // amendment is disabled + + // terNO_ACCOUNT + // otxn account does not exist. + { + test::jtx::Env env{*this, makeNetworkConfig(21337)}; + + auto const alice = Account("alice"); + auto const issuer = Account("issuer"); + env.memoize(alice); + + env.fund(XRP(1000), issuer); + env.close(); + + auto tx = claim(alice); + tx[jss::Sequence] = 0; + tx[jss::Fee] = 10; + tx[sfIssuer.jsonName] = issuer.human(); + env(tx, ter(terNO_ACCOUNT)); + env.close(); + } + + // temMALFORMED + // (issuer && isOptOut) || (!issuer && !isOptOut) + { + test::jtx::Env env{*this, makeNetworkConfig(21337)}; + + auto const alice = Account("alice"); + auto const issuer = Account("issuer"); + + env.fund(XRP(1000), alice, issuer); + env.close(); + + auto tx = claim(alice); + tx[sfIssuer.jsonName] = issuer.human(); + env(tx, txflags(1), ter(temMALFORMED)); + env.close(); + } + { + test::jtx::Env env{*this, makeNetworkConfig(21337)}; + + auto const alice = Account("alice"); + + env.fund(XRP(1000), alice); + env.close(); + + env(claim(alice), ter(temMALFORMED)); + env.close(); + } + + // tecNO_ISSUER + // issuer account does not exist. + { + test::jtx::Env env{*this, makeNetworkConfig(21337)}; + + auto const alice = Account("alice"); + auto const issuer = Account("issuer"); + env.memoize(issuer); + + env.fund(XRP(1000), alice); + env.close(); + + auto tx = claim(alice); + tx[sfIssuer.jsonName] = issuer.human(); + env(tx, ter(tecNO_ISSUER)); + env.close(); + } + } + + void + testValidNoHook(FeatureBitset features) + { + testcase("valid no hook"); + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, makeNetworkConfig(21337)}; + + auto const alice = Account("alice"); + auto const issuer = Account("issuer"); + + env.fund(XRP(1000), alice, issuer); + env.close(); + + // test claim rewards - no opt out + auto const currentLedger = env.current()->seq(); + auto const currentTime = + std::chrono::duration_cast( + env.app() + .getLedgerMaster() + .getValidatedLedger() + ->info() + .parentCloseTime.time_since_epoch()) + .count(); + + auto tx = claim(alice); + tx[sfIssuer.jsonName] = issuer.human(); + env(tx, ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT( + expectRewards( + env, alice, currentLedger, currentLedger, 0, currentTime) == + true); + + // test claim rewards - opt out + env(claim(alice), txflags(1), ter(tesSUCCESS)); + env.close(); + + BEAST_EXPECT(expectNoRewards(env, alice) == true); + } + + void + testWithFeats(FeatureBitset features) + { + testEnabled(features); + testInvalidPreflight(features); + testInvalidPreclaim(features); + testValidNoHook(features); + } + +public: + void + run() override + { + using namespace test::jtx; + auto const sa = supported_amendments(); + testWithFeats(sa); + } +}; + +BEAST_DEFINE_TESTSUITE(ClaimReward, app, ripple); + +} // namespace test +} // namespace ripple