mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-20 19:15:54 +00:00
Clarify Escrow semantics (RIPD-1571):
When creating an escrow, if the `CancelAfter` time is specified but the `FinishAfter` is not, the resulting escrow can be immediately completed using `EscrowFinish`. While this behavior is documented, it is unintuitive and can be confusing for users. This commit introduces a new fix amendment (fix1571) which prevents the creation of new Escrow entries that can be finished immediately and without any requirements. Once the amendment is activated, creating a new Escrow will require specifying the `FinishAfter` time explicitly or requires that a cryptocondition be specified.
This commit is contained in:
@@ -41,99 +41,52 @@
|
|||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Escrow allows an account holder to sequester any amount
|
Escrow
|
||||||
of XRP in its own ledger entry, until the escrow process
|
======
|
||||||
either finishes or is canceled.
|
|
||||||
|
|
||||||
If the escrow process finishes successfully, then the
|
Escrow is a feature of the XRP Ledger that allows you to send conditional
|
||||||
destination account (which must exist) will receives the
|
XRP payments. These conditional payments, called escrows, set aside XRP and
|
||||||
sequestered XRP. If the escrow is, instead, canceled,
|
deliver it later when certain conditions are met. Conditions to successfully
|
||||||
the account which created the escrow will receive the
|
finish an escrow include time-based unlocks and crypto-conditions. Escrows
|
||||||
sequestered XRP back instead.
|
can also be set to expire if not finished in time.
|
||||||
|
|
||||||
EscrowCreate
|
The XRP set aside in an escrow is locked up. No one can use or destroy the
|
||||||
|
XRP until the escrow has been successfully finished or canceled. Before the
|
||||||
|
expiration time, only the intended receiver can get the XRP. After the
|
||||||
|
expiration time, the XRP can only be returned to the sender.
|
||||||
|
|
||||||
When an escrow is created, an optional condition may
|
For more details on escrow, including examples, diagrams and more please
|
||||||
be attached. If present, that condition must be
|
visit https://ripple.com/build/escrow/#escrow
|
||||||
fulfilled for the escrow to successfully finish.
|
|
||||||
|
|
||||||
At the time of creation, one or both of the fields
|
For details on specific transactions, including fields and validation rules
|
||||||
sfCancelAfter and sfFinishAfter may be provided. If
|
please see:
|
||||||
neither field is specified, the transaction is
|
|
||||||
malformed.
|
|
||||||
|
|
||||||
Since the escrow eventually becomes a payment, an
|
`EscrowCreate`
|
||||||
optional DestinationTag and an optional SourceTag
|
--------------
|
||||||
are supported in the EscrowCreate transaction.
|
See: https://ripple.com/build/transactions/#escrowcreate
|
||||||
|
|
||||||
Validation rules:
|
`EscrowFinish`
|
||||||
|
--------------
|
||||||
|
See: https://ripple.com/build/transactions/#escrowfinish
|
||||||
|
|
||||||
sfCondition
|
`EscrowCancel`
|
||||||
If present, specifies a condition; the same
|
--------------
|
||||||
condition along with its matching fulfillment
|
See: https://ripple.com/build/transactions/#escrowcancel
|
||||||
are required during EscrowFinish.
|
|
||||||
|
|
||||||
sfCancelAfter
|
|
||||||
If present, escrow may be canceled after the
|
|
||||||
specified time (seconds after the Ripple epoch).
|
|
||||||
|
|
||||||
sfFinishAfter
|
|
||||||
If present, must be prior to sfCancelAfter.
|
|
||||||
A EscrowFinish succeeds only in ledgers after
|
|
||||||
sfFinishAfter but before sfCancelAfter.
|
|
||||||
|
|
||||||
If absent, same as parentCloseTime
|
|
||||||
|
|
||||||
Malformed if both sfCancelAfter, sfFinishAfter
|
|
||||||
are absent.
|
|
||||||
|
|
||||||
Malformed if both sfFinishAfter, sfCancelAfter
|
|
||||||
specified and sfCancelAfter <= sfFinishAfter
|
|
||||||
|
|
||||||
EscrowFinish
|
|
||||||
|
|
||||||
Any account may submit a EscrowFinish. If the escrow
|
|
||||||
ledger entry specifies a condition, the EscrowFinish
|
|
||||||
must provide the same condition and its associated
|
|
||||||
fulfillment in the sfCondition and sfFulfillment
|
|
||||||
fields, or else the EscrowFinish will fail.
|
|
||||||
|
|
||||||
If the escrow ledger entry specifies sfFinishAfter, the
|
|
||||||
transaction will fail if parentCloseTime <= sfFinishAfter.
|
|
||||||
|
|
||||||
EscrowFinish transactions must be submitted before
|
|
||||||
the escrow's sfCancelAfter if present.
|
|
||||||
|
|
||||||
If the escrow ledger entry specifies sfCancelAfter, the
|
|
||||||
transaction will fail if sfCancelAfter <= parentCloseTime.
|
|
||||||
|
|
||||||
NOTE: The reason the condition must be specified again
|
|
||||||
is because it must always be possible to verify
|
|
||||||
the condition without retrieving the escrow
|
|
||||||
ledger entry.
|
|
||||||
|
|
||||||
EscrowCancel
|
|
||||||
|
|
||||||
Any account may submit a EscrowCancel transaction.
|
|
||||||
|
|
||||||
If the escrow ledger entry does not specify a
|
|
||||||
sfCancelAfter, the cancel transaction will fail.
|
|
||||||
|
|
||||||
If parentCloseTime <= sfCancelAfter, the transaction
|
|
||||||
will fail.
|
|
||||||
|
|
||||||
When a escrow is canceled, the funds are returned to
|
|
||||||
the source account.
|
|
||||||
|
|
||||||
By careful selection of fields in each transaction,
|
|
||||||
these operations may be achieved:
|
|
||||||
|
|
||||||
* Lock up XRP for a time period
|
|
||||||
* Execute a payment conditionally
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Has the specified time passed?
|
||||||
|
|
||||||
|
@param now the current time
|
||||||
|
@param mark the cutoff point
|
||||||
|
@return true if \a now refers to a time strictly after \a mark, false otherwise.
|
||||||
|
*/
|
||||||
|
static inline bool after (NetClock::time_point now, std::uint32_t mark)
|
||||||
|
{
|
||||||
|
return now.time_since_epoch().count() > mark;
|
||||||
|
}
|
||||||
|
|
||||||
XRPAmount
|
XRPAmount
|
||||||
EscrowCreate::calculateMaxSpend(STTx const& tx)
|
EscrowCreate::calculateMaxSpend(STTx const& tx)
|
||||||
{
|
{
|
||||||
@@ -156,14 +109,26 @@ EscrowCreate::preflight (PreflightContext const& ctx)
|
|||||||
if (ctx.tx[sfAmount] <= beast::zero)
|
if (ctx.tx[sfAmount] <= beast::zero)
|
||||||
return temBAD_AMOUNT;
|
return temBAD_AMOUNT;
|
||||||
|
|
||||||
if (! ctx.tx[~sfCancelAfter] &&
|
// We must specify at least one timeout value
|
||||||
! ctx.tx[~sfFinishAfter])
|
if (! ctx.tx[~sfCancelAfter] && ! ctx.tx[~sfFinishAfter])
|
||||||
return temBAD_EXPIRATION;
|
return temBAD_EXPIRATION;
|
||||||
|
|
||||||
|
// If both finish and cancel times are specified then the cancel time must
|
||||||
|
// be strictly after the finish time.
|
||||||
if (ctx.tx[~sfCancelAfter] && ctx.tx[~sfFinishAfter] &&
|
if (ctx.tx[~sfCancelAfter] && ctx.tx[~sfFinishAfter] &&
|
||||||
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
|
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
|
||||||
return temBAD_EXPIRATION;
|
return temBAD_EXPIRATION;
|
||||||
|
|
||||||
|
if (ctx.rules.enabled(fix1571))
|
||||||
|
{
|
||||||
|
// In the absence of a FinishAfter, the escrow can be finished
|
||||||
|
// immediately, which can be confusing. When creating an escrow,
|
||||||
|
// we want to ensure that either a FinishAfter time is explicitly
|
||||||
|
// specified or a completion condition is attached.
|
||||||
|
if (! ctx.tx[~sfFinishAfter] && ! ctx.tx[~sfCondition])
|
||||||
|
return temMALFORMED;
|
||||||
|
}
|
||||||
|
|
||||||
if (auto const cb = ctx.tx[~sfCondition])
|
if (auto const cb = ctx.tx[~sfCondition])
|
||||||
{
|
{
|
||||||
using namespace ripple::cryptoconditions;
|
using namespace ripple::cryptoconditions;
|
||||||
@@ -193,20 +158,36 @@ EscrowCreate::doApply()
|
|||||||
{
|
{
|
||||||
auto const closeTime = ctx_.view ().info ().parentCloseTime;
|
auto const closeTime = ctx_.view ().info ().parentCloseTime;
|
||||||
|
|
||||||
if (ctx_.tx[~sfCancelAfter])
|
// Prior to fix1571, the cancel and finish times could be greater
|
||||||
|
// than or equal to the parent ledgers' close time.
|
||||||
|
//
|
||||||
|
// With fix1571, we require that they both be strictly greater
|
||||||
|
// than the parent ledgers' close time.
|
||||||
|
if (ctx_.view ().rules().enabled(fix1571))
|
||||||
{
|
{
|
||||||
auto const cancelAfter = ctx_.tx[sfCancelAfter];
|
if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter]))
|
||||||
|
return tecNO_PERMISSION;
|
||||||
|
|
||||||
if (closeTime.time_since_epoch().count() >= cancelAfter)
|
if (ctx_.tx[~sfFinishAfter] && after(closeTime, ctx_.tx[sfFinishAfter]))
|
||||||
return tecNO_PERMISSION;
|
return tecNO_PERMISSION;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (ctx_.tx[~sfFinishAfter])
|
|
||||||
{
|
{
|
||||||
auto const finishAfter = ctx_.tx[sfFinishAfter];
|
if (ctx_.tx[~sfCancelAfter])
|
||||||
|
{
|
||||||
|
auto const cancelAfter = ctx_.tx[sfCancelAfter];
|
||||||
|
|
||||||
if (closeTime.time_since_epoch().count() >= finishAfter)
|
if (closeTime.time_since_epoch().count() >= cancelAfter)
|
||||||
return tecNO_PERMISSION;
|
return tecNO_PERMISSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx_.tx[~sfFinishAfter])
|
||||||
|
{
|
||||||
|
auto const finishAfter = ctx_.tx[sfFinishAfter];
|
||||||
|
|
||||||
|
if (closeTime.time_since_epoch().count() >= finishAfter)
|
||||||
|
return tecNO_PERMISSION;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto const account = ctx_.tx[sfAccount];
|
auto const account = ctx_.tx[sfAccount];
|
||||||
@@ -383,17 +364,35 @@ EscrowFinish::doApply()
|
|||||||
if (! slep)
|
if (! slep)
|
||||||
return tecNO_TARGET;
|
return tecNO_TARGET;
|
||||||
|
|
||||||
// Too soon?
|
// If a cancel time is present, a finish operation should only succeed prior
|
||||||
if ((*slep)[~sfFinishAfter] &&
|
// to that time. fix1571 corrects a logic error in the check that would make
|
||||||
ctx_.view().info().parentCloseTime.time_since_epoch().count() <=
|
// a finish only succeed strictly after the cancel time.
|
||||||
(*slep)[sfFinishAfter])
|
if (ctx_.view ().rules().enabled(fix1571))
|
||||||
return tecNO_PERMISSION;
|
{
|
||||||
|
auto const now = ctx_.view().info().parentCloseTime;
|
||||||
|
|
||||||
// Too late?
|
// Too soon: can't execute before the finish time
|
||||||
if ((*slep)[~sfCancelAfter] &&
|
if ((*slep)[~sfFinishAfter] && ! after(now, (*slep)[sfFinishAfter]))
|
||||||
(*slep)[sfCancelAfter] <=
|
return tecNO_PERMISSION;
|
||||||
ctx_.view().info().parentCloseTime.time_since_epoch().count())
|
|
||||||
return tecNO_PERMISSION;
|
// Too late: can't execute after the cancel time
|
||||||
|
if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter]))
|
||||||
|
return tecNO_PERMISSION;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Too soon?
|
||||||
|
if ((*slep)[~sfFinishAfter] &&
|
||||||
|
ctx_.view().info().parentCloseTime.time_since_epoch().count() <=
|
||||||
|
(*slep)[sfFinishAfter])
|
||||||
|
return tecNO_PERMISSION;
|
||||||
|
|
||||||
|
// Too late?
|
||||||
|
if ((*slep)[~sfCancelAfter] &&
|
||||||
|
ctx_.view().info().parentCloseTime.time_since_epoch().count() <=
|
||||||
|
(*slep)[sfCancelAfter])
|
||||||
|
return tecNO_PERMISSION;
|
||||||
|
}
|
||||||
|
|
||||||
// Check cryptocondition fulfillment
|
// Check cryptocondition fulfillment
|
||||||
{
|
{
|
||||||
@@ -515,17 +514,32 @@ EscrowCancel::preflight (PreflightContext const& ctx)
|
|||||||
TER
|
TER
|
||||||
EscrowCancel::doApply()
|
EscrowCancel::doApply()
|
||||||
{
|
{
|
||||||
auto const k = keylet::escrow(
|
auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]);
|
||||||
ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]);
|
|
||||||
auto const slep = ctx_.view().peek(k);
|
auto const slep = ctx_.view().peek(k);
|
||||||
if (! slep)
|
if (! slep)
|
||||||
return tecNO_TARGET;
|
return tecNO_TARGET;
|
||||||
|
|
||||||
// Too soon?
|
if (ctx_.view ().rules().enabled(fix1571))
|
||||||
if (! (*slep)[~sfCancelAfter] ||
|
{
|
||||||
ctx_.view().info().parentCloseTime.time_since_epoch().count() <=
|
auto const now = ctx_.view().info().parentCloseTime;
|
||||||
|
|
||||||
|
// No cancel time specified: can't execute at all.
|
||||||
|
if (! (*slep)[~sfCancelAfter])
|
||||||
|
return tecNO_PERMISSION;
|
||||||
|
|
||||||
|
// Too soon: can't execute before the cancel time.
|
||||||
|
if (! after(now, (*slep)[sfCancelAfter]))
|
||||||
|
return tecNO_PERMISSION;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Too soon?
|
||||||
|
if (!(*slep)[~sfCancelAfter] ||
|
||||||
|
ctx_.view().info().parentCloseTime.time_since_epoch().count() <=
|
||||||
(*slep)[sfCancelAfter])
|
(*slep)[sfCancelAfter])
|
||||||
return tecNO_PERMISSION;
|
return tecNO_PERMISSION;
|
||||||
|
}
|
||||||
|
|
||||||
AccountID const account = (*slep)[sfAccount];
|
AccountID const account = (*slep)[sfAccount];
|
||||||
|
|
||||||
// Remove escrow from owner directory
|
// Remove escrow from owner directory
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ class FeatureCollections
|
|||||||
"fix1523",
|
"fix1523",
|
||||||
"fix1528",
|
"fix1528",
|
||||||
"DepositAuth",
|
"DepositAuth",
|
||||||
"Checks"
|
"Checks",
|
||||||
|
"fix1571"
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<uint256> features;
|
std::vector<uint256> features;
|
||||||
@@ -357,6 +358,7 @@ extern uint256 const fix1523;
|
|||||||
extern uint256 const fix1528;
|
extern uint256 const fix1528;
|
||||||
extern uint256 const featureDepositAuth;
|
extern uint256 const featureDepositAuth;
|
||||||
extern uint256 const featureChecks;
|
extern uint256 const featureChecks;
|
||||||
|
extern uint256 const fix1571;
|
||||||
|
|
||||||
} // ripple
|
} // ripple
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ detail::supportedAmendments ()
|
|||||||
{ "B9E739B8296B4A1BB29BE990B17D66E21B62A300A909F25AC55C22D6C72E1F9D fix1523" },
|
{ "B9E739B8296B4A1BB29BE990B17D66E21B62A300A909F25AC55C22D6C72E1F9D fix1523" },
|
||||||
{ "1D3463A5891F9E589C5AE839FFAC4A917CE96197098A1EF22304E1BC5B98A454 fix1528" },
|
{ "1D3463A5891F9E589C5AE839FFAC4A917CE96197098A1EF22304E1BC5B98A454 fix1528" },
|
||||||
{ "F64E1EABBE79D55B3BB82020516CEC2C582A98A6BFE20FBE9BB6A0D233418064 DepositAuth"},
|
{ "F64E1EABBE79D55B3BB82020516CEC2C582A98A6BFE20FBE9BB6A0D233418064 DepositAuth"},
|
||||||
{ "157D2D480E006395B76F948E3E07A45A05FE10230D88A7993C71F97AE4B1F2D1 Checks"}
|
{ "157D2D480E006395B76F948E3E07A45A05FE10230D88A7993C71F97AE4B1F2D1 Checks"},
|
||||||
|
{ "7117E2EC2DBF119CA55181D69819F1999ECEE1A0225A7FD2B9ED47940968479C fix1571" }
|
||||||
};
|
};
|
||||||
return supported;
|
return supported;
|
||||||
}
|
}
|
||||||
@@ -158,5 +159,6 @@ uint256 const fix1523 = *getRegisteredFeature("fix1523");
|
|||||||
uint256 const fix1528 = *getRegisteredFeature("fix1528");
|
uint256 const fix1528 = *getRegisteredFeature("fix1528");
|
||||||
uint256 const featureDepositAuth = *getRegisteredFeature("DepositAuth");
|
uint256 const featureDepositAuth = *getRegisteredFeature("DepositAuth");
|
||||||
uint256 const featureChecks = *getRegisteredFeature("Checks");
|
uint256 const featureChecks = *getRegisteredFeature("Checks");
|
||||||
|
uint256 const fix1571 = *getRegisteredFeature("fix1571");
|
||||||
|
|
||||||
} // ripple
|
} // ripple
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user