Add support for XLS-85 Token Escrow (#5185)

- Specification: https://github.com/XRPLF/XRPL-Standards/pull/272
- Amendment: `TokenEscrow`
- Enables escrowing of IOU and MPT tokens in addition to native XRP.
- Allows accounts to lock issued tokens (IOU/MPT) in escrow objects, with support for freeze, authorization, and transfer rates.
- Adds new ledger fields (`sfLockedAmount`, `sfIssuerNode`, etc.) to track locked balances for IOU and MPT escrows.
- Updates EscrowCreate, EscrowFinish, and EscrowCancel transaction logic to support IOU and MPT assets, including proper handling of trustlines and MPT authorization, transfer rates, and locked balances.
- Enforces invariant checks for escrowed IOU/MPT amounts.
- Extends GatewayBalances RPC to report locked (escrowed) balances.
This commit is contained in:
Denis Angell
2025-06-03 18:51:55 +02:00
committed by GitHub
parent 7e24adbdd0
commit 053e1af7ff
39 changed files with 6420 additions and 766 deletions

View File

@@ -3651,10 +3651,10 @@ private:
// Can't pay into AMM with escrow.
testAMM([&](AMM& ammAlice, Env& env) {
auto const baseFee = env.current()->fees().base;
env(escrow(carol, ammAlice.ammAccount(), XRP(1)),
condition(cb1),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
env(escrow::create(carol, ammAlice.ammAccount(), XRP(1)),
escrow::condition(escrow::cb1),
escrow::finish_time(env.now() + 1s),
escrow::cancel_time(env.now() + 2s),
fee(baseFee * 150),
ter(tecNO_PERMISSION));
});

View File

@@ -335,26 +335,11 @@ public:
env(check::cancel(becky, checkId));
env.close();
// Lambda to create an escrow.
auto escrowCreate = [](jtx::Account const& account,
jtx::Account const& to,
STAmount const& amount,
NetClock::time_point const& cancelAfter) {
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowCreate;
jv[jss::Account] = account.human();
jv[jss::Destination] = to.human();
jv[jss::Amount] = amount.getJson(JsonOptions::none);
jv[sfFinishAfter.jsonName] =
cancelAfter.time_since_epoch().count() + 1;
jv[sfCancelAfter.jsonName] =
cancelAfter.time_since_epoch().count() + 2;
return jv;
};
using namespace std::chrono_literals;
std::uint32_t const escrowSeq{env.seq(alice)};
env(escrowCreate(alice, becky, XRP(333), env.now() + 2s));
env(escrow::create(alice, becky, XRP(333)),
escrow::finish_time(env.now() + 3s),
escrow::cancel_time(env.now() + 4s));
env.close();
// alice and becky should be unable to delete their accounts because
@@ -366,17 +351,39 @@ public:
// Now cancel the escrow, but create a payment channel between
// alice and becky.
// Lambda to cancel an escrow.
auto escrowCancel =
[](Account const& account, Account const& from, std::uint32_t seq) {
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowCancel;
jv[jss::Account] = account.human();
jv[sfOwner.jsonName] = from.human();
jv[sfOfferSequence.jsonName] = seq;
return jv;
};
env(escrowCancel(becky, alice, escrowSeq));
bool const withTokenEscrow =
env.current()->rules().enabled(featureTokenEscrow);
if (withTokenEscrow)
{
Account const gw1("gw1");
Account const carol("carol");
auto const USD = gw1["USD"];
env.fund(XRP(100000), carol, gw1);
env(fset(gw1, asfAllowTrustLineLocking));
env.close();
env.trust(USD(10000), carol);
env.close();
env(pay(gw1, carol, USD(100)));
env.close();
std::uint32_t const escrowSeq{env.seq(carol)};
env(escrow::create(carol, becky, USD(1)),
escrow::finish_time(env.now() + 3s),
escrow::cancel_time(env.now() + 4s));
env.close();
incLgrSeqForAccDel(env, gw1);
env(acctdelete(gw1, becky),
fee(acctDelFee),
ter(tecHAS_OBLIGATIONS));
env.close();
env(escrow::cancel(becky, carol, escrowSeq));
env.close();
}
env(escrow::cancel(becky, alice, escrowSeq));
env.close();
Keylet const alicePayChanKey{

View File

@@ -714,12 +714,12 @@ struct DepositPreauth_test : public beast::unit_test::suite
if (!supportsPreauth)
{
auto const seq1 = env.seq(alice);
env(escrow(alice, becky, XRP(100)),
finish_time(env.now() + 1s));
env(escrow::create(alice, becky, XRP(100)),
escrow::finish_time(env.now() + 1s));
env.close();
// Failed as rule is disabled
env(finish(gw, alice, seq1),
env(escrow::finish(gw, alice, seq1),
fee(1500),
ter(tecNO_PERMISSION));
env.close();
@@ -1387,12 +1387,13 @@ struct DepositPreauth_test : public beast::unit_test::suite
env.close();
auto const seq = env.seq(alice);
env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s));
env(escrow::create(alice, bob, XRP(1000)),
escrow::finish_time(env.now() + 1s));
env.close();
// zelda can't finish escrow with invalid credentials
{
env(finish(zelda, alice, seq),
env(escrow::finish(zelda, alice, seq),
credentials::ids({}),
ter(temMALFORMED));
env.close();
@@ -1404,14 +1405,14 @@ struct DepositPreauth_test : public beast::unit_test::suite
"0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E"
"01E034";
env(finish(zelda, alice, seq),
env(escrow::finish(zelda, alice, seq),
credentials::ids({invalidIdx}),
ter(tecBAD_CREDENTIALS));
env.close();
}
{ // Ledger closed, time increased, zelda can't finish escrow
env(finish(zelda, alice, seq),
env(escrow::finish(zelda, alice, seq),
credentials::ids({credIdx}),
fee(1500),
ter(tecEXPIRED));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1694,15 +1694,6 @@ class MPToken_test : public beast::unit_test::suite
jv[jss::SendMax] = mpt.getJson(JsonOptions::none);
test(jv, jss::SendMax.c_str());
}
// EscrowCreate
{
Json::Value jv;
jv[jss::TransactionType] = jss::EscrowCreate;
jv[jss::Account] = alice.human();
jv[jss::Destination] = carol.human();
jv[jss::Amount] = mpt.getJson(JsonOptions::none);
test(jv, jss::Amount.c_str());
}
// OfferCreate
{
Json::Value jv = offer(alice, USD(100), mpt);