mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-23 20:17:22 +00:00
This commit reworks the BaseUInt class, which was originally taken from Bitcoin and has since been heavily modified. The internal limb type now widens from 32 to 64 bits when the requested width is a multiple of 64. This halves the iteration count when working on limbs; coupled with the use of add-with-carry intrinsics, the result is better code generation and optimized use of processor resources. The constructors have been reshuffled, constraining how an instance can be initialized. As a result, many common initialization errors will now fail at compile time instead of run time. A `BaseUInt` can now be initialized with: - A byte sequence of fixed size, i.e an `std::array`, a C array, or a fixed-extent `std::span` of `unsigned char` or `std::byte`. - A string literal, formatted as a hexadecimal string with no leading 0x. Malformed or improperly sized inputs are a compile-time error. - An unsigned integer. This constructor is mostly used for tests, and must execute at compile time. - A byte sequence whose length is not known at compile time, via the fromRaw() function which returns a seated `std::optional` only if the input is byte sequence is properly sized. The runtime raw-pointer and `std::uint64_t` constructors are gone, as are the container assignment operator and the the `fromVoid` and `fromVoidChecked` functions. Other changes: - The SFINAE-based container trait is replaced by the ByteCopySource and FixedByteRange concepts. - operator== and operator<=> are now hidden friends. The workaround in the old operator<=> is gone. - Efficient tag conversion is now possible. - `BaseUInt` is now `noexcept` and (almost) fully `constexpr`: every operation except byte access, hashing and text output is usable in constant evaluation. Cleanups: - Most invocations of `stringIsUInt256Sized` in PeerImp are now gone and leverage `fromRaw`.
3023 lines
116 KiB
C++
3023 lines
116 KiB
C++
#include <test/jtx/Account.h>
|
|
#include <test/jtx/CaptureLogs.h>
|
|
#include <test/jtx/Env.h>
|
|
#include <test/jtx/TestHelpers.h>
|
|
#include <test/jtx/acctdelete.h>
|
|
#include <test/jtx/amount.h>
|
|
#include <test/jtx/balance.h>
|
|
#include <test/jtx/batch.h>
|
|
#include <test/jtx/delegate.h>
|
|
#include <test/jtx/delivermin.h>
|
|
#include <test/jtx/did.h>
|
|
#include <test/jtx/domain.h>
|
|
#include <test/jtx/fee.h>
|
|
#include <test/jtx/flags.h>
|
|
#include <test/jtx/mpt.h>
|
|
#include <test/jtx/multisign.h>
|
|
#include <test/jtx/noop.h>
|
|
#include <test/jtx/offer.h>
|
|
#include <test/jtx/paths.h>
|
|
#include <test/jtx/pay.h>
|
|
#include <test/jtx/rate.h>
|
|
#include <test/jtx/regkey.h>
|
|
#include <test/jtx/sendmax.h>
|
|
#include <test/jtx/sig.h>
|
|
#include <test/jtx/ter.h>
|
|
#include <test/jtx/trust.h>
|
|
#include <test/jtx/txflags.h>
|
|
#include <test/jtx/utility.h>
|
|
#include <test/jtx/vault.h>
|
|
|
|
#include <xrpl/basics/Number.h>
|
|
#include <xrpl/basics/Slice.h>
|
|
#include <xrpl/basics/base_uint.h>
|
|
#include <xrpl/basics/strHex.h>
|
|
#include <xrpl/beast/unit_test/suite.h>
|
|
#include <xrpl/json/json_value.h>
|
|
#include <xrpl/json/to_string.h>
|
|
#include <xrpl/ledger/Dir.h>
|
|
#include <xrpl/ledger/helpers/DelegateHelpers.h>
|
|
#include <xrpl/protocol/Feature.h>
|
|
#include <xrpl/protocol/Indexes.h>
|
|
#include <xrpl/protocol/Issue.h>
|
|
#include <xrpl/protocol/KeyType.h>
|
|
#include <xrpl/protocol/Permissions.h>
|
|
#include <xrpl/protocol/SField.h>
|
|
#include <xrpl/protocol/STObject.h>
|
|
#include <xrpl/protocol/STTx.h>
|
|
#include <xrpl/protocol/SecretKey.h>
|
|
#include <xrpl/protocol/TER.h>
|
|
#include <xrpl/protocol/TxFlags.h>
|
|
#include <xrpl/protocol/TxSettings.h>
|
|
#include <xrpl/protocol/XRPAmount.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
|
|
namespace xrpl::test {
|
|
class Delegate_test : public beast::unit_test::Suite
|
|
{
|
|
void
|
|
testFeatureDisabled(FeatureBitset features)
|
|
{
|
|
testcase("test feature not enabled");
|
|
using namespace jtx;
|
|
|
|
Env env{*this, features};
|
|
Account const gw{"gateway"};
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(1000000), gw, alice, bob);
|
|
env.close();
|
|
|
|
auto res = features[featurePermissionDelegationV1_1] ? Ter(tesSUCCESS) : Ter(temDISABLED);
|
|
|
|
// can not set Delegate when feature disabled
|
|
env(delegate::set(gw, alice, {"Payment"}), res);
|
|
env.close();
|
|
|
|
// can not send delegating transaction when feature disabled
|
|
env(pay(gw, bob, XRP(100)), delegate::As(alice), res);
|
|
}
|
|
|
|
void
|
|
testDelegateSet()
|
|
{
|
|
testcase("test valid request creating, updating, deleting permissions");
|
|
using namespace jtx;
|
|
|
|
Env env(*this);
|
|
Account const gw{"gateway"};
|
|
Account const alice{"alice"};
|
|
env.fund(XRP(100000), gw, alice);
|
|
env.close();
|
|
|
|
// delegating an empty permission list when the delegate ledger object
|
|
// does not exist is not allowed
|
|
env(delegate::set(gw, alice, {}), Ter(tecNO_ENTRY));
|
|
env.close();
|
|
|
|
auto const permissions = std::vector<std::string>{
|
|
"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<std::string> 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<std::string>{"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(FeatureBitset features)
|
|
{
|
|
testcase("test invalid DelegateSet");
|
|
using namespace jtx;
|
|
|
|
Env env(*this, features);
|
|
Account const gw{"gateway"};
|
|
Account const alice{"alice"};
|
|
Account const 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::ValueType::Array);
|
|
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
|
|
// tecNO_TARGET
|
|
{
|
|
env(delegate::set(gw, Account("unknown"), {"Payment"}), Ter(tecNO_TARGET));
|
|
}
|
|
|
|
// Delegating to a pseudo-account is not allowed, should return tecPSEUDO_ACCOUNT
|
|
{
|
|
Vault const vault{env};
|
|
auto [tx, keylet] = vault.create({.owner = gw, .asset = xrpIssue()});
|
|
env(tx);
|
|
env.close();
|
|
|
|
auto const sleVault = env.le(keylet);
|
|
BEAST_EXPECT(sleVault);
|
|
Account const vaultPseudo{"vault", sleVault->at(sfAccount)};
|
|
env(delegate::set(gw, vaultPseudo, {"Payment"}), Ter(tecPSEUDO_ACCOUNT));
|
|
}
|
|
|
|
// non-delegable transaction
|
|
{
|
|
env(delegate::set(gw, alice, {"SetRegularKey"}), Ter(temMALFORMED));
|
|
env(delegate::set(gw, alice, {"AccountSet"}), Ter(temMALFORMED));
|
|
env(delegate::set(gw, alice, {"SignerListSet"}), Ter(temMALFORMED));
|
|
env(delegate::set(gw, alice, {"DelegateSet"}), Ter(temMALFORMED));
|
|
env(delegate::set(gw, alice, {"EnableAmendment"}), Ter(temMALFORMED));
|
|
env(delegate::set(gw, alice, {"UNLModify"}), Ter(temMALFORMED));
|
|
env(delegate::set(gw, alice, {"SetFee"}), Ter(temMALFORMED));
|
|
env(delegate::set(gw, alice, {"Batch"}), Ter(temMALFORMED));
|
|
}
|
|
}
|
|
|
|
void
|
|
testReserve()
|
|
{
|
|
testcase("test reserve");
|
|
using namespace jtx;
|
|
|
|
// reserve requirement not met
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
auto const txFee = env.current()->fees().base;
|
|
env.fund(env.current()->fees().accountReserve(0, 1) + txFee, alice);
|
|
env.fund(XRP(100000), bob);
|
|
env.close();
|
|
|
|
// alice does not have enough reserve to create Delegate
|
|
env(delegate::set(alice, bob, {"Payment"}), Ter(tecINSUFFICIENT_RESERVE));
|
|
}
|
|
|
|
// reserve recovered after deleting delegation object
|
|
{
|
|
Env env(*this);
|
|
Account const bob{"bob"};
|
|
Account const alice{"alice"};
|
|
Account const carol{"carol"};
|
|
|
|
auto const txFee = env.current()->fees().base;
|
|
|
|
env.fund(env.current()->fees().accountReserve(1, 1) + (txFee * 4), alice);
|
|
env.fund(XRP(100000), bob, carol);
|
|
env.close();
|
|
|
|
// alice consumes 1 txFee and requires 1 object reserve
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env.close();
|
|
|
|
// alice does not have enough reserve to create another delegation object
|
|
env(delegate::set(alice, carol, {"Payment"}), Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
|
|
// deleting delegation object recovers 1 reserve
|
|
env(delegate::set(alice, bob, {}));
|
|
env.close();
|
|
|
|
// now alice can delegate again
|
|
env(delegate::set(alice, carol, {"Payment"}));
|
|
}
|
|
|
|
// test reserve when sending transaction on behalf of other account
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
env.fund(drops(env.current()->fees().accountReserve(1, 1)), alice);
|
|
env.fund(drops(env.current()->fees().accountReserve(2, 1)), 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;
|
|
|
|
// Common setup: fund alice, bob, carol with 1000 XRP.
|
|
auto setup = [&](Env& env) {
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
env.fund(XRP(1000), alice, bob, carol);
|
|
env.close();
|
|
return std::make_tuple(alice, bob, carol);
|
|
};
|
|
|
|
// No fee deduction for terNO_DELEGATE_PERMISSION.
|
|
{
|
|
Env env(*this);
|
|
auto [alice, bob, carol] = setup(env);
|
|
|
|
auto const aliceBalance = env.balance(alice);
|
|
auto const bobBalance = env.balance(bob);
|
|
auto const carolBalance = env.balance(carol);
|
|
|
|
env(pay(alice, carol, XRP(100)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance);
|
|
}
|
|
|
|
// Delegate pays the fee successfully.
|
|
{
|
|
Env env(*this);
|
|
auto [alice, bob, carol] = setup(env);
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env.close();
|
|
|
|
auto const aliceBalance = env.balance(alice);
|
|
auto const bobBalance = env.balance(bob);
|
|
auto const 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);
|
|
}
|
|
|
|
// Bob has insufficient balance to pay the fee, will get terINSUF_FEE_B.
|
|
{
|
|
Env env(*this);
|
|
auto [alice, bob, carol] = setup(env);
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env.close();
|
|
|
|
auto const aliceBalance = env.balance(alice);
|
|
auto const bobBalance = env.balance(bob);
|
|
auto const 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);
|
|
}
|
|
|
|
// The delegated account has enough balance to pay and delegator has enough reserve
|
|
{
|
|
// Common setup: fund accounts and grant Bob permission to pay on Alice's behalf.
|
|
// Alice is funded with exactly (paymentAmount + reserve + baseFee): baseFee covers
|
|
// the DelegateSet tx cost, leaving Alice with exactly (paymentAmount + reserve).
|
|
// highFee = reserve + baseFee, strictly greater than reserve, so that
|
|
// max(reserve, highFee) = highFee — making the direct payment check fail.
|
|
auto setup = [&](Env& env) {
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
|
|
auto const baseFee = env.current()->fees().base;
|
|
auto const reserve = env.current()->fees().accountReserve(1, 1);
|
|
auto const paymentAmount = XRP(1);
|
|
auto const highFee = reserve + baseFee;
|
|
BEAST_EXPECT(highFee > reserve);
|
|
|
|
env.fund(paymentAmount + reserve + baseFee, alice);
|
|
env.fund(XRP(1000), bob);
|
|
env.fund(XRP(1000), carol);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env.close();
|
|
|
|
env.require(Balance(alice, paymentAmount + reserve));
|
|
|
|
return std::make_tuple(alice, bob, carol, paymentAmount, highFee, reserve);
|
|
};
|
|
|
|
// Alice's balance (paymentAmount + reserve) is insufficient to cover both
|
|
// the payment and highFee directly. Even though fees are allowed to dip
|
|
// below reserve, when Alice pays the fee herself the required funds =
|
|
// paymentAmount + max(reserve, highFee) = paymentAmount + highFee
|
|
// (since highFee > reserve), which still exceeds her balance.
|
|
// tec: highFee is consumed from Alice's balance.
|
|
{
|
|
Env env(*this);
|
|
auto [alice, bob, carol, paymentAmount, highFee, reserve] = setup(env);
|
|
auto const aliceBalance = env.balance(alice);
|
|
auto const bobBalance = env.balance(bob);
|
|
auto const carolBalance = env.balance(carol);
|
|
|
|
env(pay(alice, carol, paymentAmount), Fee(highFee), Ter(tecUNFUNDED_PAYMENT));
|
|
|
|
// tec consumes the fee from Alice; carol and bob are unaffected.
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance - highFee);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance);
|
|
}
|
|
|
|
// The payment succeeds because the delegated account pays the fee.
|
|
// Alice only needs (paymentAmount + reserve).
|
|
{
|
|
Env env(*this);
|
|
auto [alice, bob, carol, paymentAmount, highFee, reserve] = setup(env);
|
|
|
|
auto const alicePrePay = env.balance(alice, XRP);
|
|
auto const bobPrePay = env.balance(bob, XRP);
|
|
auto const carolPrePay = env.balance(carol, XRP);
|
|
|
|
env(pay(alice, carol, paymentAmount), delegate::As(bob), Fee(highFee));
|
|
env.close();
|
|
|
|
env.require(Balance(alice, alicePrePay - paymentAmount));
|
|
env.require(Balance(bob, bobPrePay - highFee));
|
|
env.require(Balance(carol, carolPrePay + paymentAmount));
|
|
}
|
|
}
|
|
|
|
// Delegated account can pay the fee even if it dips below reserve.
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
|
|
auto const baseFee = env.current()->fees().base;
|
|
auto const baseReserve = env.current()->fees().accountReserve(0, 1);
|
|
|
|
env.fund(env.current()->fees().accountReserve(1, 1) + baseFee + XRP(1), alice);
|
|
env.fund(baseReserve, bob);
|
|
env.fund(XRP(1000), carol);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env.close();
|
|
|
|
auto const alicePreTx = env.balance(alice, XRP);
|
|
auto const bobPreTx = env.balance(bob, XRP);
|
|
|
|
// After paying for this transaction, bob's balance will
|
|
// dip below the base reserve
|
|
env(pay(alice, carol, XRP(1)), delegate::As(bob));
|
|
env.close();
|
|
|
|
// Bob's balance is now less than the base reserve.
|
|
BEAST_EXPECT(env.balance(bob, XRP) < baseReserve);
|
|
env.require(Balance(bob, bobPreTx - drops(baseFee)));
|
|
|
|
// Alice's balance only decreased by the 1.0 XRP she sent.
|
|
env.require(Balance(alice, alicePreTx - XRP(1)));
|
|
}
|
|
|
|
// The delegated account has enough balance for the fee, but delegator
|
|
// runs into tecUNFUNDED_PAYMENT.
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
|
|
auto const baseFee = env.current()->fees().base;
|
|
auto const reserve = env.current()->fees().accountReserve(1, 1);
|
|
|
|
// Alice is funded with (reserve + baseFee): after DelegateSet she has
|
|
// exactly 'reserve', which is insufficient to send XRP(10) while keeping
|
|
// reserve. Bob has plenty to pay the fee.
|
|
env.fund(reserve + baseFee, alice);
|
|
env.fund(XRP(1000), bob);
|
|
env.fund(XRP(1000), carol);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env.close();
|
|
|
|
auto const alicePrePay = env.balance(alice, XRP);
|
|
auto const bobPrePay = env.balance(bob, XRP);
|
|
auto const carolPrePay = env.balance(carol, XRP);
|
|
|
|
// Bob pays the fee, but Alice has insufficient balance to send XRP(10).
|
|
env(pay(alice, carol, XRP(10)), delegate::As(bob), Ter(tecUNFUNDED_PAYMENT));
|
|
|
|
env.require(Balance(alice, alicePrePay));
|
|
env.require(Balance(bob, bobPrePay - drops(baseFee)));
|
|
env.require(Balance(carol, carolPrePay));
|
|
}
|
|
}
|
|
|
|
void
|
|
testSequence()
|
|
{
|
|
testcase("test sequence");
|
|
using namespace jtx;
|
|
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const 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 kIncrement
|
|
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 kIncrement
|
|
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 kIncrement
|
|
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 kIncrement
|
|
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;
|
|
|
|
// Delegator (alice) deletes account: Delegate object is cleaned up from
|
|
// both alice's and bob's owner directories.
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
env.fund(XRP(100000), alice, bob, carol);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env.close();
|
|
|
|
auto const delegateKey = keylet::delegate(alice.id(), bob.id());
|
|
BEAST_EXPECT(env.closed()->exists(delegateKey));
|
|
|
|
auto hasKey = [](xrpl::Dir const& dir, UInt256 const& key) {
|
|
return std::any_of( // NOLINT(modernize-use-ranges)
|
|
dir.begin(), dir.end(), [&](auto const& sle) { return sle->key() == key; });
|
|
};
|
|
|
|
// Delegate object should appear in both alice's and bob's directories
|
|
BEAST_EXPECT(
|
|
hasKey(xrpl::Dir(*env.closed(), keylet::ownerDir(alice.id())), delegateKey.key));
|
|
BEAST_EXPECT(
|
|
hasKey(xrpl::Dir(*env.closed(), keylet::ownerDir(bob.id())), delegateKey.key));
|
|
|
|
for (std::uint32_t i = 0; i < 256; ++i)
|
|
env.close();
|
|
|
|
auto const aliceBalance = env.balance(alice);
|
|
auto const carolBalance = env.balance(carol);
|
|
|
|
// alice deletes account, this will remove the Delegate object from
|
|
// both alice's and bob's owner directories
|
|
auto const deleteFee = drops(env.current()->fees().increment);
|
|
env(acctdelete(alice, carol), 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.closed()->exists(delegateKey));
|
|
// bob's directory should no longer reference the Delegate object
|
|
BEAST_EXPECT(
|
|
!hasKey(xrpl::Dir(*env.closed(), keylet::ownerDir(bob.id())), delegateKey.key));
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance + aliceBalance - deleteFee);
|
|
}
|
|
|
|
// Delegatee (bob) deletes account: Delegate object is cleaned up from
|
|
// both alice's and bob's owner directories, freeing alice's reserve so
|
|
// she can subsequently delete her own account.
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
env.fund(XRP(100000), alice, bob, carol);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env.close();
|
|
|
|
auto const delegateKey = keylet::delegate(alice.id(), bob.id());
|
|
BEAST_EXPECT(env.closed()->exists(delegateKey));
|
|
|
|
auto hasKey = [](xrpl::Dir const& dir, UInt256 const& key) {
|
|
return std::any_of( // NOLINT(modernize-use-ranges)
|
|
dir.begin(), dir.end(), [&](auto const& sle) { return sle->key() == key; });
|
|
};
|
|
|
|
BEAST_EXPECT(
|
|
hasKey(xrpl::Dir(*env.closed(), keylet::ownerDir(alice.id())), delegateKey.key));
|
|
BEAST_EXPECT(
|
|
hasKey(xrpl::Dir(*env.closed(), keylet::ownerDir(bob.id())), delegateKey.key));
|
|
|
|
// The Delegate entry counts against alice's ownerCount.
|
|
auto const sleAlice = env.closed()->read(keylet::account(alice.id()));
|
|
BEAST_EXPECT(sleAlice);
|
|
BEAST_EXPECT(sleAlice->getFieldU32(sfOwnerCount) == 1);
|
|
|
|
for (std::uint32_t i = 0; i < 256; ++i)
|
|
env.close();
|
|
|
|
auto const bobBalance = env.balance(bob);
|
|
auto const carolBalance = env.balance(carol);
|
|
|
|
// bob (the authorized/delegatee account) deletes his account.
|
|
// This must clean up the Delegate object from both alice's and
|
|
// bob's owner directories so alice's delegation does not survive
|
|
// a potential account resurrection.
|
|
auto const deleteFee = drops(env.current()->fees().increment);
|
|
env(acctdelete(bob, carol), Fee(deleteFee));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(!env.closed()->exists(keylet::account(bob.id())));
|
|
BEAST_EXPECT(!env.closed()->exists(keylet::ownerDir(bob.id())));
|
|
BEAST_EXPECT(!env.closed()->exists(delegateKey));
|
|
// alice's directory should no longer reference the Delegate object
|
|
BEAST_EXPECT(
|
|
!hasKey(xrpl::Dir(*env.closed(), keylet::ownerDir(alice.id())), delegateKey.key));
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance + bobBalance - deleteFee);
|
|
|
|
// alice's ownerCount is now 0; she can delete her own account.
|
|
auto const sleAlice2 = env.closed()->read(keylet::account(alice.id()));
|
|
BEAST_EXPECT(sleAlice2);
|
|
BEAST_EXPECT(sleAlice2->getFieldU32(sfOwnerCount) == 0);
|
|
|
|
auto const aliceDeleteFee = drops(env.current()->fees().increment);
|
|
env(acctdelete(alice, carol), Fee(aliceDeleteFee));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(!env.closed()->exists(keylet::account(alice.id())));
|
|
}
|
|
|
|
// Multiple delegators -> same delegatee: when the delegatee (bob)
|
|
// deletes his account, ALL Delegate objects (from alice and carol)
|
|
// must be cleaned up from every delegator's directory.
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
Account const dave{"dave"};
|
|
env.fund(XRP(100000), alice, bob, carol, dave);
|
|
env.close();
|
|
|
|
// Both alice and carol delegate to bob
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env(delegate::set(carol, bob, {"EscrowCreate"}));
|
|
env.close();
|
|
|
|
auto const aliceBobKey = keylet::delegate(alice.id(), bob.id());
|
|
auto const carolBobKey = keylet::delegate(carol.id(), bob.id());
|
|
|
|
auto hasKey = [](xrpl::Dir const& dir, UInt256 const& key) {
|
|
return std::any_of( // NOLINT(modernize-use-ranges)
|
|
dir.begin(), dir.end(), [&](auto const& sle) { return sle->key() == key; });
|
|
};
|
|
|
|
// Both Delegate objects exist and are in bob's directory
|
|
BEAST_EXPECT(env.closed()->exists(aliceBobKey));
|
|
BEAST_EXPECT(env.closed()->exists(carolBobKey));
|
|
BEAST_EXPECT(
|
|
hasKey(xrpl::Dir(*env.closed(), keylet::ownerDir(bob.id())), aliceBobKey.key));
|
|
BEAST_EXPECT(
|
|
hasKey(xrpl::Dir(*env.closed(), keylet::ownerDir(bob.id())), carolBobKey.key));
|
|
|
|
for (std::uint32_t i = 0; i < 256; ++i)
|
|
env.close();
|
|
|
|
auto const bobBalance = env.balance(bob);
|
|
auto const daveBalance = env.balance(dave);
|
|
|
|
auto const deleteFee = drops(env.current()->fees().increment);
|
|
env(acctdelete(bob, dave), Fee(deleteFee));
|
|
env.close();
|
|
|
|
// bob's account and directory are gone
|
|
BEAST_EXPECT(!env.closed()->exists(keylet::account(bob.id())));
|
|
BEAST_EXPECT(!env.closed()->exists(keylet::ownerDir(bob.id())));
|
|
|
|
// Both Delegate objects are erased
|
|
BEAST_EXPECT(!env.closed()->exists(aliceBobKey));
|
|
BEAST_EXPECT(!env.closed()->exists(carolBobKey));
|
|
|
|
// alice's and carol's directories no longer reference the objects
|
|
BEAST_EXPECT(
|
|
!hasKey(xrpl::Dir(*env.closed(), keylet::ownerDir(alice.id())), aliceBobKey.key));
|
|
BEAST_EXPECT(
|
|
!hasKey(xrpl::Dir(*env.closed(), keylet::ownerDir(carol.id())), carolBobKey.key));
|
|
|
|
BEAST_EXPECT(env.balance(dave) == daveBalance + bobBalance - deleteFee);
|
|
}
|
|
}
|
|
|
|
void
|
|
testDelegateTransaction()
|
|
{
|
|
testcase("test delegate transaction");
|
|
using namespace jtx;
|
|
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const 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(terNO_DELEGATE_PERMISSION));
|
|
|
|
// carol does not have permission to create check
|
|
env(check::create(alice, bob, XRP(10)),
|
|
delegate::As(carol),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
|
|
void
|
|
testPaymentGranular(FeatureBitset features)
|
|
{
|
|
testcase("test payment granular");
|
|
using namespace jtx;
|
|
|
|
// test PaymentMint and PaymentBurn
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gateway"};
|
|
Account const 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(terNO_DELEGATE_PERMISSION));
|
|
env.require(Balance(bob, bobBalance));
|
|
|
|
// 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(terNO_DELEGATE_PERMISSION));
|
|
env.close();
|
|
env.require(Balance(bob, bobBalance));
|
|
|
|
// 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(terNO_DELEGATE_PERMISSION));
|
|
env.close();
|
|
env.require(Balance(bob, bobBalance));
|
|
|
|
// 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 const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const 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(terNO_DELEGATE_PERMISSION));
|
|
env.close();
|
|
env.require(Balance(bob, bobBalance));
|
|
|
|
// 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));
|
|
}
|
|
|
|
// disallow cross currency payment with only PaymentBurn/PaymentMint
|
|
// permission
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gateway"};
|
|
Account const carol{"carol"};
|
|
auto const usd = gw["USD"];
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, gw);
|
|
env.close();
|
|
env.trust(usd(50000), alice);
|
|
env.trust(usd(50000), bob);
|
|
env.trust(usd(50000), carol);
|
|
env(pay(gw, alice, usd(10000)));
|
|
env(pay(gw, bob, usd(10000)));
|
|
env(pay(gw, carol, usd(10000)));
|
|
env.close();
|
|
|
|
// PaymentMint
|
|
{
|
|
env(offer(carol, XRP(100), usd(501)));
|
|
BEAST_EXPECT(expectOffers(env, carol, 1));
|
|
env(delegate::set(gw, bob, {"PaymentMint"}));
|
|
env.close();
|
|
|
|
// bob can not send cross currency payment on behalf of the gw,
|
|
// even with PaymentMint permission and gw being the issuer.
|
|
env(pay(gw, alice, usd(5000)),
|
|
Sendmax(XRP(1001)),
|
|
Txflags(tfPartialPayment),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
BEAST_EXPECT(expectOffers(env, carol, 1));
|
|
|
|
env(pay(gw, alice, usd(5000)),
|
|
Path(~XRP),
|
|
Txflags(tfPartialPayment),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
BEAST_EXPECT(expectOffers(env, carol, 1));
|
|
|
|
// succeed with direct payment
|
|
env(pay(gw, alice, usd(100)), delegate::As(bob));
|
|
env.close();
|
|
}
|
|
|
|
// PaymentBurn
|
|
{
|
|
env(offer(bob, XRP(100), usd(501)));
|
|
BEAST_EXPECT(expectOffers(env, bob, 1));
|
|
env(delegate::set(alice, bob, {"PaymentBurn"}));
|
|
env.close();
|
|
|
|
// bob can not send cross currency payment on behalf of alice,
|
|
// even with PaymentBurn permission and gw being the issuer.
|
|
env(pay(alice, gw, usd(5000)),
|
|
Sendmax(XRP(1001)),
|
|
Txflags(tfPartialPayment),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
BEAST_EXPECT(expectOffers(env, bob, 1));
|
|
|
|
env(pay(alice, gw, usd(5000)),
|
|
Path(~XRP),
|
|
Txflags(tfPartialPayment),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
BEAST_EXPECT(expectOffers(env, bob, 1));
|
|
|
|
// succeed with direct payment
|
|
env(pay(alice, gw, usd(100)), delegate::As(bob));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
// PaymentMint/PaymentBurn with sfSendMax of the same asset is allowed,
|
|
// same-asset SendMax is still a direct payment, not cross-currency.
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gw"};
|
|
auto const usd = gw["USD"];
|
|
env.fund(XRP(10000), alice, bob, gw);
|
|
env.trust(usd(200), alice);
|
|
env.close();
|
|
|
|
env(delegate::set(gw, bob, {"PaymentMint"}));
|
|
env.close();
|
|
|
|
// sfSendMax with same asset as sfAmount, still a direct payment
|
|
env(pay(gw, alice, usd(50)), Sendmax(usd(50)), delegate::As(bob));
|
|
env.require(Balance(alice, usd(50)));
|
|
|
|
env(delegate::set(alice, bob, {"PaymentBurn"}));
|
|
env.close();
|
|
|
|
env(pay(alice, gw, usd(30)), delegate::As(bob));
|
|
env.require(Balance(alice, usd(20)));
|
|
}
|
|
|
|
// PaymentBurn is authorized by balance direction, not trust limit.
|
|
// holder is allowed to burn even if trust limit is 0.
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gateway"};
|
|
auto const gwUSD = gw["USD"];
|
|
auto const aliceUSD = alice["USD"];
|
|
|
|
env.fund(XRP(10000), alice, bob, gw);
|
|
env.trust(gwUSD(200), alice);
|
|
env.close();
|
|
|
|
env(pay(gw, alice, gwUSD(50)));
|
|
env.require(Balance(alice, gwUSD(50)));
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"PaymentBurn"}));
|
|
env.close();
|
|
|
|
env.trust(gwUSD(0), alice);
|
|
env.close();
|
|
BEAST_EXPECT(env.limit(alice, gwUSD.issue()) == gwUSD(0));
|
|
|
|
env(trust(gw, aliceUSD(200)));
|
|
env.close();
|
|
|
|
env(pay(alice, gw, gwUSD(30)), delegate::As(bob));
|
|
env.require(Balance(alice, gwUSD(20)));
|
|
env.require(Balance(gw, aliceUSD(-20)));
|
|
}
|
|
|
|
// PaymentBurn must not exceed the balance the account holds. Redeeming past
|
|
// zero makes the payment engine issue the account's own IOUs, which is a mint.
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gateway"};
|
|
auto const gwUSD = gw["USD"];
|
|
auto const aliceUSD = alice["USD"];
|
|
|
|
env.fund(XRP(10000), alice, bob, gw);
|
|
env.trust(gwUSD(200), alice);
|
|
env.close();
|
|
|
|
env(pay(gw, alice, gwUSD(50)));
|
|
env.close();
|
|
env.require(Balance(alice, gwUSD(50)));
|
|
|
|
// gw accepts alice-issued USD, so the engine has issuing liquidity
|
|
// available once the trustline reaches zero.
|
|
env(trust(gw, aliceUSD(200)));
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"PaymentBurn"}));
|
|
env.close();
|
|
|
|
if (!features[fixCleanup3_4_0])
|
|
{
|
|
// Pre-fixCleanup3_4_0: the balance direction alone authorizes the payment, so it
|
|
// redeems alice's 50 and then mints 50 alice-issued USD.
|
|
env(pay(alice, gw, gwUSD(100)), delegate::As(bob));
|
|
env.require(Balance(alice, gwUSD(-50)));
|
|
env.require(Balance(gw, aliceUSD(50)));
|
|
}
|
|
else
|
|
{
|
|
// Post-fixCleanup3_4_0: Rejected because it exceeds what alice holds.
|
|
env(pay(alice, gw, gwUSD(100)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
env.require(Balance(alice, gwUSD(50)));
|
|
env.require(Balance(gw, aliceUSD(-50)));
|
|
|
|
// Allowed because it is less than what alice holds.
|
|
env(pay(alice, gw, gwUSD(20)), delegate::As(bob));
|
|
env.require(Balance(alice, gwUSD(30)));
|
|
env.close();
|
|
|
|
// Exactly what alice holds: allowed, and settles at zero.
|
|
env(pay(alice, gw, gwUSD(30)), delegate::As(bob));
|
|
env.require(Balance(alice, gwUSD(0)));
|
|
env.close();
|
|
|
|
// Nothing left to burn: rejected.
|
|
env(pay(alice, gw, gwUSD(1)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
env.require(Balance(gw, aliceUSD(0)));
|
|
}
|
|
}
|
|
|
|
// A delegate holding both PaymentMint and PaymentBurn may cross zero.
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gateway"};
|
|
auto const gwUSD = gw["USD"];
|
|
auto const aliceUSD = alice["USD"];
|
|
|
|
env.fund(XRP(10000), alice, bob, gw);
|
|
env.trust(gwUSD(200), alice);
|
|
env.close();
|
|
|
|
env(pay(gw, alice, gwUSD(50)));
|
|
env(trust(gw, aliceUSD(200)));
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"PaymentBurn", "PaymentMint"}));
|
|
env.close();
|
|
|
|
env(pay(alice, gw, gwUSD(100)), delegate::As(bob));
|
|
env.require(Balance(alice, gwUSD(-50)));
|
|
env.require(Balance(gw, aliceUSD(50)));
|
|
}
|
|
|
|
// Test invalid fields or flags not allowed in granular permission template
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gw"};
|
|
auto const usd = gw["USD"];
|
|
env.fund(XRP(10000), alice, bob, gw);
|
|
env.trust(usd(200), alice);
|
|
env.close();
|
|
|
|
env(delegate::set(gw, bob, {"PaymentMint"}));
|
|
env(delegate::set(alice, bob, {"PaymentBurn"}));
|
|
env.close();
|
|
|
|
// sfDeliverMin (with tfPartialPayment) is not in the PaymentMint
|
|
// or PaymentBurn template.
|
|
env(pay(gw, alice, usd(100)),
|
|
DeliverMin(usd(50)),
|
|
Txflags(tfPartialPayment),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
env(pay(alice, gw, usd(50)),
|
|
DeliverMin(usd(25)),
|
|
Txflags(tfPartialPayment),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
|
|
// sfDomainID is not in the PaymentMint or PaymentBurn template.
|
|
env(pay(gw, alice, usd(100)),
|
|
Domain(UInt256{1}),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
env(pay(alice, gw, usd(50)),
|
|
Domain(UInt256{1}),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
|
|
// Delegate account holds no granular permissions for the tx type:
|
|
// getGranularPermission returns empty set.
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gw"};
|
|
auto const usd = gw["USD"];
|
|
env.fund(XRP(10000), alice, bob, gw);
|
|
env.trust(usd(200), alice);
|
|
env.close();
|
|
|
|
// Bob holds only an AccountSet granular permission.
|
|
env(delegate::set(alice, bob, {"AccountDomainSet"}));
|
|
env.close();
|
|
|
|
// Payment has granular permissions defined in permissions.macro,
|
|
// but bob only holds AccountSet's granular permission,
|
|
// getGranularPermission returns empty.
|
|
env(pay(alice, gw, usd(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
|
|
// PaymentMint and PaymentBurn for MPT
|
|
{
|
|
std::string logs;
|
|
Env env(*this, features, std::make_unique<CaptureLogs>(&logs));
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gateway"};
|
|
|
|
MPTTester mpt(env, gw, {.holders = {alice, bob}});
|
|
mpt.create({.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer});
|
|
|
|
mpt.authorize({.account = alice});
|
|
mpt.authorize({.account = bob});
|
|
|
|
auto const gwMPT = mpt["MPT"];
|
|
env(pay(gw, alice, gwMPT(500)));
|
|
env(pay(gw, bob, gwMPT(500)));
|
|
env.close();
|
|
auto aliceMPT = env.balance(alice, gwMPT);
|
|
auto bobMPT = env.balance(bob, gwMPT);
|
|
|
|
// PaymentMint
|
|
{
|
|
env(delegate::set(gw, bob, {"PaymentMint"}));
|
|
env.close();
|
|
|
|
env(pay(gw, alice, gwMPT(50)), delegate::As(bob));
|
|
BEAST_EXPECT(env.balance(alice, gwMPT) == aliceMPT + gwMPT(50));
|
|
BEAST_EXPECT(env.balance(bob, gwMPT) == bobMPT);
|
|
aliceMPT = env.balance(alice, gwMPT);
|
|
}
|
|
|
|
// PaymentBurn
|
|
{
|
|
env(delegate::set(alice, bob, {"PaymentBurn"}));
|
|
env.close();
|
|
|
|
env(pay(alice, gw, gwMPT(50)), delegate::As(bob));
|
|
BEAST_EXPECT(env.balance(alice, gwMPT) == aliceMPT - gwMPT(50));
|
|
BEAST_EXPECT(env.balance(bob, gwMPT) == bobMPT);
|
|
aliceMPT = env.balance(alice, gwMPT);
|
|
}
|
|
|
|
// Grant both granular permissions and tx level permission.
|
|
{
|
|
env(delegate::set(alice, bob, {"PaymentBurn", "PaymentMint", "Payment"}));
|
|
env.close();
|
|
env(pay(alice, gw, gwMPT(50)), delegate::As(bob));
|
|
BEAST_EXPECT(env.balance(alice, gwMPT) == aliceMPT - gwMPT(50));
|
|
BEAST_EXPECT(env.balance(bob, gwMPT) == bobMPT);
|
|
aliceMPT = env.balance(alice, gwMPT);
|
|
env(pay(alice, bob, gwMPT(100)), delegate::As(bob));
|
|
BEAST_EXPECT(env.balance(alice, gwMPT) == aliceMPT - gwMPT(100));
|
|
BEAST_EXPECT(env.balance(bob, gwMPT) == bobMPT + gwMPT(100));
|
|
}
|
|
}
|
|
|
|
// PaymentMint/PaymentBurn must not trust IOU issuer aliases.
|
|
// In a direct IOU payment, sfAmount.issuer may be encoded as either
|
|
// endpoint, and PaySteps normalizes those aliases to the same execution.
|
|
// These cases ensure a delegate cannot flip the encoded issuer to turn a
|
|
// mint into an apparent burn, or a burn into an apparent mint.
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gateway"};
|
|
auto const gwUSD = gw["USD"];
|
|
auto const aliceUSD = alice["USD"];
|
|
|
|
env.fund(XRP(10000), alice, bob, gw);
|
|
env.trust(gwUSD(200), alice);
|
|
env.close();
|
|
|
|
// Alice holds 100 USD issued by gw.
|
|
env(pay(gw, alice, gwUSD(100)));
|
|
env.close();
|
|
env.require(Balance(alice, gwUSD(100)));
|
|
|
|
// Delegate with only PaymentBurn tries to mint by encoding
|
|
// Amount.issuer as the destination alias, alice. The actual issuer
|
|
// is gw, so this requires PaymentMint and must be rejected.
|
|
{
|
|
env(delegate::set(gw, bob, {"PaymentBurn"}));
|
|
env.close();
|
|
|
|
// Amount.issuer = alice (destination), rejected because gw is
|
|
// the actual issuer and PaymentMint is required.
|
|
env(pay(gw, alice, aliceUSD(50)),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
env.require(Balance(alice, gwUSD(100)));
|
|
|
|
// Fails because bob holds PaymentBurn, not PaymentMint.
|
|
env(pay(gw, alice, gwUSD(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
env.require(Balance(alice, gwUSD(100)));
|
|
}
|
|
|
|
// Delegate with only PaymentMint tries to burn by encoding
|
|
// Amount.issuer as the source alias, alice. The actual issuer is
|
|
// gw, so this requires PaymentBurn and must be rejected.
|
|
{
|
|
env(delegate::set(alice, bob, {"PaymentMint"}));
|
|
env.close();
|
|
|
|
// Amount.issuer = alice (account), rejected because gw is the
|
|
// actual issuer and PaymentBurn is required.
|
|
env(pay(alice, gw, aliceUSD(50)),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
env.require(Balance(alice, gwUSD(100)));
|
|
|
|
// Fails because bob holds PaymentMint, not PaymentBurn.
|
|
env(pay(alice, gw, gwUSD(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
env.require(Balance(alice, gwUSD(100)));
|
|
}
|
|
}
|
|
|
|
// Neither account nor destination is issuer.
|
|
// PaymentMint and PaymentBurn do not authorize these payments.
|
|
{
|
|
// IOU
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gateway"};
|
|
Account const gw2{"gateway2"};
|
|
auto const gwUSD = gw["USD"];
|
|
|
|
env.fund(XRP(10000), alice, bob, gw, gw2);
|
|
env.close();
|
|
|
|
env.trust(gwUSD(200), alice);
|
|
env.close();
|
|
|
|
env(pay(gw, alice, gwUSD(100)));
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"PaymentMint", "PaymentBurn"}));
|
|
env.close();
|
|
|
|
env(pay(alice, gw, gw2["USD"](50)),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
|
|
// MPT
|
|
{
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gateway"};
|
|
Account const gw2{"gateway2"};
|
|
|
|
env.fund(XRP(10000), gw2);
|
|
env.close();
|
|
|
|
MPTTester mpt(env, gw, {.holders = {alice, bob}});
|
|
mpt.create({.ownerCount = 1, .flags = tfMPTCanTransfer});
|
|
|
|
mpt.authorize({.account = alice});
|
|
mpt.authorize({.account = bob});
|
|
|
|
auto const gwMPT = mpt["MPT"];
|
|
env(pay(gw, alice, gwMPT(500)));
|
|
env(pay(gw, bob, gwMPT(500)));
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"PaymentMint", "PaymentBurn"}));
|
|
env.close();
|
|
|
|
env(pay(alice, gw2, gwMPT(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
}
|
|
|
|
// IOU issuer is an endpoint, but no trustline exists.
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gateway"};
|
|
|
|
env.fund(XRP(10000), alice, bob, gw);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"PaymentMint", "PaymentBurn"}));
|
|
env.close();
|
|
|
|
env(pay(alice, gw, alice["USD"](50)),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
|
|
// Both trust limits (who is the designated issuer) and balance direction
|
|
// (which way DirectStepI executes) must be checked. Neither alone is sufficient.
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gateway"};
|
|
auto const gwUSD = gw["USD"];
|
|
auto const aliceUSD = alice["USD"];
|
|
|
|
env.fund(XRP(10000), alice, bob, gw);
|
|
|
|
// Alice trusts gw but holds zero gw-issued USD. With balance == 0,
|
|
// DirectStepI would issue rather than redeem, so PaymentBurn must
|
|
// be rejected even though Alice's trust limit to gw is positive.
|
|
{
|
|
env.trust(gwUSD(200), alice);
|
|
env.close();
|
|
|
|
// Alice has nothing to burn.
|
|
env(delegate::set(alice, bob, {"PaymentBurn"}));
|
|
env.close();
|
|
|
|
env(pay(alice, gw, gwUSD(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
env(pay(alice, gw, aliceUSD(50)),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
|
|
// Set up a trust line where gw holds alice-issued USD. DirectStepI
|
|
// would redeem rather than issue, so PaymentMint must be rejected
|
|
// even though the endpoint identity matches.
|
|
{
|
|
// Gw sets trust to accept alice-issued USD.
|
|
env(trust(gw, aliceUSD(200)));
|
|
env.close();
|
|
|
|
// Alice issues her own USD to gw; now gw holds alice's IOUs.
|
|
env(pay(alice, gw, aliceUSD(100)));
|
|
env.close();
|
|
|
|
// In gw's view, accountHolds(gw, USD, alice) > 0, so DirectStepI redeems.
|
|
// PaymentMint must be rejected because the step would redeem, not issue.
|
|
env(delegate::set(gw, bob, {"PaymentMint"}));
|
|
env.close();
|
|
|
|
env(pay(gw, alice, gwUSD(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
env(pay(gw, alice, aliceUSD(50)),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
}
|
|
|
|
// Alice trusts gw but gw is not willing to hold alice's IOU (destLimit == 0).
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gateway"};
|
|
env.fund(XRP(10000), alice, bob, gw);
|
|
env.trust(gw["USD"](200), alice);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"PaymentMint"}));
|
|
env.close();
|
|
|
|
env(pay(alice, gw, gw["USD"](50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
env(pay(alice, gw, alice["USD"](50)),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
|
|
// Verify granular permissions of different tx types in the same SLE are scoped
|
|
// correctly. AccountSet permissions don't apply to Payment and vice versa
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gw"};
|
|
auto const usd = gw["USD"];
|
|
env.fund(XRP(10000), alice, bob, gw);
|
|
env.trust(usd(200), alice);
|
|
env.close();
|
|
|
|
// Alice granted bob with both AccountDomainSet and PaymentMint.
|
|
env(delegate::set(alice, bob, {"AccountDomainSet", "PaymentMint"}));
|
|
env.close();
|
|
|
|
// PaymentMint fails at granular semantic check because alice is not the issuer.
|
|
env(pay(alice, gw, usd(50)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
|
|
// AccountDomainSet applies correctly to AccountSet
|
|
std::string const domain = "example.com";
|
|
auto jt = noop(alice);
|
|
jt[sfDomain] = strHex(domain);
|
|
jt[sfDelegate] = bob.human();
|
|
env(jt);
|
|
BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain));
|
|
|
|
// gw gives bob PaymentMint and bob can mint on gw's behalf
|
|
env(delegate::set(gw, bob, {"PaymentMint"}));
|
|
env.close();
|
|
env(pay(gw, alice, usd(50)), delegate::As(bob));
|
|
env.require(Balance(alice, usd(50)));
|
|
}
|
|
}
|
|
|
|
void
|
|
testTrustSetGranular()
|
|
{
|
|
testcase("test TrustSet granular permissions");
|
|
using namespace jtx;
|
|
|
|
// test TrustlineUnfreeze, TrustlineFreeze and TrustlineAuthorize
|
|
{
|
|
Env env(*this);
|
|
Account const gw{"gw"};
|
|
Account const alice{"alice"};
|
|
Account const 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(terNO_DELEGATE_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(terNO_DELEGATE_PERMISSION));
|
|
env(trust(alice, gw["USD"](50), tfClearNoRipple),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
env(trust(gw, gw["USD"](0), alice, tfSetDeepFreeze),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
env(trust(gw, gw["USD"](0), alice, tfClearDeepFreeze),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
env.close();
|
|
|
|
// supported flags with wrong permission
|
|
env(trust(gw, gw["USD"](0), alice, tfSetfAuth),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
env(trust(gw, gw["USD"](0), alice, tfSetFreeze),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
env.close();
|
|
|
|
env(delegate::set(gw, bob, {"TrustlineAuthorize"}));
|
|
env.close();
|
|
env(trust(gw, gw["USD"](0), alice, tfClearFreeze),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_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(terNO_DELEGATE_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(terNO_DELEGATE_PERMISSION));
|
|
|
|
// cannot update LimitAmount with granular permission, both high and
|
|
// low account
|
|
env(trust(alice, gw["USD"](100)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
env(trust(gw, alice["USD"](100)), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
|
|
// can not set QualityIn or QualityOut
|
|
auto tx = trust(alice, gw["USD"](50));
|
|
tx["QualityIn"] = "1000";
|
|
env(tx, delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
auto tx2 = trust(alice, gw["USD"](50));
|
|
tx2["QualityOut"] = "1000";
|
|
env(tx2, delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
auto tx3 = trust(gw, alice["USD"](50));
|
|
tx3["QualityIn"] = "1000";
|
|
env(tx3, delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
auto tx4 = trust(gw, alice["USD"](50));
|
|
tx4["QualityOut"] = "1000";
|
|
env(tx4, delegate::As(bob), Ter(terNO_DELEGATE_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(terNO_DELEGATE_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 const gw{"gw"};
|
|
Account const alice{"alice"};
|
|
Account const 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(terNO_DELEGATE_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(terNO_DELEGATE_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));
|
|
}
|
|
|
|
// tfFullyCanonicalSig won't block delegated transaction
|
|
{
|
|
Env env(*this);
|
|
Account const gw{"gw"};
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(10000), gw, alice, bob);
|
|
env(fset(gw, asfRequireAuth));
|
|
env.close();
|
|
env(trust(alice, gw["USD"](50)));
|
|
env.close();
|
|
|
|
env(delegate::set(gw, bob, {"TrustlineAuthorize"}));
|
|
env.close();
|
|
env(trust(gw, gw["USD"](0), alice, tfSetfAuth | tfFullyCanonicalSig),
|
|
delegate::As(bob));
|
|
}
|
|
|
|
{
|
|
Env env(*this);
|
|
Account const gw{"gw"};
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(10000), gw, alice, bob);
|
|
|
|
env(fset(gw, asfRequireAuth));
|
|
env.close();
|
|
env(trust(alice, gw["USD"](50)));
|
|
env.close();
|
|
env(delegate::set(gw, bob, {"TrustlineAuthorize"}));
|
|
env.close();
|
|
|
|
env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::As(bob));
|
|
env.close();
|
|
|
|
// sfQualityOut is a valid TrustSet field, but not permitted in granular template
|
|
json::Value txJson = trust(gw, gw["USD"](0), alice, tfSetfAuth);
|
|
txJson[sfQualityOut.jsonName] = 100;
|
|
env(txJson, delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
|
|
// tfSetNoRipple is a valid flag for TrustSet, but not permitted in granular template
|
|
env(trust(gw, gw["USD"](0), alice, tfSetfAuth | tfSetNoRipple),
|
|
delegate::As(bob),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
}
|
|
|
|
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] = strHex(domain);
|
|
jt[sfDelegate] = bob.human();
|
|
|
|
// 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(terNO_DELEGATE_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] = "";
|
|
env(jt);
|
|
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfDomain));
|
|
|
|
// bob tries to set unauthorized flag, it will fail
|
|
std::string const failDomain = "fail_domain_update";
|
|
jt[sfFlags] = tfRequireAuth;
|
|
jt[sfDomain] = strHex(failDomain);
|
|
env(jt, Ter(terNO_DELEGATE_PERMISSION));
|
|
// reset flag number
|
|
jt[sfFlags] = 0;
|
|
|
|
// bob tries to update domain and set email hash,
|
|
// but he does not have permission to set email hash
|
|
jt[sfDomain] = strHex(domain);
|
|
std::string const mh("5F31A79367DC3137FADA860C05742EE6");
|
|
jt[sfEmailHash] = mh;
|
|
env(jt, Ter(terNO_DELEGATE_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] = strHex(rkp.first.slice());
|
|
env(jt, Ter(terNO_DELEGATE_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] = "";
|
|
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(terNO_DELEGATE_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] = bob.human();
|
|
env(jtRate, delegate::As(bob));
|
|
BEAST_EXPECT((*env.le(alice))[sfTransferRate] == 2000000000);
|
|
|
|
// bob does not have permission to set ticksize for alice
|
|
jt[sfTickSize] = 8;
|
|
env(jt, Ter(terNO_DELEGATE_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(terNO_DELEGATE_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(terNO_DELEGATE_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] = 7;
|
|
env(jt, Ter(terNO_DELEGATE_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 jv2 = noop(alice);
|
|
jv2[sfDomain] = strHex(domain);
|
|
jv2[sfDelegate] = bob.human();
|
|
jv2[sfWalletLocator] = locator;
|
|
env(jv2, Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
|
|
// can not set AccountSet flags on behalf of other account,
|
|
// in permissions.macro, the template for AccountSet does
|
|
// not allow any flag set or clear.
|
|
{
|
|
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(terNO_DELEGATE_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(terNO_DELEGATE_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(terNO_DELEGATE_PERMISSION));
|
|
env(fset(alice, asfAccountTxnID));
|
|
env.close();
|
|
BEAST_EXPECT(env.le(alice)->isFieldPresent(sfAccountTxnID));
|
|
env(fclear(alice, asfAccountTxnID), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
|
|
// bob can not set asfAuthorizedNFTokenMinter on behalf of alice
|
|
json::Value jt = fset(alice, asfAuthorizedNFTokenMinter);
|
|
jt[sfDelegate] = bob.human();
|
|
jt[sfNFTokenMinter] = bob.human();
|
|
env(jt, Ter(terNO_DELEGATE_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(terNO_DELEGATE_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(terNO_DELEGATE_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(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
|
|
// tfFullyCanonicalSig won't block delegated transaction
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(10000), alice, bob);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"AccountDomainSet", "AccountEmailHashSet"}));
|
|
env.close();
|
|
|
|
std::string const domain = "example.com";
|
|
auto jt = noop(alice);
|
|
jt[sfDomain] = strHex(domain);
|
|
jt[sfDelegate] = bob.human();
|
|
jt[sfFlags] = tfFullyCanonicalSig;
|
|
|
|
env(jt);
|
|
BEAST_EXPECT((*env.le(alice))[sfDomain] == makeSlice(domain));
|
|
}
|
|
|
|
// setting invalid field not in permissions.macro template will be rejected.
|
|
{
|
|
Env env(*this);
|
|
auto const alice = Account{"alice"};
|
|
auto const bob = Account{"bob"};
|
|
env.fund(XRP(10000), alice, bob);
|
|
env.close();
|
|
|
|
// Alice gives Bob permission to set her Domain
|
|
env(delegate::set(alice, bob, {"AccountDomainSet"}));
|
|
env.close();
|
|
|
|
std::string const domain = "example.com";
|
|
auto txJson = noop(alice);
|
|
txJson[sfDomain] = strHex(domain);
|
|
txJson[sfDelegate] = bob.human();
|
|
|
|
// sfNFTokenMinter is a valid field in AccountSet tx, but
|
|
// it is not permitted for granular template
|
|
txJson[sfNFTokenMinter] = bob.human();
|
|
|
|
env(txJson, Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
|
|
// Delegated AccountSet with no fields and no flags is allowed,
|
|
// because it is allowed in the non-delegated case as well.
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(10000), alice, bob);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"AccountDomainSet"}));
|
|
env.close();
|
|
|
|
auto jt = noop(alice);
|
|
jt[sfDelegate] = bob.human();
|
|
env(jt);
|
|
}
|
|
|
|
// Revoking all permissions deletes the SLE and subsequent attempts are rejected.
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(10000), alice, bob);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"AccountDomainSet"}));
|
|
env.close();
|
|
|
|
std::string const domain = "example.com";
|
|
auto jt = noop(alice);
|
|
jt[sfDomain] = strHex(domain);
|
|
jt[sfDelegate] = bob.human();
|
|
env(jt);
|
|
|
|
// empty DelegateSet deletes the SLE
|
|
env(delegate::set(alice, bob, {}));
|
|
env.close();
|
|
|
|
env(jt, Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
}
|
|
|
|
void
|
|
testMPTokenIssuanceSetGranular()
|
|
{
|
|
testcase("test MPTokenIssuanceSet granular");
|
|
using namespace jtx;
|
|
|
|
// test MPTokenIssuanceUnlock and MPTokenIssuanceLock permissions
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const 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 = terNO_DELEGATE_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 = terNO_DELEGATE_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 = terNO_DELEGATE_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 const alice{"alice"};
|
|
Account const 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 = terNO_DELEGATE_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 = terNO_DELEGATE_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});
|
|
}
|
|
|
|
// tfFullyCanonicalSig won't block delegated transaction
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const 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 | tfFullyCanonicalSig, .delegate = bob});
|
|
}
|
|
|
|
// field not permitted to exist in granular delegation
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(100000), alice, bob);
|
|
|
|
MPTTester mpt(env, alice, {.fund = false});
|
|
mpt.create({.flags = tfMPTCanLock});
|
|
env.close();
|
|
|
|
// alice gives granular permission to bob for MPTokenIssuanceLock
|
|
env(delegate::set(alice, bob, {"MPTokenIssuanceLock"}));
|
|
env.close();
|
|
|
|
// tfMPTSetCanLock is a valid MPTokenIssuanceSet flag but is not
|
|
// covered by the MPTokenIssuanceLock granular permission, so a
|
|
// delegate holding only that permission cannot set it.
|
|
mpt.set(
|
|
{.account = alice,
|
|
.flags = tfMPTSetCanLock,
|
|
.delegate = bob,
|
|
.err = terNO_DELEGATE_PERMISSION});
|
|
|
|
// Notice: flags not defined in permissions.macro are not permitted for delegation.
|
|
// Since preflight will check invalid flag for the tx, it is not reachable.
|
|
// If any new flag is defined into the transaction in the future,
|
|
// but is not allowed for delegation, the transaction will be rejected with
|
|
// terNO_DELEGATE_PERMISSION. The set of permitted flags for delegation is defined in
|
|
// permissions.macro.
|
|
}
|
|
}
|
|
|
|
void
|
|
testSingleSign()
|
|
{
|
|
testcase("test single sign");
|
|
using namespace jtx;
|
|
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const 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 const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const 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);
|
|
}
|
|
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
env.fund(XRP(100000), alice, bob, carol);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"TrustSet"}));
|
|
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(carol),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance);
|
|
|
|
env(pay(alice, carol, XRP(100)),
|
|
Fee(XRP(10)),
|
|
delegate::As(bob),
|
|
Sig(alice),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance);
|
|
}
|
|
|
|
{
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
env.fund(XRP(100000), alice, bob, carol);
|
|
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(terNO_DELEGATE_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance);
|
|
|
|
env(pay(alice, carol, XRP(100)),
|
|
Fee(XRP(10)),
|
|
delegate::As(bob),
|
|
Sig(carol),
|
|
Ter(terNO_DELEGATE_PERMISSION));
|
|
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 const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
Account const daria{"daria"};
|
|
Account const 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 const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
Account const daria{"daria"};
|
|
Account const edward{"edward"};
|
|
Account const fred{"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
|
|
testMultiSignDelegatorAsSigner()
|
|
{
|
|
// checkMultiSign disallows the owner of the account to
|
|
// be part of the multisigner list. When it is a delegated transaction,
|
|
// the delegate account should not be part of the multisigner list.
|
|
testcase("test delegator as multisigner in delegate's signer list");
|
|
using namespace jtx;
|
|
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
Account const daria{"daria"};
|
|
env.fund(XRP(100000), alice, bob, carol, daria);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env.close();
|
|
|
|
// bob's signer list includes the delegator alice and daria
|
|
env(signers(bob, 2, {{alice, 1}, {daria, 1}}));
|
|
env.close();
|
|
|
|
auto aliceBalance = env.balance(alice);
|
|
auto bobBalance = env.balance(bob);
|
|
auto carolBalance = env.balance(carol);
|
|
auto const amt = 100;
|
|
|
|
// alice can sign as a multisigner for bob
|
|
env(pay(alice, carol, XRP(100)), Fee(XRP(10)), delegate::As(bob), Msig(alice, daria));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance - XRP(amt));
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance - XRP(10));
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance + XRP(amt));
|
|
|
|
// alice can not sign as a multisigner if she sent the transaction by herself.
|
|
env(pay(alice, carol, XRP(100)), Fee(XRP(10)), Msig(alice, daria), Ter(telENV_RPC_FAILED));
|
|
env.close();
|
|
|
|
// Get new balances
|
|
aliceBalance = env.balance(alice);
|
|
bobBalance = env.balance(bob);
|
|
carolBalance = env.balance(carol);
|
|
|
|
// bob (the delegate) should not appear as a multisigner in his transaction sent on behalf
|
|
// of alice. STTx::checkMultiSign catches this at the local-check stage, so the jtx
|
|
// framework returns telENV_RPC_FAILED.
|
|
env(pay(alice, carol, XRP(50)),
|
|
Fee(XRP(10)),
|
|
delegate::As(bob),
|
|
Msig(alice, bob),
|
|
Ter(telENV_RPC_FAILED));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance);
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance);
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance);
|
|
}
|
|
|
|
void
|
|
testSignForDelegated()
|
|
{
|
|
// In sortAndValidateSigners, if it is a delegated transaction, the delegate account is
|
|
// the forbidden account from appearing in its own Signers array.
|
|
testcase("test sign_for with delegated transaction");
|
|
using namespace jtx;
|
|
|
|
Env env(*this);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
Account const daria{"daria"};
|
|
env.fund(XRP(100000), alice, bob, carol, daria);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {"Payment"}));
|
|
env.close();
|
|
|
|
// bob's signer list includes the delegator alice and daria
|
|
env(signers(bob, 2, {{alice, 1}, {daria, 1}}));
|
|
env.close();
|
|
|
|
auto const baseFee = env.current()->fees().base;
|
|
|
|
auto const sendAmt = 1'000'000;
|
|
auto makeDelegateTx = [&]() -> json::Value {
|
|
json::Value jv;
|
|
jv[jss::tx_json][jss::Account] = alice.human();
|
|
jv[jss::tx_json][sfDelegate.jsonName] = bob.human();
|
|
jv[jss::tx_json][jss::TransactionType] = jss::Payment;
|
|
jv[jss::tx_json][jss::Destination] = carol.human();
|
|
jv[jss::tx_json][jss::Amount] = sendAmt;
|
|
jv[jss::tx_json][jss::Fee] = std::to_string((10 * baseFee).drops());
|
|
jv[jss::tx_json][jss::Sequence] = env.seq(alice);
|
|
jv[jss::tx_json][jss::SigningPubKey] = "";
|
|
return jv;
|
|
};
|
|
|
|
// The delegator alice and daria both sign via sign_for, which is valid
|
|
{
|
|
auto const aliceBalance = env.balance(alice);
|
|
auto const bobBalance = env.balance(bob);
|
|
auto const dariaBalance = env.balance(daria);
|
|
auto const carolBalance = env.balance(carol);
|
|
|
|
json::Value jv = makeDelegateTx();
|
|
jv[jss::account] = alice.human();
|
|
jv[jss::secret] = alice.name();
|
|
auto jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
|
|
BEAST_EXPECT(jrr[jss::status] == "success");
|
|
|
|
json::Value jv2;
|
|
jv2[jss::tx_json] = jrr[jss::tx_json];
|
|
jv2[jss::account] = daria.human();
|
|
jv2[jss::secret] = daria.name();
|
|
jrr = env.rpc("json", "sign_for", to_string(jv2))[jss::result];
|
|
BEAST_EXPECT(jrr[jss::status] == "success");
|
|
|
|
json::Value jvSubmit;
|
|
jvSubmit[jss::tx_json] = jrr[jss::tx_json];
|
|
jrr = env.rpc("json", "submit_multisigned", to_string(jvSubmit))[jss::result];
|
|
BEAST_EXPECT(jrr[jss::status] == "success");
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(alice) == aliceBalance - XRPAmount(sendAmt));
|
|
BEAST_EXPECT(env.balance(bob) == bobBalance - (10 * baseFee));
|
|
BEAST_EXPECT(env.balance(daria) == dariaBalance);
|
|
BEAST_EXPECT(env.balance(carol) == carolBalance + XRPAmount(sendAmt));
|
|
}
|
|
|
|
// The delegated account bob attempts sign_for, will be rejected.
|
|
{
|
|
json::Value jv = makeDelegateTx();
|
|
jv[jss::account] = bob.human();
|
|
jv[jss::secret] = bob.name();
|
|
auto jrr = env.rpc("json", "sign_for", to_string(jv))[jss::result];
|
|
BEAST_EXPECT(jrr[jss::status] == "error");
|
|
BEAST_EXPECT(
|
|
jrr[jss::error_message].asString().contains(
|
|
"A Signer may not be the transaction's Account"));
|
|
}
|
|
}
|
|
|
|
void
|
|
testPermissionValue(FeatureBitset features)
|
|
{
|
|
testcase("test permission value");
|
|
using namespace jtx;
|
|
|
|
Env env(*this, features);
|
|
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(100000), alice, bob);
|
|
env.close();
|
|
|
|
auto buildRequest = [&](auto value) -> json::Value {
|
|
json::Value jv;
|
|
jv[jss::TransactionType] = jss::DelegateSet;
|
|
jv[jss::Account] = alice.human();
|
|
jv[sfAuthorize.jsonName] = bob.human();
|
|
|
|
json::Value permissionsJson(json::ValueType::Array);
|
|
json::Value permissionValue;
|
|
permissionValue[sfPermissionValue.jsonName] = value;
|
|
json::Value permissionObj;
|
|
permissionObj[sfPermission.jsonName] = permissionValue;
|
|
permissionsJson.append(permissionObj);
|
|
jv[sfPermissions.jsonName] = permissionsJson;
|
|
|
|
return jv;
|
|
};
|
|
|
|
// invalid permission value.
|
|
// neither granular permission nor transaction level permission
|
|
for (auto value : {0, 100000, 54321})
|
|
{
|
|
auto jv = buildRequest(value);
|
|
env(jv, Ter(temMALFORMED));
|
|
}
|
|
}
|
|
|
|
void
|
|
testTxRequireFeatures(FeatureBitset features)
|
|
{
|
|
testcase("test delegate disabled tx");
|
|
using namespace jtx;
|
|
|
|
// map of tx and required feature.
|
|
// non-delegable tx are not included.
|
|
// NFTokenMint, NFTokenBurn, NFTokenCreateOffer, NFTokenCancelOffer,
|
|
// NFTokenAcceptOffer are not included, they are tested separately.
|
|
std::unordered_map<std::string, UInt256> txRequiredFeatures{
|
|
{"AMMClawback", featureAMMClawback},
|
|
{"AMMCreate", featureAMM},
|
|
{"AMMDeposit", featureAMM},
|
|
{"AMMWithdraw", featureAMM},
|
|
{"AMMVote", featureAMM},
|
|
{"AMMBid", featureAMM},
|
|
{"AMMDelete", featureAMM},
|
|
{"XChainCreateClaimID", featureXChainBridge},
|
|
{"XChainCommit", featureXChainBridge},
|
|
{"XChainClaim", featureXChainBridge},
|
|
{"XChainAccountCreateCommit", featureXChainBridge},
|
|
{"XChainAddClaimAttestation", featureXChainBridge},
|
|
{"XChainAddAccountCreateAttestation", featureXChainBridge},
|
|
{"XChainModifyBridge", featureXChainBridge},
|
|
{"XChainCreateBridge", featureXChainBridge},
|
|
{"DIDSet", featureDID},
|
|
{"DIDDelete", featureDID},
|
|
{"OracleSet", featurePriceOracle},
|
|
{"OracleDelete", featurePriceOracle},
|
|
{"LedgerStateFix", fixNFTokenPageLinks},
|
|
{"MPTokenIssuanceCreate", featureMPTokensV1},
|
|
{"MPTokenIssuanceDestroy", featureMPTokensV1},
|
|
{"MPTokenIssuanceSet", featureMPTokensV1},
|
|
{"MPTokenAuthorize", featureMPTokensV1},
|
|
{"CredentialCreate", featureCredentials},
|
|
{"CredentialAccept", featureCredentials},
|
|
{"CredentialDelete", featureCredentials},
|
|
{"NFTokenModify", featureDynamicNFT},
|
|
{"PermissionedDomainSet", featurePermissionedDomains},
|
|
{"PermissionedDomainDelete", featurePermissionedDomains},
|
|
{"SponsorshipSet", featureSponsor},
|
|
};
|
|
|
|
// Can not delegate tx if any required feature disabled.
|
|
{
|
|
auto txAmendmentDisabled = [&](FeatureBitset features, std::string const& tx) {
|
|
BEAST_EXPECT(txRequiredFeatures.contains(tx));
|
|
|
|
Env env(*this, features - txRequiredFeatures[tx]);
|
|
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(100000), alice, bob);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {tx}), Ter(temMALFORMED));
|
|
};
|
|
|
|
for (auto const& tx : txRequiredFeatures)
|
|
txAmendmentDisabled(features, tx.first);
|
|
}
|
|
|
|
// if all the required features in txRequiredFeatures are enabled, will
|
|
// succeed
|
|
{
|
|
auto txAmendmentEnabled = [&](std::string const& tx) {
|
|
Env env(*this, features);
|
|
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(100000), alice, bob);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {tx}));
|
|
};
|
|
|
|
for (auto const& tx : txRequiredFeatures)
|
|
txAmendmentEnabled(tx.first);
|
|
}
|
|
|
|
// Granular permissions also require the amendment for their underlying
|
|
// transaction type.
|
|
{
|
|
for (auto const permission : {"MPTokenIssuanceLock", "MPTokenIssuanceUnlock"})
|
|
{
|
|
Env env(*this, features - featureMPTokensV1);
|
|
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(100000), alice, bob);
|
|
env.close();
|
|
|
|
env(delegate::set(alice, bob, {permission}), Ter(temMALFORMED));
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testGranularSandboxCheckOrder()
|
|
{
|
|
testcase("Make sure GranularSandbox is checked after transaction-level permission");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env(*this);
|
|
Account const gw{"gw"};
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(10000), gw, alice, bob);
|
|
|
|
env(fset(gw, asfRequireAuth));
|
|
env.close();
|
|
env(trust(alice, gw["USD"](50)));
|
|
env.close();
|
|
env(delegate::set(gw, bob, {"TrustlineAuthorize"}));
|
|
env.close();
|
|
|
|
env(trust(gw, gw["USD"](0), alice, tfSetfAuth), delegate::As(bob));
|
|
env.close();
|
|
|
|
// sfQualityOut is a valid TrustSet field, but not permitted in granular template
|
|
json::Value txJson = trust(gw, gw["USD"](0), alice, tfSetfAuth);
|
|
txJson[sfQualityOut.jsonName] = 100;
|
|
env(txJson, delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
|
|
// Now Alice grants Bob with transaction level permission
|
|
env(delegate::set(gw, bob, {"TrustlineAuthorize", "TrustSet"}));
|
|
env.close();
|
|
|
|
// NOTE: This case is to ensure that if a delegate possesses a
|
|
// transaction-level permission (e.g., TrustSet), the granular sandbox must not incorrectly
|
|
// block the transaction. The function checkGranularSandbox MUST be called after the
|
|
// transaction-level permission check. This test case is to avoid future refactor mistakes,
|
|
// modifying the order will fail here.
|
|
env(txJson, delegate::As(bob));
|
|
}
|
|
|
|
void
|
|
testTxDelegableCount()
|
|
{
|
|
testcase("Delegable Transactions Completeness");
|
|
|
|
std::size_t delegableCount = 0;
|
|
|
|
#pragma push_macro("UNWRAP")
|
|
#undef UNWRAP
|
|
#pragma push_macro("TRANSACTION")
|
|
#undef TRANSACTION
|
|
|
|
#define UNWRAP(...) __VA_ARGS__
|
|
#define TRANSACTION(tag, value, name, settings, ...) \
|
|
if ((xrpl::TxSettings UNWRAP settings).delegable == xrpl::Delegation::Delegable) \
|
|
{ \
|
|
delegableCount++; \
|
|
}
|
|
|
|
#include <xrpl/protocol/detail/transactions.macro>
|
|
|
|
#undef TRANSACTION
|
|
#pragma pop_macro("TRANSACTION")
|
|
#undef UNWRAP
|
|
#pragma pop_macro("UNWRAP")
|
|
|
|
// ====================================================================
|
|
// IMPORTANT NOTICE:
|
|
//
|
|
// If this test fails, it indicates that the 'Delegation::delegable' status
|
|
// in transactions.macro has been changed. Delegation allows accounts to act
|
|
// on behalf of others, significantly increasing the security surface.
|
|
//
|
|
//
|
|
// To ENSURE any added transaction is safe and compatible with delegation:
|
|
//
|
|
// 1. Verify that the transaction is intended to be delegable.
|
|
// 2. Every standard test case for that transaction MUST be
|
|
// duplicated and verified for a Delegated context.
|
|
// 3. Ensure that Fee, Reserve, and Signing are correctly handled.
|
|
//
|
|
// DO NOT modify expectedDelegableCount unless all scenarios, including
|
|
// edge cases, have been fully tested and verified.
|
|
// ====================================================================
|
|
std::size_t const expectedDelegableCount = 57;
|
|
|
|
BEAST_EXPECTS(
|
|
delegableCount == expectedDelegableCount,
|
|
"\n[SECURITY] New delegable transaction detected!"
|
|
"\n Expected: " +
|
|
std::to_string(expectedDelegableCount) +
|
|
"\n Actual: " + std::to_string(delegableCount) +
|
|
"\n Action: Verify security requirements to interact with Delegation feature");
|
|
}
|
|
|
|
void
|
|
testNonDelegableTxWithDelegate(FeatureBitset features)
|
|
{
|
|
testcase("non-delegable tx with sfDelegate is rejected at preflight");
|
|
using namespace jtx;
|
|
|
|
Env env(*this, features);
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(10000), alice, bob);
|
|
env.close();
|
|
|
|
// Transactions that are notDelegable and have no granular permissions
|
|
// will be rejected with temINVALID at preflight.
|
|
// Note: pseudo-transactions (EnableAmendment, SetFee and UNLModify) are also
|
|
// notDelegable but are excluded here — passesLocalChecks() blocks them
|
|
// before preflight1 is ever reached.
|
|
{
|
|
// SetRegularKey, SignerListSet, AccountDelete, DelegateSet.
|
|
env(regkey(alice, bob), delegate::As(bob), Ter(temINVALID));
|
|
env(signers(alice, 1, {{bob, 1}}), delegate::As(bob), Ter(temINVALID));
|
|
env(acctdelete(alice, bob), delegate::As(bob), Ter(temINVALID));
|
|
env(delegate::set(alice, bob, {"Payment"}), delegate::As(bob), Ter(temINVALID));
|
|
|
|
// SAV transactions.
|
|
{
|
|
Vault const vault{env};
|
|
auto [createTx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()});
|
|
env(createTx, delegate::As(bob), Ter(temINVALID));
|
|
|
|
env(vault.set({.owner = alice, .id = keylet.key}),
|
|
delegate::As(bob),
|
|
Ter(temINVALID));
|
|
env(vault.del({.owner = alice, .id = keylet.key}),
|
|
delegate::As(bob),
|
|
Ter(temINVALID));
|
|
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = XRP(1)}),
|
|
delegate::As(bob),
|
|
Ter(temINVALID));
|
|
env(vault.withdraw({.depositor = alice, .id = keylet.key, .amount = XRP(1)}),
|
|
delegate::As(bob),
|
|
Ter(temINVALID));
|
|
env(vault.clawback({.issuer = alice, .id = keylet.key, .holder = bob}),
|
|
delegate::As(bob),
|
|
Ter(temINVALID));
|
|
}
|
|
|
|
// Batch transaction: the outer Batch itself is non-delegable.
|
|
{
|
|
auto const seq = env.seq(alice);
|
|
auto const batchFee = batch::calcBatchFee(env, 0, 1);
|
|
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
|
|
batch::Inner(pay(alice, bob, XRP(1)), seq + 1),
|
|
delegate::As(bob),
|
|
Ter(temINVALID));
|
|
}
|
|
|
|
// Lending protocol transactions
|
|
{
|
|
Vault const vault{env};
|
|
auto [createTx, keylet] = vault.create({.owner = alice, .asset = xrpIssue()});
|
|
env(createTx);
|
|
|
|
env(loan_broker::set(alice, keylet.key), delegate::As(bob), Ter(temINVALID));
|
|
env(loan_broker::del(alice, keylet.key), delegate::As(bob), Ter(temINVALID));
|
|
env(loan_broker::coverDeposit(alice, keylet.key, XRP(1)),
|
|
delegate::As(bob),
|
|
Ter(temINVALID));
|
|
env(loan_broker::coverWithdraw(alice, keylet.key, XRP(1)),
|
|
delegate::As(bob),
|
|
Ter(temINVALID));
|
|
env(loan_broker::coverClawback(alice), delegate::As(bob), Ter(temINVALID));
|
|
|
|
env(loan::set(alice, keylet.key, Number(100)), delegate::As(bob), Ter(temINVALID));
|
|
env(loan::manage(alice, keylet.key, 0), delegate::As(bob), Ter(temINVALID));
|
|
env(loan::del(alice, keylet.key), delegate::As(bob), Ter(temINVALID));
|
|
env(loan::pay(alice, keylet.key, XRP(1)), delegate::As(bob), Ter(temINVALID));
|
|
}
|
|
}
|
|
|
|
// AccountSet is notDelegable at tx level but has granular permissions,
|
|
// so sfDelegate passes preflight and is rejected at invokeCheckPermission with
|
|
// terNO_DELEGATE_PERMISSION.
|
|
{
|
|
env(fset(alice, asfDefaultRipple), delegate::As(bob), Ter(terNO_DELEGATE_PERMISSION));
|
|
}
|
|
}
|
|
|
|
void
|
|
testDelegateUtilsNullptrCheck()
|
|
{
|
|
testcase("DelegateUtils nullptr check");
|
|
|
|
// checkTxPermission nullptr check
|
|
STTx const tx{ttPAYMENT, [](STObject&) {}};
|
|
BEAST_EXPECT(checkTxPermission(nullptr, tx) == terNO_DELEGATE_PERMISSION);
|
|
|
|
// getGranularPermission nullptr check
|
|
auto const granularPermissions = getGranularPermission(nullptr, ttPAYMENT);
|
|
BEAST_EXPECT(granularPermissions.empty());
|
|
}
|
|
|
|
void
|
|
testPermissionToTxType()
|
|
{
|
|
testcase("test Permission to Tx type");
|
|
|
|
// 0 is not a valid permission value
|
|
BEAST_EXPECT(!Permission::permissionToTxType(0));
|
|
|
|
// 1 maps to Payment transaction
|
|
BEAST_EXPECT(Permission::permissionToTxType(1) == ttPAYMENT);
|
|
|
|
// UINT16_MAX+1 is the maximum possible tx-level permission value
|
|
constexpr uint32_t maxTxPermission = std::numeric_limits<uint16_t>::max() + 1u;
|
|
BEAST_EXPECT(Permission::permissionToTxType(maxTxPermission).has_value());
|
|
|
|
// exceeding maximum value should return nullopt
|
|
BEAST_EXPECT(!Permission::permissionToTxType(maxTxPermission + 1));
|
|
|
|
// All granular permission values should return nullopt since they do not map to a TxType.
|
|
for (auto const gp : {
|
|
#pragma push_macro("GRANULAR_PERMISSION")
|
|
#undef GRANULAR_PERMISSION
|
|
#define GRANULAR_PERMISSION(type, txType, value, ...) GranularPermissionType::type,
|
|
#include <xrpl/protocol/detail/permissions.macro>
|
|
#undef GRANULAR_PERMISSION
|
|
#pragma pop_macro("GRANULAR_PERMISSION")
|
|
})
|
|
{
|
|
BEAST_EXPECT(!Permission::permissionToTxType(static_cast<uint32_t>(gp)));
|
|
}
|
|
}
|
|
|
|
void
|
|
run() override
|
|
{
|
|
FeatureBitset const all = jtx::testableAmendments();
|
|
|
|
testFeatureDisabled(all - featurePermissionDelegationV1_1);
|
|
testFeatureDisabled(all);
|
|
testDelegateSet();
|
|
testInvalidRequest(all);
|
|
testReserve();
|
|
testFee();
|
|
testSequence();
|
|
testAccountDelete();
|
|
testDelegateTransaction();
|
|
testPaymentGranular(all);
|
|
testPaymentGranular(all - fixCleanup3_4_0);
|
|
testTrustSetGranular();
|
|
testAccountSetGranular();
|
|
testMPTokenIssuanceSetGranular();
|
|
testSingleSign();
|
|
testSingleSignBadSecret();
|
|
testMultiSign();
|
|
testMultiSignQuorumNotMet();
|
|
testMultiSignDelegatorAsSigner();
|
|
testSignForDelegated();
|
|
testPermissionValue(all);
|
|
testTxRequireFeatures(all);
|
|
testGranularSandboxCheckOrder();
|
|
testTxDelegableCount();
|
|
testNonDelegableTxWithDelegate(all);
|
|
testDelegateUtilsNullptrCheck();
|
|
testPermissionToTxType();
|
|
}
|
|
};
|
|
BEAST_DEFINE_TESTSUITE(Delegate, app, xrpl);
|
|
} // namespace xrpl::test
|