Implement Lending Protocol (unsupported) (#5270)

- Spec: XLS-66
- Introduces amendment "LendingProtocol", but leaves it UNSUPPORTED to
  allow for standalone testing, future development work, and potential
  bug fixes.
- AccountInfo RPC will indicate the type of pseudo-account when
  appropriate.
- Refactors and improves several existing classes and functional areas,
  including Number, STAmount, STObject, json_value, Asset, directory
  handling, View helper functions, and unit test helpers.
This commit is contained in:
Ed Hennis
2025-12-02 11:38:17 -05:00
committed by GitHub
parent c9f17dd85d
commit 6c67f1f525
86 changed files with 18810 additions and 553 deletions

View File

@@ -2,6 +2,8 @@
#include <test/jtx/AMMTest.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/mpt.h>
#include <test/jtx/testline.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
@@ -69,14 +71,14 @@ class Vault_test : public beast::unit_test::suite
this]() -> std::tuple<PrettyAsset, Account> {
auto const vault = env.le(keylet);
BEAST_EXPECT(vault != nullptr);
if (asset.raw().holds<Issue>() && !asset.raw().native())
if (!asset.integral())
BEAST_EXPECT(vault->at(sfScale) == 6);
else
BEAST_EXPECT(vault->at(sfScale) == 0);
auto const shares =
env.le(keylet::mptIssuance(vault->at(sfShareMPTID)));
BEAST_EXPECT(shares != nullptr);
if (asset.raw().holds<Issue>() && !asset.raw().native())
if (!asset.integral())
BEAST_EXPECT(shares->at(sfAssetScale) == 6);
else
BEAST_EXPECT(shares->at(sfAssetScale) == 0);
@@ -502,7 +504,7 @@ class Vault_test : public beast::unit_test::suite
}
}
if (!asset.raw().native() && asset.raw().holds<Issue>())
if (!asset.integral())
{
testcase(prefix + " temporary authorization for 3rd party");
env(trust(erin, asset(1000)));
@@ -670,12 +672,13 @@ class Vault_test : public beast::unit_test::suite
test(env, issuer, owner, asset, vault);
};
testCase(
[&](Env& env,
Account const& issuer,
Account const& owner,
Asset const& asset,
Vault& vault) {
auto testDisabled = [&](TER resultAfterCreate = temDISABLED) {
return [&, resultAfterCreate](
Env& env,
Account const& issuer,
Account const& owner,
Asset const& asset,
Vault& vault) {
testcase("disabled single asset vault");
auto [tx, keylet] =
@@ -684,7 +687,7 @@ class Vault_test : public beast::unit_test::suite
{
auto tx = vault.set({.owner = owner, .id = keylet.key});
env(tx, ter{temDISABLED});
env(tx, data("test"), ter{resultAfterCreate});
}
{
@@ -692,7 +695,7 @@ class Vault_test : public beast::unit_test::suite
{.depositor = owner,
.id = keylet.key,
.amount = asset(10)});
env(tx, ter{temDISABLED});
env(tx, ter{resultAfterCreate});
}
{
@@ -700,7 +703,7 @@ class Vault_test : public beast::unit_test::suite
{.depositor = owner,
.id = keylet.key,
.amount = asset(10)});
env(tx, ter{temDISABLED});
env(tx, ter{resultAfterCreate});
}
{
@@ -709,15 +712,49 @@ class Vault_test : public beast::unit_test::suite
.id = keylet.key,
.holder = owner,
.amount = asset(10)});
env(tx, ter{temDISABLED});
env(tx, ter{resultAfterCreate});
}
{
auto tx = vault.del({.owner = owner, .id = keylet.key});
env(tx, ter{resultAfterCreate});
}
};
};
testCase(
testDisabled(),
{.features = testable_amendments() - featureSingleAssetVault});
testCase(
testDisabled(tecNO_ENTRY),
{.features = testable_amendments() - featureMPTokensV1});
testCase(
[&](Env& env,
Account const& issuer,
Account const& owner,
Asset const& asset,
Vault& vault) {
testcase("disabled permissioned domains");
auto [tx, keylet] =
vault.create({.owner = owner, .asset = asset});
env(tx);
tx[sfFlags] = tx[sfFlags].asUInt() | tfVaultPrivate;
tx[sfDomainID] = to_string(base_uint<256>(42ul));
env(tx, ter{temDISABLED});
{
auto tx = vault.set({.owner = owner, .id = keylet.key});
env(tx, data("Test"));
tx[sfDomainID] = to_string(base_uint<256>(13ul));
env(tx, ter{temDISABLED});
}
},
{.features = testable_amendments() - featureSingleAssetVault});
{.features = testable_amendments() - featurePermissionedDomains});
testCase([&](Env& env,
Account const& issuer,
@@ -1730,7 +1767,8 @@ class Vault_test : public beast::unit_test::suite
mptt.create(
{.flags = tfMPTCanTransfer | tfMPTCanLock |
(args.enableClawback ? tfMPTCanClawback : none) |
(args.requireAuth ? tfMPTRequireAuth : none)});
(args.requireAuth ? tfMPTRequireAuth : none),
.mutableFlags = tmfMPTCanMutateCanTransfer});
PrettyAsset asset = mptt.issuanceID();
mptt.authorize({.account = owner});
mptt.authorize({.account = depositor});
@@ -2448,6 +2486,53 @@ class Vault_test : public beast::unit_test::suite
env(tx2, ter{tecWRONG_ASSET});
env.close();
}
testCase([this](
Env& env,
Account const&,
Account const& owner,
Account const& depositor,
PrettyAsset const& asset,
Vault& vault,
MPTTester& mptt) {
testcase("MPT non-transferable");
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
tx = vault.deposit(
{.depositor = depositor,
.id = keylet.key,
.amount = asset(100)});
env(tx);
env.close();
// Remove CanTransfer
mptt.set({.mutableFlags = tmfMPTClearCanTransfer});
env.close();
env(tx, ter{tecNO_AUTH});
env.close();
tx = vault.withdraw(
{.depositor = depositor,
.id = keylet.key,
.amount = asset(100)});
env(tx, ter{tecNO_AUTH});
env.close();
// Restore CanTransfer
mptt.set({.mutableFlags = tmfMPTSetCanTransfer});
env.close();
env(tx);
env.close();
// Delete vault with zero balance
env(vault.del({.owner = owner, .id = keylet.key}));
});
}
void
@@ -2460,6 +2545,7 @@ class Vault_test : public beast::unit_test::suite
int initialXRP = 1000;
Number initialIOU = 200;
double transferRate = 1.0;
bool charlieRipple = true;
};
auto testCase =
@@ -2485,8 +2571,21 @@ class Vault_test : public beast::unit_test::suite
PrettyAsset const asset = issuer["IOU"];
env.trust(asset(1000), owner);
env.trust(asset(1000), charlie);
env(pay(issuer, owner, asset(args.initialIOU)));
env.close();
if (!args.charlieRipple)
{
env(fset(issuer, 0, asfDefaultRipple));
env.close();
env.trust(asset(1000), charlie);
env.close();
env(pay(issuer, charlie, asset(args.initialIOU)));
env.close();
env(fset(issuer, asfDefaultRipple));
}
else
env.trust(asset(1000), charlie);
env.close();
env(rate(issuer, args.transferRate));
env.close();
@@ -2864,6 +2963,94 @@ class Vault_test : public beast::unit_test::suite
env(tx1);
});
testCase(
[&, this](
Env& env,
Account const& owner,
Account const& issuer,
Account const& charlie,
auto vaultAccount,
Vault& vault,
PrettyAsset const& asset,
std::function<MPTID(ripple::Keylet)> issuanceId) {
testcase("IOU non-transferable");
auto [tx, keylet] =
vault.create({.owner = owner, .asset = asset});
tx[sfScale] = 0;
env(tx);
env.close();
// Turn on noripple on the pseudo account's trust line.
// Charlie's is already set.
env(trust(issuer, vaultAccount(keylet)["IOU"], tfSetNoRipple),
THISLINE);
{
// Charlie cannot deposit
auto tx = vault.deposit(
{.depositor = charlie,
.id = keylet.key,
.amount = asset(100)});
env(tx, ter{terNO_RIPPLE}, THISLINE);
env.close();
}
{
PrettyAsset shares = issuanceId(keylet);
auto tx1 = vault.deposit(
{.depositor = owner,
.id = keylet.key,
.amount = asset(100)});
env(tx1, THISLINE);
env.close();
// Charlie cannot receive funds
auto tx2 = vault.withdraw(
{.depositor = owner,
.id = keylet.key,
.amount = shares(100)});
tx2[sfDestination] = charlie.human();
env(tx2, ter{terNO_RIPPLE}, THISLINE);
env.close();
{
// Create MPToken for shares held by Charlie
Json::Value tx{Json::objectValue};
tx[sfAccount] = charlie.human();
tx[sfMPTokenIssuanceID] =
to_string(shares.raw().get<MPTIssue>().getMptID());
tx[sfTransactionType] = jss::MPTokenAuthorize;
env(tx);
env.close();
}
env(pay(owner, charlie, shares(100)), THISLINE);
env.close();
// Charlie cannot withdraw
auto tx3 = vault.withdraw(
{.depositor = charlie,
.id = keylet.key,
.amount = shares(100)});
env(tx3, ter{terNO_RIPPLE});
env.close();
env(pay(charlie, owner, shares(100)), THISLINE);
env.close();
}
tx = vault.withdraw(
{.depositor = owner,
.id = keylet.key,
.amount = asset(100)});
env(tx, THISLINE);
env.close();
// Delete vault with zero balance
env(vault.del({.owner = owner, .id = keylet.key}), THISLINE);
},
{.charlieRipple = false});
testCase(
[&, this](
Env& env,
@@ -4525,7 +4712,7 @@ class Vault_test : public beast::unit_test::suite
BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50"));
BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000"));
BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50"));
BEAST_EXPECT(checkString(vault, sfLossUnrealized, "0"));
BEAST_EXPECT(!vault.isMember(sfLossUnrealized.getJsonName()));
auto const strShareID = strHex(sle->at(sfShareMPTID));
BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID));