Compare commits

...
Author SHA1 Message Date
Denis Angell 9d5a406cc3 style: satisfy clang-tidy in token payment channel files 2026-10-19 14:04:04 -04:00
Denis Angell c0f4f855af chore: merge develop into token-paychan 2026-10-19 12:38:23 -04:00
Nik Bougalis 7939339a65 chore: Fix an issue that breaks CLion (#8619) 2026-10-17 22:43:53 +00:00
4ea2db2eda feat: Add TransactionProposal invariants for Cosign (#8130)
Co-authored-by: Shawn Xie <[email protected]>
Co-authored-by: Kassaking7 <[email protected]>
Co-authored-by: Shawn Xie <[email protected]>
Co-authored-by: Zhiyuan Wang <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
2026-10-16 20:59:53 +00:00
Denis Angell f0a2ecddab refactor: drop the unused rules parameter of payChanAmountPreflightHelper 2026-10-16 14:14:56 -04:00
Denis Angell ad73699c30 fix: return token amounts as objects in account_channels 2026-10-16 14:14:56 -04:00
Denis Angell 569a1aa674 test: account_channels shows token channel amounts as objects 2026-10-16 14:14:56 -04:00
Denis Angell 45f63750bc fix: count only own-issued open channel remainders in gateway_balances 2026-10-16 14:14:56 -04:00
Denis Angell 00a547668b test: gateway locked totals count only the account's own tokens 2026-10-16 14:14:55 -04:00
Denis Angell 4b82a19493 fix: use the pre-fee balance for the source's own channel close 2026-10-16 14:14:55 -04:00
Denis Angell ae6c4006d4 test: source close measures the trust line reserve before the fee 2026-10-16 14:14:55 -04:00
Denis Angell a0f04b4157 fix: check the post-lock reserve only for an XRP channel create 2026-10-16 14:14:54 -04:00
Denis Angell 7c8044fa0c test: sponsored token channel create matches escrow reserve rule 2026-10-16 14:14:54 -04:00
Denis Angell cb277e3512 fix: skip the XRP reserve checks for a token fund 2026-10-16 14:14:54 -04:00
Denis Angell bfc8b714a9 test: token fund needs no XRP reserve headroom 2026-10-16 14:14:54 -04:00
Denis Angell 925f7a0127 fix: limit the claim MPToken allowance to the submitter 2026-10-16 14:14:53 -04:00
Denis Angell 8e082ffcf0 test: a channel claim may create only the submitter's MPToken 2026-10-16 14:14:51 -04:00
Denis Angell e592a33428 fix: require fixCleanup3_2_0 and fixTokenEscrowV1 for token channels 2026-10-16 14:14:22 -04:00
Denis Angell fa9efc6e17 test: token channels require fixCleanup3_2_0 and fixTokenEscrowV1 2026-10-16 14:13:58 -04:00
Denis Angell bd8b57ba5f chore: merge develop into token-paychan 2026-10-16 14:13:53 -04:00
Ayaz Salikhov d4ffe32534 ci: Fix release/staging branches running conditions (#8615) 2026-10-16 17:06:19 +00:00
Gregory Tsipenyuk 7354816f65 fix: Assorted MPT/DEX offer related fixes - 1 (#8468) 2026-10-16 16:28:05 +00:00
Gregory Tsipenyuk 12cb3932f3 fix: Check vault share freeze at MPT strand endpoints (#8478) 2026-10-16 15:26:00 +00:00
Gregory Tsipenyuk dc64a8daac fix: Allow IOU rounding in Clawback invariant balance check (#8475) 2026-10-16 15:25:03 +00:00
Denis Angell d260b4be6d docs: state what the claim precision check guarantees 2026-10-07 10:57:54 -04:00
Denis Angell df4835d4f0 test: show IOU escrow credit rounding and the close-refund limit check 2026-10-07 10:31:12 -04:00
Denis Angell 522689aea6 chore: merge develop into token-paychan 2026-10-06 21:12:40 -04:00
Denis Angell 795eb2fdfb test: close an expired token channel through fund without creating a holding 2026-10-06 16:28:47 -04:00
Denis Angell e1ce7f40d8 fix: reject token channel locks that would round the source balance 2026-10-06 16:10:01 -04:00
Denis Angell cac9d753e1 docs: list token paychan API changes under 3.5.0 2026-10-06 13:19:57 -04:00
Denis Angell 9696e9021a refactor: move escrow and paychan helper specializations to source files 2026-10-06 13:18:28 -04:00
Denis Angell 8e82bc8de1 chore: merge develop into token-paychan 2026-10-06 13:06:44 -04:00
Denis Angell 21c4a3b02d fix: reject vault shares in token payment channels 2026-10-06 12:53:03 -04:00
Denis Angell 6a1cb5fc1a fix: keep a token channel's unclaimed remainder exact 2026-10-06 12:38:22 -04:00
Denis Angell 346316e9a3 fix: release the channel reserve before the token refund on close 2026-10-06 11:17:05 -04:00
Denis Angell 41aeb2149a fix: treat an overflowing IOU sum as inexact in isExactSum 2026-10-05 20:27:23 -04:00
Denis Angell 489d2d84fa Merge remote-tracking branch 'origin/develop' into token-paychan 2026-10-05 20:11:45 -04:00
Denis Angell 32c8893ee8 test: name the isExactSum operands so the swapped-argument check passes 2026-10-05 13:19:30 -04:00
Denis Angell 20b1c3efc2 fix: serialize token claim authorizations as the Amount field value 2026-10-05 11:42:45 -04:00
Denis Angell 59975e1742 fix: require an exact IOU sum when funding a payment channel 2026-10-05 11:32:36 -04:00
Denis Angell 0dc1ae9783 test: expect the large mantissa under MPTokensV2 in PayChanToken 2026-10-05 10:43:00 -04:00
Denis Angell a5610d8443 Merge remote-tracking branch 'refs/remotes/origin/develop' into token-paychan 2026-10-05 10:31:26 -04:00
Denis Angell 0536d4f0dd fix: include optional in PayChanToken test 2026-10-05 10:29:55 -04:00
Denis Angell 41bcd23edd fix: Guard MPT channel funding against sfAmount overflow 2026-10-03 06:41:46 +00:00
Denis Angell 4dbffc8201 fix: Validate PaymentChannelClaim Amount asset independently of Balance 2026-10-03 00:26:44 +00:00
Denis Angell d2ac3a0b58 Local merge develop into token-paychan 2026-10-02 21:26:19 +00:00
Denis Angell 830bc0d1f3 docs: Add token-enabled PaymentChannel schema changes to API-CHANGELOG 2026-10-01 20:57:24 +00:00
Denis Angell 9214e7d3fd fix: Gate MPT-denominated PaymentChannelClaim fields on featureMPTokensV1 2026-10-01 20:57:22 +00:00
Denis Angell 0c0a0e8794 fix: satisfy clang-tidy in token channel RPC tests 2026-09-30 11:49:52 -04:00
Denis Angell 86b3835d54 feat: accept token amounts in channel_authorize and channel_verify 2026-09-30 11:28:39 -04:00
Denis Angell 7990cfb8a0 fix: drop unused SeqProxy include from TestHelpers 2026-09-30 10:43:59 -04:00
Denis Angell c01a0539fe Merge remote-tracking branch 'origin/develop' into token-paychan 2026-09-30 10:41:48 -04:00
Denis Angell cdfe9cb65d fix: guard paychan rate helper and share gateway locked totals 2026-09-30 10:15:21 -04:00
Denis Angell 4258eca938 fix: include TestHelpers.h in PayChan_test for the Gate helper from develop 2026-09-13 19:25:35 -04:00
Denis Angell 01919230a5 Merge remote-tracking branch 'origin/develop' into token-paychan 2026-09-13 19:19:33 -04:00
Denis Angell 2ba6f4cc68 Merge remote-tracking branch 'origin/develop' into token-paychan
# Conflicts:
#	include/xrpl/ledger/helpers/EscrowHelpers.h
#	src/libxrpl/tx/invariants/MPTInvariant.cpp
#	src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp
#	src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp
#	src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp
#	src/test/jtx/impl/TestHelpers.cpp
2026-08-31 11:35:47 -04:00
Denis Angell c5ae59c4a0 fix: gate paychan claim MPToken invariant relaxation on featureTokenPaychan 2026-08-25 11:10:39 -04:00
Denis Angell 1ac1d40d06 fix: satisfy clang-tidy include-cleaner and brace style in paychan 2026-08-03 12:26:20 -04:00
Denis Angell fb10515cff Merge remote-tracking branch 'origin/develop' into token-paychan
# Conflicts:
#	include/xrpl/protocol/detail/features.macro
2026-08-03 11:38:43 -04:00
Denis Angell 3c85a0180f refactor: address review nits 2026-08-02 16:05:25 -04:00
Denis Angell 096fa967da fix: repair bad develop merge resolutions 2026-08-02 16:05:22 -04:00
dangell8 1ced7dc8f3 Merge branch 'develop' into token-paychan
Includes post-merge compile fixes (build-verified).
2026-07-17 15:02:03 +00:00
Denis Angell df1458a136 test: expand token paychan transactor phase coverage 2026-07-13 19:25:43 -04:00
Denis Angell cdd5ebf4c1 fix: validate paychan asset match in preclaim with tecWRONG_ASSET 2026-07-13 16:33:49 -04:00
Denis Angell 1a9a11fc31 style: remove includes flagged by misc-include-cleaner 2026-07-13 15:28:42 -04:00
Denis Angell a9430d4778 style: conform doxygen comments to check-doxygen-style hook 2026-07-13 15:11:48 -04:00
Denis Angell fed5093488 fix: adapt token paychan to develop merge helpers and sponsor reserve 2026-07-13 14:59:54 -04:00
Denis Angell 6c74fc1a7d Merge branch 'develop' into token-paychan 2026-07-13 14:05:56 -04:00
dangell8 939f8b1a49 Merge develop into token-paychan 2026-07-11 04:27:08 +00:00
Denis Angell 1f8c462c42 feat: XLS-93d Token-Enabled Payment Channels 2026-07-10 12:14:12 -04:00
Denis Angell 47df026dcc refactor: Move escrow lock/unlock helpers to EscrowHelpers.h 2026-07-10 11:55:13 -04:00
66 changed files with 9358 additions and 794 deletions

No files matched your search

+1
View File
@@ -8,6 +8,7 @@ on:
branches:
- "develop"
- "release/*"
- "staging/*"
paths:
- "crates/**/Cargo.toml"
- "crates/Cargo.lock"
+1 -1
View File
@@ -243,7 +243,7 @@ jobs:
- should-run
- build-test
# Only run when committing to a PR that targets a release branch.
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release') }}
if: ${{ github.repository == 'XRPLF/rippled' && needs.should-run.outputs.go == 'true' && github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release/') }}
uses: ./.github/workflows/reusable-upload-recipe.yml
secrets:
remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }}
+8 -6
View File
@@ -1,14 +1,16 @@
# This workflow runs all workflows to build and test the code on various Linux
# flavors, as well as on MacOS and Windows, on a scheduled basis, on merge into
# the 'develop' or 'release*' branches, or when requested manually. Upon pushes
# to the develop branch it also uploads the libxrpl recipe to the Conan remote.
# the 'develop', 'release/*' or 'staging/*' branches, or when requested
# manually. Upon pushes to the develop branch, it also uploads the libxrpl
# recipe to the Conan remote.
name: Trigger
on:
push:
branches:
- "develop"
- "release*"
- "release/*"
- "staging/*"
paths:
# These paths are unique to `on-trigger.yml`.
- ".github/workflows/on-trigger.yml"
@@ -62,9 +64,9 @@ on:
workflow_dispatch:
concurrency:
# When a PR is merged into the develop branch it will be assigned a unique
# When a PR is merged into the develop branch, it will be assigned a unique
# group identifier, so execution will continue even if another PR is merged
# while it is still running. In all other cases the group identifier is shared
# while it is still running. In all other cases, the group identifier is shared
# per branch, so that any in-progress runs are cancelled when a new commit is
# pushed.
group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.sha || github.ref }}
@@ -98,7 +100,7 @@ jobs:
# However, we do not enable ccache for events targeting a release branch,
# to protect against the rare case that the output produced by ccache is
# not identical to a regular compilation.
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release') }}
ccache_enabled: ${{ github.repository_owner == 'XRPLF' && !startsWith(github.ref, 'refs/heads/release/') }}
os: ${{ matrix.os }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+2 -1
View File
@@ -8,7 +8,8 @@ on:
push:
branches:
- "develop"
- "release*"
- "release/*"
- "staging/*"
workflow_dispatch:
jobs:
+4
View File
@@ -30,6 +30,10 @@ Version 3.5.0 is not yet released.
- `subscribe`, `unsubscribe`: Added an optional `mpt_issuances` request field, an array of MPT issuance IDs (hex strings). Subscribers receive the same `transaction` message as the `transactions` stream for each validated transaction whose metadata affects a subscribed issuance. MPT issuance subscriptions count toward the per-connection subscription limit. An empty array, a non-array value, or an invalid ID returns `invalidParams`. ([#5671](https://github.com/XRPLF/rippled/pull/5671))
- `ledger_entry`: Add full support for checks, NFT offers, payment channels, and signer lists. ([#6319](https://github.com/XRPLF/rippled/pull/6319))
- `channel_authorize`, `channel_verify`: `amount` now also accepts the transaction `Amount` JSON object for a token payment channel (`currency`, `issuer` and `value` for a trust line token; `mpt_issuance_id` and `value` for an MPT), and the claim authorization is serialized for that token. A string of drops is still used for an XRP channel. An object that names XRP, is negative or is malformed returns `channelAmtMalformed`. On the command line, the amount argument may be that JSON object as a single argument. For a token channel the signed message is the `PaymentChannelClaim` hash prefix, the channel ID and the amount serialized as an `Amount` field value without its field header; the XRP message is unchanged. ([#7935](https://github.com/XRPLF/rippled/pull/7935))
- `PaymentChannelCreate`, `PaymentChannelFund`, `PaymentChannelClaim`: `Amount` (and `Balance` on `PaymentChannelClaim`) now accept a trust line or MPT value in addition to XRP, gated by the `TokenPaychan` amendment. ([#7935](https://github.com/XRPLF/rippled/pull/7935))
- `ledger_entry`, `account_objects`: The `PayChannel` ledger entry now includes optional `TransferRate` and `IssuerNode` fields, present on a channel funded with a trust line or MPT value. ([#7935](https://github.com/XRPLF/rippled/pull/7935))
- `account_channels`: For a token payment channel, `amount` and `balance` are `Amount` JSON objects (`currency`, `issuer` and `value` for a trust line token; `mpt_issuance_id` and `value` for an MPT). An XRP channel still returns strings of drops. ([#7935](https://github.com/XRPLF/rippled/pull/7935))
### Bugfixes in 3.5.0
+17 -428
View File
@@ -1,32 +1,19 @@
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Fees.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
@@ -53,153 +40,22 @@ escrowLockPreclaimHelper(
beast::Journal j);
template <>
inline TER
TER
escrowLockPreclaimHelper<Issue>(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal j)
{
auto const& issue = amount.get<Issue>();
auto const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
auto const sleIssuer = view.read(keylet::account(issuer));
if (!sleIssuer)
return tecNO_ISSUER;
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
return tecNO_PERMISSION;
// If the account does not have a trustline to the issuer, return tecNO_LINE
auto const sleRippleState = view.read(keylet::trustLine(account, issuer, issue.currency));
if (!sleRippleState)
return tecNO_LINE;
STAmount const balance = (*sleRippleState)[sfBalance];
// If balance is positive, issuer must have higher address than account
if (balance > beast::kZero && issuer < account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If balance is negative, issuer must have lower address than account
if (balance < beast::kZero && issuer > account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(view, issue, account); !isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(view, issue, dest); !isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecFROZEN
if (isFrozen(view, account, issue))
return tecFROZEN;
// If the issuer has frozen the destination, return tecFROZEN
if (isFrozen(view, dest, issue))
return tecFROZEN;
STAmount const spendableAmount =
accountHolds(view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
// If the amount is not addable to the balance, return tecPRECISION_LOSS
if (!canAdd(spendableAmount, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
beast::Journal j);
template <>
inline TER
TER
escrowLockPreclaimHelper<MPTIssue>(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal j)
{
AccountID const issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
return tecNO_PERMISSION;
// If the issuer is not the same as the issuer of the mpt, return
// tecNO_PERMISSION
if (sleIssuance->getAccountID(sfIssuer) != issuer)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
if (!view.exists(keylet::mptoken(issuanceKey.key, account)))
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is
// authorized
if (auto const ter = requireAuth(view, mptIssue, dest, AuthType::WeakAuth); !isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (isFrozen(view, account, *sleIssuance))
return tecLOCKED;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(view, dest, *sleIssuance))
return tecLOCKED;
// If the mpt cannot be transferred, return tecNO_AUTH
if (auto const ter = canTransfer(view, mptIssue, account, dest); !isTesSuccess(ter))
return ter;
STAmount const spendableAmount = accountHolds(
view,
account,
amount.get<MPTIssue>(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
beast::Journal j);
template <ValidIssueType T>
TER
@@ -211,43 +67,22 @@ escrowLockApplyHelper(
beast::Journal journal);
template <>
inline TER
TER
escrowLockApplyHelper<Issue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter =
directSendNoFee(view, sender, issuer, amount, !amount.holds<MPTIssue>(), journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
beast::Journal journal);
template <>
inline TER
TER
escrowLockApplyHelper<MPTIssue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter = lockEscrowMPT(view, sender, amount, journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
beast::Journal journal);
template <ValidIssueType T>
TER
@@ -258,62 +93,20 @@ escrowUnlockPreclaimHelper(
bool checkFreeze = true);
template <>
inline TER
TER
escrowUnlockPreclaimHelper<Issue>(
ReadView const& view,
AccountID const& account,
STAmount const& amount,
bool checkFreeze)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == account)
return tesSUCCESS;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(view, amount.get<Issue>(), account); !isTesSuccess(ter))
return ter;
// If the issuer has deep frozen the destination, return tecFROZEN
if (checkFreeze &&
isDeepFrozen(view, account, amount.get<Issue>().currency, amount.getIssuer()))
return tecFROZEN;
return tesSUCCESS;
}
bool checkFreeze);
template <>
inline TER
TER
escrowUnlockPreclaimHelper<MPTIssue>(
ReadView const& view,
AccountID const& account,
STAmount const& amount,
bool checkFreeze)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == account)
return tesSUCCESS;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (checkFreeze && isFrozen(view, account, *sleIssuance))
return tecLOCKED;
return tesSUCCESS;
}
bool checkFreeze);
//------------------------------------------------------------------------------
@@ -332,7 +125,7 @@ escrowUnlockApplyHelper(
beast::Journal journal);
template <>
inline TER
TER
escrowUnlockApplyHelper<Issue>(
ApplyViewContext ctx,
Rate lockedRate,
@@ -343,138 +136,10 @@ escrowUnlockApplyHelper<Issue>(
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal)
{
bool const senderIssuer = issuer == sender;
bool const receiverIssuer = issuer == receiver;
if (senderIssuer)
return tecINTERNAL; // LCOV_EXCL_LINE
if (receiverIssuer)
return tesSUCCESS;
auto const& issue = amount.get<Issue>();
Keylet const trustLineKey = keylet::trustLine(receiver, issue);
bool const recvLow = issuer > receiver;
if (!ctx.view.exists(trustLineKey) && createAsset)
{
// Can the account cover the trust line's reserve?
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
if (auto const ret = checkReserve(
ctx,
sleDest,
xrpBalance,
*sponsorSle,
{.ownerCountDelta = 1},
journal,
tecNO_LINE_INSUF_RESERVE);
!isTesSuccess(ret))
{
JLOG(journal.trace()) << "Trust line does not exist. "
"Insufficient reserve to create line.";
return ret;
}
Currency const currency = issue.currency;
STAmount initialBalance(issue);
initialBalance.get<Issue>().account = noAccount();
if (TER const ter = trustCreate(
ctx.view, // payment sandbox
recvLow, // is dest low?
issuer, // source
receiver, // destination
trustLineKey.key, // ledger index
sleDest, // Account to add to
false, // authorize account
!sleDest->isFlag(lsfDefaultRipple), //
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, receiver), // limit of zero
0, // quality in
0, // quality out
*sponsorSle, // sponsor
journal); // journal
!isTesSuccess(ter))
{
return ter; // LCOV_EXCL_LINE
}
ctx.view.update(sleDest);
}
if (!ctx.view.exists(trustLineKey) && !receiverIssuer)
return tecNO_LINE;
auto const xferRate = transferRate(ctx.view, amount);
// update if issuer rate is less than locked rate
if (xferRate < lockedRate)
lockedRate = xferRate;
// Transfer Rate only applies when:
// 1. Issuer is not involved in the transfer (senderIssuer or
// receiverIssuer)
// 2. The locked rate is different from the parity rate
// NOTE: Transfer fee in escrow works a bit differently from a normal
// payment. In escrow, the fee is deducted from the locked/sending amount,
// whereas in a normal payment, the transfer fee is taken on top of the
// sending amount.
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
{
// compute transfer fee, if any
auto const xferFee =
amount.value() - divideRound(amount, lockedRate, amount.get<Issue>(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
// validate the line limit if the account submitting txn is not the receiver
// of the funds
if (!createAsset)
{
auto const sleRippleState = ctx.view.peek(trustLineKey);
if (!sleRippleState)
return tecINTERNAL; // LCOV_EXCL_LINE
// if the issuer is the high, then we use the low limit
// otherwise we use the high limit
STAmount const lineLimit =
sleRippleState->getFieldAmount(recvLow ? sfLowLimit : sfHighLimit);
STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance);
// flip the sign of the line balance if the issuer is not high
if (!recvLow)
lineBalance.negate();
// add the final amount to the line balance
lineBalance += finalAmt;
// if the transfer would exceed the line limit return tecLIMIT_EXCEEDED
if (lineLimit < lineBalance)
return tecLIMIT_EXCEEDED;
}
// if destination is not the issuer then transfer funds
if (!receiverIssuer)
{
auto const ter = directSendNoFee(ctx.view, issuer, receiver, finalAmt, true, journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
beast::Journal journal);
template <>
inline TER
TER
escrowUnlockApplyHelper<MPTIssue>(
ApplyViewContext ctx,
Rate lockedRate,
@@ -485,83 +150,7 @@ escrowUnlockApplyHelper<MPTIssue>(
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal)
{
bool const senderIssuer = issuer == sender;
bool const receiverIssuer = issuer == receiver;
auto const mptID = amount.get<MPTIssue>().getMptID();
auto const issuanceKey = keylet::mptokenIssuance(mptID);
auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver);
if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer)
{
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
if (auto const ret = checkReserve(
ctx, sleDest, xrpBalance, *sponsorSle, {.ownerCountDelta = 1}, journal);
!isTesSuccess(ret))
return ret;
if (auto const ter = createMPToken(ctx.view, mptID, receiver, *sponsorSle, 0);
!isTesSuccess(ter))
{
return ter; // LCOV_EXCL_LINE
}
// update owner count.
increaseOwnerCount(ctx.view, sleDest, *sponsorSle, 1, journal);
}
if (!ctx.view.exists(mptKeylet) && !receiverIssuer)
return tecNO_PERMISSION;
auto const xferRate = transferRate(ctx.view, amount);
// update if issuer rate is less than locked rate
if (xferRate < lockedRate)
lockedRate = xferRate;
// Transfer Rate only applies when:
// 1. Issuer is not involved in the transfer (senderIssuer or
// receiverIssuer)
// 2. The locked rate is different from the parity rate
// NOTE: Transfer fee in escrow works a bit differently from a normal
// payment. In escrow, the fee is deducted from the locked/sending amount,
// whereas in a normal payment, the transfer fee is taken on top of the
// sending amount.
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
{
if (ctx.view.rules().enabled(fixCleanup3_4_0))
{
XRPL_ASSERT(
lockedRate >= kParityRate,
"xrpl::escrowUnlockApplyHelper<MPTIssue> : lockedRate is at least parity");
// MPTs are integral, so round the delivered amount down and
// charge any fractional transfer fee to the escrowed amount.
auto const delivered =
mulRatio(amount.mpt(), kParityRate.value, lockedRate.value, false);
finalAmt = STAmount(amount.asset(), delivered.value());
}
else
{
// compute transfer fee, if any
auto const xferFee =
amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
}
return unlockEscrowMPT(
ctx.view,
sender,
receiver,
finalAmt,
ctx.view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt,
journal);
}
beast::Journal journal);
/**
* Smart escrow kill switches, driven by the voted FeeSettings.
@@ -3,9 +3,16 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rules.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <optional>
@@ -13,17 +20,81 @@
namespace xrpl {
/**
* Close a payment channel and return its remaining funds to the channel owner.
* Check that a payment channel transaction may carry a token amount.
*
* @param slep The SLE for the PayChannel object to close.
* @param view The apply view in which ledger state modifications are made.
* @param key The ledger key identifying the PayChannel entry.
* @param j Journal used for fatal-level diagnostic messages.
* @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal
* fails; tefINTERNAL if the source account SLE cannot be found.
* Token channels rely on fixCleanup3_2_0 and fixTokenEscrowV1, so both must be
* enabled alongside featureTokenPaychan.
*
* @param rules The current ledger rules used to check amendment status.
* @return temBAD_AMOUNT if featureTokenPaychan is not enabled; temDISABLED if
* fixCleanup3_2_0 or fixTokenEscrowV1 is not enabled; tesSUCCESS otherwise.
*/
NotTEC
checkTokenPayChanRules(Rules const& rules);
/**
* Validate the token amount of a PaymentChannelCreate or PaymentChannelFund
* transaction during preflight.
*
* @param amount The channel or funding amount from the transaction.
* @return tesSUCCESS if the amount is valid; temBAD_AMOUNT or temBAD_CURRENCY
* otherwise.
*/
template <ValidIssueType T>
NotTEC
payChanAmountPreflightHelper(STAmount const& amount);
template <>
NotTEC
payChanAmountPreflightHelper<Issue>(STAmount const& amount);
template <>
NotTEC
payChanAmountPreflightHelper<MPTIssue>(STAmount const& amount);
/**
* Check that locking @p amount debits the source's trust line by exactly
* @p amount.
*
* @param view The ledger view holding the source's trust line.
* @param account The channel source whose balance is debited.
* @param amount The amount to lock in the channel.
* @param j Journal passed to the balance lookup.
* @return tecPRECISION_LOSS if @p amount is an IOU and the source's balance
* less @p amount is not exactly representable; tesSUCCESS otherwise,
* including for XRP and MPT amounts.
*/
TER
closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal j);
payChanLockPrecisionHelper(
ReadView const& view,
AccountID const& account,
STAmount const& amount,
beast::Journal j);
/**
* Close a payment channel and return its remaining funds to the channel owner.
*
* @param slep The SLE for the PayChannel object to close.
* @param ctx The apply view context (view and transaction) in which ledger
* state modifications are made.
* @param key The ledger key identifying the PayChannel entry.
* @param txAccount The account submitting the transaction that closes the
* channel.
* @param submitterPreFeeBalance The submitter's XRP balance before the
* transaction fee; the source's balance when the source closes its own
* channel and its trust line must be re-created for the refund.
* @param j Journal used for fatal-level diagnostic messages.
* @return tesSUCCESS on success; tefBAD_LEDGER if a directory removal
* fails; tefINTERNAL if the source account SLE cannot be found.
*/
TER
closeChannel(
SLE::Ref slep,
ApplyViewContext ctx,
UInt256 const& key,
AccountID const& txAccount,
XRPAmount submitterPreFeeBalance,
beast::Journal j);
/**
* Add two uint32_t values with saturation at UINT32_MAX.
+25
View File
@@ -2,11 +2,17 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/XRPAmount.h>
namespace xrpl {
/**
* Serialize the PaymentChannelClaim authorization message:
* HashPrefix::PaymentChannelClaim, the channel key and the drops as a
* 64-bit integer.
*/
inline void
serializePayChanAuthorization(Serializer& msg, UInt256 const& key, XRPAmount const& amt)
{
@@ -15,4 +21,23 @@ serializePayChanAuthorization(Serializer& msg, UInt256 const& key, XRPAmount con
msg.add64(amt.drops());
}
/**
* Serialize the PaymentChannelClaim authorization message for any asset: a
* native amount uses the XRP layout; a token amount follows
* HashPrefix::PaymentChannelClaim and the channel key with STAmount::add, the
* Amount field value without its field header.
*/
inline void
serializePayChanAuthorization(Serializer& msg, UInt256 const& key, STAmount const& amt)
{
if (amt.native())
{
serializePayChanAuthorization(msg, key, amt.xrp());
return;
}
msg.add32(HashPrefix::PaymentChannelClaim);
msg.addBitString(key);
amt.add(msg);
}
} // namespace xrpl
+33
View File
@@ -805,6 +805,39 @@ canAdd(STAmount const& amt1, STAmount const& amt2);
bool
canSubtract(STAmount const& amt1, STAmount const& amt2);
/**
* Determine whether `amt1 + amt2` equals the mathematical sum.
*
* XRP and MPT sums are integer additions, so this is canAdd(). An IOU sum is
* rounded to the mantissa width and is exact only if subtracting each
* operand from it gives back the other.
*
* @param amt1 The first addend.
* @param amt2 The second addend.
* @return true if the sum is exact; false if it rounds, exceeds the largest
* amount the asset can represent, or the amounts are not comparable.
* Never throws.
*/
bool
isExactSum(STAmount const& amt1, STAmount const& amt2);
/**
* Determine whether `amt1 - amt2` equals the mathematical difference.
*
* XRP and MPT differences are integer subtractions, so this is canSubtract().
* An IOU difference is rounded to the mantissa width and is exact only if
* adding the subtrahend back gives the minuend and subtracting the difference
* from the minuend gives back the subtrahend.
*
* @param amt1 The minuend.
* @param amt2 The subtrahend.
* @return true if the difference is exact; false if it rounds, underflows,
* exceeds the largest amount the asset can represent, or the amounts are
* not comparable. Never throws.
*/
bool
isExactDifference(STAmount const& amt1, STAmount const& amt2);
/**
* Get the scale of a Number for a given asset.
*
@@ -23,6 +23,7 @@ XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultN
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (BatchV1_2, Supported::Yes, VoteBehavior::DefaultYes)
XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(TokenPaychan, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocolV1_1, Supported::Yes, VoteBehavior::DefaultNo)
@@ -376,6 +376,8 @@ LEDGER_ENTRY(ltPAYCHAN, 0x0078, PayChannel, payment_channel, ({
{sfPreviousTxnID, SoeRequired},
{sfPreviousTxnLgrSeq, SoeRequired},
{sfDestinationNode, SoeOptional},
{sfTransferRate, SoeOptional},
{sfIssuerNode, SoeOptional},
}))
/** The ledger object which tracks the AMM.
@@ -182,7 +182,7 @@ TRANSACTION(ttSIGNER_LIST_SET, 12, SignerListSet,
#endif
TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({.delegable = Delegation::Delegable}), ({
{sfDestination, SoeRequired},
{sfAmount, SoeRequired},
{sfAmount, SoeRequired, SoeMptSupported},
{sfSettleDelay, SoeRequired},
{sfPublicKey, SoeRequired},
{sfCancelAfter, SoeOptional},
@@ -195,7 +195,7 @@ TRANSACTION(ttPAYCHAN_CREATE, 13, PaymentChannelCreate, ({.delegable = Delegatio
#endif
TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, ({.delegable = Delegation::Delegable}), ({
{sfChannel, SoeRequired},
{sfAmount, SoeRequired},
{sfAmount, SoeRequired, SoeMptSupported},
{sfExpiration, SoeOptional},
}))
@@ -205,8 +205,8 @@ TRANSACTION(ttPAYCHAN_FUND, 14, PaymentChannelFund, ({.delegable = Delegation::D
#endif
TRANSACTION(ttPAYCHAN_CLAIM, 15, PaymentChannelClaim, ({.delegable = Delegation::Delegable}), ({
{sfChannel, SoeRequired},
{sfAmount, SoeOptional},
{sfBalance, SoeOptional},
{sfAmount, SoeOptional, SoeMptSupported},
{sfBalance, SoeOptional, SoeMptSupported},
{sfSignature, SoeOptional},
{sfPublicKey, SoeOptional},
{sfCredentialIDs, SoeOptional},
@@ -287,6 +287,54 @@ public:
{
return this->sle_->isFieldPresent(sfDestinationNode);
}
/**
* @brief Get sfTransferRate (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getTransferRate() const
{
if (hasTransferRate())
return this->sle_->at(sfTransferRate);
return std::nullopt;
}
/**
* @brief Check if sfTransferRate is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasTransferRate() const
{
return this->sle_->isFieldPresent(sfTransferRate);
}
/**
* @brief Get sfIssuerNode (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT64::type::value_type>
getIssuerNode() const
{
if (hasIssuerNode())
return this->sle_->at(sfIssuerNode);
return std::nullopt;
}
/**
* @brief Check if sfIssuerNode is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasIssuerNode() const
{
return this->sle_->isFieldPresent(sfIssuerNode);
}
};
/**
@@ -508,6 +556,28 @@ public:
return *this;
}
/**
* @brief Set sfTransferRate (SoeOptional)
* @return Reference to this builder for method chaining.
*/
PayChannelBuilder&
setTransferRate(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfTransferRate] = value;
return *this;
}
/**
* @brief Set sfIssuerNode (SoeOptional)
* @return Reference to this builder for method chaining.
*/
PayChannelBuilder&
setIssuerNode(std::decay_t<typename SF_UINT64::type::value_type> const& value)
{
object_[sfIssuerNode] = value;
return *this;
}
/**
* @brief Build and return the completed PayChannel wrapper.
* @param index The ledger entry index.
@@ -60,6 +60,7 @@ public:
/**
* @brief Get sfAmount (SoeOptional)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
@@ -86,6 +87,7 @@ public:
/**
* @brief Get sfBalance (SoeOptional)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
@@ -246,6 +248,7 @@ public:
/**
* @brief Set sfAmount (SoeOptional)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
PaymentChannelClaimBuilder&
@@ -257,6 +260,7 @@ public:
/**
* @brief Set sfBalance (SoeOptional)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
PaymentChannelClaimBuilder&
@@ -60,6 +60,7 @@ public:
/**
* @brief Get sfAmount (SoeRequired)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -207,6 +208,7 @@ public:
/**
* @brief Set sfAmount (SoeRequired)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
PaymentChannelCreateBuilder&
@@ -60,6 +60,7 @@ public:
/**
* @brief Get sfAmount (SoeRequired)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return The field value.
*/
[[nodiscard]]
@@ -155,6 +156,7 @@ public:
/**
* @brief Set sfAmount (SoeRequired)
* @note This field supports MPT (Multi-Purpose Token) amounts.
* @return Reference to this builder for method chaining.
*/
PaymentChannelFundBuilder&
@@ -0,0 +1,52 @@
#pragma once
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <cstdint>
#include <set>
#include <vector>
namespace xrpl {
class ReadView;
/**
* @brief Invariants for TransactionProposal ledger entries.
*
* - Only transaction types implementing proposal lifecycle operations may
* create or modify proposals, and a failed one leaves no proposal effect.
* - A proposal is deleted only by TransactionProposalCancel or together with
* the Ticket it is keyed to, and a deleted Ticket takes the proposal keyed
* to it along.
* - Collected signer arrays remain sorted, unique, and bounded.
*/
class ValidTransactionProposal
{
// The Ticket each deleted proposal was keyed to; a proposal may go with
// its Ticket, or by Cancel, and no other way.
std::vector<uint256> ticketsOfDeletedProposals_;
// The proposal each deleted Ticket was keyed to, whether or not one
// existed; none may survive its Ticket.
std::vector<Keylet> proposalsOfDeletedTickets_;
std::set<uint256> deletedTickets_;
std::uint32_t created_ = 0;
std::uint32_t modified_ = 0;
std::uint32_t deleted_ = 0;
bool invalidSignerArrays_ = false;
public:
void
visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after);
[[nodiscard]] bool
finalize(STTx const& tx, TER result, XRPAmount, ReadView const& view, beast::Journal const& j)
const;
};
} // namespace xrpl
@@ -9,6 +9,7 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/invariants/AMMInvariant.h>
#include <xrpl/tx/invariants/CosignerInvariant.h>
#include <xrpl/tx/invariants/DirectoryInvariant.h>
#include <xrpl/tx/invariants/FreezeInvariant.h>
#include <xrpl/tx/invariants/LoanBrokerInvariant.h>
@@ -483,6 +484,7 @@ using InvariantChecks = std::tuple<
ValidAmounts,
ValidMPTTransfer,
ObjectHasPseudoAccount,
ValidTransactionProposal,
SponsorshipOwnerCountsMatch,
SponsorshipAccountCountMatchesField>;
@@ -27,6 +27,8 @@ class ValidMPTIssuance
std::uint32_t mptokensCreated_ = 0;
std::uint32_t mptokensDeleted_ = 0;
// holder of the last MPToken created
std::optional<AccountID> mptokenCreatedHolder_;
// non-MPT transactions may attempt to create
// MPToken by an issuer
bool mptCreatedByIssuer_ = false;
@@ -23,6 +23,9 @@ public:
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
@@ -23,9 +23,15 @@ public:
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
@@ -0,0 +1,510 @@
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/RippleStateHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTAmount.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
namespace xrpl {
template <>
TER
escrowLockPreclaimHelper<Issue>(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal j)
{
auto const& issue = amount.get<Issue>();
auto const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
auto const sleIssuer = view.read(keylet::account(issuer));
if (!sleIssuer)
return tecNO_ISSUER;
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
return tecNO_PERMISSION;
// If the account does not have a trustline to the issuer, return tecNO_LINE
auto const sleRippleState = view.read(keylet::trustLine(account, issuer, issue.currency));
if (!sleRippleState)
return tecNO_LINE;
STAmount const balance = (*sleRippleState)[sfBalance];
// If balance is positive, issuer must have higher address than account
if (balance > beast::kZero && issuer < account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If balance is negative, issuer must have lower address than account
if (balance < beast::kZero && issuer > account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(view, issue, account); !isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(view, issue, dest); !isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecFROZEN
if (isFrozen(view, account, issue))
return tecFROZEN;
// If the issuer has frozen the destination, return tecFROZEN
if (isFrozen(view, dest, issue))
return tecFROZEN;
STAmount const spendableAmount =
accountHolds(view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
// If the amount is not addable to the balance, return tecPRECISION_LOSS
if (!canAdd(spendableAmount, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
template <>
TER
escrowLockPreclaimHelper<MPTIssue>(
ReadView const& view,
AccountID const& account,
AccountID const& dest,
STAmount const& amount,
beast::Journal j)
{
AccountID const issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
return tecNO_PERMISSION;
// If the issuer is not the same as the issuer of the mpt, return
// tecNO_PERMISSION
if (sleIssuance->getAccountID(sfIssuer) != issuer)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
if (!view.exists(keylet::mptoken(issuanceKey.key, account)))
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is
// authorized
if (auto const ter = requireAuth(view, mptIssue, dest, AuthType::WeakAuth); !isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (isFrozen(view, account, *sleIssuance))
return tecLOCKED;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(view, dest, *sleIssuance))
return tecLOCKED;
// If the mpt cannot be transferred, return tecNO_AUTH
if (auto const ter = canTransfer(view, mptIssue, account, dest); !isTesSuccess(ter))
return ter;
STAmount const spendableAmount = accountHolds(
view,
account,
amount.get<MPTIssue>(),
FreezeHandling::IgnoreFreeze,
AuthHandling::IgnoreAuth,
j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZero)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
template <>
TER
escrowLockApplyHelper<Issue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter =
directSendNoFee(view, sender, issuer, amount, !amount.holds<MPTIssue>(), journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
template <>
TER
escrowLockApplyHelper<MPTIssue>(
ApplyView& view,
AccountID const& issuer,
AccountID const& sender,
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const ter = lockEscrowMPT(view, sender, amount, journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
return tesSUCCESS;
}
template <>
TER
escrowUnlockPreclaimHelper<Issue>(
ReadView const& view,
AccountID const& account,
STAmount const& amount,
bool checkFreeze)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == account)
return tesSUCCESS;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(view, amount.get<Issue>(), account); !isTesSuccess(ter))
return ter;
// If the issuer has deep frozen the destination, return tecFROZEN
if (checkFreeze &&
isDeepFrozen(view, account, amount.get<Issue>().currency, amount.getIssuer()))
return tecFROZEN;
return tesSUCCESS;
}
template <>
TER
escrowUnlockPreclaimHelper<MPTIssue>(
ReadView const& view,
AccountID const& account,
STAmount const& amount,
bool checkFreeze)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == account)
return tesSUCCESS;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptokenIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecLOCKED
if (checkFreeze && isFrozen(view, account, *sleIssuance))
return tecLOCKED;
return tesSUCCESS;
}
template <>
TER
escrowUnlockApplyHelper<Issue>(
ApplyViewContext ctx,
Rate lockedRate,
SLE::Ref sleDest,
XRPAmount xrpBalance,
STAmount const& amount,
AccountID const& issuer,
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal)
{
bool const senderIssuer = issuer == sender;
bool const receiverIssuer = issuer == receiver;
if (senderIssuer)
return tecINTERNAL; // LCOV_EXCL_LINE
if (receiverIssuer)
return tesSUCCESS;
auto const& issue = amount.get<Issue>();
Keylet const trustLineKey = keylet::trustLine(receiver, issue);
bool const recvLow = issuer > receiver;
if (!ctx.view.exists(trustLineKey) && createAsset)
{
// Can the account cover the trust line's reserve?
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
if (auto const ret = checkReserve(
ctx,
sleDest,
xrpBalance,
*sponsorSle,
{.ownerCountDelta = 1},
journal,
tecNO_LINE_INSUF_RESERVE);
!isTesSuccess(ret))
{
JLOG(journal.trace()) << "Trust line does not exist. "
"Insufficient reserve to create line.";
return ret;
}
Currency const currency = issue.currency;
STAmount initialBalance(issue);
initialBalance.get<Issue>().account = noAccount();
if (TER const ter = trustCreate(
ctx.view, // payment sandbox
recvLow, // is dest low?
issuer, // source
receiver, // destination
trustLineKey.key, // ledger index
sleDest, // Account to add to
false, // authorize account
!sleDest->isFlag(lsfDefaultRipple), //
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, receiver), // limit of zero
0, // quality in
0, // quality out
*sponsorSle, // sponsor
journal); // journal
!isTesSuccess(ter))
{
return ter; // LCOV_EXCL_LINE
}
ctx.view.update(sleDest);
}
if (!ctx.view.exists(trustLineKey) && !receiverIssuer)
return tecNO_LINE;
auto const xferRate = transferRate(ctx.view, amount);
// update if issuer rate is less than locked rate
if (xferRate < lockedRate)
lockedRate = xferRate;
// Transfer Rate only applies when:
// 1. Issuer is not involved in the transfer (senderIssuer or
// receiverIssuer)
// 2. The locked rate is different from the parity rate
// NOTE: Transfer fee in escrow works a bit differently from a normal
// payment. In escrow, the fee is deducted from the locked/sending amount,
// whereas in a normal payment, the transfer fee is taken on top of the
// sending amount.
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
{
// compute transfer fee, if any
auto const xferFee =
amount.value() - divideRound(amount, lockedRate, amount.get<Issue>(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
// validate the line limit if the account submitting txn is not the receiver
// of the funds
if (!createAsset)
{
auto const sleRippleState = ctx.view.peek(trustLineKey);
if (!sleRippleState)
return tecINTERNAL; // LCOV_EXCL_LINE
// if the issuer is the high, then we use the low limit
// otherwise we use the high limit
STAmount const lineLimit =
sleRippleState->getFieldAmount(recvLow ? sfLowLimit : sfHighLimit);
STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance);
// flip the sign of the line balance if the issuer is not high
if (!recvLow)
lineBalance.negate();
// add the final amount to the line balance
lineBalance += finalAmt;
// if the transfer would exceed the line limit return tecLIMIT_EXCEEDED
if (lineLimit < lineBalance)
return tecLIMIT_EXCEEDED;
}
// if destination is not the issuer then transfer funds
if (!receiverIssuer)
{
auto const ter = directSendNoFee(ctx.view, issuer, receiver, finalAmt, true, journal);
if (!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
}
return tesSUCCESS;
}
template <>
TER
escrowUnlockApplyHelper<MPTIssue>(
ApplyViewContext ctx,
Rate lockedRate,
SLE::Ref sleDest,
XRPAmount xrpBalance,
STAmount const& amount,
AccountID const& issuer,
AccountID const& sender,
AccountID const& receiver,
bool createAsset,
beast::Journal journal)
{
bool const senderIssuer = issuer == sender;
bool const receiverIssuer = issuer == receiver;
auto const mptID = amount.get<MPTIssue>().getMptID();
auto const issuanceKey = keylet::mptokenIssuance(mptID);
auto const mptKeylet = keylet::mptoken(issuanceKey.key, receiver);
if (!ctx.view.exists(mptKeylet) && createAsset && !receiverIssuer)
{
auto const sponsorSle = getEffectiveTxReserveSponsor(ctx, sleDest);
if (!sponsorSle)
return sponsorSle.error(); // LCOV_EXCL_LINE
if (auto const ret = checkReserve(
ctx, sleDest, xrpBalance, *sponsorSle, {.ownerCountDelta = 1}, journal);
!isTesSuccess(ret))
return ret;
if (auto const ter = createMPToken(ctx.view, mptID, receiver, *sponsorSle, 0);
!isTesSuccess(ter))
{
return ter; // LCOV_EXCL_LINE
}
// update owner count.
increaseOwnerCount(ctx.view, sleDest, *sponsorSle, 1, journal);
}
if (!ctx.view.exists(mptKeylet) && !receiverIssuer)
return tecNO_PERMISSION;
auto const xferRate = transferRate(ctx.view, amount);
// update if issuer rate is less than locked rate
if (xferRate < lockedRate)
lockedRate = xferRate;
// Transfer Rate only applies when:
// 1. Issuer is not involved in the transfer (senderIssuer or
// receiverIssuer)
// 2. The locked rate is different from the parity rate
// NOTE: Transfer fee in escrow works a bit differently from a normal
// payment. In escrow, the fee is deducted from the locked/sending amount,
// whereas in a normal payment, the transfer fee is taken on top of the
// sending amount.
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != kParityRate)
{
if (ctx.view.rules().enabled(fixCleanup3_4_0))
{
XRPL_ASSERT(
lockedRate >= kParityRate,
"xrpl::escrowUnlockApplyHelper<MPTIssue> : lockedRate is at least parity");
// MPTs are integral, so round the delivered amount down and
// charge any fractional transfer fee to the escrowed amount.
auto const delivered =
mulRatio(amount.mpt(), kParityRate.value, lockedRate.value, false);
finalAmt = STAmount(amount.asset(), delivered.value());
}
else
{
// compute transfer fee, if any
auto const xferFee =
amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
}
return unlockEscrowMPT(
ctx.view,
sender,
receiver,
finalAmt,
ctx.view.rules().enabled(fixTokenEscrowV1) ? amount : finalAmt,
journal);
}
} // namespace xrpl
@@ -3,16 +3,26 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
#include <algorithm>
#include <cstdint>
@@ -21,9 +31,76 @@
namespace xrpl {
TER
closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal j)
NotTEC
checkTokenPayChanRules(Rules const& rules)
{
if (!rules.enabled(featureTokenPaychan))
return temBAD_AMOUNT;
if (!rules.enabled(fixCleanup3_2_0) || !rules.enabled(fixTokenEscrowV1))
return temDISABLED;
return tesSUCCESS;
}
template <>
NotTEC
payChanAmountPreflightHelper<Issue>(STAmount const& amount)
{
if (amount.native() || amount <= beast::kZero)
return temBAD_AMOUNT;
if (badCurrency() == amount.get<Issue>().currency)
return temBAD_CURRENCY;
return tesSUCCESS;
}
template <>
NotTEC
payChanAmountPreflightHelper<MPTIssue>(STAmount const& amount)
{
if (amount.native() || amount <= beast::kZero)
return temBAD_AMOUNT;
return tesSUCCESS;
}
TER
payChanLockPrecisionHelper(
ReadView const& view,
AccountID const& account,
STAmount const& amount,
beast::Journal j)
{
if (!amount.holds<Issue>())
return tesSUCCESS;
// The trust line debit rounds to the mantissa, so require an exact
// difference rather than canAdd's relative tolerance.
STAmount const spendable = accountHolds(
view,
account,
amount.get<Issue>().currency,
amount.getIssuer(),
FreezeHandling::IgnoreFreeze,
j);
if (!isExactDifference(spendable, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
TER
closeChannel(
SLE::Ref slep,
ApplyViewContext ctx,
UInt256 const& key,
AccountID const& txAccount,
XRPAmount submitterPreFeeBalance,
beast::Journal j)
{
ApplyView& view = ctx.view;
AccountID const src = (*slep)[sfAccount];
// Remove PayChan from owner directory
{
@@ -38,9 +115,9 @@ closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal
}
// Remove PayChan from recipient's owner directory, if present.
AccountID const dst = (*slep)[sfDestination];
if (auto const page = (*slep)[~sfDestinationNode])
{
auto const dst = (*slep)[sfDestination];
if (!view.dirRemove(keylet::ownerDir(dst), *page, key, true))
{
// LCOV_EXCL_START
@@ -50,15 +127,69 @@ closeChannel(SLE::Ref slep, ApplyView& view, UInt256 const& key, beast::Journal
}
}
// Transfer amount back to owner, decrement owner count
auto const sle = view.peek(keylet::account(src));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
// Release the channel's reserve first: a token refund can re-create a
// holding the owner deleted while the channel was open.
decreaseOwnerCountForObject(view, sle, slep, 1, j);
XRPL_ASSERT(
(*slep)[sfAmount] >= (*slep)[sfBalance], "xrpl::closeChannel : minimum channel amount");
(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance];
decreaseOwnerCountForObject(view, sle, slep, 1, j);
XRPL_ASSERT(
isExactDifference((*slep)[sfAmount], (*slep)[sfBalance]),
"xrpl::closeChannel : exact refund");
auto const reqDelta = (*slep)[sfAmount] - (*slep)[sfBalance];
auto const& issuer = reqDelta.getIssuer();
// Only update the balance if there is a positive delta.
if (reqDelta > beast::kZero)
{
if (isXRP(reqDelta))
{
(*sle)[sfBalance] = (*sle)[sfBalance] + reqDelta;
}
else
{
if (auto const ret = reqDelta.asset().visit([&]<typename T>(T const&) {
return escrowUnlockPreclaimHelper<T>(view, src, reqDelta, false);
});
!isTesSuccess(ret))
return ret;
bool const createAsset = src == txAccount;
if (auto const ret = reqDelta.asset().visit([&]<typename T>(T const&) {
return escrowUnlockApplyHelper<T>(
ctx,
kParityRate,
sle,
submitterPreFeeBalance,
reqDelta,
issuer,
src,
src,
createAsset,
j);
});
!isTesSuccess(ret))
return ret;
}
}
// Remove PayChan from issuer's owner directory, if present.
if (auto const optPage = (*slep)[~sfIssuerNode])
{
if (!view.dirRemove(keylet::ownerDir(issuer), *optPage, key, true))
{
// LCOV_EXCL_START
JLOG(j.fatal()) << "Could not remove paychan from issuer owner directory";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
}
view.update(sle);
// Remove PayChan from ledger
@@ -43,6 +43,9 @@ constexpr auto kProposalTxTypes = std::to_array<TxType>({
bool
isValidProposalTxnType(STObject const& proposedTx)
{
if (!proposedTx.isFieldPresent(sfTransactionType))
return false;
auto const type = proposedTx.getFieldU16(sfTransactionType);
if (std::ranges::find(kProposalTxTypes, type) != kProposalTxTypes.end())
+58
View File
@@ -522,6 +522,64 @@ canAdd(STAmount const& a, STAmount const& b)
// LCOV_EXCL_STOP
}
/**
* Integral types defer to canAdd(). For IOU amounts the rounding error of the
* sum is a multiple of the finer operand's unit in the last place, so
* subtracting the coarser operand back from the sum returns the finer one
* only when that error is zero. Both differences are checked because either
* operand may be the finer. IOUAmount throws std::overflow_error when a sum
* or difference exceeds the largest IOU value; the exact result is then not
* representable, so the sum is not exact.
*/
bool
isExactSum(STAmount const& a, STAmount const& b)
{
if (!areComparable(a, b))
return false;
if (a.integral())
return canAdd(a, b);
try
{
STAmount const sum = a + b;
return sum - a == b && sum - b == a;
}
catch (std::overflow_error const&)
{
return false;
}
}
/**
* Integral types defer to canSubtract(). For IOU amounts each check misses
* the case where one operand is dust against the other. With a dust
* subtrahend, a - b rounds to a and adding b back rounds to a again, so only
* a - diff != b shows the loss. With a dust minuend, a - b rounds to -b and
* a - diff rounds to b, so only diff + b != a shows it. Both are checked.
* IOUAmount throws std::overflow_error when a step rounds past the largest
* IOU value; the difference is then not exact.
*/
bool
isExactDifference(STAmount const& a, STAmount const& b)
{
if (!areComparable(a, b))
return false;
if (a.integral())
return canSubtract(a, b);
try
{
STAmount const diff = a - b;
return diff + b == a && a - diff == b;
}
catch (std::overflow_error const&)
{
return false;
}
}
/**
* @brief Determines if it is safe to subtract one STAmount from another.
*
+2
View File
@@ -31,6 +31,8 @@
#include <xrpl/protocol/detail/transactions.macro>
#undef TRANSACTION
#undef TRANSACTION_INCLUDE
#pragma pop_macro("TRANSACTION")
// DO NOT INCLUDE TRANSACTOR HEADER FILES HERE.
@@ -0,0 +1,225 @@
#include <xrpl/tx/invariants/CosignerInvariant.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
namespace xrpl {
namespace {
bool
validSignerArray(STArray const& signers, std::size_t limit, SField const& elementName)
{
if (signers.size() > limit)
return false;
std::optional<AccountID> previous;
for (auto const& signer : signers)
{
if (signer.getFName() != elementName || !signer.isFieldPresent(sfAccount))
return false;
auto const account = signer.getAccountID(sfAccount);
if (previous && account <= *previous)
return false;
previous = account;
}
return true;
}
bool
validNestedSigners(STObject const& signature)
{
return !signature.isFieldPresent(sfSigners) ||
validSignerArray(signature.getFieldArray(sfSigners), STTx::kMaxMultiSigners, sfSigner);
}
bool
validSignerArrays(STObject const& proposedTx)
{
if (!validNestedSigners(proposedTx))
return false;
for (auto const* field :
std::array<SField const*, 2>{&sfCounterpartySignature, &sfSponsorSignature})
{
if (proposedTx.isFieldPresent(*field) &&
!validNestedSigners(proposedTx.getFieldObject(*field)))
return false;
}
if (!proposedTx.isFieldPresent(sfBatchSigners))
return true;
auto const& batchSigners = proposedTx.getFieldArray(sfBatchSigners);
if (!validSignerArray(batchSigners, kMaxBatchSigners, sfBatchSigner))
return false;
return std::ranges::all_of(
batchSigners, [](auto const& batchSigner) { return validNestedSigners(batchSigner); });
}
// The Ticket a proposal is keyed to: the one its proposed transaction spends.
uint256
proposalTicketKey(SLE const& proposal)
{
auto const& proposedTx = proposal.getFieldObject(sfProposedTransaction);
return keylet::ticket(
proposedTx.getAccountID(sfAccount),
SeqProxy::rawTicket(proposedTx.getFieldU32(sfTicketSequence)))
.key;
}
} // namespace
void
ValidTransactionProposal::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef after)
{
// `after` is always present (on deletion it holds the final state), so it
// alone determines the entry type. LedgerEntryTypesMatch owns malformed
// type transitions; skip them rather than read one side with the other's
// schema.
if (!after || (before && before->getType() != after->getType()))
return;
auto const type = after->getType();
// Consuming a Ticket deletes the proposal keyed to it (XLS-0103 §4.5).
// Remember each Ticket that went, so finalize can tell a proposal deleted
// with its Ticket from one that simply vanished, and the proposal each
// Ticket was keyed to, so finalize can check that none stayed behind.
if (type == ltTICKET)
{
if (isDelete)
{
deletedTickets_.insert(after->key());
proposalsOfDeletedTickets_.push_back(
keylet::txProposal(
after->getAccountID(sfAccount), after->getFieldU32(sfTicketSequence)));
}
return;
}
if (type != ltTRANSACTION_PROPOSAL)
return;
if (!before)
{
++created_;
}
else if (isDelete)
{
++deleted_;
ticketsOfDeletedProposals_.push_back(proposalTicketKey(*before));
}
else
{
++modified_;
}
// A live proposal's signer lists must stay submittable: within size
// limits, sorted by account, and free of duplicates.
if (!isDelete && !validSignerArrays(after->getFieldObject(sfProposedTransaction)))
invalidSignerArrays_ = true;
}
bool
ValidTransactionProposal::finalize(
STTx const& tx,
TER result,
XRPAmount,
ReadView const& view,
beast::Journal const& j) const
{
if (!view.rules().enabled(featureCosign))
return true;
if (invalidSignerArrays_)
{
JLOG(j.fatal()) << "Invariant failed: TransactionProposal signer arrays are not canonical.";
return false;
}
// A proposal leaves the ledger in two ways (XLS-0103 §4.5): a
// TransactionProposalCancel deletes the proposal it names, and consuming
// the Ticket a proposal is keyed to deletes the proposal with it, whether
// the proposed transaction executed (or failed with a claimed-fee tec) or
// AccountDelete swept the target's Tickets.
std::uint32_t deletedWithTicket = 0;
for (auto const& ticketKey : ticketsOfDeletedProposals_)
{
if (deletedTickets_.contains(ticketKey))
++deletedWithTicket;
}
bool const succeeded = isTesSuccess(result);
bool effectsMatch = false;
if (tx.getTxnType() == ttTRANSACTION_PROPOSAL_CREATE)
{
effectsMatch = created_ == (succeeded ? 1u : 0u) && modified_ == 0 && deleted_ == 0;
}
else if (tx.getTxnType() == ttTRANSACTION_PROPOSAL_CANCEL)
{
// A Cancel deletes one proposal and nothing else. It cannot
// spend a reserved Ticket, so no proposal goes with its Ticket here.
effectsMatch = created_ == 0 && modified_ == 0 && deletedWithTicket == 0 &&
deleted_ == (succeeded ? 1u : 0u);
}
else
{
// Any other transaction deletes a proposal only together with its
// Ticket, and modifies at most one, only as a successful
// SponsorshipTransfer.
// TODO: extend this when TransactionProposalSign is implemented.
std::uint32_t const modifiable =
tx.getTxnType() == ttSPONSORSHIP_TRANSFER && succeeded ? 1 : 0;
effectsMatch = created_ == 0 && modified_ <= modifiable && deleted_ == deletedWithTicket;
}
if (!effectsMatch)
{
JLOG(j.fatal())
<< "Invariant failed: TransactionProposal changes do not match transaction result.";
return false;
}
// The other direction of the deletion rule: ticketDelete deletes the
// proposal keyed to a Ticket along with it (XLS-0103 §4.5), so no
// proposal may outlive its Ticket, orphaned on a key that can no longer
// be spent.
bool const orphaned = std::ranges::any_of(
proposalsOfDeletedTickets_, [&view](Keylet const& keylet) { return view.exists(keylet); });
if (orphaned)
{
JLOG(j.fatal())
<< "Invariant failed: TransactionProposal outlived the Ticket it is keyed to.";
return false;
}
return true;
}
} // namespace xrpl
+8 -3
View File
@@ -150,7 +150,8 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef aft
drops_ -= (*before)[sfBalance].xrp().drops();
break;
case ltPAYCHAN:
drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
if (isXRP((*before)[sfAmount]))
drops_ -= ((*before)[sfAmount] - (*before)[sfBalance]).xrp().drops();
break;
case ltESCROW:
if (isXRP((*before)[sfAmount]))
@@ -183,7 +184,7 @@ XRPNotCreated::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef aft
drops_ += (*after)[sfBalance].xrp().drops();
break;
case ltPAYCHAN:
if (!isDelete)
if (!isDelete && isXRP((*after)[sfAmount]))
drops_ += ((*after)[sfAmount] - (*after)[sfBalance]).xrp().drops();
break;
case ltESCROW:
@@ -935,8 +936,12 @@ ValidClawback::finalize(
return !mptV2Enabled;
}
// IOU balances keep 16 significant digits, so a clawback
// much smaller than the balance changes it by a rounded
// amount, possibly zero. Compare with the same rounded
// subtraction the balance update performs.
if (*afterBalance > *beforeBalance ||
(*beforeBalance - *afterBalance) != std::min(*beforeBalance, clawAmount))
*afterBalance != *beforeBalance - std::min(*beforeBalance, clawAmount))
{
JLOG(j.fatal())
<< "Invariant failed: trustline clawback balance change is invalid";
@@ -125,6 +125,7 @@ ValidMPTIssuance::visitEntry(bool isDelete, SLE::ConstRef before, SLE::ConstRef
else if (!before)
{
mptokensCreated_++;
mptokenCreatedHolder_ = after->at(sfAccount);
MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)};
if (mptIssue.getIssuer() == after->at(sfAccount))
mptCreatedByIssuer_ = true;
@@ -439,6 +440,13 @@ ValidMPTIssuance::finalize(
return true;
}
// A token paychan claim may auto-create the submitter's MPToken.
if (rules.enabled(featureTokenPaychan) && txnType == ttPAYCHAN_CLAIM &&
mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0 && mptokensDeleted_ == 0 &&
(mptokensCreated_ == 0 ||
(mptokensCreated_ == 1 && mptokenCreatedHolder_ == tx[sfAccount])))
return true;
if (hasPrivilege(tx, Privilege::MayDeleteMpt) &&
((txnType == ttAMM_DELETE && mptokensDeleted_ <= 2) || mptokensDeleted_ == 1) &&
mptokensCreated_ == 0 && mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 0)
+56 -15
View File
@@ -254,6 +254,15 @@ private:
TAmounts<TIn, TOut> const& stepAmt,
TOut const& ownerGives) const;
template <template <typename, typename> typename Offer>
void
tryConsumeOffer(
PaymentSandbox& sb,
Offer<TIn, TOut>& offer,
TAmounts<TIn, TOut> const& ofrAmt,
TAmounts<TIn, TOut> const& stepAmt,
TOut const& ownerGives) const;
// If clobQuality is available and has a better quality then return nullopt,
// otherwise if amm liquidity is available return AMM offer adjusted based
// on clobQuality.
@@ -810,10 +819,11 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
auto const amtIn = [&](TIn const& in) { return capInMPT && *maxIn_ < in ? *maxIn_ : in; };
try
{
// All arithmetic in this block runs before the offer is consumed.
// A crafted MPTokensV2 offer can overflow while transfer rates or
// crossing limits are applied; remove that unusable offer instead
// of letting it persist as a tecINTERNAL source.
// crossing limits are applied, here or in the callback's
// limitStepIn()/limitStepOut(); remove that unusable offer instead
// of letting it persist as a tecINTERNAL source. Overflows while
// consuming the offer are handled by tryConsumeOffer().
stpAmt.in = mulRatio(amtIn(ofrAmt.in), ofrInRate, QUALITY_ONE, /*roundUp*/ true);
// owner pays the transfer fee. An MPT gross-up that doesn't fit in
@@ -941,16 +951,9 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
removeOffer("Removing offer with overflowing amount calculation");
return true;
}
// An overflow can only be produced by a crafted MPT offer, and MPT
// offers require featureMPTokensV2 (enforced at OfferCreate
// preflight). So the amendment is always enabled when we get here
// and this legacy re-throw is unreachable in practice.
// LCOV_EXCL_START
XRPL_ASSERT(
sb.rules().enabled(featureMPTokensV2),
"xrpl::BookStep::forEachOffer : overflow implies MPTokensV2");
// Legacy behavior: the exception escapes flow() and the
// transaction fails with tecINTERNAL.
throw;
// LCOV_EXCL_STOP
}
};
@@ -991,6 +994,32 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
return {offers.permToRemove(), counter.count()};
}
template <class TIn, class TOut, class TDerived>
template <template <typename, typename> typename Offer>
void
BookStep<TIn, TOut, TDerived>::tryConsumeOffer(
PaymentSandbox& sb,
Offer<TIn, TOut>& offer,
TAmounts<TIn, TOut> const& ofrAmt,
TAmounts<TIn, TOut> const& stepAmt,
TOut const& ownerGives) const
{
try
{
// Under MPTokensV2, an overflow while consuming the offer fails the
// strand instead of being handled by forEachOffer().
consumeOffer(sb, offer, ofrAmt, stepAmt, ownerGives);
}
catch (std::overflow_error const&)
{
if (sb.rules().enabled(featureMPTokensV2))
{
Throw<FlowException>(tecINTERNAL, "Overflow while consuming offer");
}
throw;
}
}
template <class TIn, class TOut, class TDerived>
template <template <typename, typename> typename Offer>
void
@@ -1192,7 +1221,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
savedOuts.insert(stpAmt.out);
result = TAmounts<TIn, TOut>(sum(savedIns), sum(savedOuts));
remainingOut = out - result.out;
this->consumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
this->tryConsumeOffer(sb, offer, ofrAmt, stpAmt, ownerGives);
// return true b/c even if the payment is satisfied,
// we need to consume the offer. An offer limited by what the
// issuer can issue is not consumed, though: its remainder is still
@@ -1224,7 +1253,7 @@ BookStep<TIn, TOut, TDerived>::revImp(
savedOuts.insert(remainingOut);
result.in = sum(savedIns);
result.out = out;
this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
this->tryConsumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
// Explicitly check whether the offer is funded. Given that we have
// (stpAmt.out > remainingOut), it's natural to assume the offer
@@ -1408,7 +1437,7 @@ BookStep<TIn, TOut, TDerived>::fwdImp(
savedOuts = std::move(savedOutsAdj);
result = resultAdj;
remainingIn = in - result.in;
this->consumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
this->tryConsumeOffer(sb, offer, ofrAdjAmt, stpAdjAmt, ownerGivesAdj);
// When the mantissas of two iou amounts differ by less than ten, then
// subtracting them leaves a result of zero. This can cause the check
@@ -1512,6 +1541,18 @@ BookStep<TIn, TOut, TDerived>::check(StrandContext const& ctx) const
return temBAD_PATH;
}
// An issuer paying with its own MPT issues it into the first book. A
// holder-sourced strand records the MPT in the first MPTEndpointStep and
// so rejects a later book that outputs it again; do the same here. The
// step after such a book issues the MPT a second time and the payment
// engine cannot keep the two within MaximumAmount once StrandFlow
// rebuilds the sandbox on a limiting step.
if (ctx.isFirst && book_.in.holds<MPTIssue>() && book_.in.getIssuer() == ctx.strandSrc &&
ctx.view.rules().enabled(featureMPTokensV2))
{
ctx.seenDirectAssets[0].insert(book_.in);
}
// Do not allow two books to output the same issue. This may cause offers on
// one step to unfund offers in another step.
if (!ctx.seenBookOuts.insert(book_.out).second ||
+6 -2
View File
@@ -6,6 +6,7 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/PaymentSandbox.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h>
@@ -874,15 +875,18 @@ MPTEndpointStep<TDerived>::check(StrandContext const& ctx) const
// pure issue/redeem can't be frozen (issuer/holder)
// For the first step: check global freeze of the step's own asset.
// For the last step: check only the per-holder MPToken lock.
// For the last step: check only the per-holder lock.
// Global freeze of the deliver asset is not checked here
// because MPT semantics allow issuer<->holder transfers even when globally
// locked — only holder-to-holder DEX paths are restricted.
// For both steps: a vault share inherits the freeze of the vault's
// underlying asset.
if (!(ctx.isLast && ctx.isFirst))
{
auto const& account = ctx.isFirst ? src_ : dst_;
bool const frozen = (ctx.isFirst && isGlobalFrozen(ctx.view, mptIssue_)) ||
isIndividualFrozen(ctx.view, account, mptIssue_);
isIndividualFrozen(ctx.view, account, mptIssue_) ||
isVaultPseudoAccountFrozen(ctx.view, account, mptIssue_, 0);
if (frozen)
return terLOCKED;
}
@@ -20,8 +20,6 @@
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/Transactor.h>
#include <variant>
namespace xrpl {
NotTEC
@@ -45,11 +43,9 @@ EscrowCancel::preclaim(PreclaimContext const& ctx)
if (!isXRP(amount))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowUnlockPreclaimHelper<T>(ctx.view, account, amount, false);
},
amount.asset().value());
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
return escrowUnlockPreclaimHelper<T>(ctx.view, account, amount, false);
});
!isTesSuccess(ret))
return ret;
}
@@ -133,21 +129,19 @@ EscrowCancel::doApply()
auto const issuer = amount.getIssuer();
bool const createAsset = account == accountID_;
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowUnlockApplyHelper<T>(
ctx_.getApplyViewContext(),
kParityRate,
ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : escrow.mutableRawSle(),
preFeeBalance_,
amount,
issuer,
account, // sender and receiver are the same
account,
createAsset,
j_);
},
amount.asset().value());
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
return escrowUnlockApplyHelper<T>(
ctx_.getApplyViewContext(),
kParityRate,
ctx_.view().rules().enabled(fixCleanup3_2_0) ? sle : escrow.mutableRawSle(),
preFeeBalance_,
amount,
issuer,
account, // sender and receiver are the same
account,
createAsset,
j_);
});
!isTesSuccess(ret))
return ret; // LCOV_EXCL_LINE
@@ -37,7 +37,6 @@
#include <memory>
#include <system_error>
#include <variant>
namespace xrpl {
@@ -153,9 +152,8 @@ EscrowCreate::preflight(PreflightContext const& ctx)
if (!ctx.rules.enabled(featureTokenEscrow))
return temBAD_AMOUNT;
if (auto const ret = std::visit(
[&]<typename T>(T const&) { return escrowCreatePreflightHelper<T>(ctx); },
amount.asset().value());
if (auto const ret = amount.asset().visit(
[&]<typename T>(T const&) { return escrowCreatePreflightHelper<T>(ctx); });
!isTesSuccess(ret))
return ret;
}
@@ -256,11 +254,9 @@ EscrowCreate::preclaim(PreclaimContext const& ctx)
if (!ctx.view.rules().enabled(featureTokenEscrow))
return temDISABLED; // LCOV_EXCL_LINE
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowLockPreclaimHelper<T>(ctx.view, account, dest, amount, ctx.j);
},
amount.asset().value());
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
return escrowLockPreclaimHelper<T>(ctx.view, account, dest, amount, ctx.j);
});
!isTesSuccess(ret))
return ret;
}
@@ -423,11 +419,9 @@ EscrowCreate::doApply()
}
else
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowLockApplyHelper<T>(ctx_.view(), issuer, accountID_, amount, j_);
},
amount.asset().value());
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
return escrowLockApplyHelper<T>(ctx_.view(), issuer, accountID_, amount, j_);
});
!isTesSuccess(ret))
{
return ret; // LCOV_EXCL_LINE
@@ -31,7 +31,6 @@
#include <cstdint>
#include <optional>
#include <system_error>
#include <variant>
namespace xrpl {
@@ -242,11 +241,9 @@ EscrowFinish::preclaim(PreclaimContext const& ctx)
if (!isXRP(amount))
{
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
},
amount.asset().value());
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, amount);
});
!isTesSuccess(ret))
return ret;
}
@@ -431,21 +428,19 @@ EscrowFinish::doApply()
: kParityRate;
auto const issuer = amount.getIssuer();
bool const createAsset = destID == accountID_;
if (auto const ret = std::visit(
[&]<typename T>(T const&) {
return escrowUnlockApplyHelper<T>(
ctx_.getApplyViewContext(),
lockedRate,
sled,
preFeeBalance_,
amount,
issuer,
account,
destID,
createAsset,
j_);
},
amount.asset().value());
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
return escrowUnlockApplyHelper<T>(
ctx_.getApplyViewContext(),
lockedRate,
sled,
preFeeBalance_,
amount,
issuer,
account,
destID,
createAsset,
j_);
});
!isTesSuccess(ret))
return ret;
@@ -4,14 +4,18 @@
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/PaymentChannelHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -46,11 +50,29 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx)
return temMALFORMED;
auto const bal = ctx.tx[~sfBalance];
if (bal && (!isXRP(*bal) || *bal <= beast::kZero))
if (bal && *bal <= beast::kZero)
return temBAD_AMOUNT;
auto const amt = ctx.tx[~sfAmount];
if (amt && (!isXRP(*amt) || *amt <= beast::kZero))
if (amt && *amt <= beast::kZero)
return temBAD_AMOUNT;
if ((bal && !isXRP(*bal)) || (amt && !isXRP(*amt)))
{
if (auto const ret = checkTokenPayChanRules(ctx.rules); !isTesSuccess(ret))
return ret;
// PaymentChannelCreate and PaymentChannelFund gate an MPT-denominated
// Amount on featureMPTokensV1 (checkExtraFeatures); apply the same
// gate here for sfBalance and sfAmount.
if (((bal && bal->holds<MPTIssue>()) || (amt && amt->holds<MPTIssue>())) &&
!ctx.rules.enabled(featureMPTokensV1))
return temDISABLED;
}
// Both bal and amt must reference the same asset before comparing,
// otherwise STAmount comparison throws.
if (bal && amt && bal->asset() != amt->asset())
return temBAD_AMOUNT;
if (bal && amt && *bal > *amt)
@@ -70,8 +92,8 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx)
// The signature isn't needed if txAccount == src, but if it's
// present, check it
auto const reqBalance = bal->xrp();
auto const authAmt = amt ? amt->xrp() : reqBalance;
auto const reqBalance = *bal;
auto const authAmt = amt ? *amt : reqBalance;
if (reqBalance > authAmt)
return temBAD_AMOUNT;
@@ -96,12 +118,42 @@ PaymentChannelClaim::preflight(PreflightContext const& ctx)
TER
PaymentChannelClaim::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.rules().enabled(featureCredentials))
return Transactor::preclaim(ctx);
if (ctx.view.rules().enabled(featureCredentials))
{
if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
!isTesSuccess(err))
return err;
}
if (auto const err = credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j);
!isTesSuccess(err))
return err;
if (ctx.view.rules().enabled(featureTokenPaychan))
{
Keylet const k{ltPAYCHAN, ctx.tx[sfChannel]};
auto const slep = ctx.view.read(k);
if (!slep)
return tecNO_TARGET;
AccountID const dest = (*slep)[sfDestination];
auto const& chanFunds = slep->getFieldAmount(sfAmount);
// The requested balance and the optional amount must each match
// the channel's asset; otherwise the STAmount comparisons and
// subtractions in doApply would throw on mismatched issues. amount
// is checked independently of balance: a claim can carry Amount
// without Balance (the signature-authorized ceiling alone).
if (auto const bal = ctx.tx[~sfBalance]; bal && bal->asset() != chanFunds.asset())
return tecWRONG_ASSET;
if (auto const amt = ctx.tx[~sfAmount]; amt && amt->asset() != chanFunds.asset())
return tecWRONG_ASSET;
if (!isXRP(chanFunds) && ctx.tx.isFieldPresent(sfBalance))
{
if (auto const ret = chanFunds.asset().visit([&]<typename T>(T const&) {
return escrowUnlockPreclaimHelper<T>(ctx.view, dest, chanFunds);
});
!isTesSuccess(ret))
return ret;
}
}
return tesSUCCESS;
}
@@ -116,25 +168,36 @@ PaymentChannelClaim::doApply()
AccountID const src = (*slep)[sfAccount];
AccountID const dst = (*slep)[sfDestination];
AccountID const txAccount = ctx_.tx[sfAccount];
auto const curExpiration = (*slep)[~sfExpiration];
if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) ||
isChannelExpired(ctx_.view(), curExpiration))
{
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
return closeChannel(
slep,
ctx_.getApplyViewContext(),
k.key,
accountID_,
preFeeBalance_,
ctx_.registry.get().getJournal("View"));
}
if (txAccount != src && txAccount != dst)
if (accountID_ != src && accountID_ != dst)
return tecNO_PERMISSION;
if (ctx_.tx[~sfBalance])
{
auto const chanBalance = slep->getFieldAmount(sfBalance).xrp();
auto const chanFunds = slep->getFieldAmount(sfAmount).xrp();
auto const reqBalance = ctx_.tx[sfBalance].xrp();
auto const chanBalance = slep->getFieldAmount(sfBalance);
auto const chanFunds = slep->getFieldAmount(sfAmount);
auto const reqBalance = ctx_.tx[sfBalance];
if (txAccount == dst && !ctx_.tx[~sfSignature])
// Asset consistency between the requested balance/amount and the
// channel was validated in preclaim.
XRPL_ASSERT(
reqBalance.asset() == chanFunds.asset(),
"xrpl::PaymentChannelClaim::doApply : balance matches channel asset");
if (accountID_ == dst && !ctx_.tx[~sfSignature])
{
return ctx_.view().rules().enabled(fixCleanup3_2_0) ? TER{tecNO_PERMISSION}
: TER{temBAD_SIGNATURE};
@@ -159,27 +222,64 @@ PaymentChannelClaim::doApply()
return tecUNFUNDED_PAYMENT;
}
// The payout reqBalance - sfBalance and the unclaimed remainder
// sfAmount - reqBalance must both be exact so that the amounts
// unlocked by this claim and by the close equal what sfBalance and
// sfAmount record.
if (!isExactDifference(chanFunds, reqBalance) ||
!isExactDifference(reqBalance, chanBalance))
return tecPRECISION_LOSS;
auto const sled = ctx_.view().peek(keylet::account(dst));
if (!sled)
return tecNO_DST;
if (auto err =
verifyDepositPreauth(ctx_.tx, ctx_.view(), txAccount, dst, sled, ctx_.journal);
verifyDepositPreauth(ctx_.tx, ctx_.view(), accountID_, dst, sled, ctx_.journal);
!isTesSuccess(err))
return err;
(*slep)[sfBalance] = ctx_.tx[sfBalance];
XRPAmount const reqDelta = reqBalance - chanBalance;
STAmount const reqDelta = reqBalance - chanBalance;
XRPL_ASSERT(
reqDelta >= beast::kZero, "xrpl::PaymentChannelClaim::doApply : minimum balance delta");
(*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta;
// Transfer amount to destination
if (isXRP(reqDelta))
{
(*sled)[sfBalance] = (*sled)[sfBalance] + reqDelta;
}
else
{
Rate lockedRate = slep->isFieldPresent(sfTransferRate)
? xrpl::Rate(slep->getFieldU32(sfTransferRate))
: kParityRate;
auto const& issuer = reqDelta.getIssuer();
bool const createAsset = dst == accountID_;
if (auto const ret = reqDelta.asset().visit([&]<typename T>(T const&) {
return escrowUnlockApplyHelper<T>(
ctx_.getApplyViewContext(),
lockedRate,
sled,
preFeeBalance_,
reqDelta,
issuer,
src,
dst,
createAsset,
j_);
});
!isTesSuccess(ret))
return ret;
}
ctx_.view().update(sled);
ctx_.view().update(slep);
}
if (ctx_.tx.isFlag(tfRenew))
{
if (src != txAccount)
if (src != accountID_)
return tecNO_PERMISSION;
(*slep)[~sfExpiration] = std::nullopt;
ctx_.view().update(slep);
@@ -188,8 +288,16 @@ PaymentChannelClaim::doApply()
if (ctx_.tx.isFlag(tfClose))
{
// Channel will close immediately if dry or the receiver closes
if (dst == txAccount || (*slep)[sfBalance] == (*slep)[sfAmount])
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
if (dst == accountID_ || (*slep)[sfBalance] == (*slep)[sfAmount])
{
return closeChannel(
slep,
ctx_.getApplyViewContext(),
k.key,
accountID_,
preFeeBalance_,
ctx_.registry.get().getJournal("View"));
}
auto const settleExpiration = saturatingAdd(
ctx_.view().rules(),
@@ -1,18 +1,25 @@
#include <xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/PaymentChannelHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -51,14 +58,37 @@ namespace xrpl {
TxConsequences
PaymentChannelCreate::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
return TxConsequences{ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::kZero};
}
bool
PaymentChannelCreate::checkExtraFeatures(PreflightContext const& ctx)
{
// Only require featureMPTokensV1 when the channel amount is an MPT and
// fixCleanup3_2_0 is active; XRP/IOU channels are unaffected by this gate.
if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfAmount].holds<MPTIssue>())
return ctx.rules.enabled(featureMPTokensV1);
return true;
}
NotTEC
PaymentChannelCreate::preflight(PreflightContext const& ctx)
{
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::kZero))
auto const amount = ctx.tx[sfAmount];
if (!isXRP(amount))
{
if (auto const ret = checkTokenPayChanRules(ctx.rules); !isTesSuccess(ret))
return ret;
if (auto const ret = amount.asset().visit(
[&]<typename T>(T const&) { return payChanAmountPreflightHelper<T>(amount); });
!isTesSuccess(ret))
return ret;
}
else if (amount <= beast::kZero)
{
return temBAD_AMOUNT;
}
if (ctx.tx[sfAccount] == ctx.tx[sfDestination])
return temDST_IS_SRC;
@@ -77,6 +107,8 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx)
if (!sle)
return terNO_ACCOUNT;
auto const amount = ctx.tx[sfAmount];
// Check reserve and funds availability
if (!ctx.view.rules().enabled(featureSponsor))
{
@@ -86,15 +118,15 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx)
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
if (balance < reserve + ctx.tx[sfAmount])
if (isXRP(amount) && balance < reserve + ctx.tx[sfAmount])
return tecUNFUNDED;
}
auto const dst = ctx.tx[sfDestination];
auto const dest = ctx.tx[sfDestination];
{
// Check destination account
auto const sled = ctx.view.read(keylet::account(dst));
auto const sled = ctx.view.read(keylet::account(dest));
if (!sled)
return tecNO_DST;
@@ -115,6 +147,24 @@ PaymentChannelCreate::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}
if (!isXRP(amount))
{
// Vault shares (an MPT issued by a pseudo-account) cannot be locked
// in a channel.
if (amount.holds<MPTIssue>() && isPseudoAccount(ctx.view, amount.getIssuer()))
return tecWRONG_ASSET;
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
return escrowLockPreclaimHelper<T>(ctx.view, account, dest, amount, ctx.j);
});
!isTesSuccess(ret))
return ret;
if (auto const ret = payChanLockPrecisionHelper(ctx.view, account, amount, ctx.j);
!isTesSuccess(ret))
return ret;
}
return tesSUCCESS;
}
@@ -122,6 +172,7 @@ TER
PaymentChannelCreate::doApply()
{
auto const account = ctx_.tx[sfAccount];
auto const amount = ctx_.tx[sfAmount];
auto const sle = ctx_.view().peek(keylet::account(account));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
@@ -159,25 +210,27 @@ PaymentChannelCreate::doApply()
// - unsponsored: 1 — source owes reserve including the new increment.
auto const sourceReserve = accountReserve(
ctx_.view(), sle, j_, {.ownerCountDelta = getTxReserveSponsorID(ctx_.tx) ? 0 : 1});
if (preFeeBalance_ - ctx_.tx[sfAmount].xrp() < sourceReserve)
// Token channels lock no XRP, so only an XRP channel checks the
// source's post-lock balance, as EscrowCreate does.
if (isXRP(amount) && preFeeBalance_ - amount.xrp() < sourceReserve)
return tecUNFUNDED;
}
auto const dst = ctx_.tx[sfDestination];
auto const dest = ctx_.tx[sfDestination];
// Create PayChan in ledger.
//
// Note that we use the value from the sequence or ticket as the
// payChan sequence. For more explanation see comments in SeqProxy.h.
Keylet const payChanKeylet = keylet::payChannel(account, dst, ctx_.tx.getSeqProxy());
Keylet const payChanKeylet = keylet::payChannel(account, dest, ctx_.tx.getSeqProxy());
auto const slep = std::make_shared<SLE>(payChanKeylet);
// Funds held in this channel
(*slep)[sfAmount] = ctx_.tx[sfAmount];
(*slep)[sfAmount] = amount;
// Amount channel has already paid
(*slep)[sfBalance] = ctx_.tx[sfAmount].zeroed();
(*slep)[sfBalance] = amount.zeroed();
(*slep)[sfAccount] = account;
(*slep)[sfDestination] = dst;
(*slep)[sfDestination] = dest;
(*slep)[sfSettleDelay] = ctx_.tx[sfSettleDelay];
(*slep)[sfPublicKey] = ctx_.tx[sfPublicKey];
(*slep)[~sfCancelAfter] = ctx_.tx[~sfCancelAfter];
@@ -188,6 +241,13 @@ PaymentChannelCreate::doApply()
(*slep)[sfSequence] = ctx_.tx.getSeqProxy().value();
}
if (ctx_.view().rules().enabled(featureTokenPaychan) && !isXRP(amount))
{
auto const xferRate = transferRate(ctx_.view(), amount);
if (xferRate != kParityRate)
(*slep)[sfTransferRate] = xferRate.value;
}
ctx_.view().insert(slep);
// Add PayChan to owner directory
@@ -202,14 +262,37 @@ PaymentChannelCreate::doApply()
// Add PayChan to the recipient's owner directory
{
auto const page =
ctx_.view().dirInsert(keylet::ownerDir(dst), payChanKeylet, describeOwnerDir(dst));
ctx_.view().dirInsert(keylet::ownerDir(dest), payChanKeylet, describeOwnerDir(dest));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfDestinationNode] = *page;
}
// Add PayChan to the issuer's owner directory, if applicable
auto const& issuer = amount.getIssuer();
if (!isXRP(amount) && issuer != accountID_ && issuer != dest && !amount.holds<MPTIssue>())
{
auto page = ctx_.view().dirInsert(
keylet::ownerDir(issuer), payChanKeylet, describeOwnerDir(issuer));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
(*slep)[sfIssuerNode] = *page;
}
// Deduct owner's balance, increment owner count
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
if (isXRP(amount))
{
(*sle)[sfBalance] = (*sle)[sfBalance] - amount;
}
else
{
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
return escrowLockApplyHelper<T>(ctx_.view(), issuer, accountID_, amount, j_);
});
!isTesSuccess(ret))
return ret;
}
increaseOwnerCount(ctx_.getApplyViewContext(), sle, 1, ctx_.journal);
addSponsorToLedgerEntry(ctx_.getApplyViewContext(), slep);
ctx_.view().update(sle);
@@ -2,15 +2,19 @@
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/EscrowHelpers.h>
#include <xrpl/ledger/helpers/PaymentChannelHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Concepts.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STLedgerEntry.h>
@@ -20,12 +24,24 @@
#include <xrpl/tx/Transactor.h>
#include <xrpl/tx/applySteps.h>
#include <memory>
namespace xrpl {
TxConsequences
PaymentChannelFund::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()};
return TxConsequences{ctx.tx, isXRP(ctx.tx[sfAmount]) ? ctx.tx[sfAmount].xrp() : beast::kZero};
}
bool
PaymentChannelFund::checkExtraFeatures(PreflightContext const& ctx)
{
// Only require featureMPTokensV1 when the funding amount is an MPT and
// fixCleanup3_2_0 is active; XRP/IOU channels are unaffected by this gate.
if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfAmount].holds<MPTIssue>())
return ctx.rules.enabled(featureMPTokensV1);
return true;
}
NotTEC
@@ -34,8 +50,40 @@ PaymentChannelFund::preflight(PreflightContext const& ctx)
if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfChannel] == beast::kZero)
return temMALFORMED;
if (!isXRP(ctx.tx[sfAmount]) || (ctx.tx[sfAmount] <= beast::kZero))
auto const amount = ctx.tx[sfAmount];
if (!isXRP(amount))
{
if (auto const ret = checkTokenPayChanRules(ctx.rules); !isTesSuccess(ret))
return ret;
if (auto const ret = amount.asset().visit(
[&]<typename T>(T const&) { return payChanAmountPreflightHelper<T>(amount); });
!isTesSuccess(ret))
return ret;
}
else if (amount <= beast::kZero)
{
return temBAD_AMOUNT;
}
return tesSUCCESS;
}
TER
PaymentChannelFund::preclaim(PreclaimContext const& ctx)
{
if (ctx.view.rules().enabled(featureTokenPaychan))
{
Keylet const k{ltPAYCHAN, ctx.tx[sfChannel]};
auto const slep = ctx.view.read(k);
if (!slep)
return tecNO_ENTRY;
// The funded asset must match the channel's asset; otherwise the
// STAmount accumulation in doApply would throw on mismatched issues.
if (ctx.tx[sfAmount].asset() != (*slep)[sfAmount].asset())
return tecWRONG_ASSET;
}
return tesSUCCESS;
}
@@ -49,16 +97,22 @@ PaymentChannelFund::doApply()
return tecNO_ENTRY;
AccountID const src = (*slep)[sfAccount];
auto const txAccount = ctx_.tx[sfAccount];
AccountID const dst = (*slep)[sfDestination];
auto const curExpiration = (*slep)[~sfExpiration];
if (isChannelExpired(ctx_.view(), (*slep)[~sfCancelAfter]) ||
isChannelExpired(ctx_.view(), curExpiration))
{
return closeChannel(slep, ctx_.view(), k.key, ctx_.registry.get().getJournal("View"));
return closeChannel(
slep,
ctx_.getApplyViewContext(),
k.key,
accountID_,
preFeeBalance_,
ctx_.registry.get().getJournal("View"));
}
if (src != txAccount)
if (src != accountID_)
{
// only the owner can add funds or extend
return tecNO_PERMISSION;
@@ -82,10 +136,22 @@ PaymentChannelFund::doApply()
ctx_.view().update(slep);
}
auto const sle = ctx_.view().peek(keylet::account(txAccount));
auto const sle = ctx_.view().peek(keylet::account(accountID_));
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const amount = ctx_.tx[sfAmount];
auto const& chanAmt = slep->getFieldAmount(sfAmount);
// Asset consistency between the funded amount and the channel was
// validated in preclaim.
XRPL_ASSERT(
amount.asset() == chanAmt.asset(),
"xrpl::PaymentChannelFund::doApply : amount matches channel asset");
// A token fund locks no XRP and adds no owned object, so only an XRP fund
// checks the source's XRP reserve.
if (isXRP(amount))
{
// Check reserve and funds availability
STAmount const balance = (*sle)[sfBalance];
@@ -100,20 +166,59 @@ PaymentChannelFund::doApply()
// when a sponsor is present and would ignore the source's post-lock
// balance entirely. Funding an existing channel adds no owned object,
// so there is no owner-count delta.
if (balance < accountReserve(ctx_.view(), sle, j_) + ctx_.tx[sfAmount])
if (balance < accountReserve(ctx_.view(), sle, j_) + amount)
return tecUNFUNDED;
}
// do not allow adding funds if dst does not exist
if (AccountID const dst = (*slep)[sfDestination]; !ctx_.view().read(keylet::account(dst)))
if (!ctx_.view().read(keylet::account(dst)))
{
return tecNO_DST;
}
(*slep)[sfAmount] = (*slep)[sfAmount] + ctx_.tx[sfAmount];
ctx_.view().update(slep);
if (!isXRP(amount))
{
// Funding is subject to the same issuer controls (locking opt-in,
// authorization, freeze/lock, transferability, spendable balance)
// as channel creation.
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
return escrowLockPreclaimHelper<T>(ctx_.view(), accountID_, dst, amount, j_);
});
!isTesSuccess(ret))
return ret;
(*sle)[sfBalance] = (*sle)[sfBalance] - ctx_.tx[sfAmount];
if (auto const ret = payChanLockPrecisionHelper(ctx_.view(), accountID_, amount, j_);
!isTesSuccess(ret))
return ret;
// sfAmount must become exactly chanAmt + amount: an IOU sum that
// rounds or an MPT sum that overflows would record an increase other
// than what the source paid.
if (!isExactSum(chanAmt, amount))
return tecPRECISION_LOSS;
// The unclaimed remainder sfAmount - sfBalance must stay exact so
// that close refunds exactly what was not paid out.
if (!isExactDifference(chanAmt + amount, (*slep)[sfBalance]))
return tecPRECISION_LOSS;
}
if (isXRP(amount))
{
(*sle)[sfBalance] = (*sle)[sfBalance] - amount;
}
else
{
auto const& issuer = amount.getIssuer();
if (auto const ret = amount.asset().visit([&]<typename T>(T const&) {
return escrowLockApplyHelper<T>(ctx_.view(), issuer, accountID_, amount, j_);
});
!isTesSuccess(ret))
return ret;
}
(*slep)[sfAmount] = chanAmt + amount;
ctx_.view().update(slep);
ctx_.view().update(sle);
return tesSUCCESS;
@@ -37,7 +37,7 @@ TransactionProposalCancel::preclaim(PreclaimContext const& ctx)
// Never read the zero keylet. Preflight already rejected a zero
// ProposalID, and no real proposal can have one (every ProposalID is a
// hash), so reaching here with zero is an internal error. (and a mathematical rarity. )
// hash), so reaching here with zero is an internal error (and a mathematical rarity).
if (proposalID == beast::kZero)
return tefINTERNAL; // LCOV_EXCL_LINE
+1 -1
View File
@@ -4362,7 +4362,7 @@ private:
settleDelay,
pk,
cancelAfter),
Ter(telENV_RPC_FAILED));
Ter(tecNO_PERMISSION));
env(create(carol_, ammAlice.ammAccount(), XRP(1'000), settleDelay, pk, cancelAfter),
Ter(tecNO_PERMISSION));
+108
View File
@@ -1,4 +1,5 @@
#include <test/jtx/Account.h>
#include <test/jtx/CaptureLogs.h>
#include <test/jtx/Env.h>
#include <test/jtx/TestHelpers.h>
#include <test/jtx/amount.h>
@@ -14,13 +15,17 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <array>
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
@@ -803,6 +808,108 @@ class Clawback_test : public beast::unit_test::Suite
BEAST_EXPECT(ownerCount(env, bob) == 0);
}
void
testSubResolutionAmount(FeatureBitset features)
{
testcase("Sub-resolution amount");
using namespace test::jtx;
// IOU balances keep 16 significant digits. When alice claws back an
// amount much smaller than bob's balance, the balance changes by the
// rounded amount, which can be zero, less, or more than requested. The
// clawback invariant must accept that instead of failing the
// transaction under MPTokensV2 or logging a failure without it.
Account const alice{"alice"};
Account const bob{"bob"};
struct Case
{
std::uint64_t balanceMantissa;
int balanceExponent;
std::uint64_t clawMantissa;
int clawExponent;
std::uint64_t afterMantissa;
int afterExponent;
};
std::array const cases{
// 1e7 - 1e-10 rounds back to 1e7
Case{
.balanceMantissa = 10'000'000,
.balanceExponent = 0,
.clawMantissa = 1,
.clawExponent = -10,
.afterMantissa = 10'000'000,
.afterExponent = 0,
},
// 1234567890.123456 - 0.0000005 rounds back to the balance
Case{
.balanceMantissa = 1'234'567'890'123'456,
.balanceExponent = -6,
.clawMantissa = 5,
.clawExponent = -7,
.afterMantissa = 1'234'567'890'123'456,
.afterExponent = -6,
},
// 0.0000014 removes 0.000001
Case{
.balanceMantissa = 1'234'567'890'123'456,
.balanceExponent = -6,
.clawMantissa = 14,
.clawExponent = -7,
.afterMantissa = 1'234'567'890'123'455,
.afterExponent = -6,
},
// 0.0000016 removes 0.000002
Case{
.balanceMantissa = 1'234'567'890'123'456,
.balanceExponent = -6,
.clawMantissa = 16,
.clawExponent = -7,
.afterMantissa = 1'234'567'890'123'454,
.afterExponent = -6,
},
};
for (auto const feat : {features, features - featureMPTokensV2})
{
for (auto const& c : cases)
{
std::string logs;
{
Env env(*this, feat, std::make_unique<test::CaptureLogs>(&logs));
env.fund(XRP(1000), alice, bob);
env.close();
auto const usd = alice["USD"];
env(fset(alice, asfAllowTrustLineClawback));
env.close();
env.trust(usd(10'000'000'000), bob);
env(pay(
alice, bob, STAmount{usd.issue(), c.balanceMantissa, c.balanceExponent}));
env.close();
env(claw(
alice,
STAmount{
Issue{usd.currency, bob.id()}, c.clawMantissa, c.clawExponent}),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(
env.balance(bob, usd).value() ==
STAmount(usd.issue(), c.afterMantissa, c.afterExponent));
}
// Without MPTokensV2 a failed invariant only logs and the
// transaction still succeeds, so check the logs instead.
if (!feat[featureMPTokensV2])
BEAST_EXPECT(!logs.contains("Invariant failed"));
}
}
}
void
testTickets(FeatureBitset features)
{
@@ -872,6 +979,7 @@ class Clawback_test : public beast::unit_test::Suite
testDeleteDefaultLine(features);
testFrozenLine(features);
testAmountExceedsAvailable(features);
testSubResolutionAmount(features);
testTickets(features);
}
+78
View File
@@ -2295,6 +2295,83 @@ struct EscrowToken_test : public beast::unit_test::Suite
}
}
void
testIOUUnlockCreditRoundsAtBalancePrecision(FeatureBitset features)
{
testcase("IOU Unlock Credit Rounds At Balance Precision");
using namespace test::jtx;
using namespace std::literals;
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const gw = Account{"gateway"};
auto const usd = gw["USD"];
STAmount const limit{usd.issue(), 1, 21};
STAmount const large{usd.issue(), 1, 20};
// The unlock credits 1 to a balance of exactly 1e20; the sum keeps 16
// significant digits, so the balance stays 1e20
BEAST_EXPECT(large + usd(1) == large);
// EscrowCancel: the refund of 1 to alice is lost to rounding
{
Env env{*this, features};
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(limit, alice);
env.close();
env(pay(gw, alice, usd(1)));
env.close();
// The nonzero limit keeps alice's line after she escrows all of it
auto const seq = env.seq(alice);
env(escrow::create(alice, bob, usd(1)),
escrow::kFinishTime(env.now() + 1s),
escrow::kCancelTime(env.now() + 2s));
env.close();
BEAST_EXPECT(env.balance(alice, usd) == usd(0));
env(pay(gw, alice, large));
env.close();
env.close();
BEAST_EXPECT(env.balance(alice, usd) == large);
env(escrow::cancel(alice, alice, seq), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
BEAST_EXPECT(env.balance(alice, usd) == large);
}
// EscrowFinish: the payout of 1 to bob is lost to rounding
{
Env env{*this, features};
env.fund(XRP(10'000), alice, bob, gw);
env(fset(gw, asfAllowTrustLineLocking));
env.close();
env.trust(usd(1), alice);
env.trust(limit, bob);
env.close();
env(pay(gw, alice, usd(1)));
env(pay(gw, bob, large));
env.close();
auto const seq = env.seq(alice);
env(escrow::create(alice, bob, usd(1)), escrow::kFinishTime(env.now() + 1s));
env.close();
env.close();
BEAST_EXPECT(env.balance(bob, usd) == large);
env(escrow::finish(bob, alice, seq), Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), SeqProxy::rawSequence(seq))));
BEAST_EXPECT(env.balance(alice, usd) == usd(0));
BEAST_EXPECT(env.balance(bob, usd) == large);
}
}
void
testMPTEnablement(FeatureBitset features)
{
@@ -4300,6 +4377,7 @@ struct EscrowToken_test : public beast::unit_test::Suite
testIOUFreeze(features);
testIOUInsufficientFunds(features);
testIOUPrecisionLoss(features);
testIOUUnlockCreditRoundsAtBalancePrecision(features);
}
void
+135
View File
@@ -1215,6 +1215,61 @@ struct FlowMPT_test : public beast::unit_test::Suite
BEAST_EXPECT(env.balance(bob, y) == y(0));
}
void
testConsumeOfferOverflow(FeatureBitset features)
{
// An overflow while consuming an offer fails the strand under
// MPTokensV2, and the transaction with tecINTERNAL otherwise. Either
// way no balances change and the offer stays on the book.
testcase("BookStep consumeOffer overflow");
using namespace jtx;
Account const gw("gateway");
Account const eurGW("eur_gateway");
Account const maker("maker");
Account const alice("alice");
Account const bob("bob");
for (auto const& testFeatures : {features, features - featureMPTokensV2})
{
Env env(*this, testFeatures);
auto const withV2 = testFeatures[featureMPTokensV2];
env.fund(XRP(10'000), gw, eurGW, maker, alice, bob);
env.close();
auto const usd = gw["USD"];
auto const eur = eurGW["EUR"];
STAmount const usdMax{usd.issue(), STAmount::kMaxValue, STAmount::kMaxOffset};
STAmount const usdIn{usd.issue(), 1, 90};
env.trust(usdMax, maker, alice);
env.trust(eur(1'000), maker, bob);
env.close();
env(pay(gw, maker, usdMax));
env(pay(gw, alice, usdIn));
env(pay(eurGW, maker, eur(100)));
env(offer(maker, usdIn, eur(100)));
env.close();
env(pay(alice, bob, eur(100)),
Path(~eur),
Sendmax(usdIn),
Ter(withV2 ? TER{tecPATH_PARTIAL} : TER{tecINTERNAL}));
env.close();
env.require(
Balance(bob, eur(0)),
Balance(maker, eur(100)),
Balance(maker, usdMax),
Balance(alice, usdIn));
BEAST_EXPECT(isOffer(env, maker, usdIn, eur(100)));
}
}
void
testFalseDry(FeatureBitset features)
{
@@ -3008,6 +3063,7 @@ struct FlowMPT_test : public beast::unit_test::Suite
testBookStepInputAboveMaximumAmount(features);
testBookStepInputAboveMaximumAmountWithFee(features);
testBookStepMissingIssuance(features);
testConsumeOfferOverflow(features);
testSelfPayment1(features);
testSelfPayment2(features);
testSelfFundedXRPEndpoint(false, features);
@@ -3016,6 +3072,85 @@ struct FlowMPT_test : public beast::unit_test::Suite
testReExecuteDirectStep(features);
testSelfPayLowQualityOffer(features);
testLockedMidPathHolder(features);
testIssuerSourceMPTLoop(features);
}
void
testIssuerSourceMPTLoop(FeatureBitset features)
{
// An issuer paying with its own MPT may not route it back through a
// book that outputs the same MPT. It would issue at the first book
// and again after the second, and with the issuer's own offer in
// the second book the engine could not keep the sum within
// MaximumAmount. Holders already get temBAD_PATH_LOOP for this.
testcase("Issuer-sourced payment looping through its own MPT");
using namespace jtx;
Account const gw{"gw"};
Account const alice{"alice"};
Account const c1{"c1"};
Account const e1{"e1"};
Account const dan{"dan"};
Env env(*this, features);
env.fund(XRP(1'000), gw, alice, c1, e1, dan);
env.close();
MPT const x =
MPTTester({.env = env, .issuer = gw, .holders = {alice, c1, e1, dan}, .maxAmt = 100});
auto const eur = gw["EUR"];
auto const usd = gw["USD"];
env(trust(e1, eur(1'000)));
env(trust(e1, usd(1'000)));
env(trust(dan, eur(1'000)));
env(pay(gw, e1, eur(1'000)));
env(pay(gw, e1, usd(1'000)));
env(pay(gw, alice, x(10)));
env.close();
// Liquidity for every path below, arranged so nothing crosses at
// creation.
env(offer(gw, XRP(60), x(30))); // issuer sells x for XRP
env(offer(gw, usd(100), x(100))); // issuer sells x for usd
env.close();
env(offer(c1, x(100), XRP(100))); // buys x for XRP
env(offer(e1, XRP(70), usd(70))); // sells usd for XRP
env(offer(e1, x(100), eur(100))); // sells eur for x
env.close();
BEAST_EXPECT(expectOffers(env, gw, 2));
BEAST_EXPECT(expectOffers(env, c1, 1));
BEAST_EXPECT(expectOffers(env, e1, 2));
auto const flags = Txflags(tfNoRippleDirect | tfPartialPayment);
// x -> XRP -> x, the issuer's own offer feeds the endpoint
env(pay(gw, dan, x(100)), Sendmax(x(100)), Path(~XRP, ~x), flags, Ter(temBAD_PATH_LOOP));
// x -> XRP -> x -> eur, the issuer's own offer feeds a third book
env(pay(gw, dan, eur(100)),
Sendmax(x(100)),
Path(~XRP, ~x, ~eur),
flags,
Ter(temBAD_PATH_LOOP));
// x -> XRP -> usd -> x, a limiting book before the issuer's own offer
env(pay(gw, dan, x(100)),
Sendmax(x(100)),
Path(~XRP, ~usd, ~x),
flags,
Ter(temBAD_PATH_LOOP));
// Same loop from a holder is already rejected
env(pay(alice, dan, x(10)), Sendmax(x(10)), Path(~XRP, ~x), flags, Ter(temBAD_PATH_LOOP));
env.close();
BEAST_EXPECT(env.balance(gw, x) == x(-10));
BEAST_EXPECT(env.balance(c1, x) == x(0));
BEAST_EXPECT(env.balance(dan, x) == x(0));
// The issuer paying with another asset may still take its own offer
env(pay(gw, dan, x(50)), Sendmax(usd(50)), Path(~x), flags);
env.close();
BEAST_EXPECT(env.balance(dan, x) == x(50));
BEAST_EXPECT(env.balance(gw, x) == x(-60));
}
void
+3 -31
View File
@@ -994,8 +994,9 @@ class MPToken_test : public beast::unit_test::Suite
test::jtx::MPT const mpT1("MPT", makeMptID(env.seq(alice) + 10, alice));
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tecOBJECT_NOT_FOUND);
env(pay(alice, carol, mpT1(100)), Sendmax(mpt(100)), err);
// "paths" is invalid in V1
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(tesSUCCESS);
// "paths" is invalid in V1. In V2 any path from the issuer back
// to its own MPT is a loop, like it is for a holder.
err = !mpTokensV2 ? Ter(temMALFORMED) : Ter(temBAD_PATH_LOOP);
env(pay(alice, carol, mpt(100)), Path(~usd), err);
}
@@ -2009,35 +2010,6 @@ class MPToken_test : public beast::unit_test::Suite
};
ammBid(sfBidMin);
ammBid(sfBidMax);
// PaymentChannelCreate
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelCreate;
jv[jss::Account] = alice.human();
jv[jss::Destination] = carol.human();
jv[jss::SettleDelay] = 1;
jv[sfPublicKey.fieldName] = strHex(alice.pk().slice());
jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None);
test(jv, jss::Amount.cStr());
}
// PaymentChannelFund
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelFund;
jv[jss::Account] = alice.human();
jv[sfChannel.fieldName] = to_string(UInt256{1});
jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None);
test(jv, jss::Amount.cStr());
}
// PaymentChannelClaim
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelClaim;
jv[jss::Account] = alice.human();
jv[sfChannel.fieldName] = to_string(UInt256{1});
jv[jss::Amount] = mpt.getJson(JsonOptions::Values::None);
test(jv, jss::Amount.cStr());
}
// NFTokenCreateOffer
{
json::Value jv;
File diff suppressed because it is too large. Load diff
+125 -116
View File
@@ -99,27 +99,31 @@ struct PayChan_test : public beast::unit_test::Suite
env.fund(XRP(10000), alice, bob);
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk));
BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0));
BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0));
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000));
{
auto const preAlice = env.balance(alice);
env(fund(alice, chan, XRP(1000)));
env(paychan::fund(alice, chan, XRP(1000)));
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops);
}
auto chanBal = channelBalance(*env.current(), chan);
auto const chanAmt = channelAmount(*env.current(), chan);
auto chanBal = paychan::channelBalance(*env.current(), chan);
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
BEAST_EXPECT(chanBal == XRP(0));
BEAST_EXPECT(chanAmt == XRP(2000));
{
// bad amounts (non-xrp, negative amounts)
env(create(alice, bob, usda(1000), settleDelay, pk), Ter(temBAD_AMOUNT));
env(fund(alice, chan, usda(1000)), Ter(temBAD_AMOUNT));
// A token amount needs TokenPaychan (temBAD_AMOUNT) and
// fixCleanup3_2_0, which this suite disables (temDISABLED).
auto const tokenResult =
features[featureTokenPaychan] ? Ter(temDISABLED) : Ter(temBAD_AMOUNT);
env(create(alice, bob, usda(1000), settleDelay, pk), tokenResult);
env(fund(alice, chan, usda(1000)), tokenResult);
env(create(alice, bob, XRP(-1000), settleDelay, pk), Ter(temBAD_AMOUNT));
env(fund(alice, chan, XRP(-1000)), Ter(temBAD_AMOUNT));
}
@@ -140,9 +144,13 @@ struct PayChan_test : public beast::unit_test::Suite
auto const iou = usda(100).value();
auto const negXRP = XRP(-100).value();
auto const posXRP = XRP(100).value();
env(claim(alice, chan, iou, iou), Ter(temBAD_AMOUNT));
env(claim(alice, chan, posXRP, iou), Ter(temBAD_AMOUNT));
env(claim(alice, chan, iou, posXRP), Ter(temBAD_AMOUNT));
// A token Balance or Amount needs TokenPaychan (temBAD_AMOUNT) and
// fixCleanup3_2_0, which this suite disables (temDISABLED).
auto const tokenResult =
features[featureTokenPaychan] ? Ter(temDISABLED) : Ter(temBAD_AMOUNT);
env(claim(alice, chan, iou, iou), tokenResult);
env(claim(alice, chan, posXRP, iou), tokenResult);
env(claim(alice, chan, iou, posXRP), tokenResult);
env(claim(alice, chan, negXRP, negXRP), Ter(temBAD_AMOUNT));
env(claim(alice, chan, posXRP, negXRP), Ter(temBAD_AMOUNT));
env(claim(alice, chan, negXRP, posXRP), Ter(temBAD_AMOUNT));
@@ -162,9 +170,9 @@ struct PayChan_test : public beast::unit_test::Suite
auto const reqBal = chanBal + delta;
auto const authAmt = reqBal + XRP(100);
assert(reqBal <= chanAmt);
env(claim(alice, chan, reqBal, authAmt));
BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
env(paychan::claim(alice, chan, reqBal, authAmt));
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == reqBal);
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(env.balance(bob) == preBob + delta);
chanBal = reqBal;
}
@@ -324,14 +332,14 @@ struct PayChan_test : public beast::unit_test::Suite
NetClock::time_point const cancelAfter =
env.current()->header().parentCloseTime + 3600s;
auto const channelFunds = XRP(1000);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter));
BEAST_EXPECT(channelExists(*env.current(), chan));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk, cancelAfter));
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
env.close(cancelAfter);
{
// dst cannot claim after cancelAfter
auto const chanBal = channelBalance(*env.current(), chan);
auto const chanAmt = channelAmount(*env.current(), chan);
auto const chanBal = paychan::channelBalance(*env.current(), chan);
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const delta = XRP(500);
@@ -341,7 +349,7 @@ struct PayChan_test : public beast::unit_test::Suite
auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt);
env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()));
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(!channelExists(*env.current(), chan));
BEAST_EXPECT(!paychan::channelExists(*env.current(), chan));
BEAST_EXPECT(env.balance(bob) == preBob - feeDrops);
BEAST_EXPECT(env.balance(alice) == preAlice + channelFunds);
}
@@ -355,9 +363,9 @@ struct PayChan_test : public beast::unit_test::Suite
NetClock::time_point const cancelAfter =
env.current()->header().parentCloseTime + 3600s;
auto const channelFunds = XRP(1000);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter));
BEAST_EXPECT(channelExists(*env.current(), chan));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk, cancelAfter));
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
// third party close before cancelAfter
env(claim(carol, chan), Txflags(tfClose), Ter(tecNO_PERMISSION));
BEAST_EXPECT(channelExists(*env.current(), chan));
@@ -424,9 +432,9 @@ struct PayChan_test : public beast::unit_test::Suite
auto const minExpiration = closeTime + settleDelay;
NetClock::time_point const cancelAfter = closeTime + 7200s;
auto const channelFunds = XRP(1000);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter));
BEAST_EXPECT(channelExists(*env.current(), chan));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk, cancelAfter));
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
BEAST_EXPECT(!channelExpiration(*env.current(), chan));
// Owner closes, will close after settleDelay
env(claim(alice, chan), Txflags(tfClose));
@@ -455,7 +463,7 @@ struct PayChan_test : public beast::unit_test::Suite
env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration - 50s}),
Ter(temBAD_EXPIRATION));
BEAST_EXPECT(!channelExpiration(*env.current(), chan));
env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration}));
env(paychan::fund(alice, chan, XRP(1), NetClock::time_point{minExpiration}));
env.close(minExpiration);
// Try to extend the expiration after the expiration has already passed
env(fund(alice, chan, XRP(1), NetClock::time_point{minExpiration + 1000s}));
@@ -477,17 +485,17 @@ struct PayChan_test : public beast::unit_test::Suite
NetClock::time_point const settleTimepoint =
env.current()->header().parentCloseTime + settleDelay;
auto const channelFunds = XRP(1000);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(channelExists(*env.current(), chan));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
// Owner closes, will close after settleDelay
env(claim(alice, chan), Txflags(tfClose));
BEAST_EXPECT(channelExists(*env.current(), chan));
env.close(settleTimepoint - (settleDelay / 2));
{
// receiver can still claim
auto const chanBal = channelBalance(*env.current(), chan);
auto const chanAmt = channelAmount(*env.current(), chan);
auto const chanBal = paychan::channelBalance(*env.current(), chan);
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
auto const preBob = env.balance(bob);
auto const delta = XRP(500);
auto const reqBal = chanBal + delta;
@@ -503,8 +511,8 @@ struct PayChan_test : public beast::unit_test::Suite
env.close(settleTimepoint);
{
// past settleTime, channel will close
auto const chanBal = channelBalance(*env.current(), chan);
auto const chanAmt = channelAmount(*env.current(), chan);
auto const chanBal = paychan::channelBalance(*env.current(), chan);
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
auto const preAlice = env.balance(alice);
auto const preBob = env.balance(bob);
auto const delta = XRP(500);
@@ -533,17 +541,17 @@ struct PayChan_test : public beast::unit_test::Suite
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(channelExists(*env.current(), chan));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
// Owner tries to close channel, but it will remain open (settle delay)
env(claim(alice, chan), Txflags(tfClose));
BEAST_EXPECT(channelExists(*env.current(), chan));
{
// claim the entire amount
auto const preBob = env.balance(bob);
env(claim(alice, chan, channelFunds.value(), channelFunds.value()));
BEAST_EXPECT(channelBalance(*env.current(), chan) == channelFunds);
env(paychan::claim(alice, chan, channelFunds.value(), channelFunds.value()));
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == channelFunds);
BEAST_EXPECT(env.balance(bob) == preBob + channelFunds);
}
auto const preAlice = env.balance(alice);
@@ -568,15 +576,15 @@ struct PayChan_test : public beast::unit_test::Suite
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(channelExists(*env.current(), chan));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
// Owner tries to close channel, but it will remain open (settle delay)
env(claim(alice, chan), Txflags(tfClose));
BEAST_EXPECT(channelExists(*env.current(), chan));
{
auto chanBal = channelBalance(*env.current(), chan);
auto const chanAmt = channelAmount(*env.current(), chan);
auto chanBal = paychan::channelBalance(*env.current(), chan);
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
auto const preBob = env.balance(bob);
auto const delta = XRP(500);
@@ -591,8 +599,8 @@ struct PayChan_test : public beast::unit_test::Suite
}
{
// Claim again
auto chanBal = channelBalance(*env.current(), chan);
auto const chanAmt = channelAmount(*env.current(), chan);
auto chanBal = paychan::channelBalance(*env.current(), chan);
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
auto const preBob = env.balance(bob);
auto const delta = XRP(500);
@@ -623,9 +631,9 @@ struct PayChan_test : public beast::unit_test::Suite
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env(fset(bob, asfDisallowXRP));
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), 3600s, alice.pk()));
BEAST_EXPECT(channelExists(*env.current(), chan));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, XRP(1000), 3600s, alice.pk()));
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
}
{
@@ -634,13 +642,13 @@ struct PayChan_test : public beast::unit_test::Suite
// since it is just advisory.
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), 3600s, alice.pk()));
BEAST_EXPECT(channelExists(*env.current(), chan));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, XRP(1000), 3600s, alice.pk()));
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
env(fset(bob, asfDisallowXRP));
auto const reqBal = XRP(500).value();
env(claim(alice, chan, reqBal, reqBal));
env(paychan::claim(alice, chan, reqBal, reqBal));
}
}
@@ -692,16 +700,16 @@ struct PayChan_test : public beast::unit_test::Suite
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0));
BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000));
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0));
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000));
// alice can add more funds to the channel even though bob has
// asfDepositAuth set.
env(fund(alice, chan, XRP(1000)));
env(paychan::fund(alice, chan, XRP(1000)));
env.close();
// alice claims. Fails because bob's lsfDepositAuth flag is set.
@@ -729,7 +737,7 @@ struct PayChan_test : public beast::unit_test::Suite
// bob claims with signature. Succeeds even though bob's
// lsfDepositAuth flag is set since bob submitted the
// transaction.
env(claim(bob, chan, delta, delta, Slice(sig), pk));
env(paychan::claim(bob, chan, delta, delta, Slice(sig), pk));
env.close();
BEAST_EXPECT(env.balance(bob) == preBob + delta - baseFee);
}
@@ -760,7 +768,7 @@ struct PayChan_test : public beast::unit_test::Suite
env(deposit::auth(bob, alice));
env.close();
env(claim(alice, chan, delta, delta, Slice(sig), pk));
env(paychan::claim(alice, chan, delta, delta, Slice(sig), pk));
env.close();
BEAST_EXPECT(env.balance(bob) == preBob + delta - (3 * baseFee));
@@ -782,7 +790,7 @@ struct PayChan_test : public beast::unit_test::Suite
env.close();
// alice claims successfully.
env(claim(alice, chan, delta, delta));
env(paychan::claim(alice, chan, delta, delta));
env.close();
BEAST_EXPECT(env.balance(bob) == preBob + XRP(800) - (5 * baseFee));
}
@@ -810,12 +818,12 @@ struct PayChan_test : public beast::unit_test::Suite
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
// alice add funds to the channel
env(fund(alice, chan, XRP(1000)));
env(paychan::fund(alice, chan, XRP(1000)));
env.close();
std::string const credBadIdx =
@@ -906,12 +914,12 @@ struct PayChan_test : public beast::unit_test::Suite
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
// alice add funds to the channel
env(fund(alice, chan, XRP(1000)));
env(paychan::fund(alice, chan, XRP(1000)));
env.close();
auto const delta = XRP(500).value();
@@ -939,11 +947,11 @@ struct PayChan_test : public beast::unit_test::Suite
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
env(fund(alice, chan, XRP(1000)));
env(paychan::fund(alice, chan, XRP(1000)));
env.close();
std::string const credIdx =
"48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6EA288B"
@@ -975,12 +983,12 @@ struct PayChan_test : public beast::unit_test::Suite
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
auto const chan1 = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(channelExists(*env.current(), chan1));
auto const chan2 = channel(alice, bob, env.seq(alice));
env(create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(channelExists(*env.current(), chan2));
auto const chan1 = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(paychan::channelExists(*env.current(), chan1));
auto const chan2 = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
BEAST_EXPECT(paychan::channelExists(*env.current(), chan2));
BEAST_EXPECT(chan1 != chan2);
}
@@ -999,8 +1007,8 @@ struct PayChan_test : public beast::unit_test::Suite
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
auto const chan1Str = to_string(channel(alice, bob, env.seq(alice)));
env(create(alice, bob, channelFunds, settleDelay, pk));
auto const chan1Str = to_string(paychan::channel(alice, bob, env.seq(alice)));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
env.close();
{
// test account non-string
@@ -1106,8 +1114,8 @@ struct PayChan_test : public beast::unit_test::Suite
BEAST_EXPECT(r[jss::result][jss::channels].size() == 0);
BEAST_EXPECT(r[jss::result][jss::validated]);
}
auto const chan2Str = to_string(channel(alice, bob, env.seq(alice)));
env(create(alice, bob, channelFunds, settleDelay, pk));
auto const chan2Str = to_string(paychan::channel(alice, bob, env.seq(alice)));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
env.close();
{
auto const r = env.rpc("account_channels", alice.human(), bob.human());
@@ -1157,7 +1165,7 @@ struct PayChan_test : public beast::unit_test::Suite
auto const channelFunds = XRP(1);
for (auto const& b : bobs)
{
env(create(alice, b, channelFunds, settleDelay, alice.pk()));
env(paychan::create(alice, b, channelFunds, settleDelay, alice.pk()));
}
}
@@ -1281,8 +1289,8 @@ struct PayChan_test : public beast::unit_test::Suite
// channels where alice is the source, not the destination
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
env(create(alice, bob, channelFunds, settleDelay, alice.pk()));
env(create(bob, alice, channelFunds, settleDelay, bob.pk()));
env(paychan::create(alice, bob, channelFunds, settleDelay, alice.pk()));
env(paychan::create(bob, alice, channelFunds, settleDelay, bob.pk()));
auto const r = [&] {
json::Value jvc;
@@ -1309,8 +1317,8 @@ struct PayChan_test : public beast::unit_test::Suite
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
auto const chan1Str = to_string(channel(alice, bob, env.seq(alice)));
env(create(alice, bob, channelFunds, settleDelay, pk));
auto const chan1Str = to_string(paychan::channel(alice, bob, env.seq(alice)));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
env.close();
json::Value args{json::ValueType::Object};
@@ -1344,8 +1352,8 @@ struct PayChan_test : public beast::unit_test::Suite
auto const pk = alice.pk();
auto const settleDelay = 3600s;
auto const channelFunds = XRP(1000);
auto const chan1Str = to_string(channel(alice, bob, env.seq(alice)));
env(create(alice, bob, channelFunds, settleDelay, pk));
auto const chan1Str = to_string(paychan::channel(alice, bob, env.seq(alice)));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
env.close();
std::string chan1PkStr;
{
@@ -1367,8 +1375,8 @@ struct PayChan_test : public beast::unit_test::Suite
BEAST_EXPECT(r[jss::result][jss::channels].size() == 0);
BEAST_EXPECT(r[jss::result][jss::validated]);
}
auto const chan2Str = to_string(channel(alice, bob, env.seq(alice)));
env(create(alice, bob, channelFunds, settleDelay, pk));
auto const chan2Str = to_string(paychan::channel(alice, bob, env.seq(alice)));
env(paychan::create(alice, bob, channelFunds, settleDelay, pk));
env.close();
{
auto const r = env.rpc("account_channels", alice.human(), bob.human());
@@ -1836,8 +1844,8 @@ struct PayChan_test : public beast::unit_test::Suite
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
auto jv = create(alice, bob, XRP(1000), settleDelay, pk);
auto const chan = paychan::channel(alice, bob, env.seq(alice));
auto jv = paychan::create(alice, bob, XRP(1000), settleDelay, pk);
auto const pkHex = strHex(pk.slice());
jv["PublicKey"] = pkHex.substr(2, pkHex.size() - 2);
env(jv, Ter(temMALFORMED));
@@ -1915,7 +1923,7 @@ struct PayChan_test : public beast::unit_test::Suite
// Test with adding the paychan to the recipient's owner directory
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env(create(alice, bob, XRP(1000), settleDelay, pk));
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
auto const [chan, chanSle] = channelKeyAndSle(*env.current(), alice, bob);
BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle));
@@ -1937,7 +1945,7 @@ struct PayChan_test : public beast::unit_test::Suite
Env env(*this, features);
env.fund(XRP(10000), alice, bob);
// create the channel before the amendment activates
env(create(alice, bob, XRP(1000), settleDelay, pk));
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
auto const [chan, chanSle] = channelKeyAndSle(*env.current(), alice, bob);
BEAST_EXPECT(inOwnerDir(*env.current(), alice, chanSle));
@@ -1990,18 +1998,18 @@ struct PayChan_test : public beast::unit_test::Suite
// Create a channel from alice to bob
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, env.seq(alice));
env(create(alice, bob, XRP(1000), settleDelay, pk));
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, XRP(1000), settleDelay, pk));
env.close();
BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0));
BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000));
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0));
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000));
rmAccount(env, alice, carol, tecHAS_OBLIGATIONS);
rmAccount(env, bob, carol, TER(tecHAS_OBLIGATIONS));
auto const feeDrops = env.current()->fees().base;
auto chanBal = channelBalance(*env.current(), chan);
auto chanAmt = channelAmount(*env.current(), chan);
auto chanBal = paychan::channelBalance(*env.current(), chan);
auto chanAmt = paychan::channelAmount(*env.current(), chan);
BEAST_EXPECT(chanBal == XRP(0));
BEAST_EXPECT(chanAmt == XRP(1000));
@@ -2065,15 +2073,15 @@ struct PayChan_test : public beast::unit_test::Suite
auto const pk = alice.pk();
auto const settleDelay = 100s;
auto const chan = channel(alice, bob, aliceTicketSeq);
auto const chan = paychan::channel(alice, bob, aliceTicketSeq);
env(create(alice, bob, XRP(1000), settleDelay, pk), ticket::Use(aliceTicketSeq++));
env.require(Tickets(alice, env.seq(alice) - aliceTicketSeq));
BEAST_EXPECT(env.seq(alice) == aliceSeq);
BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0));
BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000));
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == XRP(0));
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == XRP(1000));
{
auto const preAlice = env.balance(alice);
@@ -2086,8 +2094,8 @@ struct PayChan_test : public beast::unit_test::Suite
BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops);
}
auto chanBal = channelBalance(*env.current(), chan);
auto const chanAmt = channelAmount(*env.current(), chan);
auto chanBal = paychan::channelBalance(*env.current(), chan);
auto const chanAmt = paychan::channelAmount(*env.current(), chan);
BEAST_EXPECT(chanBal == XRP(0));
BEAST_EXPECT(chanAmt == XRP(2000));
@@ -2103,8 +2111,8 @@ struct PayChan_test : public beast::unit_test::Suite
env.require(Tickets(alice, env.seq(alice) - aliceTicketSeq));
BEAST_EXPECT(env.seq(alice) == aliceSeq);
BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == reqBal);
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(env.balance(bob) == preBob + delta);
chanBal = reqBal;
}
@@ -2122,8 +2130,8 @@ struct PayChan_test : public beast::unit_test::Suite
env.require(Tickets(bob, env.seq(bob) - bobTicketSeq));
BEAST_EXPECT(env.seq(bob) == bobSeq);
BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == reqBal);
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt);
auto const feeDrops = env.current()->fees().base;
BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops);
chanBal = reqBal;
@@ -2138,8 +2146,8 @@ struct PayChan_test : public beast::unit_test::Suite
env.require(Tickets(bob, env.seq(bob) - bobTicketSeq));
BEAST_EXPECT(env.seq(bob) == bobSeq);
BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(env.balance(bob) == preBob - feeDrops);
}
{
@@ -2158,8 +2166,8 @@ struct PayChan_test : public beast::unit_test::Suite
env.require(Tickets(bob, env.seq(bob) - bobTicketSeq));
BEAST_EXPECT(env.seq(bob) == bobSeq);
BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(env.balance(bob) == preBob);
}
@@ -2169,8 +2177,8 @@ struct PayChan_test : public beast::unit_test::Suite
env.require(Tickets(bob, env.seq(bob) - bobTicketSeq));
BEAST_EXPECT(env.seq(bob) == bobSeq);
BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt);
BEAST_EXPECT(paychan::channelBalance(*env.current(), chan) == chanBal);
BEAST_EXPECT(paychan::channelAmount(*env.current(), chan) == chanAmt);
{
// Dst closes channel
@@ -2181,7 +2189,7 @@ struct PayChan_test : public beast::unit_test::Suite
env.require(Tickets(bob, env.seq(bob) - bobTicketSeq));
BEAST_EXPECT(env.seq(bob) == bobSeq);
BEAST_EXPECT(!channelExists(*env.current(), chan));
BEAST_EXPECT(!paychan::channelExists(*env.current(), chan));
auto const feeDrops = env.current()->fees().base;
auto const delta = chanAmt - chanBal;
assert(delta > beast::kZero);
@@ -2232,6 +2240,7 @@ public:
// pre-amendment behavior, so run it with the amendment disabled.
FeatureBitset const all{testableAmendments() - fixCleanup3_2_0};
testWithFeats(all);
testWithFeats(all - featureTokenPaychan);
testDepositAuthCreds();
testMetaAndOwnership(all - fixIncludeKeyletFields);
}
+36
View File
@@ -4353,6 +4353,42 @@ public:
BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0);
BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0);
}
// A sponsored token channel locks no XRP, so a source below its own
// reserve before the fee may create one, as it may a token escrow.
if (cosigning)
{
Env env{*this, testableAmendments()};
Account const gw("gw");
auto const usd = gw["USD"];
env.fund(XRP(1000000), alice, bob, sponsor, gw);
env.close();
env(fset(gw, asfAllowTrustLineLocking));
env.trust(usd(10000), alice);
env.close();
env(pay(gw, alice, usd(1000)));
env.close();
adjustAccountXRPBalance(env, alice, reserve(env, ownerCount(env, alice)));
env(noop(alice));
env.close();
BEAST_EXPECT(env.balance(alice) < reserve(env, ownerCount(env, alice)));
env(escrow::create(alice, bob, usd(100)),
escrow::kFinishTime(env.now() + 100s),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor),
Ter(tesSUCCESS));
env.close();
auto const chan = paychan::channel(alice, bob, env.seq(alice));
env(paychan::create(alice, bob, usd(100), 10s, alice.pk()),
sponsor::As(sponsor, spfSponsorReserve),
Sig(sfSponsorSignature, sponsor),
Ter(tesSUCCESS));
env.close();
BEAST_EXPECT(paychan::channelExists(*env.current(), chan));
}
}
void
@@ -215,8 +215,9 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
}
// The target account may refuse any proposal made for it, at any point in
// its lifecycle, without owning the object: anyone can create a proposal
// against any account and tie up one of its tickets (On-Chain Cosigner spec §7.2).
// its lifecycle, without owning the object: any of its signers, an
// authorized delegate, or that delegate's signers can create one and tie
// up one of its tickets (XLS-0103 §7.2).
void
testTargetCancel(FeatureBitset features)
{
@@ -683,6 +684,25 @@ struct TransactionProposalCancel_test : public beast::unit_test::Suite
BEAST_EXPECT(env.le(keylet::txProposal(proposalID)));
BEAST_EXPECT(ownerCount(env, alice) == proposal::kProposalOwnerCount);
}
// Paying with a ticket changes none of that. The failed cancel still
// consumes carol's ticket, and nothing else moves: the proposal and
// its reserve stay as they were.
{
uint256 const proposalID = makeProposal(env, alice, target, bob);
std::uint32_t const cancelTicket = proposal::createTicket(env, carol);
auto const aliceOwnerCount = ownerCount(env, alice);
env(proposal::cancel(carol, proposalID),
ticket::Use(cancelTicket),
Ter(tecNO_PERMISSION));
env.close();
BEAST_EXPECT(env.le(keylet::txProposal(proposalID)));
BEAST_EXPECT(!env.le(keylet::ticket(carol.id(), SeqProxy::rawTicket(cancelTicket))));
BEAST_EXPECT(ownerCount(env, alice) == aliceOwnerCount);
BEAST_EXPECT(ownerCount(env, carol) == 0);
}
}
// A Cancel works as an inner Batch transaction, where each inner runs its
@@ -1357,6 +1357,51 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
env.close();
}
// A proposer may pay for the Create with one of its own Tickets. The
// Ticket's reserve goes as the proposal's arrives, so the proposer's owner
// count moves by the difference.
void
testTicketedCreate(FeatureBitset features)
{
testcase("create paid for with the proposer's own ticket");
using namespace jtx;
using namespace std::chrono_literals;
Env env{*this, features};
Account const alice{"alice"}; // the proposer
Account const target{"target"}; // the account the proposal is for
Account const bob{"bob"};
env.fund(XRP(10000), alice, target, bob);
env.close();
proposal::authorizeProposer(env, target, alice);
std::uint32_t const targetTicketSeq = proposal::createTicket(env, target);
std::uint32_t const aliceTicketSeq = proposal::createTicket(env, alice);
std::uint32_t const aliceSeq = env.seq(alice);
std::uint32_t const targetOwnerCount = ownerCount(env, target);
BEAST_EXPECT(ownerCount(env, alice) == 1);
// The proposal's reserve lands on alice less the Ticket she paid with,
// and her Sequence is untouched.
env(proposal::create(
alice,
proposal::unsignedPayload(env, pay(target, bob, XRP(1)), targetTicketSeq),
proposal::expiration(env, 100s)),
ticket::Use(aliceTicketSeq),
proposal::verify::create());
env.close();
BEAST_EXPECT(proposal::entry(env, target, targetTicketSeq));
BEAST_EXPECT(!env.le(keylet::ticket(alice.id(), SeqProxy::rawTicket(aliceTicketSeq))));
BEAST_EXPECT(env.seq(alice) == aliceSeq);
BEAST_EXPECT(ownerCount(env, alice) == proposal::kProposalOwnerCount);
// The target's Ticket, the one the proposal is keyed to, stays.
BEAST_EXPECT(ownerCount(env, target) == targetOwnerCount);
}
// The proposal's reserve can instead be sponsored: the reserve is charged
// to the sponsor's account, and the ledger object records the sponsor, the
// same as any other reserve-sponsorable object (TransactionProposalCreate
@@ -1728,6 +1773,7 @@ struct TransactionProposalCreate_test : public beast::unit_test::Suite
testOtherTransactionTypes(all);
testAuxiliaryCoSignatureTypes(all);
testReserve(all);
testTicketedCreate(all);
testSponsoredReserve(all);
testSponsorshipTransfer(all);
testFeeSponsored(all);
@@ -970,6 +970,48 @@ class InvariantsMPT_test : public InvariantsBase
});
}
// A PaymentChannelClaim may create only the submitter's (A1) MPToken.
{
STTx const claim{ttPAYCHAN_CLAIM, [](STObject&) {}};
MPTID id;
Preclose const precloseIssuance = [&](Account const&, Account const&, Env& env) {
Account const gw("gw");
env.fund(XRP(1'000), gw);
MPTTester const mpt({.env = env, .issuer = gw});
id = mpt.issuanceID();
return true;
};
auto const insertToken = [&](Account const& holder, ApplyContext& ac) {
auto const sleNew = std::make_shared<SLE>(keylet::mptoken(id, holder));
(*sleNew)[sfAccount] = holder.id();
(*sleNew)[sfMPTokenIssuanceID] = id;
ac.view().insert(sleNew);
return true;
};
doInvariantCheck(
{{"a MPToken was created"}},
[&](Account const&, Account const& a2, ApplyContext& ac) {
return insertToken(a2, ac);
},
XRPAmount{},
claim,
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseIssuance,
TxAccount::A1);
doInvariantCheck(
{},
[&](Account const& a1, Account const&, ApplyContext& ac) {
return insertToken(a1, ac);
},
XRPAmount{},
claim,
{tesSUCCESS, tesSUCCESS},
precloseIssuance,
TxAccount::A1);
}
// LoanSet / VaultWithdraw MayAuthorizeMpt caps (fixCleanup3_4_0) and
// the LoanBrokerDelete cap (fixCleanup3_5_0): LoanSet allows at most
// two creates and no deletes; VaultWithdraw and LoanBrokerDelete allow
@@ -0,0 +1,549 @@
#include <test/app/invariants/InvariantsBase.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/pay.h>
#include <test/jtx/proposal.h>
#include <test/jtx/utility.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Zero.h>
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/helpers/ProposalHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/tx/ApplyContext.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <memory>
#include <optional>
#include <source_location>
#include <string>
#include <utility>
#include <vector>
namespace xrpl::test {
class InvariantsTransactionProposal_test : public InvariantsBase
{
void
testTransactionProposalInvariants()
{
using namespace jtx;
using namespace std::chrono_literals;
// Submits a real proposal, so the entry is in the closed ledger before
// the precheck runs and the precheck can modify it.
std::optional<Keylet> liveProposalKeylet;
std::optional<std::uint32_t> liveTicketSeq;
Preclose const precloseLiveProposal =
[&](Account const& owner, Account const& destination, Env& env) {
auto const ticketSeq = proposal::createTicket(env, owner);
auto const proposedTx =
proposal::unsignedPayload(env, pay(owner, destination, XRP(1)), ticketSeq);
liveProposalKeylet = keylet::txProposal(owner.id(), ticketSeq);
liveTicketSeq = ticketSeq;
env(proposal::create(owner, proposedTx, proposal::expiration(env, 100s)));
return BEAST_EXPECT(env.le(*liveProposalKeylet));
};
auto peekLiveProposal = [&](ApplyContext& ac) -> SLE::pointer {
if (!liveProposalKeylet)
return nullptr;
return ac.view().peek(*liveProposalKeylet);
};
// Deletes the live proposal as deleteProposal would: out of the
// owner's directory, with the owner's reserve released.
auto deleteLiveProposal = [&](Account const& owner, ApplyContext& ac) {
auto const sle = peekLiveProposal(ac);
if (!sle)
return false;
auto const account = ac.view().peek(keylet::account(owner.id()));
if (!account)
return false;
account->at(sfOwnerCount) -=
xrpl::proposal::proposalOwnerCount(sle->getFieldObject(sfProposedTransaction));
ac.view().update(account);
if (!ac.view().dirRemove(
keylet::ownerDir(owner.id()), sle->getFieldU64(sfOwnerNode), sle->key(), false))
return false;
ac.view().erase(sle);
return true;
};
// Consumes one of the owner's Tickets as ticketDelete would.
auto deleteTicket = [&](Account const& owner, std::uint32_t ticketSeq, ApplyContext& ac) {
auto const ticket =
ac.view().peek(keylet::ticket(owner.id(), SeqProxy::rawTicket(ticketSeq)));
auto const account = ac.view().peek(keylet::account(owner.id()));
if (!ticket || !account)
return false;
if (!ac.view().dirRemove(
keylet::ownerDir(owner.id()),
ticket->getFieldU64(sfOwnerNode),
ticket->key(),
false))
return false;
if (auto const ticketCount = account->getFieldU32(sfTicketCount); ticketCount > 1)
{
account->setFieldU32(sfTicketCount, ticketCount - 1);
}
else
{
account->makeFieldAbsent(sfTicketCount);
}
account->at(sfOwnerCount) -= 1;
ac.view().update(account);
ac.view().erase(ticket);
return true;
};
// A transaction outside the whitelist may not modify a proposal at
// all; the invariant rejects it on the count, whatever field changed.
testcase("TransactionProposal modified outside the whitelist");
doInvariantCheck(
{{"TransactionProposal changes do not match transaction result"}},
[&](Account const&, Account const&, ApplyContext& ac) {
auto const sle = peekLiveProposal(ac);
if (!sle)
return false;
auto proposedTx = sle->getFieldObject(sfProposedTransaction);
proposedTx.setFieldU32(
sfTicketSequence, proposedTx.getFieldU32(sfTicketSequence) + 1);
sle->setFieldObject(sfProposedTransaction, proposedTx);
ac.view().update(sle);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal);
// Only stages the payload and the key it would live under; nothing
// reaches the ledger, so a precheck that inserts the entry by hand
// presents the invariant with a creation.
std::optional<STObject> stagedPayload;
std::optional<Keylet> stagedProposalKeylet;
Preclose const precloseStagedPayload =
[&](Account const& owner, Account const& destination, Env& env) {
auto const ticketSeq = proposal::createTicket(env, owner);
auto const proposedJson =
proposal::unsignedPayload(env, pay(owner, destination, XRP(1)), ticketSeq);
stagedPayload.emplace(parse(proposedJson));
stagedProposalKeylet = keylet::txProposal(owner.id(), ticketSeq);
return true;
};
// Inserts the staged proposal as doApply would, with the owner's
// reserve charged.
auto insertStagedProposal = [&](Account const& owner, ApplyContext& ac) {
if (!stagedPayload || !stagedProposalKeylet)
return false;
auto const sle = std::make_shared<SLE>(*stagedProposalKeylet);
sle->setAccountID(sfOwner, owner.id());
sle->setFieldObject(sfProposedTransaction, *stagedPayload);
sle->setFieldU32(sfExpiration, 1);
sle->setFieldU64(sfOwnerNode, 0);
ac.view().insert(sle);
auto const account = ac.view().peek(keylet::account(owner.id()));
if (!account)
return false;
account->at(sfOwnerCount) += xrpl::proposal::proposalOwnerCount(*stagedPayload);
ac.view().update(account);
return true;
};
testcase("TransactionProposal effect whitelist");
doInvariantCheck(
{{"TransactionProposal changes do not match transaction result"}},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return insertStagedProposal(owner, ac);
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseStagedPayload);
// The harness runs no transactor, so a failed result has to be fed in
// directly. A failed create must leave no trace.
testcase("TransactionProposal failed create still inserts");
doInvariantCheck(
{{"TransactionProposal changes do not match transaction result"}},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return insertStagedProposal(owner, ac);
},
XRPAmount{},
STTx{ttTRANSACTION_PROPOSAL_CREATE, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseStagedPayload,
TxAccount::None,
std::source_location::current(),
tecNO_PERMISSION);
// Reassigns the live proposal's reserve to a sponsor as a
// SponsorshipTransfer would. The sponsored and sponsoring counts move
// with it so that SponsorshipOwnerCountsMatch stays quiet.
auto const reassignLiveProposal =
[&](Account const& owner, Account const& sponsor, ApplyContext& ac) {
auto const sle = peekLiveProposal(ac);
if (!sle)
return false;
auto const reserve =
xrpl::proposal::proposalOwnerCount(sle->getFieldObject(sfProposedTransaction));
sle->setAccountID(sfSponsor, sponsor.id());
ac.view().update(sle);
auto const account = ac.view().peek(keylet::account(owner.id()));
auto const sponsorRoot = ac.view().peek(keylet::account(sponsor.id()));
if (!account || !sponsorRoot)
return false;
account->at(sfSponsoredOwnerCount) += reserve;
ac.view().update(account);
sponsorRoot->at(sfSponsoringOwnerCount) += reserve;
ac.view().update(sponsorRoot);
return true;
};
// A failed transfer must not have reassigned anything. The counters are
// kept consistent so only the result mismatch is left to catch.
testcase("TransactionProposal failed sponsorship transfer still reassigns");
doInvariantCheck(
{{"TransactionProposal changes do not match transaction result"}},
reassignLiveProposal,
XRPAmount{},
STTx{ttSPONSORSHIP_TRANSFER, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal,
TxAccount::None,
std::source_location::current(),
tecNO_PERMISSION);
// A proposal leaves the ledger only through TransactionProposalCancel
// or together with the Ticket it is keyed to. The harness runs no
// transactor, so each deletion below is staged by hand, releasing the
// reserve as a real one would, to leave the deletion itself as the
// only possible violation.
testcase("TransactionProposal deletion outside the whitelist");
doInvariantCheck(
{{"TransactionProposal changes do not match transaction result"}},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return deleteLiveProposal(owner, ac);
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal);
// The owner holds a second Ticket, one no proposal is keyed to.
std::optional<std::uint32_t> spareTicketSeq;
Preclose const precloseLiveProposalAndSpareTicket =
[&](Account const& owner, Account const& destination, Env& env) {
spareTicketSeq = proposal::createTicket(env, owner);
return precloseLiveProposal(owner, destination, env);
};
// Consuming some other Ticket of the owner's is no reason for the
// proposal to go.
testcase("TransactionProposal deleted with an unrelated ticket");
doInvariantCheck(
{{"TransactionProposal changes do not match transaction result"}},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return spareTicketSeq && deleteTicket(owner, *spareTicketSeq, ac) &&
deleteLiveProposal(owner, ac);
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposalAndSpareTicket);
// And a deleted Ticket takes the proposal keyed to it along: here the
// Ticket went and the proposal stayed behind.
testcase("TransactionProposal outlives its ticket");
doInvariantCheck(
{{"TransactionProposal outlived the Ticket it is keyed to"}},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return liveTicketSeq && deleteTicket(owner, *liveTicketSeq, ac);
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal);
// A Cancel names the live proposal and may pay with the owner's spare
// Ticket. Both exist only once the preclose has run, so the Env is
// built here rather than by the harness.
struct CancelOptions
{
// The Cancel pays with A1's spare Ticket.
bool paySpareTicket = false;
TER initialResult = tesSUCCESS;
};
auto const checkCancel = [&](std::vector<std::string> const& expectLogs,
Precheck const& precheck,
std::initializer_list<TER> ters,
Preclose const& preclose,
CancelOptions const& options = CancelOptions(),
std::source_location const& loc =
std::source_location::current()) {
Account const a1{"A1"};
Account const a2{"A2"};
Env env = makeEnv(testableAmendments());
env.fund(XRP(1000), a1, a2);
if (!BEAST_EXPECT(preclose(a1, a2, env)) || !BEAST_EXPECT(liveProposalKeylet))
return;
env.close();
STTx const tx{ttTRANSACTION_PROPOSAL_CANCEL, [&](STObject& obj) {
obj.setAccountID(sfAccount, a1.id());
obj.setFieldH256(sfProposalID, liveProposalKeylet->key);
if (options.paySpareTicket && spareTicketSeq)
{
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfTicketSequence, *spareTicketSeq);
}
}};
doInvariantCheck(
std::move(env),
a1,
a2,
expectLogs,
precheck,
XRPAmount{},
tx,
ters,
loc,
options.initialResult);
};
auto const cancelDeletesLiveProposal =
[&](Account const& owner, Account const&, ApplyContext& ac) {
return deleteLiveProposal(owner, ac);
};
// A Cancel that did not succeed deleted nothing.
testcase("TransactionProposal failed cancel still deletes");
{
CancelOptions options;
options.initialResult = tecNO_PERMISSION;
checkCancel(
{{"TransactionProposal changes do not match transaction result"}},
cancelDeletesLiveProposal,
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal,
options);
}
// A Cancel cannot spend a reserved Ticket, so the proposal it deletes
// never goes with its Ticket.
testcase("TransactionProposal cancel consumes the reserved ticket");
checkCancel(
{{"TransactionProposal changes do not match transaction result"}},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return liveTicketSeq && deleteTicket(owner, *liveTicketSeq, ac) &&
deleteLiveProposal(owner, ac);
},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal);
// Distinct AccountIDs in ascending order, so an array built from them
// violates only the bound on its length.
auto ascendingAccounts = [](std::size_t count) {
std::vector<AccountID> accounts;
accounts.reserve(count);
for (std::size_t i = 0; i < count; ++i)
{
AccountID account{beast::kZero};
account.data()[0] = static_cast<std::uint8_t>(i + 1);
accounts.push_back(account);
}
return accounts;
};
auto signerArray = [](std::vector<AccountID> const& accounts) {
STArray signers{sfSigners};
for (auto const& account : accounts)
{
STObject signer{sfSigner};
signer.setAccountID(sfAccount, account);
signers.pushBack(signer);
}
return signers;
};
// No transaction type on this amendment branch collects signatures yet,
// so every rejection below has to be provoked by editing the stored
// payload directly.
auto const testBrokenSignerArray = [&](std::string const& name, auto const& breakPayload) {
testcase("TransactionProposal canonical signer arrays: " + name);
doInvariantCheck(
{{"TransactionProposal signer arrays are not canonical"}},
[&](Account const&, Account const& signerAccount, ApplyContext& ac) {
auto const sle = peekLiveProposal(ac);
if (!sle)
return false;
auto proposedTx = sle->getFieldObject(sfProposedTransaction);
breakPayload(proposedTx, signerAccount);
sle->setFieldObject(sfProposedTransaction, proposedTx);
ac.view().update(sle);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
precloseLiveProposal);
};
testBrokenSignerArray("duplicate entries", [&](STObject& tx, Account const& signer) {
tx.setFieldArray(sfSigners, signerArray({signer.id(), signer.id()}));
});
testBrokenSignerArray("descending order", [&](STObject& tx, Account const&) {
auto accounts = ascendingAccounts(2);
std::ranges::reverse(accounts);
tx.setFieldArray(sfSigners, signerArray(accounts));
});
testBrokenSignerArray("past the multi-sign limit", [&](STObject& tx, Account const&) {
tx.setFieldArray(sfSigners, signerArray(ascendingAccounts(STTx::kMaxMultiSigners + 1)));
});
testBrokenSignerArray("entry without an Account", [](STObject& tx, Account const&) {
STArray signers{sfSigners};
signers.pushBack(STObject{sfSigner});
tx.setFieldArray(sfSigners, signers);
});
for (auto const* field :
std::array<SField const*, 2>{&sfCounterpartySignature, &sfSponsorSignature})
{
testBrokenSignerArray(
"duplicates under " + field->getName(), [&](STObject& tx, Account const& signer) {
STObject signature{*field};
signature.setFieldArray(sfSigners, signerArray({signer.id(), signer.id()}));
tx.setFieldObject(*field, signature);
});
}
testBrokenSignerArray("past the batch-signer limit", [&](STObject& tx, Account const&) {
STArray batchSigners{sfBatchSigners};
for (auto const& account : ascendingAccounts(kMaxBatchSigners + 1))
{
STObject batchSigner{sfBatchSigner};
batchSigner.setAccountID(sfAccount, account);
batchSigners.pushBack(batchSigner);
}
tx.setFieldArray(sfBatchSigners, batchSigners);
});
testBrokenSignerArray(
"duplicates under a BatchSigner", [&](STObject& tx, Account const& signer) {
STObject batchSigner{sfBatchSigner};
batchSigner.setAccountID(sfAccount, signer.id());
batchSigner.setFieldArray(sfSigners, signerArray({signer.id(), signer.id()}));
STArray batchSigners{sfBatchSigners};
batchSigners.pushBack(batchSigner);
tx.setFieldArray(sfBatchSigners, batchSigners);
});
// Negative controls. These share the setup of the failure cases above,
// so they confirm each of those failures came from the defect it names
// rather than from the hand-built ledger state.
std::initializer_list<TER> const passTers = {tesSUCCESS, tesSUCCESS};
testcase("TransactionProposal well-formed create");
doInvariantCheck(
{},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return insertStagedProposal(owner, ac);
},
XRPAmount{},
STTx{ttTRANSACTION_PROPOSAL_CREATE, [](STObject&) {}},
passTers,
precloseStagedPayload);
testcase("TransactionProposal left untouched");
doInvariantCheck(
{},
[&](Account const&, Account const&, ApplyContext& ac) {
return liveProposalKeylet && ac.view().read(*liveProposalKeylet) != nullptr;
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
passTers,
precloseLiveProposal);
// A Cancel deletes the proposal it names.
testcase("TransactionProposal cancelled");
checkCancel({}, cancelDeletesLiveProposal, passTers, precloseLiveProposal);
// The Ticket a Cancel pays with is not the one its proposal is keyed
// to, so the proposal did not go with its Ticket.
testcase("TransactionProposal cancel paid with the canceller's own ticket");
{
CancelOptions options;
options.paySpareTicket = true;
checkCancel(
{},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return spareTicketSeq && deleteTicket(owner, *spareTicketSeq, ac) &&
deleteLiveProposal(owner, ac);
},
passTers,
precloseLiveProposalAndSpareTicket,
options);
}
// A SponsorshipTransfer reassigns the Sponsor.
testcase("TransactionProposal sponsor reassigned");
doInvariantCheck(
{},
reassignLiveProposal,
XRPAmount{},
STTx{ttSPONSORSHIP_TRANSFER, [](STObject&) {}},
passTers,
precloseLiveProposal);
// The proposed transaction executing spends the proposal's Ticket and
// takes the proposal with it.
testcase("TransactionProposal deleted with its ticket");
doInvariantCheck(
{},
[&](Account const& owner, Account const&, ApplyContext& ac) {
return liveTicketSeq && deleteTicket(owner, *liveTicketSeq, ac) &&
deleteLiveProposal(owner, ac);
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
passTers,
precloseLiveProposal);
}
void
run() override
{
testTransactionProposalInvariants();
}
};
BEAST_DEFINE_TESTSUITE(InvariantsTransactionProposal, app, xrpl);
} // namespace xrpl::test
+242 -4
View File
@@ -15,6 +15,7 @@
#include <test/jtx/ter.h>
#include <test/jtx/ticket.h>
#include <test/jtx/trust.h>
#include <test/jtx/txflags.h>
#include <test/jtx/vault.h>
#include <xrpl/basics/Number.h>
@@ -1609,26 +1610,263 @@ private:
env.close();
// Direct vault share payment inherits the underlying lock via
// sfReferenceHolding.
// sfReferenceHolding. Caught in the strand check, like a global
// or individual share lock.
BEAST_EXPECT(shareBalance(alice) == 499);
BEAST_EXPECT(shareBalance(bob) == 501);
env(pay(alice, bob, shares(1)), Ter{tecLOCKED});
env(pay(alice, bob, shares(1)), Ter{tecPATH_DRY});
env.close();
BEAST_EXPECT(shareBalance(alice) == 499);
BEAST_EXPECT(shareBalance(bob) == 501);
// The same inherited lock must also block DEX payment paths that
// would consume an offer selling vault shares.
// would consume an offer selling vault shares. bob, the
// destination, inherits the lock and is caught in the strand
// check.
env(pay(carol, bob, shares(1)),
Sendmax(XRP(1)),
Path(BookSpec{shares.raw()}),
Ter{tecPATH_PARTIAL});
Ter{tecPATH_DRY});
env.close();
BEAST_EXPECT(shareBalance(alice) == 499);
BEAST_EXPECT(shareBalance(bob) == 501);
BEAST_EXPECT(expectOffers(env, alice, 1));
}
// A holder individually frozen on the underlying can neither sell
// nor buy vault shares through an unfrozen holder's offer. The
// inherited freeze is caught when the strand is built, before the
// offer is consumed.
auto const testShareDEXEndpointFrozen =
[&](Env& env,
PrettyAsset const& asset,
Account const& owner,
Account const& alice,
Account const& bob,
Account const& carol,
std::function<void(bool)> const& setAliceFrozen) {
Vault const vault{env};
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
env.close();
PrettyAsset const shares = MPTIssue(env.le(keylet)->at(sfShareMPTID));
auto const shareMptID = shares.raw().get<MPTIssue>().getMptID();
auto const shareBalance = [&](Account const& account) {
auto const sle = env.le(keylet::mptoken(shareMptID, account));
return sle ? sle->at(sfMPTAmount) : 0;
};
// bob bids XRP for shares and asks more XRP for shares, so that
// his offers don't cross
env(offer(bob, shares(1), XRP(1)));
env(offer(bob, XRP(2), shares(1)));
env.close();
setAliceFrozen(true);
auto const aliceShares = shareBalance(alice);
auto const bobShares = shareBalance(bob);
auto const expectUnchanged = [&] {
BEAST_EXPECT(shareBalance(alice) == aliceShares);
BEAST_EXPECT(shareBalance(bob) == bobShares);
BEAST_EXPECT(expectOffers(env, bob, 2));
};
// alice sells shares: the first step's source is frozen
env(pay(alice, carol, XRP(1)),
Sendmax(shares(1)),
Path(~XRP),
Txflags(tfNoRippleDirect),
Ter{tecPATH_DRY});
env.close();
expectUnchanged();
// carol buys shares for alice: the last step's destination is
// frozen
env(pay(carol, alice, shares(1)),
Sendmax(XRP(2)),
Path(BookSpec{shares.raw()}),
Txflags(tfNoRippleDirect),
Ter{tecPATH_DRY});
env.close();
expectUnchanged();
// Both succeed once alice is unfrozen
setAliceFrozen(false);
env(pay(alice, carol, XRP(1)),
Sendmax(shares(1)),
Path(~XRP),
Txflags(tfNoRippleDirect));
env.close();
BEAST_EXPECT(shareBalance(bob) == bobShares + 1);
env(pay(carol, alice, shares(1)),
Sendmax(XRP(2)),
Path(BookSpec{shares.raw()}),
Txflags(tfNoRippleDirect));
env.close();
BEAST_EXPECT(shareBalance(alice) == aliceShares);
BEAST_EXPECT(shareBalance(bob) == bobShares);
BEAST_EXPECT(expectOffers(env, bob, 0));
};
// Vault shares as the intermediate asset between two books. Only the
// offer owners hold the shares in transit. An owner individually
// frozen on the underlying can neither give nor receive the shares.
auto const testShareDEXMiddleFrozen = [&](Env& env,
PrettyAsset const& asset,
Account const& owner,
Account const& alice,
Account const& bob,
Account const& carol,
std::function<void(bool)> const& setAliceFrozen) {
Vault const vault{env};
auto [tx, keylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(500)}));
env(vault.deposit({.depositor = bob, .id = keylet.key, .amount = asset(500)}));
env.close();
PrettyAsset const shares = MPTIssue(env.le(keylet)->at(sfShareMPTID));
auto const shareMptID = shares.raw().get<MPTIssue>().getMptID();
auto const shareBalance = [&](Account const& account) {
auto const sle = env.le(keylet::mptoken(shareMptID, account));
return sle ? sle->at(sfMPTAmount) : 0;
};
// alice gives the shares: XRP -> shares (alice) -> asset (bob)
{
env(offer(alice, XRP(1), shares(1)));
env(offer(bob, shares(1), asset(1)));
env.close();
setAliceFrozen(true);
auto const aliceShares = shareBalance(alice);
auto const bobShares = shareBalance(bob);
env(pay(carol, owner, asset(1)),
Sendmax(XRP(1)),
Path(BookSpec{shares.raw()}, BookSpec{asset.raw()}),
Txflags(tfNoRippleDirect),
Ter{tecPATH_PARTIAL});
env.close();
BEAST_EXPECT(shareBalance(alice) == aliceShares);
BEAST_EXPECT(shareBalance(bob) == bobShares);
setAliceFrozen(false);
env(pay(carol, owner, asset(1)),
Sendmax(XRP(1)),
Path(BookSpec{shares.raw()}, BookSpec{asset.raw()}),
Txflags(tfNoRippleDirect));
env.close();
BEAST_EXPECT(shareBalance(alice) == aliceShares - 1);
BEAST_EXPECT(shareBalance(bob) == bobShares + 1);
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(expectOffers(env, bob, 0));
}
// alice receives the shares: asset -> shares (bob) -> XRP (alice)
{
env(offer(bob, asset(1), shares(1)));
env(offer(alice, shares(1), XRP(1)));
env.close();
setAliceFrozen(true);
auto const aliceShares = shareBalance(alice);
auto const bobShares = shareBalance(bob);
env(pay(owner, carol, XRP(1)),
Sendmax(asset(1)),
Path(BookSpec{shares.raw()}, ~XRP),
Txflags(tfNoRippleDirect),
Ter{tecPATH_PARTIAL});
env.close();
BEAST_EXPECT(shareBalance(alice) == aliceShares);
BEAST_EXPECT(shareBalance(bob) == bobShares);
setAliceFrozen(false);
env(pay(owner, carol, XRP(1)),
Sendmax(asset(1)),
Path(BookSpec{shares.raw()}, ~XRP),
Txflags(tfNoRippleDirect));
env.close();
BEAST_EXPECT(shareBalance(alice) == aliceShares + 1);
BEAST_EXPECT(shareBalance(bob) == bobShares - 1);
BEAST_EXPECT(expectOffers(env, alice, 0));
BEAST_EXPECT(expectOffers(env, bob, 0));
}
};
{
testcase("MPT individually locked: vault share DEX inherits underlying lock");
Env env{*this, testableAmendments()};
Account const issuer{"issuer"};
Account const owner{"owner"};
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
env.close();
MPTTester asset{{
.env = env,
.issuer = issuer,
.holders = {owner, alice, bob},
.flags = tfMPTCanTransfer | tfMPTCanTrade | tfMPTCanLock,
}};
env(pay(issuer, owner, asset(1'000)));
env(pay(issuer, alice, asset(2'000)));
env(pay(issuer, bob, asset(2'000)));
env.close();
auto const setAliceFrozen = [&](bool frozen) {
asset.set({.holder = alice, .flags = frozen ? tfMPTLock : tfMPTUnlock});
env.close();
};
testShareDEXEndpointFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
testShareDEXMiddleFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
}
{
testcase("IOU individually frozen: vault share DEX inherits underlying freeze");
Env env{*this, testableAmendments()};
Account const issuer{"issuer"};
Account const owner{"owner"};
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(10'000), issuer, owner, alice, bob, carol);
env.close();
// Deliver the IOU from a book to a holder
env(fset(issuer, asfDefaultRipple));
PrettyAsset const asset = issuer["IOU"];
env.trust(asset(10'000), owner, alice, bob);
env(pay(issuer, owner, asset(1'000)));
env(pay(issuer, alice, asset(2'000)));
env(pay(issuer, bob, asset(2'000)));
env.close();
auto const setAliceFrozen = [&](bool frozen) {
env(trust(issuer, asset(0), alice, frozen ? tfSetFreeze : tfClearFreeze));
env.close();
};
testShareDEXEndpointFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
testShareDEXMiddleFrozen(env, asset, owner, alice, bob, carol, setAliceFrozen);
}
{
testcase("MPT CanTrade governance: share inherits underlying on DEX and AMM");
+21 -1
View File
@@ -1,5 +1,8 @@
#include <test/jtx/paychan.h>
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
@@ -12,11 +15,13 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <cstdint>
@@ -39,6 +44,7 @@ create(
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelCreate;
jv[jss::Flags] = tfFullyCanonicalSig;
jv[jss::Account] = to_string(account);
jv[jss::Destination] = to_string(to);
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
@@ -60,6 +66,7 @@ fund(
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelFund;
jv[jss::Flags] = tfFullyCanonicalSig;
jv[jss::Account] = to_string(account);
jv[sfChannel.fieldName] = to_string(channel);
jv[jss::Amount] = amount.getJson(JsonOptions::Values::None);
@@ -79,6 +86,7 @@ claim(
{
json::Value jv;
jv[jss::TransactionType] = jss::PaymentChannelClaim;
jv[jss::Flags] = tfFullyCanonicalSig;
jv[jss::Account] = to_string(account);
jv["Channel"] = to_string(channel);
if (amount)
@@ -133,8 +141,20 @@ signClaimAuth(
STAmount const& authAmt)
{
Serializer msg;
serializePayChanAuthorization(msg, channel, authAmt.xrp());
serializePayChanAuthorization(msg, channel, authAmt);
return sign(pk, sk, msg.slice());
}
Rate
rate(Env const& env, Account const& account, Account const& dest, std::uint32_t const& seq)
{
auto const sle =
env.le(keylet::payChannel(account.id(), dest.id(), SeqProxy::rawSequence(seq)));
if (!sle)
return Rate{0};
if (sle->isFieldPresent(sfTransferRate))
return xrpl::Rate((*sle)[sfTransferRate]);
return kParityRate;
}
} // namespace xrpl::test::jtx::paychan
+22 -4
View File
@@ -238,6 +238,13 @@ verify::Create::operator()(Env& env, JTx& jt) const
// Every entry the transaction could touch, read whole, so what follows can
// say that nothing moved rather than that the fields we named did not.
auto const& view = *env.current();
// A proposer paying with one of its own Tickets consumes it whenever the
// transaction is applied: on success, and on a claimed-fee tec.
std::optional<std::uint32_t> ownTicketSeq;
if (jt.jv.isMember(sfTicketSequence.jsonName))
ownTicketSeq = jt.jv[sfTicketSequence.jsonName].asUInt();
auto const proposalKeylet = keylet::txProposal(target, ticketSeq);
auto const ownerCountBefore = env.ownerCount(proposer);
auto const sponsoredOwnerCountBefore = env.sponsoredOwnerCount(proposer);
@@ -254,11 +261,15 @@ verify::Create::operator()(Env& env, JTx& jt) const
auto const& view = *applied.current();
auto const created = isTesSuccess(applied.ter());
bool const ticketConsumed = ownTicketSeq && (created || isTecClaim(applied.ter()));
std::uint32_t const ticketReturned = ticketConsumed ? 1 : 0;
// The proposer owns the proposal even when another account covers its
// reserve. A proposed Batch costs more owner-count increments.
// reserve. A proposed Batch costs more owner-count increments. A
// Ticket the proposer paid with gives one back.
test.expect(
applied.ownerCount(proposer) == ownerCountBefore + (created ? cost : 0),
applied.ownerCount(proposer) ==
ownerCountBefore + (created ? cost : 0) - ticketReturned,
"proposal reserve");
test.expect(
applied.sponsoredOwnerCount(proposer) ==
@@ -291,6 +302,14 @@ verify::Create::operator()(Env& env, JTx& jt) const
auto const sleProposal = view.read(proposalKeylet);
// The Ticket the proposer paid with has left its directory.
auto expectedDir = proposerDirBefore;
if (ticketConsumed)
{
expectedDir.erase(
keylet::ticket(proposer.id(), SeqProxy::rawTicket(*ownTicketSeq)).key);
}
if (!created)
{
// A create that did not succeed leaves the proposal as it found
@@ -300,7 +319,7 @@ verify::Create::operator()(Env& env, JTx& jt) const
// Nor did the directory gain a listing for an entry that does not
// exist.
test.expect(
ownerDirKeys(view, proposer.id()) == proposerDirBefore, "proposal owner directory");
ownerDirKeys(view, proposer.id()) == expectedDir, "proposal owner directory");
return;
}
@@ -353,7 +372,6 @@ verify::Create::operator()(Env& env, JTx& jt) const
// The proposal is listed in the proposer's directory on the page its
// OwnerNode names, and nothing else listed moved.
auto expectedDir = proposerDirBefore;
expectedDir.emplace(sleProposal->key(), sleProposal->getFieldU64(sfOwnerNode));
test.expect(ownerDirKeys(view, proposer.id()) == expectedDir, "proposal owner directory");
});
+5
View File
@@ -1,6 +1,7 @@
#pragma once
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
@@ -10,6 +11,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Rate.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SecretKey.h>
@@ -85,4 +87,7 @@ signClaimAuth(
uint256 const& channel,
STAmount const& authAmt);
Rate
rate(Env const& env, Account const& account, Account const& dest, std::uint32_t const& seq);
} // namespace xrpl::test::jtx::paychan
+3 -1
View File
@@ -77,7 +77,9 @@ public:
* @brief Check the ledger effects a TransactionProposalCreate must have,
* whatever its outcome: on tesSUCCESS a new proposal holding what was
* submitted, listed in the proposer's directory and paid for by its reserve;
* otherwise nothing moved. Nothing of the target's moves either way.
* otherwise nothing moved. Nothing of the target's moves either way. A Ticket
* the proposer pays with is expected to be consumed; attach ticket::Use()
* before this condition so it sees the TicketSequence.
*
* @code
* env(proposal::create(alice, payload, expiration), proposal::verify::create());
+218
View File
@@ -1001,6 +1001,222 @@ public:
}
}
void
testIsExactSum()
{
testcase("is exact sum");
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}};
// Exact IOU sum
{
STAmount const amt1(usd, 500);
STAmount const amt2(usd, 1500);
BEAST_EXPECT(isExactSum(amt1, amt2) == true);
}
// Exact IOU sum with a zero operand
{
STAmount const amt1(usd, 0);
STAmount const amt2(usd, 15, -19);
BEAST_EXPECT(isExactSum(amt1, amt2) == true);
}
// Exact IOU sum whose exponent exceeds both operands'
{
STAmount const amt1(usd, std::uint64_t{9999999999999999});
STAmount const amt2(usd, 1);
BEAST_EXPECT(isExactSum(amt1, amt2) == true);
}
// IOU sum that drops the finer operand: 1.5e-18 + 1 rounds to 1, and
// 1 - 1.5e-18 rounds back to 1, so only the difference with the
// coarser operand exposes the loss
{
STAmount const dust(usd, 15, -19);
STAmount const one(usd, 1);
BEAST_EXPECT(isExactSum(dust, one) == false);
BEAST_EXPECT(isExactSum(one, dust) == false);
}
// IOU sum that rounds the smaller operand away entirely
{
STAmount const amt1(usd, 1, 15);
STAmount const amt2(usd, 1, -3);
BEAST_EXPECT(isExactSum(amt1, amt2) == false);
}
// IOU sum that rounds to a different nonzero increase
{
STAmount const amt1(usd, std::uint64_t{1234567890123456});
STAmount const amt2(usd, 600025, -2);
BEAST_EXPECT(isExactSum(amt1, amt2) == false);
}
// IOU sum past the largest IOU value: the sum throws, so it is not
// exact
{
STAmount const max(usd, STAmount::kMaxValue, STAmount::kMaxOffset);
BEAST_EXPECT(max.mantissa() == STAmount::kMaxValue);
BEAST_EXPECT(max.exponent() == STAmount::kMaxOffset);
try
{
auto const _ = max + max;
BEAST_EXPECT(false);
}
catch (std::overflow_error const& e)
{
BEAST_EXPECT(e.what() == std::string("value overflow"));
}
BEAST_EXPECT(isExactSum(max, max) == false);
}
// Exact XRP sum
{
STAmount const amt1(XRPAmount(500));
STAmount const amt2(XRPAmount(1500));
BEAST_EXPECT(isExactSum(amt1, amt2) == true);
}
// XRP overflow
{
STAmount const amt1(std::numeric_limits<XRPAmount::value_type>::max());
STAmount const amt2(XRPAmount(1));
BEAST_EXPECT(isExactSum(amt1, amt2) == false);
}
// Exact MPT sum
{
STAmount const amt1(mpt, 500);
STAmount const amt2(mpt, 1500);
BEAST_EXPECT(isExactSum(amt1, amt2) == true);
}
// MPT overflow
{
STAmount const amt1(mpt, std::numeric_limits<MPTAmount::value_type>::max());
STAmount const amt2(mpt, 1);
BEAST_EXPECT(isExactSum(amt1, amt2) == false);
}
// Not comparable
{
STAmount const amt1(XRPAmount(1));
STAmount const amt2(usd, 1);
BEAST_EXPECT(isExactSum(amt1, amt2) == false);
}
}
void
testIsExactDifference()
{
testcase("is exact difference");
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
MPTIssue const mpt{MPTIssue{makeMptID(1, AccountID(0x4985601))}};
// Exact IOU difference
{
STAmount const amt1(usd, 1500);
STAmount const amt2(usd, 500);
BEAST_EXPECT(isExactDifference(amt1, amt2) == true);
}
// Exact IOU difference with a zero subtrahend
{
STAmount const amt1(usd, 15, -19);
STAmount const amt2(usd, 0);
BEAST_EXPECT(isExactDifference(amt1, amt2) == true);
}
// Exact IOU difference of equal operands
{
STAmount const amt1(usd, 4000);
BEAST_EXPECT(isExactDifference(amt1, amt1) == true);
}
// IOU difference that rounds to a different nonzero decrease
{
STAmount const amt1(usd, std::uint64_t{1234567890123456});
STAmount const amt2(usd, 600025, -2);
BEAST_EXPECT(isExactDifference(amt1, amt2) == false);
}
// IOU difference that drops a dust subtrahend: 1 - 1.5e-18 rounds to
// 1 and 1 + 1.5e-18 rounds back to 1, so only the minuend minus the
// difference exposes the loss
{
STAmount const amt1(usd, 1);
STAmount const amt2(usd, 15, -19);
BEAST_EXPECT(isExactDifference(amt1, amt2) == false);
}
// IOU difference that drops a dust minuend: 1.5e-18 - 1 rounds to -1
// and 1.5e-18 - (-1) rounds back to 1, so only the difference plus
// the subtrahend exposes the loss
{
STAmount const amt1(usd, 15, -19);
STAmount const amt2(usd, 1);
BEAST_EXPECT(isExactDifference(amt1, amt2) == false);
}
// IOU difference whose check overflows: max - 1.5e80 rounds half to
// even to 9999999999999998e80, and adding 1.5e80 back rounds past the
// largest IOU value, so the difference is not exact
{
STAmount const max(usd, STAmount::kMaxValue, STAmount::kMaxOffset);
STAmount const claw(usd, 15, 79);
STAmount const diff = max - claw;
BEAST_EXPECT(diff == STAmount(usd, std::uint64_t{9999999999999998}, 80));
try
{
auto const _ = diff + claw;
BEAST_EXPECT(false);
}
catch (std::overflow_error const& e)
{
BEAST_EXPECT(e.what() == std::string("value overflow"));
}
BEAST_EXPECT(isExactDifference(max, claw) == false);
}
// Exact XRP difference
{
STAmount const amt1(XRPAmount(1500));
STAmount const amt2(XRPAmount(500));
BEAST_EXPECT(isExactDifference(amt1, amt2) == true);
}
// XRP underflow
{
STAmount const amt1(XRPAmount(1));
STAmount const amt2(XRPAmount(2));
BEAST_EXPECT(isExactDifference(amt1, amt2) == false);
}
// Exact MPT difference
{
STAmount const amt1(mpt, 1500);
STAmount const amt2(mpt, 500);
BEAST_EXPECT(isExactDifference(amt1, amt2) == true);
}
// MPT underflow
{
STAmount const amt1(mpt, 1);
STAmount const amt2(mpt, 2);
BEAST_EXPECT(isExactDifference(amt1, amt2) == false);
}
// Not comparable
{
STAmount const amt1(XRPAmount(1));
STAmount const amt2(usd, 1);
BEAST_EXPECT(isExactDifference(amt1, amt2) == false);
}
}
void
testMPTRateRounding()
{
@@ -1577,6 +1793,8 @@ public:
testCanAddXRP();
testCanAddIOU();
testCanAddMPT();
testIsExactSum();
testIsExactDifference();
testMPTRateRounding();
testMPTOperandIOUResult();
testMPTGetRate();
+74
View File
@@ -2157,6 +2157,53 @@ static RPCCallTestData const kRpcCallTestArray[] = {
]
})",
},
{
"channel_authorize: token amount.",
__LINE__,
{
"channel_authorize",
"secret_can_be_anything",
"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
R"({"currency":"USD","issuer":"rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","value":"10.5"})",
},
RPCCallTestData::Exception::NoException,
R"({
"method" : "channel_authorize",
"params" : [
{
"api_version" : %API_VER%,
"amount" : {
"currency" : "USD",
"issuer" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
"value" : "10.5"
},
"channel_id" : "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
"secret" : "secret_can_be_anything"
}
]
})",
},
{
"channel_authorize: amount JSON not an object.",
__LINE__,
{
"channel_authorize",
"secret_can_be_anything",
"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
R"(["10","USD"])",
},
RPCCallTestData::Exception::NoException,
R"({
"method" : "channel_authorize",
"params" : [
{
"error" : "channelAmtMalformed",
"error_code" : 43,
"error_message" : "Payment channel amount is malformed."
}
]
})",
},
{
"channel_authorize: too few arguments.",
__LINE__,
@@ -2378,6 +2425,33 @@ static RPCCallTestData const kRpcCallTestArray[] = {
]
})",
},
{
"channel_verify: token amount.",
__LINE__,
{
"channel_verify",
"aB4BXXLuPu8DpVuyq1DBiu3SrPdtK9AYZisKhu8mvkoiUD8J9Gov",
"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
R"({"mpt_issuance_id":"00000001A407AF5856CCF3C42619DAA925813FC955C72983","value":"10"})",
"DEADBEEF",
},
RPCCallTestData::Exception::NoException,
R"({
"method" : "channel_verify",
"params" : [
{
"api_version" : %API_VER%,
"amount" : {
"mpt_issuance_id" : "00000001A407AF5856CCF3C42619DAA925813FC955C72983",
"value" : "10"
},
"channel_id" : "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF",
"public_key" : "aB4BXXLuPu8DpVuyq1DBiu3SrPdtK9AYZisKhu8mvkoiUD8J9Gov",
"signature" : "DEADBEEF"
}
]
})",
},
{
"channel_verify: too few arguments.",
__LINE__,
@@ -35,6 +35,8 @@ TEST(PayChannelTests, BuilderSettersRoundTrip)
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const destinationNodeValue = canonical_UINT64();
auto const transferRateValue = canonical_UINT32();
auto const issuerNodeValue = canonical_UINT64();
PayChannelBuilder builder{
accountValue,
@@ -54,6 +56,8 @@ TEST(PayChannelTests, BuilderSettersRoundTrip)
builder.setSourceTag(sourceTagValue);
builder.setDestinationTag(destinationTagValue);
builder.setDestinationNode(destinationNodeValue);
builder.setTransferRate(transferRateValue);
builder.setIssuerNode(issuerNodeValue);
builder.setLedgerIndex(index);
builder.setFlags(0x1u);
@@ -166,6 +170,22 @@ TEST(PayChannelTests, BuilderSettersRoundTrip)
EXPECT_TRUE(entry.hasDestinationNode());
}
{
auto const& expected = transferRateValue;
auto const actualOpt = entry.getTransferRate();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfTransferRate");
EXPECT_TRUE(entry.hasTransferRate());
}
{
auto const& expected = issuerNodeValue;
auto const actualOpt = entry.getIssuerNode();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfIssuerNode");
EXPECT_TRUE(entry.hasIssuerNode());
}
EXPECT_TRUE(entry.hasLedgerIndex());
auto const ledgerIndex = entry.getLedgerIndex();
ASSERT_TRUE(ledgerIndex.has_value());
@@ -194,6 +214,8 @@ TEST(PayChannelTests, BuilderFromSleRoundTrip)
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
auto const destinationNodeValue = canonical_UINT64();
auto const transferRateValue = canonical_UINT32();
auto const issuerNodeValue = canonical_UINT64();
auto sle = std::make_shared<SLE>(PayChannel::entryType, index);
@@ -212,6 +234,8 @@ TEST(PayChannelTests, BuilderFromSleRoundTrip)
sle->at(sfPreviousTxnID) = previousTxnIDValue;
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
sle->at(sfDestinationNode) = destinationNodeValue;
sle->at(sfTransferRate) = transferRateValue;
sle->at(sfIssuerNode) = issuerNodeValue;
PayChannelBuilder builderFromSle{sle};
EXPECT_TRUE(builderFromSle.validate());
@@ -390,6 +414,32 @@ TEST(PayChannelTests, BuilderFromSleRoundTrip)
expectEqualField(expected, *fromBuilderOpt, "sfDestinationNode");
}
{
auto const& expected = transferRateValue;
auto const fromSleOpt = entryFromSle.getTransferRate();
auto const fromBuilderOpt = entryFromBuilder.getTransferRate();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfTransferRate");
expectEqualField(expected, *fromBuilderOpt, "sfTransferRate");
}
{
auto const& expected = issuerNodeValue;
auto const fromSleOpt = entryFromSle.getIssuerNode();
auto const fromBuilderOpt = entryFromBuilder.getIssuerNode();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfIssuerNode");
expectEqualField(expected, *fromBuilderOpt, "sfIssuerNode");
}
EXPECT_EQ(entryFromSle.getKey(), index);
EXPECT_EQ(entryFromBuilder.getKey(), index);
}
@@ -472,5 +522,9 @@ TEST(PayChannelTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(entry.getDestinationTag().has_value());
EXPECT_FALSE(entry.hasDestinationNode());
EXPECT_FALSE(entry.getDestinationNode().has_value());
EXPECT_FALSE(entry.hasTransferRate());
EXPECT_FALSE(entry.getTransferRate().has_value());
EXPECT_FALSE(entry.hasIssuerNode());
EXPECT_FALSE(entry.getIssuerNode().has_value());
}
}
+7
View File
@@ -48,6 +48,13 @@ TEST(ProposalHelpers, plain_payment_is_valid)
EXPECT_TRUE(proposal::isValidProposalTxnType(txOfType(ttPAYMENT)));
}
// No TransactionType at all. STTx construction rejects this first, but the
// helper takes any STObject, so it must return false rather than throw.
TEST(ProposalHelpers, typeless_object_is_rejected)
{
EXPECT_FALSE(proposal::isValidProposalTxnType(STObject{sfGeneric}));
}
// A nested TransactionProposalCreate. In practice STTx construction rejects
// this earlier (the payload lacks TransactionProposalCreate's own template
// fields), but the defense here re-checks that guard so the two cannot
+28 -6
View File
@@ -129,6 +129,26 @@ struct Command
unsigned maxParams = kUnlimitedParams;
};
/**
* Convert the amount argument of channel_authorize or channel_verify to its
* request field: a string of drops is kept as is, and a string holding a JSON
* object is parsed so the server reads it as a token Amount.
*
* @return std::nullopt if the argument is neither.
*/
std::optional<json::Value>
parseChannelAmount(json::Value const& arg)
{
if (!arg.isString())
return std::nullopt;
if (toUInt64(arg.asString()))
return arg;
json::Value amount;
if (json::Reader().parse(arg.asString(), amount) && amount.isObject())
return amount;
return std::nullopt;
}
} // namespace
//
@@ -831,7 +851,7 @@ private:
return parseAccountRaw2(jvParams, jss::destination_account);
}
// channel_authorize: <private_key> [<key_type>] <channel_id> <drops>
// channel_authorize: <private_key> [<key_type>] <channel_id> <drops>|<amount_json>
json::Value
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
parseChannelAuthorize(json::Value const& jvParams)
@@ -865,9 +885,10 @@ private:
index++;
}
if (!jvParams[index].isString() || !toUInt64(jvParams[index].asString()))
auto const amount = parseChannelAmount(jvParams[index]);
if (!amount)
return rpcError(RpcChannelAmtMalformed);
jvRequest[jss::amount] = jvParams[index];
jvRequest[jss::amount] = *amount;
// If additional parameters are appended, be sure to increment index
// here
@@ -875,7 +896,7 @@ private:
return jvRequest;
}
// channel_verify <public_key> <channel_id> <drops> <signature>
// channel_verify <public_key> <channel_id> <drops>|<amount_json> <signature>
json::Value
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
parseChannelVerify(json::Value const& jvParams)
@@ -896,9 +917,10 @@ private:
}
jvRequest[jss::channel_id] = jvParams[1u].asString();
if (!jvParams[2u].isString() || !toUInt64(jvParams[2u].asString()))
auto const amount = parseChannelAmount(jvParams[2u]);
if (!amount)
return rpcError(RpcChannelAmtMalformed);
jvRequest[jss::amount] = jvParams[2u];
jvRequest[jss::amount] = *amount;
jvRequest[jss::signature] = jvParams[3u].asString();
+26
View File
@@ -7,6 +7,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/contract.h>
@@ -20,12 +21,16 @@
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/KeyType.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/tokens.h>
@@ -529,4 +534,25 @@ parseSubUnsubJson(
return RpcSuccess;
}
bool
serializeChannelAuthorization(Serializer& msg, uint256 const& channelId, json::Value const& amount)
{
if (amount.isString())
{
std::optional<std::uint64_t> const drops = toUInt64(amount.asString());
if (!drops)
return false;
serializePayChanAuthorization(msg, channelId, XRPAmount(*drops));
return true;
}
// amountFromJsonNoThrow rejects an object naming XRP; only the sign is checked here
STAmount tokenAmount;
if (!amount.isObject() || !amountFromJsonNoThrow(tokenAmount, amount) || tokenAmount.negative())
return false;
serializePayChanAuthorization(msg, channelId, tokenAmount);
return true;
}
} // namespace xrpl::rpc
+16
View File
@@ -19,6 +19,7 @@
#include <xrpl/protocol/STLedgerEntry.h> // IWYU pragma: keep
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/server/NetworkOPs.h>
@@ -213,6 +214,21 @@ keypairForSignature(
json::Value& error,
unsigned int apiVersion = kApiVersionIfUnspecified);
/**
* Serialize the payment channel claim authorization message for the `amount`
* of a channel_authorize or channel_verify request.
*
* @param msg Serializer the message is appended to.
* @param channelId The channel the claim is for.
* @param amount A string of drops for an XRP channel, or the transaction
* Amount JSON object (currency/issuer/value or mpt_issuance_id/value) for a
* token channel.
* @return false, with nothing appended, if `amount` is neither a string of
* drops nor a valid non-negative token Amount object.
*/
bool
serializeChannelAuthorization(Serializer& msg, uint256 const& channelId, json::Value const& amount);
/**
* Parse subscribe/unsubscribe parameters
*/
+3 -13
View File
@@ -6,16 +6,13 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/tokens.h>
#include <xrpl/resource/Fees.h>
#include <cstdint>
#include <optional>
#include <string>
@@ -24,7 +21,7 @@ namespace xrpl {
// {
// public_key: <public_key>
// channel_id: 256-bit channel id
// drops: 64-bit uint (as string)
// amount: drops as a string (XRP channel) or an Amount object (token channel)
// signature: signature to verify
// }
json::Value
@@ -62,14 +59,10 @@ doChannelVerify(rpc::JsonContext& context)
if (!channelId.parseHex(params[jss::channel_id].asString()))
return rpcError(RpcChannelMalformed);
std::optional<std::uint64_t> const optDrops =
params[jss::amount].isString() ? toUInt64(params[jss::amount].asString()) : std::nullopt;
if (!optDrops)
Serializer msg;
if (!rpc::serializeChannelAuthorization(msg, channelId, params[jss::amount]))
return rpcError(RpcChannelAmtMalformed);
std::uint64_t const drops = *optDrops;
if (!params[jss::signature].isString())
return rpcError(RpcInvalidParams);
@@ -77,9 +70,6 @@ doChannelVerify(rpc::JsonContext& context)
if (!sig || sig->empty())
return rpcError(RpcInvalidParams);
Serializer msg;
serializePayChanAuthorization(msg, channelId, XRPAmount(drops));
json::Value result;
result[jss::signature_verified] = verify(*pk, msg.slice(), makeSlice(*sig));
return result;
@@ -48,8 +48,13 @@ appendChannelJson(SLE const& channel, json::Value& channels)
jDst[jss::channel_id] = to_string(channel.key());
jDst[jss::account] = to_string(channel[sfAccount]);
jDst[jss::destination_account] = to_string(channel[sfDestination]);
jDst[jss::amount] = channel[sfAmount].getText();
jDst[jss::balance] = channel[sfBalance].getText();
// A token amount is an Amount JSON object; an XRP amount stays a string of drops.
auto const amountJson = [](STAmount const& amount) {
return amount.native() ? json::Value{amount.getText()}
: amount.getJson(JsonOptions::Values::None);
};
jDst[jss::amount] = amountJson(channel[sfAmount]);
jDst[jss::balance] = amountJson(channel[sfBalance]);
if (publicKeyType(channel[sfPublicKey]))
{
PublicKey const pk(channel[sfPublicKey]);
@@ -148,6 +148,31 @@ doGatewayBalances(rpc::JsonContext& context)
std::map<AccountID, std::vector<STAmount>> frozenBalances;
std::map<Currency, STAmount> locked;
// Accumulate one locked amount into its per-currency total, clamping to
// the largest valid STAmount on overflow.
auto const addLocked = [&locked](STAmount const& delta) {
auto& bal = locked[delta.get<Issue>().currency];
if (bal == beast::kZero)
{
// This is needed to set the currency code correctly
bal = delta;
return;
}
try
{
bal += delta;
}
catch (std::runtime_error const&)
{
// Presumably the exception was caused by overflow.
// On overflow return the largest valid STAmount.
// Very large sums of STAmount are approximations
// anyway.
bal = STAmount(bal.get<Issue>(), STAmount::kMaxValue, STAmount::kMaxOffset);
}
};
// Traverse the cold wallet's trust lines
{
forEachItem(*ledger, accountID, [&](SLE::ConstRef sle) {
@@ -158,27 +183,21 @@ doGatewayBalances(rpc::JsonContext& context)
if (escrow.holds<MPTIssue>())
return;
auto& bal = locked[escrow.get<Issue>().currency];
if (bal == beast::kZero)
{
// This is needed to set the currency code correctly
bal = escrow;
}
else
{
try
{
bal += escrow;
}
catch (std::runtime_error const&)
{
// Presumably the exception was caused by overflow.
// On overflow return the largest valid STAmount.
// Very large sums of STAmount are approximations
// anyway.
bal = STAmount(bal.get<Issue>(), STAmount::kMaxValue, STAmount::kMaxOffset);
}
}
addLocked(escrow);
}
if (sle->getType() == ltPAYCHAN)
{
// Only unclaimed remainders of the account's own tokens count.
auto const& amount = sle->getFieldAmount(sfAmount);
if (amount.native() || amount.holds<MPTIssue>() || amount.getIssuer() != accountID)
return;
auto const remainder = amount - sle->getFieldAmount(sfBalance);
if (remainder == beast::kZero)
return;
addLocked(remainder);
}
auto rs = PathFindTrustLine::makeItem(accountID, sle);
@@ -3,21 +3,17 @@
#include <xrpld/rpc/Role.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/Fees.h>
#include <cstdint>
#include <exception>
#include <optional>
#include <utility>
@@ -28,7 +24,7 @@ namespace xrpl {
// secret_key: <signing_secret_key>
// key_type: optional; either ed25519 or secp256k1 (default to secp256k1)
// channel_id: 256-bit channel id
// drops: 64-bit uint (as string)
// amount: drops as a string (XRP channel) or an Amount object (token channel)
// }
json::Value
doChannelAuthorize(rpc::JsonContext& context)
@@ -73,16 +69,9 @@ doChannelAuthorize(rpc::JsonContext& context)
if (!channelId.parseHex(params[jss::channel_id].asString()))
return rpcError(RpcChannelMalformed);
std::optional<std::uint64_t> const optDrops =
params[jss::amount].isString() ? toUInt64(params[jss::amount].asString()) : std::nullopt;
if (!optDrops)
return rpcError(RpcChannelAmtMalformed);
std::uint64_t const drops = *optDrops;
Serializer msg;
serializePayChanAuthorization(msg, channelId, XRPAmount(drops));
if (!rpc::serializeChannelAuthorization(msg, channelId, params[jss::amount]))
return rpcError(RpcChannelAmtMalformed);
try
{