//------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled Copyright (c) 2025 Ripple Labs Inc. 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 #include #include namespace ripple { namespace test { class Delegate_test : public beast::unit_test::suite { void testFeatureDisabled() { testcase("test featurePermissionDelegation not enabled"); using namespace jtx; Env env{*this, supported_amendments() - featurePermissionDelegation}; Account gw{"gateway"}; Account alice{"alice"}; Account bob{"bob"}; env.fund(XRP(1000000), gw, alice, bob); env.close(); // can not set Delegate when feature disabled env(delegate::set(gw, alice, {"Payment"}), ter(temDISABLED)); // can not send delegating transaction when feature disabled env(pay(alice, bob, XRP(100)), delegate::as(bob), ter(temDISABLED)); } void testDelegateSet() { testcase("test valid request creating, updating, deleting permissions"); using namespace jtx; Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(100000), gw, alice); env.close(); // delegating an empty permission list when the delegate ledger object // does not exist will not create the ledger object env(delegate::set(gw, alice, std::vector{})); env.close(); auto const entry = delegate::entry(env, gw, alice); BEAST_EXPECT(entry[jss::result][jss::error] == "entryNotFound"); auto const permissions = std::vector{ "Payment", "EscrowCreate", "EscrowFinish", "TrustlineAuthorize", "CheckCreate"}; env(delegate::set(gw, alice, permissions)); env.close(); // this lambda function is used to compare the json value of ledger // entry response with the given vector of permissions. auto comparePermissions = [&](Json::Value const& jle, std::vector const& permissions, Account const& account, Account const& authorize) { BEAST_EXPECT( !jle[jss::result].isMember(jss::error) && jle[jss::result].isMember(jss::node)); BEAST_EXPECT( jle[jss::result][jss::node]["LedgerEntryType"] == jss::Delegate); BEAST_EXPECT( jle[jss::result][jss::node][jss::Account] == account.human()); BEAST_EXPECT( jle[jss::result][jss::node][sfAuthorize.jsonName] == authorize.human()); auto const& jPermissions = jle[jss::result][jss::node][sfPermissions.jsonName]; unsigned i = 0; for (auto const& permission : permissions) { BEAST_EXPECT( jPermissions[i][sfPermission.jsonName] [sfPermissionValue.jsonName] == permission); i++; } }; // get ledger entry with valid parameter comparePermissions( delegate::entry(env, gw, alice), permissions, gw, alice); // gw updates permission auto const newPermissions = std::vector{ "Payment", "AMMCreate", "AMMDeposit", "AMMWithdraw"}; env(delegate::set(gw, alice, newPermissions)); env.close(); // get ledger entry again, permissions should be updated to // newPermissions comparePermissions( delegate::entry(env, gw, alice), newPermissions, gw, alice); // gw deletes all permissions delegated to alice, this will delete // the // ledger entry env(delegate::set(gw, alice, {})); env.close(); auto const jle = delegate::entry(env, gw, alice); BEAST_EXPECT(jle[jss::result][jss::error] == "entryNotFound"); // alice can delegate permissions to gw as well env(delegate::set(alice, gw, permissions)); env.close(); comparePermissions( delegate::entry(env, alice, gw), permissions, alice, gw); auto const response = delegate::entry(env, gw, alice); // alice has not been granted any permissions by gw BEAST_EXPECT(response[jss::result][jss::error] == "entryNotFound"); } void testInvalidRequest() { testcase("test invalid DelegateSet"); using namespace jtx; Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; Account bob{"bob"}; env.fund(XRP(100000), gw, alice, bob); env.close(); // when permissions size exceeds the limit 10, should return // temARRAY_TOO_LARGE { env(delegate::set( gw, alice, {"Payment", "EscrowCreate", "EscrowFinish", "EscrowCancel", "CheckCreate", "CheckCash", "CheckCancel", "DepositPreauth", "TrustSet", "NFTokenMint", "NFTokenBurn"}), ter(temARRAY_TOO_LARGE)); } // alice can not authorize herself { env(delegate::set(alice, alice, {"Payment"}), ter(temMALFORMED)); } // bad fee { Json::Value jv; jv[jss::TransactionType] = jss::DelegateSet; jv[jss::Account] = gw.human(); jv[sfAuthorize.jsonName] = alice.human(); Json::Value permissionsJson(Json::arrayValue); Json::Value permissionValue; permissionValue[sfPermissionValue.jsonName] = "Payment"; Json::Value permissionObj; permissionObj[sfPermission.jsonName] = permissionValue; permissionsJson.append(permissionObj); jv[sfPermissions.jsonName] = permissionsJson; jv[sfFee.jsonName] = -1; env(jv, ter(temBAD_FEE)); } // when provided permissions contains duplicate values, should return // temMALFORMED { env(delegate::set( gw, alice, {"Payment", "EscrowCreate", "EscrowFinish", "TrustlineAuthorize", "CheckCreate", "TrustlineAuthorize"}), ter(temMALFORMED)); } // when authorizing account which does not exist, should return // terNO_ACCOUNT { env(delegate::set(gw, Account("unknown"), {"Payment"}), ter(terNO_ACCOUNT)); } // non-delegatable transaction { env(delegate::set(gw, alice, {"SetRegularKey"}), ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"AccountSet"}), ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"SignerListSet"}), ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"DelegateSet"}), ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"SetRegularKey"}), ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"EnableAmendment"}), ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"UNLModify"}), ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"SetFee"}), ter(tecNO_PERMISSION)); } } void testReserve() { testcase("test reserve"); using namespace jtx; // test reserve for DelegateSet { Env env(*this); Account alice{"alice"}; Account bob{"bob"}; Account carol{"carol"}; env.fund(drops(env.current()->fees().accountReserve(0)), alice); env.fund( drops(env.current()->fees().accountReserve(1)), bob, carol); env.close(); // alice does not have enough reserve to create Delegate env(delegate::set(alice, bob, {"Payment"}), ter(tecINSUFFICIENT_RESERVE)); // bob has enough reserve env(delegate::set(bob, alice, {"Payment"})); env.close(); // now bob create another Delegate, he does not have // enough reserve env(delegate::set(bob, carol, {"Payment"}), ter(tecINSUFFICIENT_RESERVE)); } // test reserve when sending transaction on behalf of other account { Env env(*this); Account alice{"alice"}; Account bob{"bob"}; env.fund(drops(env.current()->fees().accountReserve(1)), alice); env.fund(drops(env.current()->fees().accountReserve(2)), bob); env.close(); // alice gives bob permission env(delegate::set(alice, bob, {"DIDSet", "DIDDelete"})); env.close(); // bob set DID on behalf of alice, but alice does not have enough // reserve env(did::set(alice), did::uri("uri"), delegate::as(bob), ter(tecINSUFFICIENT_RESERVE)); // bob can set DID for himself because he has enough reserve env(did::set(bob), did::uri("uri")); env.close(); } } void testFee() { testcase("test fee"); using namespace jtx; Env env(*this); Account alice{"alice"}; Account bob{"bob"}; Account carol{"carol"}; env.fund(XRP(10000), alice, carol); env.fund(XRP(1000), bob); env.close(); { // Fee should be checked before permission check, // otherwise tecNO_PERMISSION returned when permission check fails // could cause context reset to pay fee because it is tec error auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto carolBalance = env.balance(carol); env(pay(alice, carol, XRP(100)), fee(XRP(2000)), delegate::as(bob), ter(terINSUF_FEE_B)); env.close(); BEAST_EXPECT(env.balance(alice) == aliceBalance); BEAST_EXPECT(env.balance(bob) == bobBalance); BEAST_EXPECT(env.balance(carol) == carolBalance); } env(delegate::set(alice, bob, {"Payment"})); env.close(); { // Delegate pays the fee auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto carolBalance = env.balance(carol); auto const sendAmt = XRP(100); auto const feeAmt = XRP(10); env(pay(alice, carol, sendAmt), fee(feeAmt), delegate::as(bob)); env.close(); BEAST_EXPECT(env.balance(alice) == aliceBalance - sendAmt); BEAST_EXPECT(env.balance(bob) == bobBalance - feeAmt); BEAST_EXPECT(env.balance(carol) == carolBalance + sendAmt); } { // insufficient balance to pay fee auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto carolBalance = env.balance(carol); env(pay(alice, carol, XRP(100)), fee(XRP(2000)), delegate::as(bob), ter(terINSUF_FEE_B)); env.close(); BEAST_EXPECT(env.balance(alice) == aliceBalance); BEAST_EXPECT(env.balance(bob) == bobBalance); BEAST_EXPECT(env.balance(carol) == carolBalance); } { // fee is paid by Delegate // on context reset (tec error) auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto carolBalance = env.balance(carol); auto const feeAmt = XRP(10); env(pay(alice, carol, XRP(20000)), fee(feeAmt), delegate::as(bob), ter(tecUNFUNDED_PAYMENT)); env.close(); BEAST_EXPECT(env.balance(alice) == aliceBalance); BEAST_EXPECT(env.balance(bob) == bobBalance - feeAmt); BEAST_EXPECT(env.balance(carol) == carolBalance); } } void testSequence() { testcase("test sequence"); using namespace jtx; Env env(*this); Account alice{"alice"}; Account bob{"bob"}; Account carol{"carol"}; env.fund(XRP(10000), alice, bob, carol); env.close(); auto aliceSeq = env.seq(alice); auto bobSeq = env.seq(bob); env(delegate::set(alice, bob, {"Payment"})); env(delegate::set(bob, alice, {"Payment"})); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); BEAST_EXPECT(env.seq(bob) == bobSeq + 1); aliceSeq = env.seq(alice); bobSeq = env.seq(bob); for (auto i = 0; i < 20; ++i) { // bob is the delegated account, his sequence won't increment env(pay(alice, carol, XRP(10)), fee(XRP(10)), delegate::as(bob)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); BEAST_EXPECT(env.seq(bob) == bobSeq); aliceSeq = env.seq(alice); // bob sends payment for himself, his sequence will increment env(pay(bob, carol, XRP(10)), fee(XRP(10))); BEAST_EXPECT(env.seq(alice) == aliceSeq); BEAST_EXPECT(env.seq(bob) == bobSeq + 1); bobSeq = env.seq(bob); // alice is the delegated account, her sequence won't increment env(pay(bob, carol, XRP(10)), fee(XRP(10)), delegate::as(alice)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq); BEAST_EXPECT(env.seq(bob) == bobSeq + 1); bobSeq = env.seq(bob); // alice sends payment for herself, her sequence will increment env(pay(alice, carol, XRP(10)), fee(XRP(10))); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); BEAST_EXPECT(env.seq(bob) == bobSeq); aliceSeq = env.seq(alice); } } void testAccountDelete() { testcase("test deleting account"); using namespace jtx; Env env(*this); Account alice{"alice"}; Account bob{"bob"}; env.fund(XRP(100000), alice, bob); env.close(); env(delegate::set(alice, bob, {"Payment"})); env.close(); BEAST_EXPECT( env.closed()->exists(keylet::delegate(alice.id(), bob.id()))); for (std::uint32_t i = 0; i < 256; ++i) env.close(); auto const aliceBalance = env.balance(alice); auto const bobBalance = env.balance(bob); // alice deletes account, this will remove the Delegate object auto const deleteFee = drops(env.current()->fees().increment); env(acctdelete(alice, bob), fee(deleteFee)); env.close(); BEAST_EXPECT(!env.closed()->exists(keylet::account(alice.id()))); BEAST_EXPECT(!env.closed()->exists(keylet::ownerDir(alice.id()))); BEAST_EXPECT(env.balance(bob) == bobBalance + aliceBalance - deleteFee); BEAST_EXPECT( !env.closed()->exists(keylet::delegate(alice.id(), bob.id()))); } void testDelegateTransaction() { testcase("test delegate transaction"); using namespace jtx; Env env(*this); Account alice{"alice"}; Account bob{"bob"}; Account carol{"carol"}; XRPAmount const baseFee{env.current()->fees().base}; // use different initial amount to distinguish the source balance env.fund(XRP(10000), alice); env.fund(XRP(20000), bob); env.fund(XRP(30000), carol); env.close(); auto aliceBalance = env.balance(alice, XRP); auto bobBalance = env.balance(bob, XRP); auto carolBalance = env.balance(carol, XRP); // can not send transaction on one's own behalf env(pay(alice, bob, XRP(50)), delegate::as(alice), ter(temBAD_SIGNER)); env.require(balance(alice, aliceBalance)); env(delegate::set(alice, bob, {"Payment"})); env.close(); env.require(balance(alice, aliceBalance - drops(baseFee))); aliceBalance = env.balance(alice, XRP); // bob pays 50 XRP to carol on behalf of alice env(pay(alice, carol, XRP(50)), delegate::as(bob)); env.close(); env.require(balance(alice, aliceBalance - XRP(50))); env.require(balance(carol, carolBalance + XRP(50))); // bob pays the fee env.require(balance(bob, bobBalance - drops(baseFee))); aliceBalance = env.balance(alice, XRP); bobBalance = env.balance(bob, XRP); carolBalance = env.balance(carol, XRP); // bob pays 50 XRP to bob self on behalf of alice env(pay(alice, bob, XRP(50)), delegate::as(bob)); env.close(); env.require(balance(alice, aliceBalance - XRP(50))); env.require(balance(bob, bobBalance + XRP(50) - drops(baseFee))); aliceBalance = env.balance(alice, XRP); bobBalance = env.balance(bob, XRP); // bob pay 50 XRP to alice herself on behalf of alice env(pay(alice, alice, XRP(50)), delegate::as(bob), ter(temREDUNDANT)); env.close(); // bob does not have permission to create check env(check::create(alice, bob, XRP(10)), delegate::as(bob), ter(tecNO_PERMISSION)); // carol does not have permission to create check env(check::create(alice, bob, XRP(10)), delegate::as(carol), ter(tecNO_PERMISSION)); } void testPaymentGranular() { testcase("test payment granular"); using namespace jtx; // test PaymentMint and PaymentBurn { Env env(*this); Account alice{"alice"}; Account bob{"bob"}; Account gw{"gateway"}; Account gw2{"gateway2"}; auto const USD = gw["USD"]; auto const EUR = gw2["EUR"]; env.fund(XRP(10000), alice); env.fund(XRP(20000), bob); env.fund(XRP(40000), gw, gw2); env.trust(USD(200), alice); env.trust(EUR(400), gw); env.close(); XRPAmount const baseFee{env.current()->fees().base}; auto aliceBalance = env.balance(alice, XRP); auto bobBalance = env.balance(bob, XRP); auto gwBalance = env.balance(gw, XRP); auto gw2Balance = env.balance(gw2, XRP); // delegate ledger object is not created yet env(pay(gw, alice, USD(50)), delegate::as(bob), ter(tecNO_PERMISSION)); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); // gw gives bob burn permission env(delegate::set(gw, bob, {"PaymentBurn"})); env.close(); env.require(balance(gw, gwBalance - drops(baseFee))); gwBalance = env.balance(gw, XRP); // bob sends a payment transaction on behalf of gw env(pay(gw, alice, USD(50)), delegate::as(bob), ter(tecNO_PERMISSION)); env.close(); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); // gw gives bob mint permission, alice gives bob burn permission env(delegate::set(gw, bob, {"PaymentMint"})); env(delegate::set(alice, bob, {"PaymentBurn"})); env.close(); env.require(balance(alice, aliceBalance - drops(baseFee))); env.require(balance(gw, gwBalance - drops(baseFee))); aliceBalance = env.balance(alice, XRP); gwBalance = env.balance(gw, XRP); // can not send XRP env(pay(gw, alice, XRP(50)), delegate::as(bob), ter(tecNO_PERMISSION)); env.close(); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); // mint 50 USD env(pay(gw, alice, USD(50)), delegate::as(bob)); env.close(); env.require(balance(bob, bobBalance - drops(baseFee))); env.require(balance(gw, gwBalance)); env.require(balance(gw, alice["USD"](-50))); env.require(balance(alice, USD(50))); BEAST_EXPECT(env.balance(bob, USD) == USD(0)); bobBalance = env.balance(bob, XRP); // burn 30 USD env(pay(alice, gw, USD(30)), delegate::as(bob)); env.close(); env.require(balance(bob, bobBalance - drops(baseFee))); env.require(balance(gw, gwBalance)); env.require(balance(gw, alice["USD"](-20))); env.require(balance(alice, USD(20))); BEAST_EXPECT(env.balance(bob, USD) == USD(0)); bobBalance = env.balance(bob, XRP); // bob has both mint and burn permissions env(delegate::set(gw, bob, {"PaymentMint", "PaymentBurn"})); env.close(); env.require(balance(gw, gwBalance - drops(baseFee))); gwBalance = env.balance(gw, XRP); // mint 100 USD for gw env(pay(gw, alice, USD(100)), delegate::as(bob)); env.close(); env.require(balance(gw, alice["USD"](-120))); env.require(balance(alice, USD(120))); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); // gw2 pays gw 200 EUR env(pay(gw2, gw, EUR(200))); env.close(); env.require(balance(gw2, gw2Balance - drops(baseFee))); gw2Balance = env.balance(gw2, XRP); env.require(balance(gw2, gw["EUR"](-200))); env.require(balance(gw, EUR(200))); // burn 100 EUR for gw env(pay(gw, gw2, EUR(100)), delegate::as(bob)); env.close(); env.require(balance(gw2, gw["EUR"](-100))); env.require(balance(gw, EUR(100))); env.require(balance(bob, bobBalance - drops(baseFee))); env.require(balance(gw, gwBalance)); env.require(balance(gw2, gw2Balance)); env.require(balance(alice, aliceBalance)); } // test PaymentMint won't affect Payment transaction level delegation. { Env env(*this); Account alice{"alice"}; Account bob{"bob"}; Account gw{"gateway"}; auto const USD = gw["USD"]; env.fund(XRP(10000), alice); env.fund(XRP(20000), bob); env.fund(XRP(40000), gw); env.trust(USD(200), alice); env.close(); XRPAmount const baseFee{env.current()->fees().base}; auto aliceBalance = env.balance(alice, XRP); auto bobBalance = env.balance(bob, XRP); auto gwBalance = env.balance(gw, XRP); // gw gives bob PaymentBurn permission env(delegate::set(gw, bob, {"PaymentBurn"})); env.close(); env.require(balance(gw, gwBalance - drops(baseFee))); gwBalance = env.balance(gw, XRP); // bob can not mint on behalf of gw because he only has burn // permission env(pay(gw, alice, USD(50)), delegate::as(bob), ter(tecNO_PERMISSION)); env.close(); env.require(balance(bob, bobBalance - drops(baseFee))); bobBalance = env.balance(bob, XRP); // gw gives bob Payment permission as well env(delegate::set(gw, bob, {"PaymentBurn", "Payment"})); env.close(); env.require(balance(gw, gwBalance - drops(baseFee))); gwBalance = env.balance(gw, XRP); // bob now can mint on behalf of gw env(pay(gw, alice, USD(50)), delegate::as(bob)); env.close(); env.require(balance(bob, bobBalance - drops(baseFee))); env.require(balance(gw, gwBalance)); env.require(balance(alice, aliceBalance)); env.require(balance(gw, alice["USD"](-50))); env.require(balance(alice, USD(50))); BEAST_EXPECT(env.balance(bob, USD) == USD(0)); } } void testTrustSetGranular() { testcase("test TrustSet granular permissions"); using namespace jtx; // test TrustlineUnfreeze, TrustlineFreeze and TrustlineAuthorize { Env env(*this); Account gw{"gw"}; Account alice{"alice"}; Account bob{"bob"}; env.fund(XRP(10000), gw, alice, bob); env(fset(gw, asfRequireAuth)); env.close(); env(delegate::set(alice, bob, {"TrustlineUnfreeze"})); env.close(); // bob can not create trustline on behalf of alice because he only // has unfreeze permission env(trust(alice, gw["USD"](50)), delegate::as(bob), ter(tecNO_PERMISSION)); env.close(); // alice creates trustline by herself env(trust(alice, gw["USD"](50))); env.close(); // gw gives bob unfreeze permission env(delegate::set(gw, bob, {"TrustlineUnfreeze"})); env.close(); // unsupported flags env(trust(alice, gw["USD"](50), tfSetNoRipple), delegate::as(bob), ter(tecNO_PERMISSION)); env(trust(alice, gw["USD"](50), tfClearNoRipple), delegate::as(bob), ter(tecNO_PERMISSION)); env(trust(gw, gw["USD"](0), alice, tfSetDeepFreeze), delegate::as(bob), ter(tecNO_PERMISSION)); env(trust(gw, gw["USD"](0), alice, tfClearDeepFreeze), delegate::as(bob), ter(tecNO_PERMISSION)); env.close(); // supported flags with wrong permission env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::as(bob), ter(tecNO_PERMISSION)); env(trust(gw, gw["USD"](0), alice, tfSetFreeze), delegate::as(bob), ter(tecNO_PERMISSION)); env.close(); env(delegate::set(gw, bob, {"TrustlineAuthorize"})); env.close(); env(trust(gw, gw["USD"](0), alice, tfClearFreeze), delegate::as(bob), ter(tecNO_PERMISSION)); env.close(); // although trustline authorize is granted, bob can not change the // limit number env(trust(gw, gw["USD"](50), alice, tfSetfAuth), delegate::as(bob), ter(tecNO_PERMISSION)); env.close(); // supported flags with correct permission env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::as(bob)); env.close(); env(delegate::set( gw, bob, {"TrustlineAuthorize", "TrustlineFreeze"})); env.close(); env(trust(gw, gw["USD"](0), alice, tfSetFreeze), delegate::as(bob)); env.close(); env(delegate::set( gw, bob, {"TrustlineAuthorize", "TrustlineUnfreeze"})); env.close(); env(trust(gw, gw["USD"](0), alice, tfClearFreeze), delegate::as(bob)); env.close(); // but bob can not freeze trustline because he no longer has freeze // permission env(trust(gw, gw["USD"](0), alice, tfSetFreeze), delegate::as(bob), ter(tecNO_PERMISSION)); // cannot update LimitAmount with granular permission, both high and // low account env(trust(alice, gw["USD"](100)), delegate::as(bob), ter(tecNO_PERMISSION)); env(trust(gw, alice["USD"](100)), delegate::as(bob), ter(tecNO_PERMISSION)); // can not set QualityIn or QualityOut auto tx = trust(alice, gw["USD"](50)); tx["QualityIn"] = "1000"; env(tx, delegate::as(bob), ter(tecNO_PERMISSION)); auto tx2 = trust(alice, gw["USD"](50)); tx2["QualityOut"] = "1000"; env(tx2, delegate::as(bob), ter(tecNO_PERMISSION)); auto tx3 = trust(gw, alice["USD"](50)); tx3["QualityIn"] = "1000"; env(tx3, delegate::as(bob), ter(tecNO_PERMISSION)); auto tx4 = trust(gw, alice["USD"](50)); tx4["QualityOut"] = "1000"; env(tx4, delegate::as(bob), ter(tecNO_PERMISSION)); // granting TrustSet can make it work env(delegate::set(gw, bob, {"TrustSet"})); env.close(); auto tx5 = trust(gw, alice["USD"](50)); tx5["QualityOut"] = "1000"; env(tx5, delegate::as(bob)); auto tx6 = trust(alice, gw["USD"](50)); tx6["QualityOut"] = "1000"; env(tx6, delegate::as(bob), ter(tecNO_PERMISSION)); env(delegate::set(alice, bob, {"TrustSet"})); env.close(); env(tx6, delegate::as(bob)); } // test mix of transaction level delegation and granular delegation { Env env(*this); Account gw{"gw"}; Account alice{"alice"}; Account bob{"bob"}; env.fund(XRP(10000), gw, alice, bob); env(fset(gw, asfRequireAuth)); env.close(); // bob does not have permission env(trust(alice, gw["USD"](50)), delegate::as(bob), ter(tecNO_PERMISSION)); env(delegate::set( alice, bob, {"TrustlineUnfreeze", "NFTokenCreateOffer"})); env.close(); // bob still does not have permission env(trust(alice, gw["USD"](50)), delegate::as(bob), ter(tecNO_PERMISSION)); // add TrustSet permission and some unrelated permission env(delegate::set( alice, bob, {"TrustlineUnfreeze", "NFTokenCreateOffer", "TrustSet", "AccountTransferRateSet"})); env.close(); env(trust(alice, gw["USD"](50)), delegate::as(bob)); env.close(); env(delegate::set( gw, bob, {"TrustlineUnfreeze", "NFTokenCreateOffer", "TrustSet", "AccountTransferRateSet"})); env.close(); // since bob has TrustSet permission, he does not need // TrustlineFreeze granular permission to freeze the trustline env(trust(gw, gw["USD"](0), alice, tfSetFreeze), delegate::as(bob)); env(trust(gw, gw["USD"](0), alice, tfClearFreeze), delegate::as(bob)); // bob can perform all the operations regarding TrustSet env(trust(gw, gw["USD"](0), alice, tfSetFreeze), delegate::as(bob)); env(trust(gw, gw["USD"](0), alice, tfSetDeepFreeze), delegate::as(bob)); env(trust(gw, gw["USD"](0), alice, tfClearDeepFreeze), delegate::as(bob)); env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::as(bob)); env(trust(alice, gw["USD"](50), tfSetNoRipple), delegate::as(bob)); env(trust(alice, gw["USD"](50), tfClearNoRipple), delegate::as(bob)); } } void testAccountSetGranular() { testcase("test AccountSet granular permissions"); using namespace jtx; // test AccountDomainSet, AccountEmailHashSet, // AccountMessageKeySet,AccountTransferRateSet, and AccountTickSizeSet // granular permissions { Env env(*this); auto const alice = Account{"alice"}; auto const bob = Account{"bob"}; env.fund(XRP(10000), alice, bob); env.close(); // alice gives bob some random permission, which is not related to // the AccountSet transaction env(delegate::set(alice, bob, {"TrustlineUnfreeze"})); env.close(); // bob does not have permission to set domain // on behalf of alice std::string const domain = "example.com"; auto jt = noop(alice); jt[sfDomain.fieldName] = strHex(domain); jt[sfDelegate.fieldName] = bob.human(); jt[sfFlags.fieldName] = tfFullyCanonicalSig; // add granular permission related to AccountSet but is not the // correct permission for domain set env(delegate::set( alice, bob, {"TrustlineUnfreeze", "AccountEmailHashSet"})); env.close(); env(jt, ter(tecNO_PERMISSION)); // alice give granular permission of AccountDomainSet to bob env(delegate::set(alice, bob, {"AccountDomainSet"})); env.close(); // bob set account domain on behalf of alice env(jt); BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); // bob can reset domain jt[sfDomain.fieldName] = ""; env(jt); BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfDomain)); // if flag is not equal to tfFullyCanonicalSig, which means bob // is trying to set the flag at the same time, it will fail std::string const failDomain = "fail_domain_update"; jt[sfFlags.fieldName] = tfRequireAuth; jt[sfDomain.fieldName] = strHex(failDomain); env(jt, ter(tecNO_PERMISSION)); // reset flag number jt[sfFlags.fieldName] = tfFullyCanonicalSig; // bob tries to update domain and set email hash, // but he does not have permission to set email hash jt[sfDomain.fieldName] = strHex(domain); std::string const mh("5F31A79367DC3137FADA860C05742EE6"); jt[sfEmailHash.fieldName] = mh; env(jt, ter(tecNO_PERMISSION)); // alice give granular permission of AccountEmailHashSet to bob env(delegate::set( alice, bob, {"AccountDomainSet", "AccountEmailHashSet"})); env.close(); env(jt); BEAST_EXPECT(to_string((*env.le(alice))[sfEmailHash]) == mh); BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain)); // bob does not have permission to set message key for alice auto const rkp = randomKeyPair(KeyType::ed25519); jt[sfMessageKey.fieldName] = strHex(rkp.first.slice()); env(jt, ter(tecNO_PERMISSION)); // alice give granular permission of AccountMessageKeySet to bob env(delegate::set( alice, bob, {"AccountDomainSet", "AccountEmailHashSet", "AccountMessageKeySet"})); env.close(); // bob can set message key for alice env(jt); BEAST_EXPECT( strHex((*env.le(alice))[sfMessageKey]) == strHex(rkp.first.slice())); jt[sfMessageKey.fieldName] = ""; env(jt); BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfMessageKey)); // bob does not have permission to set transfer rate for alice env(rate(alice, 2.0), delegate::as(bob), ter(tecNO_PERMISSION)); // alice give granular permission of AccountTransferRateSet to bob env(delegate::set( alice, bob, {"AccountDomainSet", "AccountEmailHashSet", "AccountMessageKeySet", "AccountTransferRateSet"})); env.close(); auto jtRate = rate(alice, 2.0); jtRate[sfDelegate.fieldName] = bob.human(); jtRate[sfFlags.fieldName] = tfFullyCanonicalSig; env(jtRate, delegate::as(bob)); BEAST_EXPECT((*env.le(alice))[sfTransferRate] == 2000000000); // bob does not have permission to set ticksize for alice jt[sfTickSize.fieldName] = 8; env(jt, ter(tecNO_PERMISSION)); // alice give granular permission of AccountTickSizeSet to bob env(delegate::set( alice, bob, {"AccountDomainSet", "AccountEmailHashSet", "AccountMessageKeySet", "AccountTransferRateSet", "AccountTickSizeSet"})); env.close(); env(jt); BEAST_EXPECT((*env.le(alice))[sfTickSize] == 8); // can not set asfRequireAuth flag for alice env(fset(alice, asfRequireAuth), delegate::as(bob), ter(tecNO_PERMISSION)); // reset Delegate will delete the Delegate // object env(delegate::set(alice, bob, {})); // bib still does not have permission to set asfRequireAuth for // alice env(fset(alice, asfRequireAuth), delegate::as(bob), ter(tecNO_PERMISSION)); // alice can set for herself env(fset(alice, asfRequireAuth)); env.require(flags(alice, asfRequireAuth)); env.close(); // can not update tick size because bob no longer has permission jt[sfTickSize.fieldName] = 7; env(jt, ter(tecNO_PERMISSION)); env(delegate::set( alice, bob, {"AccountDomainSet", "AccountEmailHashSet", "AccountMessageKeySet"})); env.close(); // bob does not have permission to set wallet locater for alice std::string const locator = "9633EC8AF54F16B5286DB1D7B519EF49EEFC050C0C8AC4384F1D88ACD1BFDF" "05"; auto jt2 = noop(alice); jt2[sfDomain.fieldName] = strHex(domain); jt2[sfDelegate.fieldName] = bob.human(); jt2[sfWalletLocator.fieldName] = locator; jt2[sfFlags.fieldName] = tfFullyCanonicalSig; env(jt2, ter(tecNO_PERMISSION)); } // can not set AccountSet flags on behalf of other account { Env env(*this); auto const alice = Account{"alice"}; auto const bob = Account{"bob"}; env.fund(XRP(10000), alice, bob); env.close(); auto testSetClearFlag = [&](std::uint32_t flag) { // bob can not set flag on behalf of alice env(fset(alice, flag), delegate::as(bob), ter(tecNO_PERMISSION)); // alice set by herself env(fset(alice, flag)); env.close(); env.require(flags(alice, flag)); // bob can not clear on behalf of alice env(fclear(alice, flag), delegate::as(bob), ter(tecNO_PERMISSION)); }; // testSetClearFlag(asfNoFreeze); testSetClearFlag(asfRequireAuth); testSetClearFlag(asfAllowTrustLineClawback); // alice gives some granular permissions to bob env(delegate::set( alice, bob, {"AccountDomainSet", "AccountEmailHashSet", "AccountMessageKeySet"})); env.close(); testSetClearFlag(asfDefaultRipple); testSetClearFlag(asfDepositAuth); testSetClearFlag(asfDisallowIncomingCheck); testSetClearFlag(asfDisallowIncomingNFTokenOffer); testSetClearFlag(asfDisallowIncomingPayChan); testSetClearFlag(asfDisallowIncomingTrustline); testSetClearFlag(asfDisallowXRP); testSetClearFlag(asfRequireDest); testSetClearFlag(asfGlobalFreeze); // bob can not set asfAccountTxnID on behalf of alice env(fset(alice, asfAccountTxnID), delegate::as(bob), ter(tecNO_PERMISSION)); env(fset(alice, asfAccountTxnID)); env.close(); BEAST_EXPECT(env.le(alice)->isFieldPresent(sfAccountTxnID)); env(fclear(alice, asfAccountTxnID), delegate::as(bob), ter(tecNO_PERMISSION)); // bob can not set asfAuthorizedNFTokenMinter on behalf of alice Json::Value jt = fset(alice, asfAuthorizedNFTokenMinter); jt[sfDelegate.fieldName] = bob.human(); jt[sfNFTokenMinter.fieldName] = bob.human(); env(jt, ter(tecNO_PERMISSION)); // bob gives alice some permissions env(delegate::set( bob, alice, {"AccountDomainSet", "AccountEmailHashSet", "AccountMessageKeySet"})); env.close(); // since we can not set asfNoFreeze if asfAllowTrustLineClawback is // set, which can not be clear either. Test alice set asfNoFreeze on // behalf of bob. env(fset(alice, asfNoFreeze), delegate::as(bob), ter(tecNO_PERMISSION)); env(fset(bob, asfNoFreeze)); env.close(); env.require(flags(bob, asfNoFreeze)); // alice can not clear on behalf of bob env(fclear(alice, asfNoFreeze), delegate::as(bob), ter(tecNO_PERMISSION)); // bob can not set asfDisableMaster on behalf of alice Account const bobKey{"bobKey", KeyType::secp256k1}; env(regkey(bob, bobKey)); env.close(); env(fset(alice, asfDisableMaster), delegate::as(bob), sig(bob), ter(tecNO_PERMISSION)); } } void testMPTokenIssuanceSetGranular() { testcase("test MPTokenIssuanceSet granular"); using namespace jtx; // test MPTokenIssuanceUnlock and MPTokenIssuanceLock permissions { Env env(*this); Account alice{"alice"}; Account bob{"bob"}; env.fund(XRP(100000), alice, bob); env.close(); MPTTester mpt(env, alice, {.fund = false}); env.close(); mpt.create({.flags = tfMPTCanLock}); env.close(); // delegate ledger object is not created yet mpt.set( {.account = alice, .flags = tfMPTLock, .delegate = bob, .err = tecNO_PERMISSION}); // alice gives granular permission to bob of MPTokenIssuanceUnlock env(delegate::set(alice, bob, {"MPTokenIssuanceUnlock"})); env.close(); // bob does not have lock permission mpt.set( {.account = alice, .flags = tfMPTLock, .delegate = bob, .err = tecNO_PERMISSION}); // bob now has lock permission, but does not have unlock permission env(delegate::set(alice, bob, {"MPTokenIssuanceLock"})); env.close(); mpt.set({.account = alice, .flags = tfMPTLock, .delegate = bob}); mpt.set( {.account = alice, .flags = tfMPTUnlock, .delegate = bob, .err = tecNO_PERMISSION}); // now bob can lock and unlock env(delegate::set( alice, bob, {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"})); env.close(); mpt.set({.account = alice, .flags = tfMPTUnlock, .delegate = bob}); mpt.set({.account = alice, .flags = tfMPTLock, .delegate = bob}); env.close(); } // test mix of granular and transaction level permission { Env env(*this); Account alice{"alice"}; Account bob{"bob"}; env.fund(XRP(100000), alice, bob); env.close(); MPTTester mpt(env, alice, {.fund = false}); env.close(); mpt.create({.flags = tfMPTCanLock}); env.close(); // alice gives granular permission to bob of MPTokenIssuanceLock env(delegate::set(alice, bob, {"MPTokenIssuanceLock"})); env.close(); mpt.set({.account = alice, .flags = tfMPTLock, .delegate = bob}); // bob does not have unlock permission mpt.set( {.account = alice, .flags = tfMPTUnlock, .delegate = bob, .err = tecNO_PERMISSION}); // alice gives bob some unrelated permission with // MPTokenIssuanceLock env(delegate::set( alice, bob, {"NFTokenMint", "MPTokenIssuanceLock", "NFTokenBurn"})); env.close(); // bob can not unlock mpt.set( {.account = alice, .flags = tfMPTUnlock, .delegate = bob, .err = tecNO_PERMISSION}); // alice add MPTokenIssuanceSet to permissions env(delegate::set( alice, bob, {"NFTokenMint", "MPTokenIssuanceLock", "NFTokenBurn", "MPTokenIssuanceSet"})); mpt.set({.account = alice, .flags = tfMPTUnlock, .delegate = bob}); // alice can lock by herself mpt.set({.account = alice, .flags = tfMPTLock}); mpt.set({.account = alice, .flags = tfMPTUnlock, .delegate = bob}); mpt.set({.account = alice, .flags = tfMPTLock, .delegate = bob}); } } void testSingleSign() { testcase("test single sign"); using namespace jtx; Env env(*this); Account alice{"alice"}; Account bob{"bob"}; Account carol{"carol"}; env.fund(XRP(100000), alice, bob, carol); env.close(); env(delegate::set(alice, bob, {"Payment"})); env.close(); auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto carolBalance = env.balance(carol); env(pay(alice, carol, XRP(100)), fee(XRP(10)), delegate::as(bob), sig(bob)); env.close(); BEAST_EXPECT(env.balance(alice) == aliceBalance - XRP(100)); BEAST_EXPECT(env.balance(bob) == bobBalance - XRP(10)); BEAST_EXPECT(env.balance(carol) == carolBalance + XRP(100)); } void testSingleSignBadSecret() { testcase("test single sign with bad secret"); using namespace jtx; Env env(*this); Account alice{"alice"}; Account bob{"bob"}; Account carol{"carol"}; env.fund(XRP(100000), alice, bob, carol); env.close(); env(delegate::set(alice, bob, {"Payment"})); env.close(); auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto carolBalance = env.balance(carol); env(pay(alice, carol, XRP(100)), fee(XRP(10)), delegate::as(bob), sig(alice), ter(tefBAD_AUTH)); env.close(); BEAST_EXPECT(env.balance(alice) == aliceBalance); BEAST_EXPECT(env.balance(bob) == bobBalance); BEAST_EXPECT(env.balance(carol) == carolBalance); } void testMultiSign() { testcase("test multi sign"); using namespace jtx; Env env(*this); Account alice{"alice"}; Account bob{"bob"}; Account carol{"carol"}; Account daria{"daria"}; Account edward{"edward"}; env.fund(XRP(100000), alice, bob, carol, daria, edward); env.close(); env(signers(bob, 2, {{daria, 1}, {edward, 1}})); env.close(); env(delegate::set(alice, bob, {"Payment"})); env.close(); auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto carolBalance = env.balance(carol); auto dariaBalance = env.balance(daria); auto edwardBalance = env.balance(edward); env(pay(alice, carol, XRP(100)), fee(XRP(10)), delegate::as(bob), msig(daria, edward)); env.close(); BEAST_EXPECT(env.balance(alice) == aliceBalance - XRP(100)); BEAST_EXPECT(env.balance(bob) == bobBalance - XRP(10)); BEAST_EXPECT(env.balance(carol) == carolBalance + XRP(100)); BEAST_EXPECT(env.balance(daria) == dariaBalance); BEAST_EXPECT(env.balance(edward) == edwardBalance); } void testMultiSignQuorumNotMet() { testcase("test multi sign which does not meet quorum"); using namespace jtx; Env env(*this); Account alice{"alice"}; Account bob{"bob"}; Account carol{"carol"}; Account daria = Account{"daria"}; Account edward = Account{"edward"}; Account fred = Account{"fred"}; env.fund(XRP(100000), alice, bob, carol, daria, edward, fred); env.close(); env(signers(bob, 3, {{daria, 1}, {edward, 1}, {fred, 1}})); env.close(); env(delegate::set(alice, bob, {"Payment"})); env.close(); auto aliceBalance = env.balance(alice); auto bobBalance = env.balance(bob); auto carolBalance = env.balance(carol); auto dariaBalance = env.balance(daria); auto edwardBalance = env.balance(edward); env(pay(alice, carol, XRP(100)), fee(XRP(10)), delegate::as(bob), msig(daria, edward), ter(tefBAD_QUORUM)); env.close(); BEAST_EXPECT(env.balance(alice) == aliceBalance); BEAST_EXPECT(env.balance(bob) == bobBalance); BEAST_EXPECT(env.balance(carol) == carolBalance); BEAST_EXPECT(env.balance(daria) == dariaBalance); BEAST_EXPECT(env.balance(edward) == edwardBalance); } void run() override { testFeatureDisabled(); testDelegateSet(); testInvalidRequest(); testReserve(); testFee(); testSequence(); testAccountDelete(); testDelegateTransaction(); testPaymentGranular(); testTrustSetGranular(); testAccountSetGranular(); testMPTokenIssuanceSetGranular(); testSingleSign(); testSingleSignBadSecret(); testMultiSign(); testMultiSignQuorumNotMet(); } }; BEAST_DEFINE_TESTSUITE(Delegate, app, ripple); } // namespace test } // namespace ripple