mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-29 10:00:30 +00:00
fix: Use trustline balance direction to validate IOU PaymentMint/PaymentBurn (#7584)
This commit is contained in:
@@ -53,6 +53,7 @@
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
@@ -1105,10 +1106,43 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
env(delegate::set(alice, bob, {"PaymentBurn"}));
|
||||
env.close();
|
||||
|
||||
env(pay(alice, gw, usd(30)), Sendmax(usd(30)), delegate::As(bob));
|
||||
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)));
|
||||
}
|
||||
|
||||
// Test invalid fields or flags not allowed in granular permission template
|
||||
{
|
||||
Env env(*this, features);
|
||||
@@ -1184,22 +1218,22 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
mpt.authorize({.account = alice});
|
||||
mpt.authorize({.account = bob});
|
||||
|
||||
auto const MPT = mpt["MPT"]; // NOLINT(readability-identifier-naming)
|
||||
env(pay(gw, alice, MPT(500)));
|
||||
env(pay(gw, bob, MPT(500)));
|
||||
auto const gwMPT = mpt["MPT"];
|
||||
env(pay(gw, alice, gwMPT(500)));
|
||||
env(pay(gw, bob, gwMPT(500)));
|
||||
env.close();
|
||||
auto aliceMPT = env.balance(alice, MPT);
|
||||
auto bobMPT = env.balance(bob, MPT);
|
||||
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, MPT(50)), delegate::As(bob));
|
||||
BEAST_EXPECT(env.balance(alice, MPT) == aliceMPT + MPT(50));
|
||||
BEAST_EXPECT(env.balance(bob, MPT) == bobMPT);
|
||||
aliceMPT = env.balance(alice, MPT);
|
||||
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
|
||||
@@ -1207,26 +1241,235 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
env(delegate::set(alice, bob, {"PaymentBurn"}));
|
||||
env.close();
|
||||
|
||||
env(pay(alice, gw, MPT(50)), delegate::As(bob));
|
||||
BEAST_EXPECT(env.balance(alice, MPT) == aliceMPT - MPT(50));
|
||||
BEAST_EXPECT(env.balance(bob, MPT) == bobMPT);
|
||||
aliceMPT = env.balance(alice, MPT);
|
||||
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, MPT(50)), delegate::As(bob));
|
||||
BEAST_EXPECT(env.balance(alice, MPT) == aliceMPT - MPT(50));
|
||||
BEAST_EXPECT(env.balance(bob, MPT) == bobMPT);
|
||||
aliceMPT = env.balance(alice, MPT);
|
||||
env(pay(alice, bob, MPT(100)), delegate::As(bob));
|
||||
BEAST_EXPECT(env.balance(alice, MPT) == aliceMPT - MPT(100));
|
||||
BEAST_EXPECT(env.balance(bob, MPT) == bobMPT + MPT(100));
|
||||
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
|
||||
{
|
||||
@@ -2617,6 +2860,38 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
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
|
||||
{
|
||||
@@ -2647,6 +2922,7 @@ class Delegate_test : public beast::unit_test::Suite
|
||||
testTxDelegableCount();
|
||||
testNonDelegableTxWithDelegate(all);
|
||||
testDelegateUtilsNullptrCheck();
|
||||
testPermissionToTxType();
|
||||
}
|
||||
};
|
||||
BEAST_DEFINE_TESTSUITE(Delegate, app, xrpl);
|
||||
|
||||
Reference in New Issue
Block a user