Compare commits

...
Author SHA1 Message Date
Ed Hennis 03ea1deed1 Merge remote-tracking branch 'upstream/develop' into ximinez/lending-rebased
* upstream/develop:
  chore: Set version 3.1.0-b0 (5986)
  ci: Clean workspace on Windows self-hosted runners (6024)
  docs: fix spelling in comments (6002)
  fix: floating point representation errors in vault (5997)
  ci: Specify bash as the default shell in workflows (6021)
  refactor: Add `XRPL_RETIRE_FIX` and `XRPL_RETIRE_FEATURE` macros (6014)
  refactor: Retire DepositPreAuth and DepositAuth amendments (5978)
  chore: Move running of unit tests out of coverage target (6018)
  refactor: Retire PayChanRecipientOwnerDir amendment (5946)
  ci: Update Conan to 2.22.2 (6019)
  refactor: Update RocksDB, SQLite, Doctest (6015)
  fix: JSON parsing of negative `STNumber` and `STAmount` (5990)
  chore: Make CMake improvements (6010)
  refactor: Retire CheckCashMakesTrustLine amendment (5974)
  chore: Unify build & test, add ctest to coverage (6013)
  chore: Point xrpld symlink to rippled (6012)
  chore: Removes unnecessary creation of symlink in CMake install file (6009)
  fix: domain order book insertion 5998
  refactor: Retire fixTrustLinesToSelf amendment (5989)
2025-11-12 18:54:49 -05:00
Ed Hennis bdffe99022 Disable inner Batch transactions for Vault and Loan types 2025-11-12 18:42:04 -05:00
Ed Hennis 9582f75ce2 Convert sfNextPaymentDueDate from optional to default
- Simplifies some of the updates and checks
2025-11-12 18:42:04 -05:00
Ed Hennis d786035fe0 Reorder payment options to do full early payment first
- Since all the special cases are now specified with flags, the order is
  less important.
- Avoids computing the periodic payment parts that are not needed for
  full payment computation.
- A late payment without the late payment flag will override everything
  else, though.
2025-11-12 18:42:04 -05:00
Ed Hennis ba19d7f32f Add tfLoanLatePayment flag; full payment is no longer a special case
- A regular payment that is late, or a tfLoanLatePayment that is not
  late will fail.
- Flags are mutually exclusive.
- Add a few interest computation shortcuts and overflow prevention
  checks that return 0 if there's no time to compute for.
2025-11-12 18:42:04 -05:00
Ed Hennis c438852b41 review feedback: Use the specific type in the "SendMulti" functions 2025-11-12 18:42:04 -05:00
Ed Hennis fb941ed1af Fix service fee accounting when a borrower is the broker (#6016)
- Add unit-test to verify the fix.
2025-11-12 18:42:04 -05:00
Ed Hennis b50d728461 Fix build issue - unused variable 2025-11-12 18:42:04 -05:00
Ed Hennis fa93ca7128 test: Add a test case reproducing the issue in RIPD-3902 2025-11-12 18:42:03 -05:00
Ed Hennis 7e94916d08 Always round cover minimum calculations up
- Addresses RIPD-4016.
- Add and update testRoundingAllowsUndercoverage() unit test from ticket.
2025-11-12 18:42:03 -05:00
Ed Hennis f3677c9bed fix: Check for empty or zero VaultID in LoanBrokerSet
- Resolves RIPD-4067.
2025-11-12 18:42:03 -05:00
Ed Hennis 1fa366576e doc: Expand explanation for LoanBrokerCoverClawback::determineBrokerID 2025-11-12 18:42:03 -05:00
Ed Hennis 6e4f6eab3b Minor test fixes
- Don't deposit cover if the amount is 0.
- Handle missing next payment date in getCurrentState.
2025-11-12 18:42:03 -05:00
Ed Hennis 348e7f7cfc Fix RIPD-3901 - faulty assert
- Assert requires that an overpayment reduces the value of a loan. If
  the overall loan interest is low enough, it could leave it unchanged.
  Update the assert to require that the overpayment does not increase
  the value of the loan.
- Adds a unit test provided by @gregtatcam to demonstrate this issue.
2025-11-12 18:42:03 -05:00
Ed Hennis 43174d31d1 Update the test case for RIPD-3459 with more detailed output data 2025-11-12 18:42:02 -05:00
Ed Hennis 4fbf687166 Add test case to reproduce RIPD-3459
- Improve a few loan test helper functions.
- Make Loan.GracePeriod a default field.
2025-11-12 18:42:02 -05:00
Ed Hennis cc46e94308 Check for Grace Period overflow in LoanSet 2025-11-12 18:42:02 -05:00
Ed Hennis e485d5d6ad Add Grace Period to testLoanNextPaymentDueDateOverflow
- Demonstrates that Grace Period can overflow.
- Expected to fail.
2025-11-12 18:42:02 -05:00
Gregory Tsipenyuk 54d738b93e Implicitly authorize Vault and LoanBroker pseudo-accounts (#5976)
- Vault and LoanBroker pseudo-accounts can hold MPTs, regardless of MPTRequireAuth setting.
- Add requireAuth check in LoanBrokerCoverDeposit and LoanPay.
- Fail attempts to unauthorize pseudo-accounts by MPT issuers.
2025-11-12 18:42:02 -05:00
Ed Hennis 3fdd3cb495 Refactor loan test helpers into reusable functions
- Try to reproduce RIPD-3831
2025-11-12 18:42:02 -05:00
Gregory Tsipenyuk 2a9eec6c4f Fix empty/zero broker ID in LoanBrokerSet and LoanSet (#5977)
- Add checks for empty/zero brokerID where needed
- Extend unit-tests.
2025-11-12 18:42:02 -05:00
Ed Hennis 3a45fc3364 fix: Use ".value()" instead of "->" when with STObject::Proxy objects
- Turns out that "Proxy::operator->" is not a safe substitute for
  "Proxy::value()." if the field is not required. The implementation
  is different such that "operator->" will return a null ptr if the
  field is not present. This includes default fields with a value of
  zero!
2025-11-12 18:41:54 -05:00
Ed Hennis 8428255866 Handle zero rate in computePaymentFactor 2025-11-12 18:06:00 -05:00
Ed Hennis e001681ada fix: Improve rounding for IOU loans
- Scale the loan to the Vault, so that amounts moving to the vault are
  less likely to have rounding errors.
- Similar to LoanPay, when LoanManage defaults a loan, round the amounts
  to the Vault scale (because the Vault scale can change) before
  applying them to the Vault.
2025-11-12 18:05:59 -05:00
Ed Hennis d2f8e3817e Another arbitrary test case to try 2025-11-12 18:05:59 -05:00
Ed Hennis a38e09e327 Add testDustManipulation test, which is expected to fail 2025-11-12 18:05:59 -05:00
Ed Hennis 165478b929 Remove the shortage code completely 2025-11-05 13:28:03 -05:00
Ed Hennis 95fdbe520f Partially revert aed8e2b166 Fill in payment computation shortages (#5941)
- Do not attempt to fill in payment computation shortages. Fixes, tests,
  and optimizations are left intact.
2025-11-05 12:48:14 -05:00
Ed Hennis 34db04ffc1 Update Lending files for the xrpl rename 2025-11-04 19:37:58 -05:00
Ed Hennis 65e6746b5c Merge branch 'develop' into ximinez/lending-XLS-66 2025-11-04 18:02:38 -05:00
Ed Hennis aed8e2b166 Fill in payment computation shortages (#5941)
- Ensures a consistent fixed payment amount for the entire life of the
  loan, except the final payment, which is guaranteed to be the same or
  smaller.
- Convert some Loan structs to compute values that had need manual
  updates to stay consistent.
- Fail the transaction in `LoanPay` if it violates the Vault `assetsAvailable <=
  assetsTotal` invariant.
- Use constexpr to check that min mantissa value for Number and STAmount
  is a power of 10, and compute the max in terms of the min.
- Improve unit tests:
  - Use BrokerParameters and Loan Parameters instead of semi-global
    class values
  - In tests, check that the expected number of loan payments are made.
  - Add LoanBatch manual test to generate a set number of random loans,
    set them up, and pay them off.
  - Add LoanArbitrary manual test to run a single test with specific
    (hard-coded for now) parameters.
  - Add Number support to XRP_t.
2025-11-04 22:56:16 +00:00
Ed Hennis 7925cc4052 fix: Check the borrower's balance in LoanSet for reserve
- Previously, the submitting Account's balance was checked, which could
  have been the borrower or the lender.
2025-11-03 20:39:01 -05:00
Ed Hennis 245bac1769 Merge branch 'develop' into ximinez/lending-XLS-66 2025-11-03 18:04:04 -05:00
Ed Hennis cdd6ad4ba4 test: Fix test set up, since the order of preclaim checks changed
- Need to have multisign set up to get to the fee check
2025-11-03 18:00:10 -05:00
Ed Hennis 89979022f2 Merge remote-tracking branch 'upstream/develop' into ximinez/lending-XLS-66
* upstream/develop:
  refactor: Retire ImmediateOfferKilled amendment (5973)
  ci: Update CI image hashes to use netstat (5987)
  chore: Remove version number in find_dependency for OpenSSL (5985)
  refactor: Modularize shamap and nodestore (5668)
  refactor: Retire fixMasterKeyAsRegularKey amendment (5959)
  refactor: Retire fixReducedOffersV1 amendment (5972)
  refactor: Retire fixAmendmentMajorityCalc amendment (5961)
  refactor: Clean up `TxMeta` (5845)
  fix: Address permission delegation vulnerability (5825)
2025-11-03 13:09:03 -05:00
Ed Hennis 4920e65b14 fix: LoanBrokerSet is malformed if Cover fields don't align
- sfCoverRateMinimum and sfCoverRateLiquidation must be both zero or
  both non-zero, because both are used in the default amount
  calculations, which is the only place they're really meaningful.
2025-11-03 12:31:20 -05:00
Bronek Kozicki b472cc3493 Unit tests for multisigned LoanSet counterparty 2025-10-31 16:51:37 -04:00
Ed Hennis 27389cd7e8 Merge branch 'develop' into ximinez/lending-XLS-66 2025-10-31 13:51:23 -04:00
Ed Hennis 2b9f70b0bd Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66
* XRPLF/develop:
  refactor: Improve txset handling (5951)
  Remove directory size limit (5935)
  fix: Change Credential sfSubjectNode to optional (5936)
  refactor: Add support for extra transaction signature validation (5851)
  refactor: Retire fixQualityUpperBound amendment (5960)
  refactor: Retire fix1623 amendment (5928)
  refactor: Retire fixTakerDryOfferRemoval amendment (5958)
  ci: Check whether test failures are caused by port exhaustion (5938)
  chore: Use new prepare-runner (5970)
2025-10-31 13:50:46 -04:00
Gregory Tsipenyuk e8dddcbb7f Fix LoanBrokerDelete with empty or zero broker ID (RIPD-3858) (#5967) 2025-10-31 03:01:05 +00:00
Ed Hennis 7a758a42f9 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66
* XRPLF/develop:
  refactor: Retire fixSTAmountCanonicalize code (5956)
2025-10-29 14:26:32 -04:00
Ed Hennis 298f6deac9 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66
* XRPLF/develop:
  refactor: Retire fixRmSmallIncreasedQOffers amendment (5955)
  refactor: Sorts retired amendments to reduce conflicts (5966)
2025-10-29 13:45:08 -04:00
Ed Hennis 71565a909e Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66
* XRPLF/develop:
  refactor: Retire fixCheckThreading amendment (5957)
  refactor: Retire fix1578 amendment (5927)
  refactor: Retire fix1571 amendment (5925)
  feat: Add public key to log messages (5678)
  Retire fix1781 amendment (5931)
  ci: Use nproc-2 to set parallelism for builds and tests (5939)
2025-10-29 12:57:37 -04:00
Ed Hennis 52490730b2 Check for null Vault SLE in LoanBrokerCoverDeposit 2025-10-29 12:51:12 -04:00
Ed Hennis 64e0ee4be9 Add POC unit test for null vault dereference 2025-10-29 12:25:03 -04:00
Ed Hennis d0c60cdf59 Fix broken unit test case 2025-10-28 18:47:52 -04:00
Ed Hennis 0febc60cbe Ignore the contents of SigningPubKey when multisigning with a target 2025-10-28 18:37:15 -04:00
Ed Hennis 122d8f96b3 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66
* XRPLF/develop:
  ci: Use commit hash so workflows are not canceled when merging multiple PRs (5950)
  ci: Only upload codecov reports in the original repo, not in forks (5953)
  ci: Only log into Conan when uploading packages (5952)
  fix: invariant error in fee-sized `VaultWithdraw` (5876)
  fix: account_tx limit parameter validation for malformed values (5891)
  refactor: Retire fix1543 amendment (5926)
  ci: Only run .exe files during test phase on Windows (5947)
  refactor: Migrate json unit tests to use doctest (5533)
  Change `fixMPTDeliveredAmount` to `Supported::yes` (5833)
  fix: Upload all test binaries (5932)
  chore: Better pre-commit failure message (5940)
2025-10-28 18:21:12 -04:00
Ed Hennis ec60dcf90d Ensure payment does not exceed Loan balance
- Also disallow extra parameters on the ALWAYS_OR_UNREACHABLE macro,
  which should prevent me from mixing up XRPL_ASSERT and
  XRPL_ASSERT_PARTS.
2025-10-28 16:27:45 -04:00
Ed Hennis 310852ba2d refactor: Payment component calculation will target next true state
- Compute the next "true" state, round the values off, then compute the
  deltas needed to get the current state to that state. Plus some data
  integrity checks.
- Add `Number::zero`, which is longer to type, but more readable than
  `Number{}`.
- Prepare to improve Loan unit tests: track managementFeeRate in
  BrokerInfo, define a LoanParameters object for creation options and
  start adding support for it, track and verify loan state while making
  payments.
2025-10-28 16:27:44 -04:00
Ed Hennis f4404eafbd Fix build error
- Use an explicit cast to std::int64_t in std::max call, because
  suffixes are inconsistent.
2025-10-26 21:52:01 -04:00
Ed Hennis fe5bd82ed8 Merge branch 'develop' into ximinez/lending-XLS-66 2025-10-26 19:12:45 -04:00
Ed Hennis da14c6f018 Prevent numerical overflow of payment due dates
- Addresses FIND-013 from audit.
- Bases the limit on the current ledger time, and ensures that
  "payments * interval <= limit". This allows a loan to potentially run through
  "the end of time" successfully, but not go a second over.
- Wrote several test cases, including a few that go right to "the end of
  time".
2025-10-26 18:57:16 -04:00
Ed Hennis 5ceefdfbd5 Add repro unit test for FIND-008, with modifications, from audit 2025-10-26 16:03:59 -04:00
Ed Hennis b8b9e7a51e Add repro unit tests, with modifications, from audit
- FIND-005, FIND-009, and FIND-010.
- Add the finding number to existing tests - FIND-001, FIND-003,
  FIND-012, FIND-007.
- Tweak the interest rate failure log messages in LoanSet.
2025-10-26 15:31:32 -04:00
Ed Hennis 269fb44cd7 Add unit test from security audit (FIND-007)
- The issue identified in this finding had previously been fixed, but
  the test is useful to verify.
2025-10-26 13:51:46 -04:00
Ed Hennis 40ef91e7e0 Limit how many payments can be made in a single LoanPay
- Addresses FIND-005 from audit.
- Tuning values defined in Protocol.h. Optimal values TBD.
  - loanPaymentsPerFeeIncrement: calculateBaseFee estimates the number
    of payments included in the Amount and charges
    "baseFee * number / loanPaymentsPerFeeIncrement".
  - loanMaximumPaymentsPerTransaction: If the number of payments
    (including overpayments if applicable) hits this limit, stop
    processing more payments, but DO NOT FAIL.
- Fix the rounding in LoanSet for Guard 4 (sufficient computed payments)
- Tweak several test parameters to account for the new limits.
- Change payment component rounding for IOUs to "towards_zero".
- Add some safety limits to loan calculations to prevent nonsensical
  values.
2025-10-26 13:09:35 -04:00
Ed Hennis fe4269cf8b Avoid negative payments: limit origination fee to the loan principal
- Addresses FIND-006 from audit.
- Removes the "minimum" check for sfLoanOriginationFee, and replaces it
  with a "valid range" check with the max value being
  sfPrincipalRequested.
- Reuses the test from the report, with some modifications.
- Also adds some more test cases for existing interest rate tests.
2025-10-24 18:33:57 -04:00
Ed Hennis a3b82023d6 refactor: Pull computeOverpaymentComponents into a separate function 2025-10-24 16:00:32 -04:00
Ed Hennis 0950d41fce refactor: De-templatize and move LendingHelper functions from .h to .cpp
- Changes:
  1. Removed the `AssetType` template parameter from all functions in
     favor of just using the `Asset` class.
  2. Fully moved all `ripple::detail` functions from .h to .cpp.
  3. Moved all definitions of non `detail` functions from .h to .cpp,
     except roundPeriodicPayment, just because it's small and I want it
     to be visible. Left declarations in .h
  4. Moved `PaymentSpecialCase`, `PaymentComponents` and
     `computePaymentComponents` into `detail` and updated references.
2025-10-24 15:44:46 -04:00
Ed Hennis 45819477d3 refactor: change what values are tracked as Payment Components - Part 2
- PaymentComponents is an class used internally to break a payment value
  into principal, interest, and fees.
2025-10-24 15:09:35 -04:00
Ed Hennis 83e249eb8a refactor: change what values are tracked as Payment Components - Part 1
- This is an intermediate commit. It leaves the old variables in place,
  so I can do verifications that the new computations are correct. They
  will be removed in the next commit.
- PaymentComponents is an class used internally to break a payment value
  into principal, interest, and fees.
2025-10-24 15:09:35 -04:00
Bronek Kozicki 07fcbd5498 Add authorization check to LoanSet (#5875)
- Add tests for unauthorized borrower and lender
2025-10-24 13:27:27 -04:00
Gregory TsipenyukandEd Hennis 88a770c71b Don't create empty holding for MPT or trust line issuer (#5877)
- Adds a check to the MPToken creation invariant to ensure none are created for the issuer.
- `addEmptyHolding()` will return success without doing anything for these scenarios. There is nothing to do, as with XRP.

---------

Co-authored-by: Ed Hennis <[email protected]>
2025-10-24 12:32:26 -04:00
Ed Hennis 78ef800e30 Fix more problems introduced by 6adb2ec
- Renamed canSendToAccount to canWithdraw, because the semantics are
  a little different from a payment. Notably, if withdrawing to self,
  you can still include a destination tag.
- Simplified the interface to canWithdraw to just pass in the
  STTx.
- preflightDestinationAndTag is pretty pointless now, so removed it.
2025-10-24 01:19:45 -04:00
Ed Hennis 7f2f6b8791 Merge remote-tracking branch 'mywork/ximinez/crashed-tests' into ximinez/lending-XLS-66
* mywork/ximinez/crashed-tests:
  test: Count crashed test suites
2025-10-24 00:59:33 -04:00
Ed Hennis 7c45f6a673 Fix broken Vault unit tests introduced in 6adb2ec
- canSendToAccount will check if a destination tag is required _before_
  checking if the sender is the destination. This is the original
  VaultWithdraw behavior, and I want to stay consistent.
2025-10-24 00:30:06 -04:00
Ed Hennis 66b547f6fc Review feedback from @Tapanito
- Correct interest rounding calculation
- Fix some comments
- Don't explicitly break out of the multiple payment loop for the final
  payment. Assert that it will exit by the loop condition.
2025-10-23 19:27:11 -04:00
Ed Hennis 6adb2eca76 Review feedbak from @tequdev, plus helpers
- Fix LoanSet.calculateBaseFee with multisign.
- Cleanups.
- Add View helper functions for Loan and Vault transactions
  - preflightDestinationAndTag
  - checkDestinationAndTag
  - canSendToAccount
- Used the helpers in appropriate Loan and Vault transactions.
  - They could also be used in older transactions, I'll save that for
    later.
2025-10-23 18:11:32 -04:00
Gregory Tsipenyuk f60e298627 Extend LoanBroaker and Loan unit-tests. (#5863)
- Add convenience functions to MPT test-framework.
2025-10-23 19:51:30 +00:00
Ed Hennis 9f5bc8f0da Fix more build errors 2025-10-23 14:28:03 -04:00
Ed Hennis e306a7c75f Fix some build errors 2025-10-23 14:11:31 -04:00
Ed Hennis 59364be5f8 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66
* XRPLF/develop:
  refactor: Retire fix1515 amendment (5920)
  Use "${ENVVAR}" instead of ${{ env.ENVVAR }} syntax in GitHub Actions (5923)
2025-10-23 13:25:59 -04:00
Ed Hennis aa696ed61a Merge branch 'develop' into ximinez/crashed-tests 2025-10-23 13:24:37 -04:00
Ed Hennis 044b9b8217 *facepalm* 2025-10-23 12:54:31 -04:00
Ed Hennis f78c5f65bc Allow issuers to send LoanPay
- Implement a new helper accountCanSend, which is like accountHolds, but
  returns a meaningful value for issuers, and will include the available
  credit on the other side of a trust line. (The sfHighLimit or
  sfLowLimit as appropriate.)
- Use this new helper when checking the available balance in LoanPay.
2025-10-23 01:46:43 -04:00
Ed Hennis 6ad7d1c076 Rewrite functionality calculations to use updated calculations 2025-10-23 01:02:03 -04:00
Ed Hennis 9814ec0309 Implement tfLoanFullPayment; use updated full payment calculations 2025-10-23 00:54:58 -04:00
Ed Hennis b3593fac1c test: Count crashed test suites 2025-10-22 20:23:54 -04:00
Ed Hennis 30e2650ff9 Use a loan scale of 0 for integral asset types
- Loan scale is completely irrelevant to integral types (XRP, MPT), and
  the field is "soeDEFAULT", so when set to 0, it won't be stored on
  ledger, saving a little bit of space.
2025-10-22 17:15:34 -04:00
Ed Hennis 4aa6baef26 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66
* XRPLF/develop:
  Moved fix1513 to retire state (5919)
2025-10-22 11:43:39 -04:00
Ed Hennis dd938c4e7b Add missing header file 2025-10-22 10:56:36 -04:00
Ed Hennis 92144efbdf Check that the borrower has sufficient funds to make the LoanPay 2025-10-22 00:38:05 -04:00
Ed Hennis 5da586bffc Add support for explicit overpayment flag in LoanPay
- Uses the same name and value as for LoanSet: tfLoanOverpayment.
- Untested.
- Also create several placeholders for missing test cases.
2025-10-21 23:55:46 -04:00
Ed Hennis 70ae693821 Merge branch 'develop' into ximinez/lending-XLS-66 2025-10-21 22:20:17 -04:00
Ed Hennis f6098fd80b Expand use of Asset::integral() 2025-10-21 20:01:55 -04:00
Gregory Tsipenyuk 8757022312 test: Add unit-test to verify correct behavior on broker deep freeze (#5904)
* Add unit-test to verify correct behavior on broker deep freeze
2025-10-21 23:23:27 +00:00
Ed Hennis 587cb2533c Fix build error - XRPL_ASSERT Antithesis expansion 2025-10-21 18:12:52 -04:00
Ed Hennis 92431e11c8 Fix build error - unused variables 2025-10-21 18:05:56 -04:00
Ed Hennis dfc4933c3b Convert Loans to track management fee and use updated rounding
- Move management fee calculations out of transactors an into the
  appropriate functions in LendingHelpers.h.
- Rewrite how overpayments are handled. May changed based on numerical
  analysis.
- Update, fix, and clean up unit tests. Includes adding tolerances for
  some checks where an exact match is unlikely.
- Add "integral()" function to Asset to simplify a common check.
2025-10-21 17:47:13 -04:00
Ed Hennis 1bb306baf4 Fix formatting 2025-10-20 13:40:27 -04:00
Ed Hennis 98d1cf07ed Compare Numbers against beast::zero 2025-10-17 18:29:09 -04:00
Ed Hennis baabf142e4 Fix some build errors 2025-10-17 18:29:09 -04:00
Ed Hennis f9bd9f390a Remove some unused variables, increase rounding tolerance 2025-10-17 18:29:09 -04:00
Ed Hennis d7196a8e56 Get remaining Loan unit tests working
- Rename some of the helper functions / lambdas.
- Update tracked interestOwed field better at final payoff.
- Add checks in LoanSet that the fields can be represented in the asset
  type, and update test that fails those checks
  (testLoanPayComputePeriodicPaymentValidRateInvariant)
- Also check that the computed periodic payment can be represented as
  the asset type, and doesn't round _UP_ to 0.
- Update asserts to account for more scenarios, including initial loan
  computation.
2025-10-17 18:29:09 -04:00
Ed Hennis bda6bb46d1 Fix the singlePayment test helper lambda 2025-10-17 18:29:09 -04:00
Ed Hennis ec20974cae Simplify rules for creating holdings in LoanSet
- Lender and borrower both signed the transaction. Duh.
2025-10-17 18:29:08 -04:00
Ed Hennis 1efc532b21 Implement Vito's new loan payment part rounding algorithm, and more
- Implement AccountSendMulti
- Document the derivations of loan components.
- Add "loanPrincipalFromPeriodicPayment" helper.
- Removed sfReferencePrincipal
- LoanSet and LoanPay can create MPTokens as a side effect
- LoanPay will send the fee to cover if the broker owner is deep frozen,
  and fail if both of them are deep frozen.
- LoanPay will check auth for the receivers, or create holdings for the
  submitting account if needed.
- LoanSet will fail if principal requested is not positive
- Handle overpayment in a separate function
- Add a test helper to check that balance changes went as expected
- Fix more tests
2025-10-17 18:29:08 -04:00
Ed Hennis 2dd239c59f Update payment component computation and rounding, and more tests
- Tests not expected to pass.
- Check in LoanSet if a loan with interest actually has interest.
  tecPRECISION_LOSS if not.
- Add checks in LoanPay for deep froze broker owner and pseudoaccount.
- Fix management fee calculations in LoanPay and associated LoanBroker
  and Vault data updates.
- Make state tracking next payment due date optional.
- Add a test case showing multiple payments combined.
- Update more tests to work with the new fields.
2025-10-17 18:29:08 -04:00
Ed Hennis fc2163708a Update InvariantCheck, LoanManage, and LoanPay to make tests work
- Primarily updating tests, and fixing stuff that didn't work.
- Tests still not expected to pass.
- Add Json::Value::isMember(StaticString) so SFields can be used.
- Validate more fields in ValidLoan Invariant
2025-10-17 18:29:08 -04:00
Ed Hennis eeec90ee74 Handle overpayment calculations 2025-10-17 18:29:08 -04:00
Ed Hennis 8d982758cb Ensure interest portion doesn't go negative
- Ensure principal part is not bigger than whole payment.
- Add some documentation.
2025-10-17 18:29:08 -04:00
Ed Hennis 1b31dbc4c9 Fix build errors 2025-10-17 18:29:07 -04:00
Ed Hennis f8b63228b6 Cool idea: Charge one extra base fee per 25 payments 2025-10-17 18:29:07 -04:00
Ed Hennis 0b8cd2d7ca Cool idea: Make Loan.NextDueDate optional; clear it when complete
- Check "simple" loanMakePayment failure conditions early
2025-10-17 18:29:07 -04:00
Ed Hennis a8de91c57d Add LoanPaymentParts::operator+=
- Builds, but tests are not expected to pass
2025-10-17 18:29:07 -04:00
Ed Hennis 97e2c10359 Continue progress updating the LoanHelpers
- May not build
2025-10-17 18:29:07 -04:00
Ed Hennis 96d0258f51 Update LoanPay
- Enable the rest of LoanPay.
- Start updating the helper functions.
- Tests are not expected to pass.
2025-10-17 18:29:07 -04:00
Ed Hennis fb8dafa6a8 Refactor Number, add tests, update LoanManage, update LoanPay
- Loan tests are not expected to pass.
- Refactor Number to put rounding logic into reusable functions.
- Add Number tests to explicitly test rounding - may be redundant, but
  easy to reason about.  Verifies that the default rounding matches
  banker's rounding.
- Enable LoanManage. Apparently any remaining issues were fixed by the
  previous commit's cleanups.
- Partially enabled LoanPay.
2025-10-17 18:29:06 -04:00
Ed Hennis 3cc8e5564a Start converting LoanManage to handle pre-computed fields
- Tests are not expected to pass
2025-10-17 18:29:06 -04:00
Ed Hennis e1939d0d20 Convert LoanSet to precomputed all values
- Tests are not expected to pass
2025-10-17 18:29:06 -04:00
Ed Hennis 2509293943 Disable blocks of code using #if LOANCOMPLETE to enable build
- Will add those back later as the appropriate helpers are updated.
- Tests are not expected to pass.
2025-10-17 18:29:06 -04:00
Ed Hennis 63edf035a6 Start converting Loans to use fixed payments and track value
- Not expected to build
2025-10-17 18:29:06 -04:00
Ed Hennis d353f4a2e6 Remove conditional LoanDraw code 2025-10-17 18:29:06 -04:00
Ed Hennis 9f1ed7ed61 Merge branch 'develop' into ximinez/lending-XLS-66 2025-10-17 18:21:49 -04:00
Ed Hennis d700f6876d Merge remote-tracking branch 'mywork/ximinez/lending-sttx-checksign' into ximinez/lending-XLS-66
* mywork/ximinez/lending-sttx-checksign:
  Review feedback from @a1q123456
  Add jtx, STObject, and RPC support for sig object fields
  Add support for extra transaction signature validation
2025-10-17 18:21:12 -04:00
Ed Hennis 9c28578262 Review feedback from @a1q123456
- Convert STTx::getSignature to be static.
- Make the STTx::checkSign that takes a signature object private, and
  change it and all the other relevant functions to take the signature
  object as a `const&`.
- Change `getBatchTransactionIDs` to return a `const&` to reduce the
  number of vector copies. Rename `batchTxnIds_` to CamelCase. Update
  the text of the internal comment.
2025-10-17 16:26:23 -04:00
Ed Hennis 134998d86f Merge branch 'develop' into ximinez/lending-XLS-66 2025-10-16 13:12:20 -04:00
Ed Hennis 307d5474b4 Merge branch 'develop' into ximinez/lending-sttx-checksign 2025-10-16 13:12:17 -04:00
Ed Hennis 1212902be6 Merge branch 'develop' into ximinez/lending-XLS-66 2025-10-16 10:48:57 -04:00
Ed Hennis 48d458afc3 Merge branch 'develop' into ximinez/lending-sttx-checksign 2025-10-16 10:48:52 -04:00
Ed Hennis e6e9003821 Merge branch 'develop' into ximinez/lending-XLS-66 2025-10-10 13:47:05 -04:00
Ed Hennis 66dbc05a9f Update ValidVault to allow LoanPay to change LossUnrealized
- Making a payment on an impaired loan will unimpair the loan, which
  changes Vault.LossUnrealized.
- Add a step in unit tests to impair a loan before making a payment, to
  verify, and prevent future similar regressions.
- Resolves regression RIPD-3650
2025-10-10 13:15:28 -04:00
Ed Hennis 7f4cdefc24 Merge branch 'develop' into ximinez/lending-sttx-checksign 2025-10-10 13:01:41 -04:00
Ed Hennis 8152bcb1f3 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66
* XRPLF/develop:
  refactor: Update Conan dependencies: OpenSSL (5873)
  Add vault invariants (5518)
  test: Add more tests for Simulate RPC metadata (5827)
  chore: Fix release build error (5864)
  refactor: Update CI strategy matrix to use new RHEL 9 and RHEL 10 images (5856)
  chore: exclude all `UNREACHABLE` blocks from codecov (5846)
  Set version to 3.0.0-b1 (5859)
2025-10-09 16:15:22 -04:00
Ed Hennis 53e82a1c11 Merge branch 'develop' into ximinez/lending-sttx-checksign 2025-10-09 15:14:55 -04:00
Ed Hennis c179135e15 Fix the build and test errors introduced by the previous merge:
** Vault Invariants for Loan txs need to be fleshed out. **

- Give the appropriate Loan transactions vault modification privileges.
- Give the appropriate Loan transactions the ability to authorize
  (create in this case) MPTokens.
- Check that LoanManage does not leave Vault in an inconsistent state
  (AssetsAvailable > AssetsTotal). For IOU vaults, if the difference is
  dust, "round up".
2025-10-09 15:09:49 -04:00
Ed Hennis 5117ef831e Merge branch 'develop' into ximinez/lending-XLS-66 2025-10-09 14:15:26 -04:00
Gregory Tsipenyuk f35d0dc829 Add convenience functions to MPT test-framework. (#5870) 2025-10-09 04:22:38 +00:00
Ed Hennis 311da8fdcf Merge branch 'develop' into ximinez/lending-sttx-checksign 2025-10-08 14:21:41 -04:00
Ed Hennis 7120cce996 Add jtx, STObject, and RPC support for sig object fields 2025-10-06 15:40:14 -04:00
Ed Hennis 23045fcbef Add support for extra transaction signature validation
- Restructures `STTx` signature checking code to be able to handle
  a `sigObject`, which may be the full transaction, or may be an object
  field containing a separate signature. Either way, the `sigObject` can
  be a single- or multi-sign signature.
- This is distinct from 550f90a75e (#5594), which changed the check in
  Transactor, which validates whether a given account is allowed to sign
  for the given transaction. This cryptographically checks the signature
  validity.
2025-10-03 14:56:39 -04:00
Ed Hennis d576e9d214 Include exception messages in multi signature check result 2025-10-02 22:08:03 -04:00
Ed Hennis 5863f05343 Merge branch 'develop' into ximinez/lending-XLS-66 2025-10-02 11:03:17 -04:00
Ed Hennis ce23d0aa54 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66
* XRPLF/develop:
  fix: Transaction sig checking functions do not get a full context (5829)
2025-10-01 18:13:53 -04:00
Ed Hennis aac25e8c79 fixup! Review feedback from @Bronek: Add an assert 2025-10-01 15:56:06 -04:00
Ed Hennis 1888784e8e Review feedback from @Bronek: Add an assert 2025-10-01 14:15:45 -04:00
Ed Hennis 681c179174 Merge branch 'develop' into ximinez/lending-XLS-66 2025-10-01 13:14:33 -04:00
Ed Hennis 05739c59cf Merge branch 'develop' into ximinez/lending-XLS-66 2025-09-30 22:29:07 -04:00
Ed Hennis b127b24dae Addressed review feedback from @gregtatcam:
- All current items are done
- Mostly comments
- Restructured PaymentParts (formerly PeriodicPaymentParts) to bring
  along fees, and removed the computed / combined PeriodicPayment from
  places that should be using PaymentParts instead.
2025-09-30 22:25:53 -04:00
Ed Hennis c2a0824da0 Merge remote-tracking branch 'mywork/ximinez/lending-tx-fix' into ximinez/lending-XLS-66
* mywork/ximinez/lending-tx-fix:
  fix: Transaction sig checking functions do not get a full context
  ci: Upload artifacts during build and test in a separate job (5817)
2025-09-30 16:28:10 -04:00
Ed Hennis e1b234cc51 fix: Transaction sig checking functions do not get a full context
- Fixes a bug introduced by PR #5594, commit 550f90a75e, which
  introduced the concept of a "sigObject", in which the signature is
  checked without necessarily being the transaction.
- Fortunately, no code uses the "sigObject" as anything other than the
  transactor yet, so the bug is harmless for now.
- This fix removes the "PreclaimContext" from the parameter list, and
  adds only the individual parts needed by the function, none of which
  are the transaction.
2025-09-30 14:39:08 -04:00
Ed Hennis 03d8cf2c12 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66
* XRPLF/develop:
  refactor: Add support for extra transaction signatures (5594)
  refactor: Restructure Transactor::preflight to reduce boilerplate (5592)
2025-09-29 21:49:58 -04:00
Ed Hennis 7a3f7aebcf Review feedback from @gregtatcam
* Write more comments explaining what's going on.
* Rename some variables.
* Do a final safety check for valid values in `LoanPay`.
2025-09-29 21:42:03 -04:00
Ed Hennis f78d27e9b3 Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  switch `fixIncludeKeyletFields` to `Supported::yes` (5819)
2025-09-29 13:38:43 -04:00
Ed Hennis e38e911190 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-29 13:32:12 -04:00
Ed Hennis 44313d9a30 Merge branch 'develop' into ximinez/lending-refactoring-3 2025-09-29 13:32:08 -04:00
Ed Hennis a3365545df Review feedback from @Bronek
- Exclude impossible logging from code coverage, too.
2025-09-28 19:10:12 -04:00
Ed Hennis 1d85727675 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-26 19:26:13 -04:00
Ed Hennis da3c2beae0 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-26 19:26:06 -04:00
Ed Hennis 6b7136906f Merge branch 'develop' into ximinez/lending-refactoring-3 2025-09-26 19:26:02 -04:00
Ed Hennis 7612fd0ba6 Review feedback from @gregtatcam
- Amendment checking correctness.
- Sync some changes between LoanBrokerCoverWithdraw and CoverDeposit.
- Check for nulls.
- Add documentation.
2025-09-26 19:23:13 -04:00
Ed Hennis 0bd6b1ae49 Review feedback from @mvadari
- Clarify the purpose of the assert in calculateOwnerReserveFee
2025-09-26 16:05:15 -04:00
Ed Hennis 66216a3d41 Review feedback from @mvadari
- Add a test for preflightCheckSigningKey
2025-09-26 15:36:12 -04:00
Ed Hennis 4b90f97e52 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-26 13:41:43 -04:00
Ed Hennis f1a2fe3d0a Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-26 13:41:35 -04:00
Ed Hennis d76f31cb7c Merge branch 'develop' into ximinez/lending-refactoring-3 2025-09-26 13:41:31 -04:00
Ed Hennis b5caa9c63d Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-26 12:09:25 -04:00
Ed Hennis 3931eb39d5 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-26 12:09:17 -04:00
Ed Hennis 322308f7d1 Merge branch 'develop' into ximinez/lending-refactoring-3 2025-09-26 12:09:13 -04:00
Ed Hennis 4c8f7968c4 Tweak payment rounding 2025-09-25 16:11:02 -04:00
Ed Hennis 6e2965f01a Add test cases for Vault prereq amendments being disabled 2025-09-25 15:26:30 -04:00
Ed Hennis dba3af9318 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-25 13:27:17 -04:00
Ed Hennis 7304ee68d5 Merge branch 'develop' into ximinez/lending-refactoring-3 2025-09-25 13:27:13 -04:00
Ed Hennis 759d114ab8 Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  docs: Add warning about using std::counting_semaphore (#5595)
  Revert "Update Conan dependencies: OpenSSL" (#5807)
  test: Add STInteger and STParsedJSON tests (#5726)
  Review feedback from @mvadari
2025-09-24 16:10:16 -04:00
Ed Hennis 3c93dbbc7c Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-24 15:49:59 -04:00
Ed Hennis 97819b23ba Merge branch 'develop' into ximinez/lending-refactoring-3 2025-09-24 15:49:56 -04:00
Ed Hennis 9bfa88cd86 Fix build error
- New code referenced `isEnabled`, which was renamed in an updated prereq PR
2025-09-24 15:38:53 -04:00
Ed Hennis de5af71cb6 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-22 11:26:20 -04:00
Ed Hennis 67578b8f50 Review feedback from @mvadari
- Clean up some of the flag and extra feature checks for readability
2025-09-22 11:17:13 -04:00
Ed Hennis e72a9d4bf2 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-20 17:12:32 -04:00
Ed Hennis 5b576b841c Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-20 17:12:26 -04:00
Ed Hennis 267b537a13 Review feedback from @Bronek
- Fix a moved assert message
- Rename `Transactor::isEnabled` to `checkExtraFeatures`, and finish
  cleaning up a few classes I missed on the first pass (Vault*
  & XChainBridge*).
2025-09-20 17:09:48 -04:00
Ed Hennis 543f1e702f Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-20 15:44:46 -04:00
Ed Hennis f9dcce2d7e Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-20 15:44:39 -04:00
Ed Hennis 928af9633f Merge branch 'develop' into ximinez/lending-refactoring-3 2025-09-20 15:44:36 -04:00
Ed Hennis 9e84c37306 Refactor full payment into a function, and update some formatting 2025-09-18 14:54:49 -04:00
Ed Hennis 2aa626c205 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-18 14:08:42 -04:00
Ed Hennis 36c2fa1e56 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-18 14:08:35 -04:00
Ed Hennis 886b0a487c Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-18 14:08:31 -04:00
Ed Hennis 7849df5feb Merge branch 'develop' into ximinez/lending-refactoring-2 2025-09-18 14:08:27 -04:00
Ed Hennis 55baf35d9c Fix build errors due to private type access 2025-09-18 13:30:50 -04:00
Ed Hennis 90ca9ea71a Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  refactor: Modularise ledger (5493)
  chore: Add unit tests dir to code coverage excludes (5803)
  chore: Build and test all configs for daily scheduled run (5801)
  chore: Limits CI build and test parallelism to reduce resource contention (5799)
  fix(amendment): Add missing fields for keylets to ledger objects (5646)
  Rename mutable flags (5797)
  Set version to 2.6.1-rc1
  Downgrade to boost 1.83
  Set version to 2.5.1
  Fix: Don't flag consensus as stalled prematurely (5658)
2025-09-18 13:17:29 -04:00
Ed Hennis 47eb1e8db2 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-18 12:26:50 -04:00
Ed Hennis 540b76fe42 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-18 12:26:46 -04:00
Ed Hennis d4135a9115 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-18 12:26:42 -04:00
Ed Hennis 4c6a34d3c1 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-09-18 12:26:38 -04:00
Ed Hennis be5390b8c7 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-refactoring-1
* XRPLF/develop:
  refactor: Modularise ledger (5493)
  chore: Add unit tests dir to code coverage excludes (5803)
  chore: Build and test all configs for daily scheduled run (5801)
  chore: Limits CI build and test parallelism to reduce resource contention (5799)
  fix(amendment): Add missing fields for keylets to ledger objects (5646)
  Rename mutable flags (5797)
  Set version to 2.5.1
  Fix: Don't flag consensus as stalled prematurely (5658)
2025-09-18 12:03:29 -04:00
Ed Hennis b5ddc812ea Review feedback from @gregtatcam:
- In progress
- Add explanatory comments, and start refactoring
  loanComputePaymentParts into functions for readability.
2025-09-17 19:37:59 -04:00
Ed Hennis b9a2eb3399 Review feedback from @gregtatcam
- Remove a redundant struct LoanPaymentParts declaration.
- Add pointers to the XLS-66 spec for all of the LendingHeler formulas.
- Changed if/else if in LoanManage.
2025-09-17 16:54:50 -04:00
Ed Hennis 2c0c4567f4 Rewrite LoanManage flag check to skip if field not set or 0 2025-09-17 15:41:03 -04:00
Ed Hennis d1c708ca01 Remove A BUNCH of unnecessary includes from the Loan/Lending files 2025-09-17 13:31:56 -04:00
Ed Hennis ae27376863 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-17 10:49:29 -04:00
Ed Hennis e237f968c7 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-17 10:49:20 -04:00
Ed Hennis b02b58e3b5 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-17 10:49:15 -04:00
Ed Hennis d10ac3b169 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-17 10:49:11 -04:00
Ed Hennis c37f863201 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-09-17 10:49:07 -04:00
Ed Hennis 1fbd3fcfa4 Handle Flags not present in LoanManage
- Addresses RIPD-3418
2025-09-16 19:31:01 -04:00
Ed Hennis 70755ed1d3 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-16 19:04:59 -04:00
Ed Hennis 6926c511cb Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-16 19:04:53 -04:00
Ed Hennis e01dd6d5b0 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-16 19:04:49 -04:00
Ed Hennis 0bfd404111 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-16 19:04:45 -04:00
Ed Hennis 5c072579d7 Review feedback from @yinyiqian1 and @Bronek
- Rewrite all of the templates in Units.h to use concepts.
- Restrict to_short_string to reasonably sized values.
- Rephrase some comments, and fix some typos.
2025-09-16 18:50:24 -04:00
Ed Hennis c118121337 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-16 17:19:54 -04:00
Ed Hennis 8a4c607af9 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-16 17:19:47 -04:00
Ed Hennis f85ce9c742 Remove or simplify Transactor::isEnabled overrides
- Remove the ones where the only condition was the required amendment
  specified in transactor.macro.
- Simplify the one that had other conditions, removing the required
  amendment from the conditions.
2025-09-16 17:15:57 -04:00
Ed Hennis 49e1f97476 Fix formatting 2025-09-16 17:10:28 -04:00
Ed Hennis 47aa95fcc4 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-16 15:07:11 -04:00
Ed Hennis dc5b0918da Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-16 15:07:04 -04:00
Ed Hennis cb991523f8 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-16 15:07:00 -04:00
Ed Hennis 9683344940 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-16 15:06:55 -04:00
Ed Hennis 5ac905be01 Fix issuer MPT balance sign on tests introduced by 5571 2025-09-16 14:51:18 -04:00
Ed Hennis 4d5afbea3a Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  Bugfix: Adds graceful peer disconnection (5669)
  Support DynamicMPT XLS-94d (5705)
  Only notify clio for PRs targeting the release and master branches (5794)
  refactor: Wrap GitHub CI conditionals in curly braces (5796)
2025-09-16 11:26:18 -04:00
Ed Hennis 9d08fe9a03 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-16 11:15:04 -04:00
Ed Hennis 84e97f0ef5 Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3
* XRPLF/ximinez/lending-refactoring-2:
  Bugfix: Adds graceful peer disconnection (5669)
  Support DynamicMPT XLS-94d (5705)
  Only notify clio for PRs targeting the release and master branches (5794)
  refactor: Wrap GitHub CI conditionals in curly braces (5796)
2025-09-16 11:14:06 -04:00
Ed Hennis 4d24b5eac2 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-16 10:46:45 -04:00
Ed Hennis 935749fe1f Merge branch 'develop' into ximinez/lending-refactoring-1 2025-09-16 10:46:41 -04:00
Ed Hennis 49eb1cc54e Remove LoanDraw transaction (#5792) 2025-09-16 00:30:02 +00:00
Ed Hennis dee972e9cd Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  Fix: EscrowTokenV1 (5571)
  fix: Skip processing transaction batch if the batch is empty (5670)
  ci: Fix conan secrets in `upload-conan-deps` (5785)
  Fix code coverage error (5765)
  docs: Add remote to `conan lock create` command (5770)
  refactor: clean up `CTID.h` (5681)
2025-09-15 11:25:13 -04:00
Ed Hennis bdeed43304 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-15 11:13:45 -04:00
Ed Hennis 7eff303160 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-15 11:13:40 -04:00
Ed Hennis aa1341f42e Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-15 11:13:36 -04:00
Ed Hennis 91031dbec4 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-09-15 11:13:32 -04:00
Ed Hennis 03da5da815 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-11 10:33:38 -04:00
Ed Hennis fc860c1a1d Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-11 10:33:30 -04:00
Ed Hennis 2545bc5936 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-11 10:33:26 -04:00
Ed Hennis 9a9588eaf6 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-11 10:33:22 -04:00
Ed Hennis abee3dd55c Merge branch 'develop' into ximinez/lending-refactoring-1 2025-09-11 10:33:17 -04:00
Ed Hennis 22d9ade2b0 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-10 20:36:49 -04:00
Ed Hennis 3ea0fa67cb Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-10 20:36:43 -04:00
Ed Hennis 1b295c7c00 Use Permission to check if a Transactor is enabled
- Adds a Permission::getTxFeature lookup function to find the
  controlling amendment for a Transactor, if any.
- Returns temDISABLED from preflight if there is an amendment and it's
  not enabled.
- Still need to go through and remove all the now-redundant isEnabled
  functions.
2025-09-10 20:33:01 -04:00
Ed Hennis dd52a50acc Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  fixup! Make a few tweaks to the changes in 43fe49e7
  Fix Vault tests broken by negative MPT issuer balance
  Make a few tweaks to the changes in 43fe49e7
  fix: Add restrictions to Permission Delegation: fixDelegateV1_1 (5650)
  ci: Add missing dependencies to workflows (5783)
  ci: Use default conan install format (5784)
  Switch CI pipeline bookworm:gcc-13 from arm64 to amd64 (5779)
2025-09-10 20:01:15 -04:00
Ed Hennis 80a2d8f789 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-10 19:39:58 -04:00
Ed Hennis e5f99e8ee7 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-10 19:39:55 -04:00
Ed Hennis b925ed44a0 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-10 19:39:51 -04:00
Ed Hennis 14ff03821c Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-refactoring-1
* XRPLF/develop:
  fix: Add restrictions to Permission Delegation: fixDelegateV1_1 (5650)
  ci: Add missing dependencies to workflows (5783)
  ci: Use default conan install format (5784)
  Switch CI pipeline bookworm:gcc-13 from arm64 to amd64 (5779)
2025-09-10 19:34:05 -04:00
Ed Hennis 5ff6bc496a fixup! Make a few tweaks to the changes in 43fe49e7 2025-09-10 18:20:47 -04:00
Ed Hennis 7aaad4a8ef Fix Vault tests broken by negative MPT issuer balance 2025-09-10 17:00:05 -04:00
Ed Hennis 1897ac5ee7 Make a few tweaks to the changes in 43fe49e7
- This was the first merge after I got back from sabbatical. I made
  these same changes in "ximinez/lending-XLS-66" commit 9d052dc, but
  after seeing some test failures, I think they belong here.
2025-09-10 16:49:13 -04:00
Ed Hennis 5894cd2103 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-09 17:14:41 -04:00
Ed Hennis 2f89182fd3 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-09 17:14:34 -04:00
Ed Hennis 4191ddd702 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-09 17:14:30 -04:00
Ed Hennis 37888b97ba Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-09 17:14:27 -04:00
Ed Hennis 10f7e4a02a Merge branch 'develop' into ximinez/lending-refactoring-1 2025-09-09 17:14:23 -04:00
Ed Hennis e5941d5b23 Simplify flag exclusion check in LoanManage 2025-09-09 16:29:35 -04:00
Ed Hennis 3f1a0f3a3d Fix some build errors from yesterday 2025-09-09 15:25:51 -04:00
Ed Hennis fa2c595a59 Add more value validation in LoanSet
- Don't allow negative numbers.
- Don't send the origination fee if it's defined, but 0.
2025-09-08 17:57:56 -04:00
Ed Hennis fc6fe9802e Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-08 11:42:09 -04:00
Ed Hennis b3c40ad408 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-08 11:42:01 -04:00
Ed Hennis 779e94bc03 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-08 11:41:57 -04:00
Ed Hennis f653ca03d9 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-08 11:41:53 -04:00
Ed Hennis d46e477092 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-09-08 11:41:50 -04:00
Ed Hennis 24f58a2e94 Fix test loop 2025-09-05 23:46:48 -04:00
Ed Hennis e3358ad124 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-05 20:29:04 -04:00
Ed Hennis 5102147709 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-05 20:28:58 -04:00
Ed Hennis 0a54c09de1 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-05 20:28:54 -04:00
Ed Hennis d594ca1401 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-05 20:28:50 -04:00
Ed Hennis 5335c04616 Fix constexpr expression 2025-09-05 20:28:12 -04:00
Ed Hennis 6db14ccb13 More review feedback from @gregtatcam
- Also fix a few build errors that I missed earlier.
- Updated freeze check rules for LoanSet.
- Fixed the debt total calculation and check in LoanSet.
- Removed _some_ unnecessary includes.
2025-09-05 20:00:10 -04:00
Ed Hennis 582f56487d Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-05 17:44:28 -04:00
Ed Hennis d47c039941 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-05 17:44:20 -04:00
Ed Hennis bf4ae57da4 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-05 17:44:17 -04:00
Ed Hennis 83702e79bf Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-05 17:44:13 -04:00
Ed Hennis 6379363bad Merge branch 'develop' into ximinez/lending-refactoring-1 2025-09-05 17:44:10 -04:00
Ed Hennis f0c96ccfe6 Review feedback from @yinyiqian1
- Rewrite to_short_string to call strHex directly instead of building
  the whole hex string first.
- Change PrettyAsset::scale_ back to a uint32 since the Number
  conversion elides any potential multiplication overflow.
- Clean ups.
2025-09-05 17:41:06 -04:00
Ed Hennis 4c726838d1 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-04 20:25:16 -04:00
Ed Hennis 47bcc67d56 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-04 20:25:09 -04:00
Ed Hennis 4b48f9612b Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-04 20:25:05 -04:00
Ed Hennis 7bba34832b Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-04 20:25:02 -04:00
Ed Hennis 76ae61feb5 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-09-04 20:24:58 -04:00
Ed Hennis adb260fe17 Review feedback from @dangell7 and @gregtatcam
- Mostly adding comments.
- Fixed some function parameter names that weren't updated after
  a copy/paste.
- LoanBrokerCoverWithdraw does not need to check for freeze when sending
  to the issuer.
- Reorder the fund transfer cases in LoanBrokerCoverWithdraw to make it
  clearer that some transfers are direct, and some use the payment
  engine.
- Only look up the vault ID once in LoanBrokerSet
2025-09-04 20:21:30 -04:00
Ed Hennis 8444a944af Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-04 16:44:10 -04:00
Ed Hennis 6d20e59212 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-04 16:44:03 -04:00
Ed Hennis c49cf6cec6 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-04 16:43:59 -04:00
Ed Hennis e24f034b17 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-04 16:43:56 -04:00
Ed Hennis 72733b57e4 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-09-04 16:43:52 -04:00
Ed Hennis 7a78449f6d Fix minor review feedback from @Bronek
- Clean up assert, log, and comments
2025-09-04 15:53:04 -04:00
Ed Hennis 0d500343ed New invariant privilege "mayDeleteMPT", used by VauleDeposit & Clawback 2025-09-04 14:08:54 -04:00
Ed Hennis f15e88c009 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-04 13:15:41 -04:00
Ed Hennis 0069c0c5fa Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-04 13:15:35 -04:00
Ed Hennis 450d38293d Remove unused variable 2025-09-04 13:14:19 -04:00
Ed Hennis 6d9001edac Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-04 12:37:00 -04:00
Ed Hennis 3dc14e7430 Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4
* XRPLF/ximinez/lending-refactoring-3:
  ci: Use cleanup-workspace action (5763)
  Add `Scale` to SingleAssetVault (5652)
2025-09-04 12:35:39 -04:00
Ed Hennis 9a8c2523ae Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3
* XRPLF/ximinez/lending-refactoring-2:
  ci: Use cleanup-workspace action (5763)
  Add `Scale` to SingleAssetVault (5652)
2025-09-04 12:29:41 -04:00
Ed Hennis 115161f7d2 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-04 12:27:02 -04:00
Ed Hennis 77fcb9b9b3 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-09-04 12:26:58 -04:00
Ed Hennis d2e8144c53 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-refactoring-1
* XRPLF/develop:
  Add `Scale` to SingleAssetVault (5652)
2025-09-04 12:25:49 -04:00
Ed Hennis 9d45fd1873 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-03 21:01:09 -04:00
Ed Hennis 4c43a0bca4 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-03 21:01:01 -04:00
Ed Hennis bb491431bd Review feedback from @dangell7 and @mvadari
- Rewrite isTesSuccess to use TERSubset::operator bool
- Add Transactor::preflightSigValidated for expensive operations that
  should be done after signature validation. These things would have
  been done after preflight2 before this refactor.
- Split Batch and EscrowFinish preflight to use preflightSigValidated, too.
2025-09-03 20:51:40 -04:00
Ed Hennis f090434c2d Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-03 19:21:10 -04:00
Ed Hennis 5f7b4a5e13 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-03 19:21:00 -04:00
Ed Hennis 11b8f9978e Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-03 19:20:57 -04:00
Ed Hennis 58551da698 Review feedback from @dangell7 and @Bronek
- Added more `STParsedJSON` test cases.
- Add a `safe_cast` conversion in `parseUint32`.
2025-09-03 19:18:24 -04:00
Ed Hennis 5ade8756f6 Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  Review feedback from @Bronek
  Rewrite Units.h and same safe_cast.h restrictions to use concepts
* Also add required support for LoanBroker and Loan ledger entries
2025-09-03 18:41:24 -04:00
Ed Hennis ac4c4905df Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-03 18:10:26 -04:00
Ed Hennis a5f1879afb Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-03 18:10:23 -04:00
Ed Hennis 8a77ac71ed Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-03 18:10:19 -04:00
Ed Hennis a72c237373 Review feedback from @Bronek
- Remove unnecessary #include
- Explanatory comments
- Make the MPT InvariantCheck related to EscrowFinish amendment safe
- Convert SField maps to unordered_maps
- Make jtx::fee::operator() clearer
- Rename checkMyPrivilege to hasPrivilege
2025-09-03 18:04:05 -04:00
Ed Hennis faae2514b9 Rewrite Units.h and same safe_cast.h restrictions to use concepts 2025-09-03 16:14:28 -04:00
Ed Hennis bcbbf71f29 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-09-03 14:04:25 -04:00
Ed Hennis 12994d4251 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-09-03 14:04:15 -04:00
Ed Hennis 22679673f6 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-09-03 14:04:12 -04:00
Ed Hennis a14cbf117c Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-09-03 14:04:08 -04:00
Ed Hennis 5ce07e769f Merge branch 'develop' into ximinez/lending-refactoring-1 2025-09-03 14:04:05 -04:00
Ed Hennis 08a5e8428a Require all SFields to have a name
- From @bronek https://github.com/XRPLF/rippled/pull/5590#discussion_r2290589770
- Adds consistency, and ensures sfInvalid and sfGeneric are handled the
  same as all other SFields
2025-08-29 20:11:28 -04:00
Ed Hennis a8e35f8399 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-08-29 17:58:21 -04:00
Ed Hennis 1f85888ae8 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-29 15:53:07 -04:00
Ed Hennis 183450db46 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-29 15:53:04 -04:00
Ed Hennis 64959b980f Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-29 15:53:00 -04:00
Ed Hennis 0b833e17ae Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-29 15:52:57 -04:00
Ed Hennis 2e3c5543f2 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-29 10:43:01 -04:00
Ed Hennis 0475398a17 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-29 10:42:57 -04:00
Ed Hennis bf483a2e94 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-29 10:42:54 -04:00
Ed Hennis 5e3539d6bb Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-08-28 18:17:49 -04:00
Ed Hennis a90265dd2a Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-28 18:17:40 -04:00
Ed Hennis 8498ed9df4 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-28 18:17:36 -04:00
Ed Hennis 4169b0a6b7 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-28 18:17:33 -04:00
Ed Hennis 024339c99b Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-28 18:17:29 -04:00
Ed Hennis 044697c438 fix: Allow the borrower to delete a completed / inactive loan, too. 2025-08-27 15:26:45 -04:00
Ed Hennis 2aebeeb966 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-08-27 11:15:23 -04:00
Ed Hennis 4c79fe46c9 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-27 11:15:14 -04:00
Ed Hennis 51abafea26 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-27 11:15:10 -04:00
Ed Hennis f1f1117b08 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-27 11:15:06 -04:00
Ed Hennis 5bb12ac1db Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-27 11:15:03 -04:00
Ed Hennis dbd9a05e10 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-08-25 19:51:13 -04:00
Ed Hennis 8981885196 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-25 19:51:04 -04:00
Ed Hennis a7390c070f Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-25 19:51:01 -04:00
Ed Hennis 4d0f895cda Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-25 19:50:57 -04:00
Ed Hennis 48162219ac Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-25 19:50:54 -04:00
Ed Hennis 1cb4a49ad3 Make a note about PR #5650 integration 2025-08-25 19:48:31 -04:00
Ed Hennis 18ffbbd42d Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-22 20:34:42 -04:00
Ed Hennis 7f1c0e55d6 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-08-22 19:31:36 -04:00
Ed Hennis 1876ffcd71 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-22 19:31:24 -04:00
Ed Hennis 3ef1e4269b Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-22 19:31:20 -04:00
Ed Hennis ae25e0f058 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-22 19:31:17 -04:00
Ed Hennis 7d634c1e62 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-22 19:31:14 -04:00
Ed Hennis f713208d89 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-08-22 19:09:07 -04:00
Ed Hennis 5d98799205 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-22 19:08:55 -04:00
Ed Hennis 9086c678a9 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-22 19:08:52 -04:00
Ed Hennis e434563e2e Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-22 19:08:48 -04:00
Ed Hennis 29300fc972 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-refactoring-1
* XRPLF/develop:
  chore: Update clang-format and prettier with pre-commit (5709)
  fix(test): handle null metadata for unvalidated tx in Env::meta (5715)
  chore: Workaround for CI build errors on arm64 (5717)
  chore: Fix file formatting (5718)
  fix: Skip notify-clio when running in a fork, reorder config fields (5712)
  chore: Reverts formatting changes to external files, adds formatting changes to proto files (5711)
2025-08-22 19:07:28 -04:00
Ed Hennis a88aceb846 Reformat transactions.macro 2025-08-22 16:02:59 -04:00
Ed Hennis 9986346d60 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-08-21 11:39:20 -04:00
Ed Hennis 7c642783f4 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-21 11:39:09 -04:00
Ed Hennis 0f1a2324bc Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-21 11:39:05 -04:00
Ed Hennis 2d570267de Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-21 11:39:01 -04:00
Ed Hennis 97849b6e70 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-21 11:38:57 -04:00
Ed Hennis 5d2a7d651e Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  Review feedback from @dangell7
2025-08-19 20:22:54 -04:00
Ed Hennis b26477a21e Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-19 20:11:45 -04:00
Ed Hennis 422a99bb26 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-19 20:11:42 -04:00
Ed Hennis b447fc54c8 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-19 20:11:38 -04:00
Ed Hennis 9920037d13 Review feedback from @dangell7
- Cleaned up some of the `LEDGER_ENTRY` macros by eliding unnecessary
  parameters.
- Define the transaction privileges in one place (InvariantCheck.cpp).
- Give `EscrowFinish` the `mayAuthorizeMPT` privilege.
- Rename the test helper `expectLine` to `expectHolding` since
  it handles both trust lines and MPTs.
- Restructure the ""pseudo-account has 2 pseudo-account fields set"
  invariant test to loop over all defined pseudo-account fields.
- Fix `operator<<` for `PrettyAmount` to handle `MPTIssue`s.
- Add enforcement of the `AccountRootsDeletedClean` invariant if SAV is
  enabled, and clarify the comment for the pseudo-account field check.
- Delete the 100% redundant `ttMPTOKEN_ISSUANCE_SET` check in
  `ValidMPTIssuance`.
2025-08-19 19:52:30 -04:00
Ed Hennis 714e12f44f Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-08-19 16:14:20 -04:00
Ed Hennis fba91487ec Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-19 16:05:34 -04:00
Ed Hennis 49ea5cc152 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-19 16:05:30 -04:00
Ed Hennis 87e0c25a34 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-19 16:05:26 -04:00
Ed Hennis a648249b90 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-19 16:05:21 -04:00
Ed Hennis 083ab7b054 Prevent Vault unrealized loss from exceeding "unavailable" assets 2025-08-19 15:15:10 -04:00
Ed Hennis b6ef337e45 Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  fix: Modify jobs to use '>>' instead of 'tee' for GITHUB_OUTPUT (5699)
  refactor: Revamp CI workflows (5661)
  refactor: Decouple net from xrpld and move rpc-related classes to the rpc folder (5477)
  Set version to 2.6.0-rc2
  docs: Updates list of maintainers and reviewers (5687)
  fix: Change log to debug level for AMM offer retrieval and IOU payment check (5686)
  fix: Add -Wno-deprecated-declarations for Clang only (5680)
  Update .git-blame-ignore-revs for 5657 (5675)
  Fix BUILD.md instruction (5676)
  Set version to 2.6.0-rc1
  fix: Improve logging of the reason to refuse a peer connection (5664)
  fix: Make test suite names match the directory name (5597)
  chore: Run prettier on all files (5657)
  chore: Set CONAN_REMOTE_URL also for forks (5662)
  chore: Cleanup bin/ directory (5660)
  perf: Optimize hash performance by avoiding allocating hash state object (5469)
2025-08-18 20:36:35 -04:00
Ed Hennis e62efa2a90 Fix unit tests broken by the addition of signature_target
- Specifically some of the tests for sign and submit
2025-08-18 20:32:53 -04:00
Ed Hennis 3867945b46 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-18 12:44:41 -04:00
Ed Hennis 084896189d Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-18 12:25:55 -04:00
Ed Hennis e9033898da Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-18 12:25:51 -04:00
Ed Hennis d952e9de1a Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-refactoring-1
* XRPLF/develop:
  fix: Modify jobs to use '>>' instead of 'tee' for GITHUB_OUTPUT (#5699)
  refactor: Revamp CI workflows (#5661)
  refactor: Decouple net from xrpld and move rpc-related classes to the rpc folder (#5477)
  Set version to 2.6.0-rc2
  docs: Updates list of maintainers and reviewers (#5687)
  fix: Change log to debug level for AMM offer retrieval and IOU payment check (#5686)
  fix: Add -Wno-deprecated-declarations for Clang only (#5680)
  Update .git-blame-ignore-revs for #5657 (#5675)
  Fix BUILD.md instruction (#5676)
  Set version to 2.6.0-rc1
  fix: Improve logging of the reason to refuse a peer connection (#5664)
  fix: Make test suite names match the directory name (#5597)
  chore: Run prettier on all files (#5657)
  chore: Set CONAN_REMOTE_URL also for forks (#5662)
  chore: Cleanup bin/ directory (#5660)
  perf: Optimize hash performance by avoiding allocating hash state object (#5469)
2025-08-18 12:22:43 -04:00
Ed Hennis bc3c9e1534 fixup! Add Counterparty signing support 2025-08-09 01:19:38 -04:00
Ed Hennis 6d137e44dc Add Counterparty signing support
- Add a new parameter "signature_target" to "sign". Supports single and
  multisign, but I haven't written tests for multisign yet.
- Skip account validation if this field is set, like multisigning.
- Unit tests demonstrating examples.
2025-08-09 00:55:00 -04:00
Ed Hennis 6778521f12 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-08-08 18:30:40 -04:00
Ed Hennis b0f3ef4969 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-08 18:23:37 -04:00
Ed Hennis c01ef3f155 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-08 18:23:34 -04:00
Ed Hennis 241f22bab7 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-08 18:23:30 -04:00
Ed Hennis a387a7aadf Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-08 18:23:27 -04:00
Ed Hennis d5e137de82 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-refactoring-1
* XRPLF/develop:
  Switch Conan 1 commands to Conan 2 and fix credentials (#5655)
  perf: Move mutex to the partition level (#5486)
  Upload Conan dependencies upon merge into develop (#5654)
2025-08-08 11:31:44 -04:00
Ed Hennis e0cdd65352 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-08-06 21:08:14 -04:00
Ed Hennis 2c1ccdbc84 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-06 21:03:14 -04:00
Ed Hennis 36204d2ce9 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-06 21:03:10 -04:00
Ed Hennis 4639c1c351 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-06 21:03:07 -04:00
Ed Hennis c863bd25e0 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-06 21:03:04 -04:00
Ed Hennis 7dcc682e29 Make LoanSet.CounterpartySignature optional in the Tx layout
- Still required for the transaction to succeed (except inside a Batch,
  because the batch signers take care of that).
- Started adding tests for Loan-related RPC and low-level math checks.
  Currently only implemented "sign" on a LoanSet to verify it can be
  done.
2025-08-06 20:48:13 -04:00
Ed Hennis 162875616d Correct more issues related to 0-interest loans and some rounding issues
- Addresses FIND-012 from audit.
- If computePeriodicPaymentParts rounds the principal part to 0, add
  a small amount so that some principal is paid regardless of how
  extreme the loan parameters are. For XRP and MPTs, this just adds 1.
  For IOUs, compute an epsilon based on the scale of the original loan.
  (IOUs untested.)
  - Also move this function out of the detail namespace so direct unit
    tests can be written. (Pending.)
- Adds the testLoanPayComputePeriodicPaymentValidRateInvariant from
  auditors with some minor modifications.
- Fixes an assert that the periodic rate > 0, which won't be true if the
  loan interest rate is 0.
2025-08-06 11:47:42 -04:00
Ed Hennis 9847025099 Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  Fix formatting
  fix: Ensures canonical order for `PriceDataSeries` upon `PriceOracle` creation (#5485)
  Add code coverage for STParsedJSON edge cases
  refactor: Decouple ledger from xrpld/app (#5492)
2025-08-05 21:30:31 -04:00
Ed Hennis bb183ea09a Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-05 21:16:57 -04:00
Ed Hennis 9fd45e7aa7 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-05 21:16:54 -04:00
Ed Hennis 924b05ea9f Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-05 21:16:50 -04:00
Ed Hennis a0a0916108 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-05 21:16:47 -04:00
Ed Hennis c78b9aedac Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-08-05 21:16:34 -04:00
Ed Hennis b07087f10b Fix formatting 2025-08-05 13:20:29 -04:00
Ed HennisandDenis Angell f89c88dbb8 Add code coverage for STParsedJSON edge cases
- Move several tests from `STObject_test` to new `STParsedJSON_test`
  unchanged.
- Add 3 test cases to cover edge cases for UInt16 values.

Co-authored-by: Denis Angell <[email protected]>
2025-08-05 12:45:05 -04:00
Ed Hennis 0c76153b2c Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-08-04 13:05:06 -04:00
Ed Hennis 70a23bcca3 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-08-04 13:05:02 -04:00
Ed Hennis a66ef800bc Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-08-04 13:04:59 -04:00
Ed Hennis 5f5f60a024 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-08-04 13:04:55 -04:00
Ed Hennis d58399d417 Correct handling of LoanBroker.DebtMaximum zero values
- Addresses FIND-003 from audit.
- Behavior changed to treat a DebtMaximum value of 0 to mean "no limit",
  as defined in spec.
- No need to add unit tests, because the previous commit covers 0, and
  tests already exist for non-zero limits.
2025-07-30 18:08:05 -04:00
Ed Hennis a1e9091f1e Add the "testBatchBypassCounterparty" test from auditors
- (With some minor modifications)
2025-07-30 17:25:58 -04:00
Ed Hennis 84acebeb7f Enforce valid range of LoanSet.InterestRate
- Addresses FIND-002 from audit.
- Enforces a range of 0-100% in 1/10 bips.
- Also add a couple of unit test checks.
2025-07-30 13:56:50 -04:00
Ed Hennis 83ba11d505 Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  fix `DeliveredAmount` and `delivered_amount` in transaction metadata for direct MPT transfer (#5569)
2025-07-30 11:07:03 -04:00
Ed Hennis 97469a1c02 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-07-29 20:33:43 -04:00
Ed Hennis c74dc6baf2 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-07-29 20:33:40 -04:00
Ed Hennis 1d90ae7c25 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-07-29 20:33:36 -04:00
Ed Hennis 39db7381b2 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-07-29 20:33:32 -04:00
Ed Hennis ec8eaf0d73 fixup! Ensure Counterparty Signatures are properly handled by Batch tx 2025-07-29 20:31:31 -04:00
Ed Hennis 9d1a23a811 Ensure Counterparty Signatures are properly handled by Batch tx
- Addresses FIND-001 from audit
- LoanSet::preflight will require Counterparty to be set (it's normally
  optional) for an inner batch transaction, because the checks are done
  before the LoanBroker object can be accessed.
- Adjust LoanSet::calculateBaseFee to not charge extra if an inner
  transaction.
- Adds a Loan-specific test to Batch_test.
2025-07-29 19:46:01 -04:00
Ed Hennis 421cbb9abd Add the "testBatchBypassCounterparty" test from auditors
- (With some minor modifications)
2025-07-29 19:34:02 -04:00
Ed Hennis bba9119a75 Fix a couple of errors
- Mark variable only used in asserts as [[maybe_unused]].
- Restore commented out line that I think I resolved wrong in an earlier
  merge to fix Vault tests.
2025-07-29 14:58:36 -04:00
Ed Hennis 6f00f296c8 Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-07-29 12:31:48 -04:00
Ed Hennis aee4719df4 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-07-29 11:54:30 -04:00
Ed Hennis 5f2402479d Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-07-29 11:54:26 -04:00
Ed Hennis c3fa4fb71c Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-07-29 11:54:23 -04:00
Ed Hennis a8d0d763b0 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-07-29 11:54:19 -04:00
Ed Hennis 6310b2544c Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  Build options cleanup (#5581)
  Updates Conan dependencies: Boost 1.86 (#5264)
  VaultWithdraw destination account bugfix (#5572)
2025-07-28 21:10:12 -04:00
Ed Hennis 3c0ef8f8d3 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-07-28 20:57:29 -04:00
Ed Hennis ebe39e9320 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-07-28 20:57:24 -04:00
Ed Hennis 1b8da176bf Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-07-28 20:57:21 -04:00
Ed Hennis 66dd0de019 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-07-28 20:57:17 -04:00
Ed Hennis a2be55fbc9 Check LoanBrokerCoverWithdraw Destination and DestinationTag fields
- See also #5572 / e7a7bb8
2025-07-28 19:04:55 -04:00
Ed Hennis 41c24094eb Merge branch 'ximinez/lending-refactoring-4' into ximinez/lending-XLS-66 2025-07-24 17:00:57 -04:00
Ed Hennis 3b446f144f Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-07-24 16:13:47 -04:00
Ed Hennis 760a9aad83 Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3
* XRPLF/ximinez/lending-refactoring-2:
  refactor: Update rocksdb (#5568)
  Switch instrumentation workflow to use dependencies (#5607)
  chore: Rename conan profile to `default` (#5599)
  Include `network_id` in validations and subscription stream responses (#5579)
  Add support for `DomainID` in `MPTokenIssuance` transactions (#5509)
  chore: Remove unused code after flow cross retirement (#5575)
  Remove obsolete owner pays fee feature and XRPL_ABANDON stanza (#5550)
  refactor: Makes HashRouter flags more type-safe (#5371)
  Fix clang-format CI job (#5598)
2025-07-24 16:12:48 -04:00
Ed Hennis c73372297c Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-07-24 15:50:17 -04:00
Ed Hennis 1f331bf8d8 Merge branch 'develop' into ximinez/lending-refactoring-1 2025-07-24 15:50:13 -04:00
Ed Hennis 57c78c56ba fixup! fixup! Fix divide by 0 error on 0-interest loans; allow broker to self-lend 2025-07-24 15:08:18 -04:00
Ed Hennis 1adb410e8e fixup! Fix divide by 0 error on 0-interest loans; allow broker to self-lend 2025-07-24 13:58:05 -04:00
Ed Hennis 6444ad4393 Merge remote-tracking branch 'XRPLF/ximinez/lending-refactoring-4' into ximinez/lending-XLS-66
* XRPLF/ximinez/lending-refactoring-4:
  fixup! Rename Transactor preflight functions
  Rename Transactor preflight functions
  fixup! Make preflight1 and preflight2 private static Transactor functions
  Make preflight1 and preflight2 private static Transactor functions
  Fix formatting
2025-07-23 17:45:51 -04:00
Ed Hennis 1e7462606c Fix divide by 0 error on 0-interest loans; allow broker to self-lend
* Unit tests for self lending
2025-07-23 17:24:53 -04:00
Ed Hennis 68aecfe8a2 Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4 2025-07-22 14:09:47 -04:00
Ed Hennis 8ffe1838d4 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3 2025-07-22 14:09:43 -04:00
Ed Hennis 30becab0b6 Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2 2025-07-22 14:09:40 -04:00
Ed Hennis e037ae3d3a Merge branch 'develop' into ximinez/lending-refactoring-1 2025-07-22 14:09:37 -04:00
Ed Hennis 2f3c79837e Merge branch 'develop' into ximinez/lending-XLS-66 2025-07-22 14:09:23 -04:00
Ed Hennis 4550d2bfba Fix build error 2025-07-22 14:06:17 -04:00
Ed HennisandBronek Kozicki 5edaad41af Merge branch 'ximinez/lending-refactoring-3' into ximinez/lending-refactoring-4
* ximinez/lending-refactoring-3: (61 commits)
  fixup! Rename Transactor preflight functions
  Rename Transactor preflight functions
  fixup! Make preflight1 and preflight2 private static Transactor functions
  Make preflight1 and preflight2 private static Transactor functions
  Fix formatting
  Remove `include(default)` from libxrpl profile (#5587)
  refactor: Change boost::shared_mutex to std::shared_mutex (#5576)
  Fix macos runner (#5585)
  Remove the type filter from "ledger" RPC command (#4934)
  refactor: Update date, libarchive, nudb, openssl, sqlite3, xxhash packages (#5567)
  test: Run unit tests regardless of 'Supported' amendment status (#5537)
  Retire Flow Cross amendment (#5562)
  chore: Update CI to use Conan 2 (#5556)
  fixAMMClawbackRounding: adjust last holder's LPToken balance (#5513)
  chore: Add gcc-12 workaround (#5554)
  Add MPT related txns into issuer's account history  (#5530)
  chore: Remove unused headers (#5526)
  fix: add allowTrustLineLocking flag for account_info (#5525)
  Downgrade required CMake version for Antithesis SDK (#5548)
  fix: Link with boost libraries explicitly (#5546)
  ...

Co-authored-by: Bronek Kozicki <[email protected]>
2025-07-22 13:34:43 -04:00
Ed Hennis 24447969be fixup! Rename Transactor preflight functions 2025-07-22 12:43:54 -04:00
Ed Hennis c76e485d01 Rename Transactor preflight functions
- Rename Transactor::preflight to invokePreflight.
- Rename doPreflight back to preflight.
- Update instructions.
- With preflight1 & 2 now uncallable, in-flight code in other
  branches should be easier to convert.
2025-07-22 12:11:00 -04:00
Ed Hennis 021bee8dd4 fixup! Make preflight1 and preflight2 private static Transactor functions 2025-07-22 12:10:58 -04:00
Ed Hennis 6c5945f9e8 Make preflight1 and preflight2 private static Transactor functions
- They should never be called by derived classes.
2025-07-21 23:26:29 -04:00
Ed Hennis 96ad348fb0 Merge branch 'ximinez/lending-refactoring-2' into ximinez/lending-refactoring-3
* ximinez/lending-refactoring-2: (57 commits)
  Fix formatting
  Remove `include(default)` from libxrpl profile (#5587)
  refactor: Change boost::shared_mutex to std::shared_mutex (#5576)
  Fix macos runner (#5585)
  Remove the type filter from "ledger" RPC command (#4934)
  refactor: Update date, libarchive, nudb, openssl, sqlite3, xxhash packages (#5567)
  test: Run unit tests regardless of 'Supported' amendment status (#5537)
  Retire Flow Cross amendment (#5562)
  chore: Update CI to use Conan 2 (#5556)
  fixAMMClawbackRounding: adjust last holder's LPToken balance (#5513)
  chore: Add gcc-12 workaround (#5554)
  Add MPT related txns into issuer's account history  (#5530)
  chore: Remove unused headers (#5526)
  fix: add allowTrustLineLocking flag for account_info (#5525)
  Downgrade required CMake version for Antithesis SDK (#5548)
  fix: Link with boost libraries explicitly (#5546)
  chore: Fix compilation error with clang-20 and cleanup (#5543)
  test: Remove circular jtx.h dependencies (#5544)
  Decouple CredentialHelpers from xrpld/app/tx (#5487)
  fix: crash when trace-logging in tests (#5529)
  ...
2025-07-21 23:26:23 -04:00
Ed Hennis 72f33d8f3b Merge branch 'ximinez/lending-refactoring-1' into ximinez/lending-refactoring-2
* ximinez/lending-refactoring-1: (57 commits)
  Fix formatting
  Remove `include(default)` from libxrpl profile (#5587)
  refactor: Change boost::shared_mutex to std::shared_mutex (#5576)
  Fix macos runner (#5585)
  Remove the type filter from "ledger" RPC command (#4934)
  refactor: Update date, libarchive, nudb, openssl, sqlite3, xxhash packages (#5567)
  test: Run unit tests regardless of 'Supported' amendment status (#5537)
  Retire Flow Cross amendment (#5562)
  chore: Update CI to use Conan 2 (#5556)
  fixAMMClawbackRounding: adjust last holder's LPToken balance (#5513)
  chore: Add gcc-12 workaround (#5554)
  Add MPT related txns into issuer's account history  (#5530)
  chore: Remove unused headers (#5526)
  fix: add allowTrustLineLocking flag for account_info (#5525)
  Downgrade required CMake version for Antithesis SDK (#5548)
  fix: Link with boost libraries explicitly (#5546)
  chore: Fix compilation error with clang-20 and cleanup (#5543)
  test: Remove circular jtx.h dependencies (#5544)
  Decouple CredentialHelpers from xrpld/app/tx (#5487)
  fix: crash when trace-logging in tests (#5529)
  ...
2025-07-21 20:48:44 -04:00
Ed Hennis 96f2a65f64 Fix formatting 2025-07-21 20:37:16 -04:00
Ed Hennis 43fe49e756 Merge remote-tracking branch 'upstream/develop' into ximinez/lending-refactoring-1
* upstream/develop: (56 commits)
  Remove `include(default)` from libxrpl profile (#5587)
  refactor: Change boost::shared_mutex to std::shared_mutex (#5576)
  Fix macos runner (#5585)
  Remove the type filter from "ledger" RPC command (#4934)
  refactor: Update date, libarchive, nudb, openssl, sqlite3, xxhash packages (#5567)
  test: Run unit tests regardless of 'Supported' amendment status (#5537)
  Retire Flow Cross amendment (#5562)
  chore: Update CI to use Conan 2 (#5556)
  fixAMMClawbackRounding: adjust last holder's LPToken balance (#5513)
  chore: Add gcc-12 workaround (#5554)
  Add MPT related txns into issuer's account history  (#5530)
  chore: Remove unused headers (#5526)
  fix: add allowTrustLineLocking flag for account_info (#5525)
  Downgrade required CMake version for Antithesis SDK (#5548)
  fix: Link with boost libraries explicitly (#5546)
  chore: Fix compilation error with clang-20 and cleanup (#5543)
  test: Remove circular jtx.h dependencies (#5544)
  Decouple CredentialHelpers from xrpld/app/tx (#5487)
  fix: crash when trace-logging in tests (#5529)
  test: switch some unit tests to doctest (#5383)
  ...
2025-07-21 19:53:20 -04:00
Ed Hennis e14ca4c438 Merge branch 'develop' into ximinez/lending-XLS-66 2025-07-21 18:20:32 -04:00
Ed Hennis ed00018adc fixup! Add pseudo account type to injected data in RPC account_info result 2025-07-18 20:42:00 -04:00
Ed Hennis f7aef49509 Add pseudo account type to injected data in RPC account_info result
- Low hanging fruit
2025-07-18 20:32:07 -04:00
Ed Hennis e1edd43205 fixup! Fix LoanBrokerCoverClawback tests? 2025-07-18 19:19:08 -04:00
Ed Hennis 03d253ae20 Fix LoanBrokerCoverClawback tests? 2025-07-18 18:58:58 -04:00
Ed Hennis 577b047cc1 Merge branch 'develop' into ximinez/lending-XLS-66 2025-07-18 18:33:00 -04:00
Ed Hennis c46d894192 Implement LoanBrokerCoverClawback and many test cases
- Not all tests are passing yet
2025-07-17 20:41:55 -04:00
Ed Hennis e4480569f7 TEMP: Change VaultWithdraw back so tests pass 2025-07-16 18:58:25 -04:00
Ed Hennis e6e6360c1a Support MPT payments via Payment tx for LBCoverWithdraw.Destination 2025-07-16 18:31:10 -04:00
Ed Hennis 6b4236343a Get CoverWithdraw IOU payments working
- Clean up some of the payment parameters
- Also factor out Payment::makeMPTDirectPayment for future use
2025-07-16 17:41:23 -04:00
Ed Hennis 45abab3a78 Add missing headers 2025-07-16 15:29:59 -04:00
Ed Hennis 4d7492a00d Use new "testable_amendments" function in new tests 2025-07-16 14:03:50 -04:00
Ed Hennis b83af2d54a Fix formatting 2025-07-16 13:49:23 -04:00
Ed Hennis 10b73525be fixup! Start implementing LoanBrokerCoverWithdraw.Destination field 2025-07-16 12:55:23 -04:00
Ed Hennis 89dbb48860 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66
* XRPLF/develop:
  test: Run unit tests regardless of 'Supported' amendment status (#5537)
  Retire Flow Cross amendment (#5562)
2025-07-16 12:53:36 -04:00
Ed Hennis 16854fff1a Merge branch 'develop' into ximinez/lending-XLS-66 2025-07-15 19:36:14 -04:00
Ed Hennis b34f59eafc Start implementing LoanBrokerCoverWithdraw.Destination field
- Refactor the IOU payment code out of the Payment transactor and use it
  for different destinations. This should enforce all of the trust line
  rules without having to reinvent a dozen wheels.
- TODO: Same for MPTs.
2025-07-15 19:30:58 -04:00
Ed Hennis 38cb371c72 Make requireAuth work more similarly between IOUs and MPTs
- Unfortunately, to not change behavior, a new "Legacy" authorization
  type was created. It acts like "StrongAuth" for MPTs and "WeakAuth"
  for IOUs.
2025-07-15 17:48:14 -04:00
Ed Hennis 5199c5e073 Improve / add freeze checking helper functions
- Add an override of isDeepFrozen that can take an Asset, and thus an
  MPTIssue. The MPT version just calls isFrozen, since they're
  equivalent for MPTs.
- Add wrappers checkFrozen and checkDeepFrozen that return the
  appropriate TER code, so the Asset type doesn't have to be checked
  at every #*%@ing caller.
- Convert the Loan* transactors to use these functions.
2025-07-15 15:32:21 -04:00
Ed Hennis 3ef72d28bd fixup! Make a few tweaks to the changes in 907cc19a 2025-07-15 14:20:44 -04:00
Ed Hennis 06906a8bd3 Refactor the bulk of testLifecycle into a function
- Allows tweaking loan properties
2025-07-14 18:54:48 -04:00
Ed Hennis 0a3c728a96 Merge remote-tracking branch 'XRPLF/develop' into ximinez/lending-XLS-66
* XRPLF/develop:
  fixAMMClawbackRounding: adjust last holder's LPToken balance (#5513)
  chore: Add gcc-12 workaround (#5554)
  Add MPT related txns into issuer's account history  (#5530)
  chore: Remove unused headers (#5526)
2025-07-11 19:21:03 -04:00
Ed Hennis 37745cb5b2 Rename Transactor preflight functions
- Rename Transactor::preflight to invokePreflight.
- Rename doPreflight back to preflight.
- Update instructions.
- With preflight1 & 2 now uncallable, in-flight code in other
  branches should be easier to convert.
2025-07-11 19:17:11 -04:00
Ed Hennis 269eac9a15 Mark the LendingProtocol amendment as unsupported 2025-07-11 18:55:08 -04:00
Ed Hennis d82461ea70 Make preflight1 and preflight2 private static Transactor functions
- They should never be called by derived classes.
2025-07-11 18:42:39 -04:00
Ed Hennis e9d2dfe329 fixup! Make a few tweaks to the changes in merge cc83ea8 2025-07-11 17:14:53 -04:00
Ed Hennis 9d052dc86c Make a few tweaks to the changes in 907cc19a 2025-07-11 17:06:26 -04:00
Ed Hennis ad5d28bc48 Make a few tweaks to the changes in merge cc83ea8
- Not necessarily wrong, just not how I want it.
- Fixes a build error in LoanBroker_test
- In CreateOffer, fixes a logic error where _no_ offers will work if
  PermissionedDex amendment is enabled, and FlowCross is not
2025-07-11 16:14:10 -04:00
Ed Hennis 2b5a8ebf2f Merge branch 'develop' into ximinez/lending-XLS-66 2025-07-10 12:30:06 -04:00
Ed Hennis 28999a242c Merge branch 'develop' into ximinez/lending-XLS-66 2025-07-03 15:51:14 -04:00
Ed Hennis 186821f971 Merge branch 'develop' into ximinez/lending-XLS-66 2025-07-02 19:04:56 -04:00
Bronek Kozicki 1c8e4c346b Merge branch 'develop' into ximinez/lending-XLS-66 2025-06-27 10:45:37 +01:00
Bronek Kozicki 6bcd037264 Merge branch 'develop' into ximinez/lending-XLS-66 2025-06-23 10:33:05 +01:00
Bronek Kozicki 439e2d4934 Merge branch 'release' into ximinez/lending-XLS-66 2025-06-23 10:04:20 +01:00
Bronek Kozicki f812774637 Merge branch 'develop' into ximinez/lending-XLS-66 2025-06-10 23:03:43 +08:00
Bronek Kozicki d82693c2d3 Merge branch 'develop' into ximinez/lending-XLS-66 2025-06-10 21:51:58 +08:00
Bronek Kozicki 907cc19a41 Merge branch 'develop' into ximinez/lending-XLS-66 2025-06-05 13:04:15 +01:00
Bronek Kozicki cc83ea8eb6 Merge branch 'develop' into ximinez/lending-XLS-66 2025-06-04 19:35:45 +01:00
Ed Hennis 527e0c916f Lending protocol implementation (XLS-0066)
- Add the LendingProtocol amendment
- Add Loan Broker and Loan ledger objects:
- Also add new SFields, Keylet functions, and an Invariant to verify no
  illegal field modification
- Update list of "constant" fields from spec
- Also add a general check for all object types for the type and index
  fields
- refactor: Check transaction flags in preflight0
- Adds a flagMask parameter to preflight1 so that it's impossible to
  forget to check flags.
- Also adds a short hash prefix to all Transactor log messages.
- refactor: Generalize Transactor preflight:
- Derived classes no longer need to explicitly check amendments, nor
  call into preflight1 or preflight2.
- implemeng LoanBrokerSet
- Transactions: LoanDelete, LoanManage, LoanDraw, LoanPay
- LoanBrokerSet creation mostly done. Need update.
- Also added a lookup table for pseudo account fields.
- Update changed field name.
- Modify modifiable fields in an update. Note there are only two.
- Add a node field to dirLink, defaulting sfOwnerNode, so other
  relationships can be updated.
- Create some helper classes for transaction fields
- Test that they work by converting some of the existing classes
- Finish creating helper classes for JTx fields
- Also change the pseudo account field lookup to a function that uses
  a switch
- Update tests, update pseudo-account checking
- Generalize some of the Invariant checks using macro files
  - Valid ledger entry type
  - Valid new account root and pseudo account check
- Enumerate transaction privileges for invariants
  - Allows them to be defined in transactions.macro instead of needing to
    scrutinize every existing Invariant class.
  - List is not necessarily comprehensive, but does cover every check
    where more than one transaction type is involved.
- Reserve a few values between Vault and Lending for future use
- Pseudo-account improvements
  - Define pseudo-account fields with an sfield flag
  - Pseudo-account invariant checks rules whenever a pseudo-account is
    created or modified.
- Move some helper functions.
- Check the regular key in the pseudo-transaction invariant check.
- Transactor::checkSign will always fail for a pseudo-account, so even
  if someone figures out how to get a good signature, it won't work.
- Fix account creation to check both amendments
- Add a validity range for sfDebtMaximum
- Change more "failed" messages. The goal here is to be able to search
  the log for "failed" and ONLY get test failures.
- NoModifiedUnmodifiableFields and ValidPseudoAccounts
- Move the Invariants_test class into the test namespace
- Clang wants an explicit ctor to emplace in a vector
- Refactor: Add a Transactor base function to make it easier to get the
  owner reserve increment as a fee.
- Refactor: Add an overload jtx::fee(increment) to pay an owner reserve.
- Initial implementation of LoanBrokerDelete
- Generalize the LoanBroker lifecycle test
- Refactor ApplyView::dirAdd to give access to low-level operations
  - Takes a page from #5362, which may turn out to be useful!
- Start writing Loan Broker invariants and tests
  - Specifically those mentioned for LoanBrokerDelete
- Move all detail namespaces to be under ripple
  - Avoids problems with namespace collisions / ambiguous symbol issues
    with unity builds, especially when adding or removing files.
- Add LoanBrokerCoverDeposit transaction
- Add LoanBrokerCoverWithdraw transaction
- Start writing tests for LoanBrokerCover*
- Add support for `Asset` and `MPTIssue` to some `jtx` helper classes
  and functions (`balance`, `expectLine`)
- Add support for pseudo-accounts to `jtx::Account` by allowing directly
  setting the AccountID without a matching key.
- Add Asset and MPTIssue support to more jtx objects / functions
  - Unfortunately, to work around some ambiguous symbol compilation
    errors, I had to change the implicit conversion from IOU to Asset to
    a conversion from IOU to PrettyAsset, and add a more explicit
    `asset()` function. This workaround only required changing two
    existing tests, so seems acceptable.
- Ensure that an account is not deleted with an XRP balance
  - Updates the AccountRootsDeletedClean invariant
- Finish up the Loan Broker tests
- Move inclusion of Transactor headers to transactions.macro
  - Only need to update in one place when adding a new transaction.
- Start implementing LoanSet transactor
  - Add some more values and functions to make it easier to work with
    basis point values / bips.
  - Fix several earlier mistakes.
- Generalize the check*Sign functions to support CounterParty
  - checkSign, checkSingleSign, and checkMultiSign in STTx and Transactor
- Start writing Loan tests
  - Required adding support for counterparty signature to jtx framework:
    arbitrary signature field destination, multiple signer callbacks
- Get Counterparty signing working
- Add more LoanSet unit tests, added LoanBroker LoanSequence field
  - LoanSequence will prevent loan key collisions
- Change Loan object indexing, fix several broken LoanSet unit tests
  - Loan objects will now only be indexed by LoanBrokerID and
    LoanSequence, which is a new field in LoanBroker. Also changes
    Loan.Sequence to Loan.LoanSequence to match up.
  - Several tests weren't working because of `PrettyAsset` scaling. Also,
    `PrettyAsset` calculations could overflow. Made that less likely by
    changing the type of `scale_`.
  - LoanSet will fail if an account tries to loan to itself.
- Ensure that an account is not deleted with a non-zero owner count
  - Updates the AccountRootsDeletedClean invariant
- Add unit tests to create a Loan successfully
  - Fix a few field initializations in LoanSet
- Refactor issuance validity check in VaultCreate
  - Utility function: canAddHolding
  - Call canAddHolding from any transactor that call addEmptyHolding
    (LoanBrokerSet, LoanSet)
- Start implementing LoanManage transaction
  - Also add a ValidLoan invariant
- Finish `LoanManage` functionality and tests, modulo LoanDraw/Pay
- Allow existing trust lines to loan brokers to be managed (by issuer)
- Implement LoanDelete, and fix a bunch of math errors in LoanManage
- Update to match latest spec: compute interest, LoanBroker reserves
- refactor: Define getFlagsMask in the base Transactor class
  - Returns tfUniversalMask for most transactors
  - Only transactors that use other flags need to override
- Implement LoanDraw, and made good progress on related tests
- Start implementing LoanPay transaction
- Implement LoanPay & most tests
- Also add an XRPL_ASSERT_PARTS, which splits the parts of the assert message
    so I don't have to remember the proper formatting.
Start writing LoanPay transaction tests
2025-05-21 11:39:55 +01:00
Ed Hennis fb5d94bbef Refactor 4: Transactor extra signing support 2025-05-21 11:39:37 +01:00
Ed Hennis 4fe3ec8a08 Refactor 3: Transactors 2025-05-21 11:39:35 +01:00
Ed Hennis 937b67cbc0 Refactor 2: STParsed Json 2025-05-21 11:39:34 +01:00
Ed Hennis 4e50087612 Refactoring 1 2025-05-21 11:39:26 +01:00
83 changed files with 18113 additions and 709 deletions

No files matched your search

+15 -17
View File
@@ -13,6 +13,15 @@ class Number;
std::string
to_string(Number const& amount);
template <typename T>
constexpr bool
isPowerOfTen(T value)
{
while (value >= 10 && value % 10 == 0)
value /= 10;
return value == 1;
}
class Number
{
using rep = std::int64_t;
@@ -22,7 +31,9 @@ class Number
public:
// The range for the mantissa when normalized
constexpr static std::int64_t minMantissa = 1'000'000'000'000'000LL;
constexpr static std::int64_t maxMantissa = 9'999'999'999'999'999LL;
static_assert(isPowerOfTen(minMantissa));
constexpr static std::int64_t maxMantissa = minMantissa * 10 - 1;
static_assert(maxMantissa == 9'999'999'999'999'999LL);
// The range for the exponent when normalized
constexpr static int minExponent = -32768;
@@ -132,22 +143,7 @@ public:
}
Number
truncate() const noexcept
{
if (exponent_ >= 0 || mantissa_ == 0)
return *this;
Number ret = *this;
while (ret.exponent_ < 0 && ret.mantissa_ != 0)
{
ret.exponent_ += 1;
ret.mantissa_ /= rep(10);
}
// We are guaranteed that normalize() will never throw an exception
// because exponent is either negative or zero at this point.
ret.normalize();
return ret;
}
truncate() const noexcept;
friend constexpr bool
operator>(Number const& x, Number const& y) noexcept
@@ -192,6 +188,8 @@ private:
class Guard;
};
constexpr static Number numZero{};
inline constexpr Number::Number(rep mantissa, int exponent, unchecked) noexcept
: mantissa_{mantissa}, exponent_{exponent}
{
+1 -1
View File
@@ -13,7 +13,7 @@
// The duplication is because Visual Studio 2019 cannot compile that header
// even with the option -Zc:__cplusplus added.
#define ALWAYS(cond, message, ...) assert((message) && (cond))
#define ALWAYS_OR_UNREACHABLE(cond, message, ...) assert((message) && (cond))
#define ALWAYS_OR_UNREACHABLE(cond, message) assert((message) && (cond))
#define SOMETIMES(cond, message, ...)
#define REACHABLE(message, ...)
#define UNREACHABLE(message, ...) assert((message) && false)
+3
View File
@@ -381,6 +381,9 @@ public:
/// Return true if the object has a member named key.
bool
isMember(std::string const& key) const;
/// Return true if the object has a member named key.
bool
isMember(StaticString const& key) const;
/// \brief Return a list of the member names.
///
+39
View File
@@ -368,6 +368,45 @@ public:
emptyDirDelete(Keylet const& directory);
};
namespace directory {
/** Helper functions for managing low-level directory operations.
These are not part of the ApplyView interface.
Don't use them unless you really, really know what you're doing.
Instead use dirAdd, dirInsert, etc.
*/
std::uint64_t
createRoot(
ApplyView& view,
Keylet const& directory,
uint256 const& key,
std::function<void(std::shared_ptr<SLE> const&)> const& describe);
auto
findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start);
std::uint64_t
insertKey(
ApplyView& view,
SLE::ref node,
std::uint64_t page,
bool preserveOrder,
STVector256& indexes,
uint256 const& key);
std::optional<std::uint64_t>
insertPage(
ApplyView& view,
std::uint64_t page,
SLE::pointer node,
std::uint64_t nextPage,
SLE::ref next,
uint256 const& key,
Keylet const& directory,
std::function<void(std::shared_ptr<SLE> const&)> const& describe);
} // namespace directory
} // namespace ripple
#endif
+213 -6
View File
@@ -223,6 +223,80 @@ isDeepFrozen(
Currency const& currency,
AccountID const& issuer);
[[nodiscard]] inline bool
isDeepFrozen(
ReadView const& view,
AccountID const& account,
Issue const& issue,
int = 0 /*ignored*/)
{
return isDeepFrozen(view, account, issue.currency, issue.account);
}
[[nodiscard]] inline bool
isDeepFrozen(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptIssue,
int depth = 0)
{
// Unlike IOUs, frozen / locked MPTs are not allowed to send or receive
// funds, so checking "deep frozen" is the same as checking "frozen".
return isFrozen(view, account, mptIssue, depth);
}
/**
* isFrozen check is recursive for MPT shares in a vault, descending to
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
* purely defensive, as we currently do not allow such vaults to be created.
*/
[[nodiscard]] inline bool
isDeepFrozen(
ReadView const& view,
AccountID const& account,
Asset const& asset,
int depth = 0)
{
return std::visit(
[&](auto const& issue) {
return isDeepFrozen(view, account, issue, depth);
},
asset.value());
}
[[nodiscard]] inline TER
checkDeepFrozen(
ReadView const& view,
AccountID const& account,
Issue const& issue)
{
return isDeepFrozen(view, account, issue) ? (TER)tecFROZEN
: (TER)tesSUCCESS;
}
[[nodiscard]] inline TER
checkDeepFrozen(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptIssue)
{
return isDeepFrozen(view, account, mptIssue) ? (TER)tecLOCKED
: (TER)tesSUCCESS;
}
[[nodiscard]] inline TER
checkDeepFrozen(
ReadView const& view,
AccountID const& account,
Asset const& asset)
{
return std::visit(
[&](auto const& issue) {
return checkDeepFrozen(view, account, issue);
},
asset.value());
}
[[nodiscard]] bool
isLPTokenFrozen(
ReadView const& view,
@@ -268,6 +342,49 @@ accountHolds(
AuthHandling zeroIfUnauthorized,
beast::Journal j);
// Returns the amount an account can spend total.
//
// These functions use accountHolds, but unlike accountHolds:
// * The account can go into debt.
// * If the account is the asset issuer the only limit is defined by the asset /
// issuance.
//
// <-- saAmount: amount of currency held by account. May be negative.
[[nodiscard]] STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
Currency const& currency,
AccountID const& issuer,
FreezeHandling zeroIfFrozen,
beast::Journal j);
[[nodiscard]] STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
Issue const& issue,
FreezeHandling zeroIfFrozen,
beast::Journal j);
[[nodiscard]] STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptIssue,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j);
[[nodiscard]] STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
Asset const& asset,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j);
// Returns the amount an account can spend of the currency type saDefault, or
// returns saDefault if this account is the issuer of the currency in
// question. Should be used in favor of accountHolds when questioning how much
@@ -514,7 +631,11 @@ dirNext(
describeOwnerDir(AccountID const& account);
[[nodiscard]] TER
dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr<SLE>& object);
dirLink(
ApplyView& view,
AccountID const& owner,
std::shared_ptr<SLE>& object,
SF_UINT64 const& node = sfOwnerNode);
AccountID
pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey);
@@ -533,14 +654,17 @@ createPseudoAccount(
uint256 const& pseudoOwnerKey,
SField const& ownerField);
// Returns true iff sleAcct is a pseudo-account.
// Returns true iff sleAcct is a pseudo-account or specific
// pseudo-accounts in pseudoFieldFilter.
//
// Returns false if sleAcct is
// * NOT a pseudo-account OR
// * NOT a ltACCOUNT_ROOT OR
// * null pointer
[[nodiscard]] bool
isPseudoAccount(std::shared_ptr<SLE const> sleAcct);
isPseudoAccount(
std::shared_ptr<SLE const> sleAcct,
std::set<SField const*> const& pseudoFieldFilter = {});
// Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account if
// set
@@ -554,14 +678,80 @@ isPseudoAccount(std::shared_ptr<SLE const> sleAcct);
getPseudoAccountFields();
[[nodiscard]] inline bool
isPseudoAccount(ReadView const& view, AccountID accountId)
isPseudoAccount(
ReadView const& view,
AccountID const& accountId,
std::set<SField const*> const& pseudoFieldFilter = {})
{
return isPseudoAccount(view.read(keylet::account(accountId)));
return isPseudoAccount(
view.read(keylet::account(accountId)), pseudoFieldFilter);
}
[[nodiscard]] TER
canAddHolding(ReadView const& view, Asset const& asset);
/** Validates that the destination SLE and tag are valid
- Checks that the SLE is not null.
- If the SLE requires a destination tag, checks that there is a tag.
*/
[[nodiscard]] TER
checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag);
/** Checks that can withdraw funds from an object to itself or a destination.
*
* The receiver may be either the submitting account (sfAccount) or a different
* destination account (sfDestination).
*
* - Checks that the receiver account exists.
* - If the receiver requires a destination tag, check that one exists, even
* if withdrawing to self.
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it.
*/
[[nodiscard]] TER
canWithdraw(
AccountID const& from,
ReadView const& view,
AccountID const& to,
SLE::const_ref toSle,
bool hasDestinationTag);
/** Checks that can withdraw funds from an object to itself or a destination.
*
* The receiver may be either the submitting account (sfAccount) or a different
* destination account (sfDestination).
*
* - Checks that the receiver account exists.
* - If the receiver requires a destination tag, check that one exists, even
* if withdrawing to self.
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it.
*/
[[nodiscard]] TER
canWithdraw(
AccountID const& from,
ReadView const& view,
AccountID const& to,
bool hasDestinationTag);
/** Checks that can withdraw funds from an object to itself or a destination.
*
* The receiver may be either the submitting account (sfAccount) or a different
* destination account (sfDestination).
*
* - Checks that the receiver account exists.
* - If the receiver requires a destination tag, check that one exists, even
* if withdrawing to self.
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it.
*/
[[nodiscard]] TER
canWithdraw(ReadView const& view, STTx const& tx);
/// Any transactors that call addEmptyHolding() in doApply must call
/// canAddHolding() in preflight with the same View and Asset
[[nodiscard]] TER
@@ -731,6 +921,22 @@ accountSend(
beast::Journal j,
WaiveTransferFee waiveFee = WaiveTransferFee::No);
using MultiplePaymentDestinations = std::vector<std::pair<AccountID, Number>>;
/** Like accountSend, except one account is sending multiple payments (with the
* same asset!) simultaneously
*
* Calls static accountSendMultiIOU if saAmount represents Issue.
* Calls static accountSendMultiMPT if saAmount represents MPTIssue.
*/
[[nodiscard]] TER
accountSendMulti(
ApplyView& view,
AccountID const& senderID,
Asset const& asset,
MultiplePaymentDestinations const& receivers,
beast::Journal j,
WaiveTransferFee waiveFee = WaiveTransferFee::No);
[[nodiscard]] TER
issueIOU(
ApplyView& view,
@@ -802,7 +1008,8 @@ requireAuth(
* purely defensive, as we currently do not allow such vaults to be created.
*
* If StrongAuth then return tecNO_AUTH if MPToken doesn't exist or
* lsfMPTRequireAuth is set and MPToken is not authorized.
* lsfMPTRequireAuth is set and MPToken is not authorized. Vault and LoanBroker
* pseudo-accounts are implicitly authorized.
*
* If WeakAuth then return tecNO_AUTH if lsfMPTRequireAuth is set and MPToken
* doesn't exist or is not authorized (explicitly or via credentials, if
+6
View File
@@ -84,6 +84,12 @@ public:
return holds<Issue>() && get<Issue>().native();
}
bool
integral() const
{
return !holds<Issue>() || get<Issue>().native();
}
friend constexpr bool
operator==(Asset const& lhs, Asset const& rhs);
+18
View File
@@ -327,6 +327,24 @@ vault(uint256 const& vaultKey)
return {ltVAULT, vaultKey};
}
Keylet
loanbroker(AccountID const& owner, std::uint32_t seq) noexcept;
inline Keylet
loanbroker(uint256 const& key)
{
return {ltLOAN_BROKER, key};
}
Keylet
loan(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept;
inline Keylet
loan(uint256 const& key)
{
return {ltLOAN, key};
}
Keylet
permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;
+5
View File
@@ -186,6 +186,11 @@ enum LedgerSpecificFlags {
// ltVAULT
lsfVaultPrivate = 0x00010000,
// ltLOAN
lsfLoanDefault = 0x00010000,
lsfLoanImpaired = 0x00020000,
lsfLoanOverpayment = 0x00040000, // True, loan allows overpayments
};
//------------------------------------------------------------------------------
+133
View File
@@ -3,6 +3,7 @@
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Units.h>
#include <cstdint>
@@ -65,6 +66,138 @@ std::size_t constexpr maxDeletableTokenOfferEntries = 500;
*/
std::uint16_t constexpr maxTransferFee = 50000;
/** There are 10,000 basis points (bips) in 100%.
*
* Basis points represent 0.01%.
*
* Given a value X, to find the amount for B bps,
* use X * B / bipsPerUnity
*
* Example: If a loan broker has 999 XRP of debt, and must maintain 1,000 bps of
* that debt as cover (10%), then the minimum cover amount is 999,000,000 drops
* * 1000 / bipsPerUnity = 99,900,00 drops or 99.9 XRP.
*
* Given a percentage P, to find the number of bps that percentage represents,
* use P * bipsPerUnity.
*
* Example: 50% is 0.50 * bipsPerUnity = 5,000 bps.
*/
Bips32 constexpr bipsPerUnity(100 * 100);
static_assert(bipsPerUnity == Bips32{10'000});
TenthBips32 constexpr tenthBipsPerUnity(bipsPerUnity.value() * 10);
static_assert(tenthBipsPerUnity == TenthBips32(100'000));
constexpr Bips32
percentageToBips(std::uint32_t percentage)
{
return Bips32(percentage * bipsPerUnity.value() / 100);
}
constexpr TenthBips32
percentageToTenthBips(std::uint32_t percentage)
{
return TenthBips32(percentage * tenthBipsPerUnity.value() / 100);
}
template <typename T, class TBips>
constexpr T
bipsOfValue(T value, Bips<TBips> bips)
{
return value * bips.value() / bipsPerUnity.value();
}
template <typename T, class TBips>
constexpr T
tenthBipsOfValue(T value, TenthBips<TBips> bips)
{
return value * bips.value() / tenthBipsPerUnity.value();
}
/** The maximum management fee rate allowed by a loan broker in 1/10 bips.
Valid values are between 0 and 10% inclusive.
*/
TenthBips16 constexpr maxManagementFeeRate(
unsafe_cast<std::uint16_t>(percentageToTenthBips(10).value()));
static_assert(maxManagementFeeRate == TenthBips16(std::uint16_t(10'000u)));
/** The maximum coverage rate required of a loan broker in 1/10 bips.
Valid values are between 0 and 100% inclusive.
*/
TenthBips32 constexpr maxCoverRate = percentageToTenthBips(100);
static_assert(maxCoverRate == TenthBips32(100'000u));
/** The maximum overpayment fee on a loan in 1/10 bips.
*
Valid values are between 0 and 100% inclusive.
*/
TenthBips32 constexpr maxOverpaymentFee = percentageToTenthBips(100);
static_assert(maxOverpaymentFee == TenthBips32(100'000u));
/** Annualized interest rate of the Loan in 1/10 bips.
*
* Valid values are between 0 and 100% inclusive.
*/
TenthBips32 constexpr maxInterestRate = percentageToTenthBips(100);
static_assert(maxInterestRate == TenthBips32(100'000u));
/** The maximum premium added to the interest rate for late payments on a loan
* in 1/10 bips.
*
* Valid values are between 0 and 100% inclusive.
*/
TenthBips32 constexpr maxLateInterestRate = percentageToTenthBips(100);
static_assert(maxLateInterestRate == TenthBips32(100'000u));
/** The maximum close interest rate charged for repaying a loan early in 1/10
* bips.
*
* Valid values are between 0 and 100% inclusive.
*/
TenthBips32 constexpr maxCloseInterestRate = percentageToTenthBips(100);
static_assert(maxCloseInterestRate == TenthBips32(100'000u));
/** The maximum overpayment interest rate charged on loan overpayments in 1/10
* bips.
*
* Valid values are between 0 and 100% inclusive.
*/
TenthBips32 constexpr maxOverpaymentInterestRate = percentageToTenthBips(100);
static_assert(maxOverpaymentInterestRate == TenthBips32(100'000u));
/** LoanPay transaction cost will be one base fee per X combined payments
*
* The number of payments is estimated based on the Amount paid and the Loan's
* Fixed Payment size. Overpayments (indicated with the tfLoanOverpayment flag)
* count as one more payment.
*
* This number was chosen arbitrarily, but should not be changed once released
* without an amendment
*/
static constexpr int loanPaymentsPerFeeIncrement = 5;
/** Maximum number of combined payments that a LoanPay transaction will process
*
* This limit is enforced during the loan payment process, and thus is not
* estimated. If the limit is hit, no further payments or overpayments will be
* processed, no matter how much of the transation Amount is left, but the
* transaction will succeed with the payments that have been processed up to
* that point.
*
* This limit is independent of loanPaymentsPerFeeIncrement, so a transaction
* could potentially be charged for many more payments than actually get
* processed. Users should take care not to submit a transaction paying more
* than loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment. Because
* overpayments are charged as a payment, if submitting
* loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment, users should not
* set the tfLoanOverpayment flag.
*
* Even though they're independent, loanMaximumPaymentsPerTransaction should be
* a multiple of loanPaymentsPerFeeIncrement.
*
* This number was chosen arbitrarily, but should not be changed once released
* without an amendment
*/
static constexpr int loanMaximumPaymentsPerTransaction = 100;
/** The maximum length of a URI inside an NFT */
std::size_t constexpr maxTokenURILength = 256;
+2 -2
View File
@@ -120,8 +120,8 @@ field_code(int id, int index)
SFields are created at compile time.
Each SField, once constructed, lives until program termination, and there
is only one instance per fieldType/fieldValue pair which serves the entire
application.
is only one instance per fieldType/fieldValue pair which serves the
entire application.
*/
class SField
{
+66 -8
View File
@@ -47,16 +47,18 @@ public:
static int const cMaxOffset = 80;
// Maximum native value supported by the code
static std::uint64_t const cMinValue = 1000000000000000ull;
static std::uint64_t const cMaxValue = 9999999999999999ull;
static std::uint64_t const cMaxNative = 9000000000000000000ull;
constexpr static std::uint64_t cMinValue = 1'000'000'000'000'000ull;
static_assert(isPowerOfTen(cMinValue));
constexpr static std::uint64_t cMaxValue = cMinValue * 10 - 1;
static_assert(cMaxValue == 9'999'999'999'999'999ull);
constexpr static std::uint64_t cMaxNative = 9'000'000'000'000'000'000ull;
// Max native value on network.
static std::uint64_t const cMaxNativeN = 100000000000000000ull;
static std::uint64_t const cIssuedCurrency = 0x8000000000000000ull;
static std::uint64_t const cPositive = 0x4000000000000000ull;
static std::uint64_t const cMPToken = 0x2000000000000000ull;
static std::uint64_t const cValueMask = ~(cPositive | cMPToken);
constexpr static std::uint64_t cMaxNativeN = 100'000'000'000'000'000ull;
constexpr static std::uint64_t cIssuedCurrency = 0x8'000'000'000'000'000ull;
constexpr static std::uint64_t cPositive = 0x4'000'000'000'000'000ull;
constexpr static std::uint64_t cMPToken = 0x2'000'000'000'000'000ull;
constexpr static std::uint64_t cValueMask = ~(cPositive | cMPToken);
static std::uint64_t const uRateOne;
@@ -155,6 +157,9 @@ public:
int
exponent() const noexcept;
bool
integral() const noexcept;
bool
native() const noexcept;
@@ -435,6 +440,12 @@ STAmount::exponent() const noexcept
return mOffset;
}
inline bool
STAmount::integral() const noexcept
{
return mAsset.integral();
}
inline bool
STAmount::native() const noexcept
{
@@ -676,6 +687,53 @@ divRoundStrict(
std::uint64_t
getRate(STAmount const& offerOut, STAmount const& offerIn);
/** Round an arbitrary precision Amount to the precision of an STAmount that has
* a given exponent.
*
* This is used to ensure that calculations involving IOU amounts do not collect
* dust beyond the precision of the reference value.
*
* @param value The value to be rounded
* @param scale An exponent value to establish the precision limit of
* `value`. Should be larger than `value.exponent()`.
* @param rounding Optional Number rounding mode
*
*/
STAmount
roundToScale(
STAmount value,
std::int32_t scale,
Number::rounding_mode rounding = Number::getround());
/** Round an arbitrary precision Number to the precision of a given Asset.
*
* This is used to ensure that calculations do not collect dust beyond the
* precision of the reference value for IOUs, or fractional amounts for the
* integral types XRP and MPT.
*
* @param asset The relevant asset
* @param value The value to be rounded
* @param scale Only relevant to IOU assets. An exponent value to establish the
* precision limit of `value`. Should be larger than `value.exponent()`.
* @param rounding Optional Number rounding mode
*/
template <AssetType A>
Number
roundToAsset(
A const& asset,
Number const& value,
std::int32_t scale,
Number::rounding_mode rounding = Number::getround())
{
NumberRoundModeGuard mg(rounding);
STAmount const ret{asset, value};
if (ret.integral())
return ret;
// Not that the ctor will round integral types (XRP, MPT) via canonicalize,
// so no extra work is needed for those.
return roundToScale(ret, scale);
}
//------------------------------------------------------------------------------
inline bool
+28 -1
View File
@@ -482,6 +482,8 @@ public:
value_type
operator*() const;
/// Do not use operator->() unless the field is required, or you've checked
/// that it's set.
T const*
operator->() const;
@@ -505,7 +507,26 @@ protected:
// Constraint += and -= ValueProxy operators
// to value types that support arithmetic operations
template <typename U>
concept IsArithmetic = std::is_arithmetic_v<U> || std::is_same_v<U, STAmount>;
concept IsArithmeticNumber = std::is_arithmetic_v<U> ||
std::is_same_v<U, Number> || std::is_same_v<U, STAmount>;
template <
typename U,
typename Value = typename U::value_type,
typename Unit = typename U::unit_type>
concept IsArithmeticValueUnit =
std::is_same_v<U, unit::ValueUnit<Unit, Value>> &&
IsArithmeticNumber<Value> && std::is_class_v<Unit>;
template <typename U, typename Value = typename U::value_type>
concept IsArithmeticST = !IsArithmeticValueUnit<U> && IsArithmeticNumber<Value>;
template <typename U>
concept IsArithmetic =
IsArithmeticNumber<U> || IsArithmeticST<U> || IsArithmeticValueUnit<U>;
template <class T, class U>
concept Addable = requires(T t, U u) { t = t + u; };
template <typename T, typename U>
concept IsArithmeticCompatible =
IsArithmetic<typename T::value_type> && Addable<typename T::value_type, U>;
template <class T>
class STObject::ValueProxy : public Proxy<T>
@@ -525,10 +546,12 @@ public:
// Convenience operators for value types supporting
// arithmetic operations
template <IsArithmetic U>
requires IsArithmeticCompatible<T, U>
ValueProxy&
operator+=(U const& u);
template <IsArithmetic U>
requires IsArithmeticCompatible<T, U>
ValueProxy&
operator-=(U const& u);
@@ -718,6 +741,8 @@ STObject::Proxy<T>::operator*() const -> value_type
return this->value();
}
/// Do not use operator->() unless the field is required, or you've checked that
/// it's set.
template <class T>
T const*
STObject::Proxy<T>::operator->() const
@@ -764,6 +789,7 @@ STObject::ValueProxy<T>::operator=(U&& u)
template <typename T>
template <IsArithmetic U>
requires IsArithmeticCompatible<T, U>
STObject::ValueProxy<T>&
STObject::ValueProxy<T>::operator+=(U const& u)
{
@@ -773,6 +799,7 @@ STObject::ValueProxy<T>::operator+=(U const& u)
template <class T>
template <IsArithmetic U>
requires IsArithmeticCompatible<T, U>
STObject::ValueProxy<T>&
STObject::ValueProxy<T>::operator-=(U const& u)
{
+20
View File
@@ -266,6 +266,26 @@ constexpr std::uint32_t tfIndependent = 0x00080000;
constexpr std::uint32_t const tfBatchMask =
~(tfUniversal | tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent) | tfInnerBatchTxn;
// LoanSet and LoanPay flags:
// LoanSet: True, indicates the loan supports overpayments
// LoanPay: True, indicates any excess in this payment can be used
// as an overpayment. False, no overpayments will be taken.
constexpr std::uint32_t const tfLoanOverpayment = 0x00010000;
// LoanPay exclusive flags:
// tfLoanFullPayment: True, indicates that the payment is
constexpr std::uint32_t const tfLoanFullPayment = 0x00020000;
constexpr std::uint32_t const tfLoanLatePayment = 0x00040000;
constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal |
tfLoanOverpayment);
constexpr std::uint32_t const tfLoanPayMask = ~(tfUniversal |
tfLoanOverpayment | tfLoanFullPayment | tfLoanLatePayment);
// LoanManage flags:
constexpr std::uint32_t const tfLoanDefault = 0x00010000;
constexpr std::uint32_t const tfLoanImpair = 0x00020000;
constexpr std::uint32_t const tfLoanUnimpair = 0x00040000;
constexpr std::uint32_t const tfLoanManageMask = ~(tfUniversal | tfLoanDefault | tfLoanImpair | tfLoanUnimpair);
// clang-format on
} // namespace ripple
@@ -11,9 +11,12 @@
#error "undefined macro: XRPL_RETIRE_FIX"
#endif
// clang-format off
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(LendingProtocol, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (IncludeKeyletFields, Supported::yes, VoteBehavior::DefaultNo)
@@ -136,3 +139,5 @@ XRPL_RETIRE_FEATURE(PayChan)
XRPL_RETIRE_FEATURE(SortedDirectories)
XRPL_RETIRE_FEATURE(TickSize)
XRPL_RETIRE_FEATURE(TrustSetAuth)
// clang-format on
@@ -149,6 +149,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
{sfFirstNFTokenSequence, soeOPTIONAL},
{sfAMMID, soeOPTIONAL}, // pseudo-account designator
{sfVaultID, soeOPTIONAL}, // pseudo-account designator
{sfLoanBrokerID, soeOPTIONAL}, // pseudo-account designator
}))
/** A ledger object which contains a list of object identifiers.
@@ -490,5 +491,117 @@ LEDGER_ENTRY(ltVAULT, 0x0084, Vault, vault, ({
// no PermissionedDomainID ever (use MPTIssuance.sfDomainID)
}))
/** Reserve 0x0084-0x0087 for future Vault-related objects. */
/** A ledger object representing a loan broker
\sa keylet::loanbroker
*/
LEDGER_ENTRY(ltLOAN_BROKER, 0x0088, LoanBroker, loan_broker, ({
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfSequence, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
{sfVaultNode, soeREQUIRED},
{sfVaultID, soeREQUIRED},
{sfAccount, soeREQUIRED},
{sfOwner, soeREQUIRED},
{sfLoanSequence, soeREQUIRED},
{sfData, soeDEFAULT},
{sfManagementFeeRate, soeDEFAULT},
{sfOwnerCount, soeDEFAULT},
{sfDebtTotal, soeDEFAULT},
{sfDebtMaximum, soeDEFAULT},
{sfCoverAvailable, soeDEFAULT},
{sfCoverRateMinimum, soeDEFAULT},
{sfCoverRateLiquidation, soeDEFAULT},
}))
/** A ledger object representing a loan between a Borrower and a Loan Broker
\sa keylet::loan
*/
LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
{sfLoanBrokerNode, soeREQUIRED},
{sfLoanBrokerID, soeREQUIRED},
{sfLoanSequence, soeREQUIRED},
{sfBorrower, soeREQUIRED},
{sfLoanOriginationFee, soeDEFAULT},
{sfLoanServiceFee, soeDEFAULT},
{sfLatePaymentFee, soeDEFAULT},
{sfClosePaymentFee, soeDEFAULT},
{sfOverpaymentFee, soeDEFAULT},
{sfInterestRate, soeDEFAULT},
{sfLateInterestRate, soeDEFAULT},
{sfCloseInterestRate, soeDEFAULT},
{sfOverpaymentInterestRate, soeDEFAULT},
{sfStartDate, soeREQUIRED},
{sfPaymentInterval, soeREQUIRED},
{sfGracePeriod, soeDEFAULT},
{sfPreviousPaymentDate, soeDEFAULT},
{sfNextPaymentDueDate, soeDEFAULT},
// The loan object tracks these values:
//
// - PaymentRemaining: The number of payments left in the loan. When it
// reaches 0, the loan is paid off, and all other relevant values
// must also be 0.
//
// - PeriodicPayment: The fixed, unrounded amount to be paid each
// interval. Stored with as much precision as possible.
// Payment transactions must round this value *UP*.
//
// - TotalValueOutstanding: The rounded total amount owed by the
// borrower to the lender / vault.
//
// - PrincipalOutstanding: The rounded portion of the
// TotalValueOutstanding that is from the principal borrowed.
//
// - ManagementFeeOutstanding: The rounded portion of the
// TotalValueOutstanding that represents management fees
// specifically owed to the broker based on the initial
// loan parameters.
//
// There are additional values that can be computed from these:
//
// - InterestOutstanding = TotalValueOutstanding - PrincipalOutstanding
// The total amount of interest still pending on the loan,
// independent of management fees.
//
// - InterestOwedToVault = InterestOutstanding - ManagementFeeOutstanding
// The amount of the total interest that is owed to the vault, and
// will be sent to as part of a payment.
//
// - TrueTotalLoanValue = PaymentRemaining * PeriodicPayment
// The unrounded true total value of the loan.
//
// - TrueTotalPrincialOutstanding can be computed using the algorithm
// in the ripple::detail::loanPrincipalFromPeriodicPayment function.
//
// - TrueTotalInterestOutstanding = TrueTotalLoanValue -
// TrueTotalPrincipalOutstanding
// The unrounded true total interest remaining.
//
// - TrueTotalManagementFeeOutstanding = TrueTotalInterestOutstanding *
// LoanBroker.ManagementFeeRate
// The unrounded true total fee still owed to the broker.
//
// Note the the "True" values may differ significantly from the tracked
// rounded values.
{sfPaymentRemaining, soeDEFAULT},
{sfPeriodicPayment, soeREQUIRED},
{sfPrincipalOutstanding, soeDEFAULT},
{sfTotalValueOutstanding, soeDEFAULT},
{sfManagementFeeOutstanding, soeDEFAULT},
// Based on the computed total value at creation, used for
// rounding calculated values so they are all on a
// consistent scale - that is, they all have the same
// number of digits after the decimal point (excluding
// trailing zeros).
{sfLoanScale, soeDEFAULT},
}))
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE
+41 -4
View File
@@ -5,6 +5,8 @@
#error "undefined macro: TYPED_SFIELD"
#endif
// clang-format off
// untyped
UNTYPED_SFIELD(sfLedgerEntry, LEDGERENTRY, 257)
UNTYPED_SFIELD(sfTransaction, TRANSACTION, 257)
@@ -40,6 +42,7 @@ TYPED_SFIELD(sfHookEmitCount, UINT16, 18)
TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19)
TYPED_SFIELD(sfHookApiVersion, UINT16, 20)
TYPED_SFIELD(sfLedgerFixType, UINT16, 21)
TYPED_SFIELD(sfManagementFeeRate, UINT16, 22) // 1/10 basis points (bips)
// 32-bit integers (common)
TYPED_SFIELD(sfNetworkID, UINT32, 1)
@@ -96,6 +99,21 @@ TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
TYPED_SFIELD(sfMutableFlags, UINT32, 53)
TYPED_SFIELD(sfStartDate, UINT32, 54)
TYPED_SFIELD(sfPaymentInterval, UINT32, 55)
TYPED_SFIELD(sfGracePeriod, UINT32, 56)
TYPED_SFIELD(sfPreviousPaymentDate, UINT32, 57)
TYPED_SFIELD(sfNextPaymentDueDate, UINT32, 58)
TYPED_SFIELD(sfPaymentRemaining, UINT32, 59)
TYPED_SFIELD(sfPaymentTotal, UINT32, 60)
TYPED_SFIELD(sfLoanSequence, UINT32, 61)
TYPED_SFIELD(sfCoverRateMinimum, UINT32, 62) // 1/10 basis points (bips)
TYPED_SFIELD(sfCoverRateLiquidation, UINT32, 63) // 1/10 basis points (bips)
TYPED_SFIELD(sfOverpaymentFee, UINT32, 64) // 1/10 basis points (bips)
TYPED_SFIELD(sfInterestRate, UINT32, 65) // 1/10 basis points (bips)
TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bips)
TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips)
TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips)
// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
@@ -127,6 +145,8 @@ TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SFie
TYPED_SFIELD(sfIssuerNode, UINT64, 27)
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default)
TYPED_SFIELD(sfVaultNode, UINT64, 30)
TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31)
// 128-bit
TYPED_SFIELD(sfEmailHash, UINT128, 1)
@@ -181,6 +201,9 @@ TYPED_SFIELD(sfDomainID, UINT256, 34)
TYPED_SFIELD(sfVaultID, UINT256, 35,
SField::sMD_PseudoAccount | SField::sMD_Default)
TYPED_SFIELD(sfParentBatchID, UINT256, 36)
TYPED_SFIELD(sfLoanBrokerID, UINT256, 37,
SField::sMD_PseudoAccount | SField::sMD_Default)
TYPED_SFIELD(sfLoanID, UINT256, 38)
// number (common)
TYPED_SFIELD(sfNumber, NUMBER, 1)
@@ -188,12 +211,21 @@ TYPED_SFIELD(sfAssetsAvailable, NUMBER, 2)
TYPED_SFIELD(sfAssetsMaximum, NUMBER, 3)
TYPED_SFIELD(sfAssetsTotal, NUMBER, 4)
TYPED_SFIELD(sfLossUnrealized, NUMBER, 5)
TYPED_SFIELD(sfDebtTotal, NUMBER, 6)
TYPED_SFIELD(sfDebtMaximum, NUMBER, 7)
TYPED_SFIELD(sfCoverAvailable, NUMBER, 8)
TYPED_SFIELD(sfLoanOriginationFee, NUMBER, 9)
TYPED_SFIELD(sfLoanServiceFee, NUMBER, 10)
TYPED_SFIELD(sfLatePaymentFee, NUMBER, 11)
TYPED_SFIELD(sfClosePaymentFee, NUMBER, 12)
TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13)
TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15)
TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16)
TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17)
// int32
// NOTE: Do not use `sfDummyInt32`. It's so far the only use of INT32
// in this file and has been defined here for test only.
// TODO: Replace `sfDummyInt32` with actually useful field.
TYPED_SFIELD(sfDummyInt32, INT32, 1) // for tests only
TYPED_SFIELD(sfLoanScale, INT32, 1)
// currency amount (common)
TYPED_SFIELD(sfAmount, AMOUNT, 1)
@@ -289,6 +321,8 @@ TYPED_SFIELD(sfAttestationRewardAccount, ACCOUNT, 21)
TYPED_SFIELD(sfLockingChainDoor, ACCOUNT, 22)
TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23)
TYPED_SFIELD(sfSubject, ACCOUNT, 24)
TYPED_SFIELD(sfBorrower, ACCOUNT, 25)
TYPED_SFIELD(sfCounterparty, ACCOUNT, 26)
// vector of 256-bit
TYPED_SFIELD(sfIndexes, VECTOR256, 1, SField::sMD_Never)
@@ -352,6 +386,7 @@ UNTYPED_SFIELD(sfCredential, OBJECT, 33)
UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34)
UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35)
UNTYPED_SFIELD(sfBook, OBJECT, 36)
UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::sMD_Default, SField::notSigning)
// array of objects (common)
// ARRAY/1 is reserved for end of array
@@ -386,3 +421,5 @@ UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28)
UNTYPED_SFIELD(sfPermissions, ARRAY, 29)
UNTYPED_SFIELD(sfRawTransactions, ARRAY, 30)
UNTYPED_SFIELD(sfBatchSigners, ARRAY, 31, SField::sMD_Default, SField::notSigning)
// clang-format on
@@ -925,6 +925,139 @@ TRANSACTION(ttBATCH, 71, Batch,
{sfBatchSigners, soeOPTIONAL},
}))
/** Reserve 72-73 for future Vault-related transactions */
/** This transaction creates and updates a Loan Broker */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanBrokerSet.h>
#endif
TRANSACTION(ttLOAN_BROKER_SET, 74, LoanBrokerSet,
Delegation::delegatable,
featureLendingProtocol,
createPseudoAcct | mayAuthorizeMPT, ({
{sfVaultID, soeREQUIRED},
{sfLoanBrokerID, soeOPTIONAL},
{sfData, soeOPTIONAL},
{sfManagementFeeRate, soeOPTIONAL},
{sfDebtMaximum, soeOPTIONAL},
{sfCoverRateMinimum, soeOPTIONAL},
{sfCoverRateLiquidation, soeOPTIONAL},
}))
/** This transaction deletes a Loan Broker */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanBrokerDelete.h>
#endif
TRANSACTION(ttLOAN_BROKER_DELETE, 75, LoanBrokerDelete,
Delegation::delegatable,
featureLendingProtocol,
mustDeleteAcct | mayAuthorizeMPT, ({
{sfLoanBrokerID, soeREQUIRED},
}))
/** This transaction deposits First Loss Capital into a Loan Broker */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanBrokerCoverDeposit.h>
#endif
TRANSACTION(ttLOAN_BROKER_COVER_DEPOSIT, 76, LoanBrokerCoverDeposit,
Delegation::delegatable,
featureLendingProtocol,
noPriv, ({
{sfLoanBrokerID, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
}))
/** This transaction withdraws First Loss Capital from a Loan Broker */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h>
#endif
TRANSACTION(ttLOAN_BROKER_COVER_WITHDRAW, 77, LoanBrokerCoverWithdraw,
Delegation::delegatable,
featureLendingProtocol,
mayAuthorizeMPT, ({
{sfLoanBrokerID, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
{sfDestination, soeOPTIONAL},
{sfDestinationTag, soeOPTIONAL},
}))
/** This transaction claws back First Loss Capital from a Loan Broker to
the issuer of the capital */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanBrokerCoverClawback.h>
#endif
TRANSACTION(ttLOAN_BROKER_COVER_CLAWBACK, 78, LoanBrokerCoverClawback,
Delegation::delegatable,
featureLendingProtocol,
noPriv, ({
{sfLoanBrokerID, soeOPTIONAL},
{sfAmount, soeOPTIONAL, soeMPTSupported},
}))
/** This transaction creates a Loan */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanSet.h>
#endif
TRANSACTION(ttLOAN_SET, 80, LoanSet,
Delegation::delegatable,
featureLendingProtocol,
mayAuthorizeMPT | mustModifyVault, ({
{sfLoanBrokerID, soeREQUIRED},
{sfData, soeOPTIONAL},
{sfCounterparty, soeOPTIONAL},
{sfCounterpartySignature, soeOPTIONAL},
{sfLoanOriginationFee, soeOPTIONAL},
{sfLoanServiceFee, soeOPTIONAL},
{sfLatePaymentFee, soeOPTIONAL},
{sfClosePaymentFee, soeOPTIONAL},
{sfOverpaymentFee, soeOPTIONAL},
{sfInterestRate, soeOPTIONAL},
{sfLateInterestRate, soeOPTIONAL},
{sfCloseInterestRate, soeOPTIONAL},
{sfOverpaymentInterestRate, soeOPTIONAL},
{sfPrincipalRequested, soeREQUIRED},
{sfPaymentTotal, soeOPTIONAL},
{sfPaymentInterval, soeOPTIONAL},
{sfGracePeriod, soeOPTIONAL},
}))
/** This transaction deletes an existing Loan */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanDelete.h>
#endif
TRANSACTION(ttLOAN_DELETE, 81, LoanDelete,
Delegation::delegatable,
featureLendingProtocol,
noPriv, ({
{sfLoanID, soeREQUIRED},
}))
/** This transaction is used to change the delinquency status of an existing Loan */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanManage.h>
#endif
TRANSACTION(ttLOAN_MANAGE, 82, LoanManage,
Delegation::delegatable,
featureLendingProtocol,
// All of the LoanManage options will modify the vault, but the
// transaction can succeed without options, essentially making it
// a noop.
mayModifyVault, ({
{sfLoanID, soeREQUIRED},
}))
/** The Borrower uses this transaction to make a Payment on the Loan. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanPay.h>
#endif
TRANSACTION(ttLOAN_PAY, 84, LoanPay,
Delegation::delegatable,
featureLendingProtocol,
mayAuthorizeMPT | mustModifyVault, ({
{sfLoanID, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
}))
/** This system-generated transaction type is used to update the status of the various amendments.
For details, see: https://xrpl.org/amendments.html
+5
View File
@@ -40,6 +40,8 @@ JSS(BaseAsset); // in: Oracle
JSS(BidMax); // in: AMM Bid
JSS(BidMin); // in: AMM Bid
JSS(ClearFlag); // field.
JSS(Counterparty); // field.
JSS(CounterpartySignature);// field.
JSS(DeliverMax); // out: alias to Amount
JSS(DeliverMin); // in: TransactionSign
JSS(Destination); // in: TransactionSign; field.
@@ -373,6 +375,8 @@ JSS(load_factor_local); // out: NetworkOPs
JSS(load_factor_net); // out: NetworkOPs
JSS(load_factor_server); // out: NetworkOPs
JSS(load_fee); // out: LoadFeeTrackImp, NetworkOPs
JSS(loan_broker_id); // in: LedgerEntry
JSS(loan_seq); // in: LedgerEntry
JSS(local); // out: resource/Logic.h
JSS(local_txs); // out: GetCounts
JSS(local_static_keys); // out: ValidatorList
@@ -485,6 +489,7 @@ JSS(propose_seq); // out: LedgerPropose
JSS(proposers); // out: NetworkOPs, LedgerConsensus
JSS(protocol); // out: NetworkOPs, PeerImp
JSS(proxied); // out: RPC ping
JSS(pseudo_account); // out: AccountInfo
JSS(pubkey_node); // out: NetworkOPs
JSS(pubkey_publisher); // out: ValidatorList
JSS(pubkey_validator); // out: NetworkOPs, ValidatorList
+93 -65
View File
@@ -74,6 +74,18 @@ public:
// tie, round towards even.
int
round() noexcept;
// Modify the result to the correctly rounded value
void
doRoundUp(rep& mantissa, int& exponent, std::string location);
// Modify the result to the correctly rounded value
void
doRoundDown(rep& mantissa, int& exponent);
// Modify the result to the correctly rounded value
void
doRound(rep& drops);
};
inline void
@@ -151,6 +163,61 @@ Number::Guard::round() noexcept
return 0;
}
void
Number::Guard::doRoundUp(rep& mantissa, int& exponent, std::string location)
{
auto r = round();
if (r == 1 || (r == 0 && (mantissa & 1) == 1))
{
++mantissa;
if (mantissa > maxMantissa)
{
mantissa /= 10;
++exponent;
}
}
if (exponent < minExponent)
{
mantissa = 0;
exponent = Number{}.exponent_;
}
if (exponent > maxExponent)
throw std::overflow_error(location);
}
void
Number::Guard::doRoundDown(rep& mantissa, int& exponent)
{
auto r = round();
if (r == 1 || (r == 0 && (mantissa & 1) == 1))
{
--mantissa;
if (mantissa < minMantissa)
{
mantissa *= 10;
--exponent;
}
}
if (exponent < minExponent)
{
mantissa = 0;
exponent = Number{}.exponent_;
}
}
// Modify the result to the correctly rounded value
void
Number::Guard::doRound(rep& drops)
{
auto r = round();
if (r == 1 || (r == 0 && (drops & 1) == 1))
{
++drops;
}
if (is_negative())
drops = -drops;
}
// Number
constexpr Number one{1000000000000000, -15, Number::unchecked{}};
@@ -190,18 +257,7 @@ Number::normalize()
return;
}
auto r = g.round();
if (r == 1 || (r == 0 && (mantissa_ & 1) == 1))
{
++mantissa_;
if (mantissa_ > maxMantissa)
{
mantissa_ /= 10;
++exponent_;
}
}
if (exponent_ > maxExponent)
throw std::overflow_error("Number::normalize 2");
g.doRoundUp(mantissa_, exponent_, "Number::normalize 2");
if (negative)
mantissa_ = -mantissa_;
@@ -273,18 +329,7 @@ Number::operator+=(Number const& y)
xm /= 10;
++xe;
}
auto r = g.round();
if (r == 1 || (r == 0 && (xm & 1) == 1))
{
++xm;
if (xm > maxMantissa)
{
xm /= 10;
++xe;
}
}
if (xe > maxExponent)
throw std::overflow_error("Number::addition overflow");
g.doRoundUp(xm, xe, "Number::addition overflow");
}
else
{
@@ -304,21 +349,7 @@ Number::operator+=(Number const& y)
xm -= g.pop();
--xe;
}
auto r = g.round();
if (r == 1 || (r == 0 && (xm & 1) == 1))
{
--xm;
if (xm < minMantissa)
{
xm *= 10;
--xe;
}
}
if (xe < minExponent)
{
xm = 0;
xe = Number{}.exponent_;
}
g.doRoundDown(xm, xe);
}
mantissa_ = xm * xn;
exponent_ = xe;
@@ -398,25 +429,10 @@ Number::operator*=(Number const& y)
}
xm = static_cast<rep>(zm);
xe = ze;
auto r = g.round();
if (r == 1 || (r == 0 && (xm & 1) == 1))
{
++xm;
if (xm > maxMantissa)
{
xm /= 10;
++xe;
}
}
if (xe < minExponent)
{
xm = 0;
xe = Number{}.exponent_;
}
if (xe > maxExponent)
throw std::overflow_error(
"Number::multiplication overflow : exponent is " +
std::to_string(xe));
g.doRoundUp(
xm,
xe,
"Number::multiplication overflow : exponent is " + std::to_string(xe));
mantissa_ = xm * zn;
exponent_ = xe;
XRPL_ASSERT(
@@ -481,17 +497,29 @@ Number::operator rep() const
throw std::overflow_error("Number::operator rep() overflow");
drops *= 10;
}
auto r = g.round();
if (r == 1 || (r == 0 && (drops & 1) == 1))
{
++drops;
}
if (g.is_negative())
drops = -drops;
g.doRound(drops);
}
return drops;
}
Number
Number::truncate() const noexcept
{
if (exponent_ >= 0 || mantissa_ == 0)
return *this;
Number ret = *this;
while (ret.exponent_ < 0 && ret.mantissa_ != 0)
{
ret.exponent_ += 1;
ret.mantissa_ /= rep(10);
}
// We are guaranteed that normalize() will never throw an exception
// because exponent is either negative or zero at this point.
ret.normalize();
return ret;
}
std::string
to_string(Number const& amount)
{
+6
View File
@@ -1042,6 +1042,12 @@ Value::isMember(std::string const& key) const
return isMember(key.c_str());
}
bool
Value::isMember(StaticString const& key) const
{
return isMember(key.c_str());
}
Value::Members
Value::getMemberNames() const
{
+101 -49
View File
@@ -8,73 +8,92 @@
namespace ripple {
std::optional<std::uint64_t>
ApplyView::dirAdd(
bool preserveOrder,
namespace directory {
std::uint64_t
createRoot(
ApplyView& view,
Keylet const& directory,
uint256 const& key,
std::function<void(std::shared_ptr<SLE> const&)> const& describe)
{
auto root = peek(directory);
auto newRoot = std::make_shared<SLE>(directory);
newRoot->setFieldH256(sfRootIndex, directory.key);
describe(newRoot);
if (!root)
{
// No root, make it.
root = std::make_shared<SLE>(directory);
root->setFieldH256(sfRootIndex, directory.key);
describe(root);
STVector256 v;
v.push_back(key);
newRoot->setFieldV256(sfIndexes, v);
STVector256 v;
v.push_back(key);
root->setFieldV256(sfIndexes, v);
view.insert(newRoot);
return std::uint64_t{0};
}
insert(root);
return std::uint64_t{0};
}
auto
findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start)
{
std::uint64_t page = start->getFieldU64(sfIndexPrevious);
std::uint64_t page = root->getFieldU64(sfIndexPrevious);
auto node = root;
auto node = start;
if (page)
{
node = peek(keylet::page(directory, page));
node = view.peek(keylet::page(directory, page));
if (!node)
LogicError("Directory chain: root back-pointer broken.");
}
auto indexes = node->getFieldV256(sfIndexes);
return std::make_tuple(page, node, indexes);
}
// If there's space, we use it:
if (indexes.size() < dirNodeMaxEntries)
std::uint64_t
insertKey(
ApplyView& view,
SLE::ref node,
std::uint64_t page,
bool preserveOrder,
STVector256& indexes,
uint256 const& key)
{
if (preserveOrder)
{
if (preserveOrder)
{
if (std::find(indexes.begin(), indexes.end(), key) != indexes.end())
LogicError("dirInsert: double insertion");
if (std::find(indexes.begin(), indexes.end(), key) != indexes.end())
LogicError("dirInsert: double insertion");
indexes.push_back(key);
}
else
{
// We can't be sure if this page is already sorted because
// it may be a legacy page we haven't yet touched. Take
// the time to sort it.
std::sort(indexes.begin(), indexes.end());
indexes.push_back(key);
}
else
{
// We can't be sure if this page is already sorted because
// it may be a legacy page we haven't yet touched. Take
// the time to sort it.
std::sort(indexes.begin(), indexes.end());
auto pos = std::lower_bound(indexes.begin(), indexes.end(), key);
auto pos = std::lower_bound(indexes.begin(), indexes.end(), key);
if (pos != indexes.end() && key == *pos)
LogicError("dirInsert: double insertion");
if (pos != indexes.end() && key == *pos)
LogicError("dirInsert: double insertion");
indexes.insert(pos, key);
}
node->setFieldV256(sfIndexes, indexes);
update(node);
return page;
indexes.insert(pos, key);
}
node->setFieldV256(sfIndexes, indexes);
view.update(node);
return page;
}
std::optional<std::uint64_t>
insertPage(
ApplyView& view,
std::uint64_t page,
SLE::pointer node,
std::uint64_t nextPage,
SLE::ref next,
uint256 const& key,
Keylet const& directory,
std::function<void(std::shared_ptr<SLE> const&)> const& describe)
{
// We rely on modulo arithmetic of unsigned integers (guaranteed in
// [basic.fundamental] paragraph 2) to detect page representation overflow.
// For signed integers this would be UB, hence static_assert here.
@@ -88,20 +107,20 @@ ApplyView::dirAdd(
// Check whether we're out of pages.
if (page == 0)
return std::nullopt;
if (!rules().enabled(fixDirectoryLimit) &&
if (!view.rules().enabled(fixDirectoryLimit) &&
page >= dirNodeMaxPages) // Old pages limit
return std::nullopt;
// We are about to create a new node; we'll link it to
// the chain first:
node->setFieldU64(sfIndexNext, page);
update(node);
view.update(node);
root->setFieldU64(sfIndexPrevious, page);
update(root);
next->setFieldU64(sfIndexPrevious, page);
view.update(next);
// Insert the new key:
indexes.clear();
STVector256 indexes;
indexes.push_back(key);
node = std::make_shared<SLE>(keylet::page(directory, page));
@@ -112,12 +131,45 @@ ApplyView::dirAdd(
// it's the default.
if (page != 1)
node->setFieldU64(sfIndexPrevious, page - 1);
if (nextPage)
node->setFieldU64(sfIndexNext, nextPage);
describe(node);
insert(node);
view.insert(node);
return page;
}
} // namespace directory
std::optional<std::uint64_t>
ApplyView::dirAdd(
bool preserveOrder,
Keylet const& directory,
uint256 const& key,
std::function<void(std::shared_ptr<SLE> const&)> const& describe)
{
auto root = peek(directory);
if (!root)
{
// No root, make it.
return directory::createRoot(*this, directory, key, describe);
}
auto [page, node, indexes] =
directory::findPreviousPage(*this, directory, root);
// If there's space, we use it:
if (indexes.size() < dirNodeMaxEntries)
{
return directory::insertKey(
*this, node, page, preserveOrder, indexes, key);
}
return directory::insertPage(
*this, page, node, 0, root, key, directory, describe);
}
bool
ApplyView::emptyDirDelete(Keylet const& directory)
{
+653 -77
View File
@@ -364,6 +364,99 @@ isLPTokenFrozen(
isFrozen(view, account, asset2.currency, asset2.account);
}
static SLE::const_pointer
getLineIfUsable(
ReadView const& view,
AccountID const& account,
Currency const& currency,
AccountID const& issuer,
FreezeHandling zeroIfFrozen,
beast::Journal j)
{
auto const sle = view.read(keylet::line(account, issuer, currency));
if (!sle)
{
return nullptr;
}
if (zeroIfFrozen == fhZERO_IF_FROZEN)
{
if (isFrozen(view, account, currency, issuer) ||
isDeepFrozen(view, account, currency, issuer))
{
return nullptr;
}
// when fixFrozenLPTokenTransfer is enabled, if currency is lptoken,
// we need to check if the associated assets have been frozen
if (view.rules().enabled(fixFrozenLPTokenTransfer))
{
auto const sleIssuer = view.read(keylet::account(issuer));
if (!sleIssuer)
{
return nullptr; // LCOV_EXCL_LINE
}
else if (sleIssuer->isFieldPresent(sfAMMID))
{
auto const sleAmm =
view.read(keylet::amm((*sleIssuer)[sfAMMID]));
if (!sleAmm ||
isLPTokenFrozen(
view,
account,
(*sleAmm)[sfAsset].get<Issue>(),
(*sleAmm)[sfAsset2].get<Issue>()))
{
return nullptr;
}
}
}
}
return sle;
}
static STAmount
getTrustLineBalance(
ReadView const& view,
SLE::const_ref sle,
AccountID const& account,
Currency const& currency,
AccountID const& issuer,
bool includeOppositeLimit,
beast::Journal j)
{
STAmount amount;
if (sle)
{
amount = sle->getFieldAmount(sfBalance);
bool const accountHigh = account > issuer;
auto const& oppositeField = accountHigh ? sfLowLimit : sfHighLimit;
if (accountHigh)
{
// Put balance in account terms.
amount.negate();
}
if (includeOppositeLimit)
{
amount += sle->getFieldAmount(oppositeField);
}
amount.setIssuer(issuer);
}
else
{
amount.clear(Issue{currency, issuer});
}
JLOG(j.trace()) << "getTrustLineBalance:"
<< " account=" << to_string(account)
<< " amount=" << amount.getFullText();
return view.balanceHook(account, issuer, amount);
}
STAmount
accountHolds(
ReadView const& view,
@@ -380,71 +473,10 @@ accountHolds(
}
// IOU: Return balance on trust line modulo freeze
auto const sle = view.read(keylet::line(account, issuer, currency));
auto const allowBalance = [&]() {
if (!sle)
{
return false;
}
SLE::const_pointer const sle =
getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j);
if (zeroIfFrozen == fhZERO_IF_FROZEN)
{
if (isFrozen(view, account, currency, issuer) ||
isDeepFrozen(view, account, currency, issuer))
{
return false;
}
// when fixFrozenLPTokenTransfer is enabled, if currency is lptoken,
// we need to check if the associated assets have been frozen
if (view.rules().enabled(fixFrozenLPTokenTransfer))
{
auto const sleIssuer = view.read(keylet::account(issuer));
if (!sleIssuer)
{
return false; // LCOV_EXCL_LINE
}
else if (sleIssuer->isFieldPresent(sfAMMID))
{
auto const sleAmm =
view.read(keylet::amm((*sleIssuer)[sfAMMID]));
if (!sleAmm ||
isLPTokenFrozen(
view,
account,
(*sleAmm)[sfAsset].get<Issue>(),
(*sleAmm)[sfAsset2].get<Issue>()))
{
return false;
}
}
}
}
return true;
}();
if (allowBalance)
{
amount = sle->getFieldAmount(sfBalance);
if (account > issuer)
{
// Put balance in account terms.
amount.negate();
}
amount.setIssuer(issuer);
}
else
{
amount.clear(Issue{currency, issuer});
}
JLOG(j.trace()) << "accountHolds:"
<< " account=" << to_string(account)
<< " amount=" << amount.getFullText();
return view.balanceHook(account, issuer, amount);
return getTrustLineBalance(view, sle, account, currency, issuer, false, j);
}
STAmount
@@ -531,6 +563,96 @@ accountHolds(
asset.value());
}
STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
Currency const& currency,
AccountID const& issuer,
FreezeHandling zeroIfFrozen,
beast::Journal j)
{
if (isXRP(currency))
return accountHolds(view, account, currency, issuer, zeroIfFrozen, j);
if (account == issuer)
// If the account is the issuer, then their limit is effectively
// infinite
return STAmount{
Issue{currency, issuer}, STAmount::cMaxValue, STAmount::cMaxOffset};
// IOU: Return balance on trust line modulo freeze
SLE::const_pointer const sle =
getLineIfUsable(view, account, currency, issuer, zeroIfFrozen, j);
return getTrustLineBalance(view, sle, account, currency, issuer, true, j);
}
STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
Issue const& issue,
FreezeHandling zeroIfFrozen,
beast::Journal j)
{
return accountCanSend(
view, account, issue.currency, issue.account, zeroIfFrozen, j);
}
STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
MPTIssue const& mptIssue,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j)
{
if (account == mptIssue.getIssuer())
{
// if the account is the issuer, and the issuance exists, their limit is
// the issuance limit minus the outstanding value
auto const issuance =
view.read(keylet::mptIssuance(mptIssue.getMptID()));
if (!issuance)
{
return STAmount{mptIssue};
}
return STAmount{
mptIssue,
issuance->at(~sfMaximumAmount).value_or(maxMPTokenAmount) -
issuance->at(sfOutstandingAmount)};
}
return accountHolds(
view, account, mptIssue, zeroIfFrozen, zeroIfUnauthorized, j);
}
[[nodiscard]] STAmount
accountCanSend(
ReadView const& view,
AccountID const& account,
Asset const& asset,
FreezeHandling zeroIfFrozen,
AuthHandling zeroIfUnauthorized,
beast::Journal j)
{
return std::visit(
[&](auto const& value) {
if constexpr (std::is_same_v<
std::remove_cvref_t<decltype(value)>,
Issue>)
{
return accountCanSend(view, account, value, zeroIfFrozen, j);
}
return accountCanSend(
view, account, value, zeroIfFrozen, zeroIfUnauthorized, j);
},
asset.value());
}
STAmount
accountFunds(
ReadView const& view,
@@ -1036,13 +1158,17 @@ describeOwnerDir(AccountID const& account)
}
TER
dirLink(ApplyView& view, AccountID const& owner, std::shared_ptr<SLE>& object)
dirLink(
ApplyView& view,
AccountID const& owner,
std::shared_ptr<SLE>& object,
SF_UINT64 const& node)
{
auto const page = view.dirInsert(
keylet::ownerDir(owner), object->key(), describeOwnerDir(owner));
if (!page)
return tecDIR_FULL; // LCOV_EXCL_LINE
object->setFieldU64(sfOwnerNode, *page);
object->setFieldU64(node, *page);
return tesSUCCESS;
}
@@ -1078,7 +1204,8 @@ getPseudoAccountFields()
{
// LCOV_EXCL_START
LogicError(
"ripple::isPseudoAccount : unable to find account root ledger "
"ripple::getPseudoAccountFields : unable to find account root "
"ledger "
"format");
// LCOV_EXCL_STOP
}
@@ -1096,7 +1223,9 @@ getPseudoAccountFields()
}
[[nodiscard]] bool
isPseudoAccount(std::shared_ptr<SLE const> sleAcct)
isPseudoAccount(
std::shared_ptr<SLE const> sleAcct,
std::set<SField const*> const& pseudoFieldFilter)
{
auto const& fields = getPseudoAccountFields();
@@ -1104,8 +1233,12 @@ isPseudoAccount(std::shared_ptr<SLE const> sleAcct)
// semantics of true return value clean.
return sleAcct && sleAcct->getType() == ltACCOUNT_ROOT &&
std::count_if(
fields.begin(), fields.end(), [&sleAcct](SField const* sf) -> bool {
return sleAcct->isFieldPresent(*sf);
fields.begin(),
fields.end(),
[&sleAcct, &pseudoFieldFilter](SField const* sf) -> bool {
return sleAcct->isFieldPresent(*sf) &&
(pseudoFieldFilter.empty() ||
pseudoFieldFilter.contains(sf));
}) > 0;
}
@@ -1115,7 +1248,8 @@ createPseudoAccount(
uint256 const& pseudoOwnerKey,
SField const& ownerField)
{
[[maybe_unused]] auto const& fields = getPseudoAccountFields();
[[maybe_unused]]
auto const& fields = getPseudoAccountFields();
XRPL_ASSERT(
std::count_if(
fields.begin(),
@@ -1137,9 +1271,10 @@ createPseudoAccount(
// Pseudo-accounts can't submit transactions, so set the sequence number
// to 0 to make them easier to spot and verify, and add an extra level
// of protection.
std::uint32_t const seqno = //
view.rules().enabled(featureSingleAssetVault) //
? 0 //
std::uint32_t const seqno = //
view.rules().enabled(featureSingleAssetVault) || //
view.rules().enabled(featureLendingProtocol) //
? 0 //
: view.seq();
account->setFieldU32(sfSequence, seqno);
// Ignore reserves requirement, disable the master key, allow default
@@ -1193,6 +1328,64 @@ canAddHolding(ReadView const& view, Asset const& asset)
asset.value());
}
[[nodiscard]] TER
checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag)
{
if (toSle == nullptr)
return tecNO_DST;
// The tag is basically account-specific information we don't
// understand, but we can require someone to fill it in.
if (toSle->isFlag(lsfRequireDestTag) && !hasDestinationTag)
return tecDST_TAG_NEEDED; // Cannot send without a tag
return tesSUCCESS;
}
[[nodiscard]] TER
canWithdraw(
AccountID const& from,
ReadView const& view,
AccountID const& to,
SLE::const_ref toSle,
bool hasDestinationTag)
{
if (auto const ret = checkDestinationAndTag(toSle, hasDestinationTag))
return ret;
if (from == to)
return tesSUCCESS;
if (toSle->isFlag(lsfDepositAuth))
{
if (!view.exists(keylet::depositPreauth(to, from)))
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
[[nodiscard]] TER
canWithdraw(
AccountID const& from,
ReadView const& view,
AccountID const& to,
bool hasDestinationTag)
{
auto const toSle = view.read(keylet::account(to));
return canWithdraw(from, view, to, toSle, hasDestinationTag);
}
[[nodiscard]] TER
canWithdraw(ReadView const& view, STTx const& tx)
{
auto const from = tx[sfAccount];
auto const to = tx[~sfDestination].value_or(from);
return canWithdraw(from, view, to, tx.isFieldPresent(sfDestinationTag));
}
[[nodiscard]] TER
addEmptyHolding(
ApplyView& view,
@@ -1201,8 +1394,8 @@ addEmptyHolding(
Issue const& issue,
beast::Journal journal)
{
// Every account can hold XRP.
if (issue.native())
// Every account can hold XRP. An issuer can issue directly.
if (issue.native() || accountID == issue.getIssuer())
return tesSUCCESS;
auto const& issuerId = issue.getIssuer();
@@ -1263,6 +1456,8 @@ addEmptyHolding(
return tefINTERNAL; // LCOV_EXCL_LINE
if (view.peek(keylet::mptoken(mptID, accountID)))
return tecDUPLICATE;
if (accountID == mptIssue.getIssuer())
return tesSUCCESS;
return authorizeMPToken(view, priorBalance, mptID, accountID, journal);
}
@@ -1326,6 +1521,18 @@ authorizeMPToken(
if (priorBalance < reserveCreate)
return tecINSUFFICIENT_RESERVE;
// Defensive check before we attempt to create MPToken for the issuer
auto const mpt = view.read(keylet::mptIssuance(mptIssuanceID));
if (!mpt || mpt->getAccountID(sfIssuer) == account)
{
// LCOV_EXCL_START
UNREACHABLE(
"ripple::authorizeMPToken : invalid issuance or issuers token");
if (view.rules().enabled(featureLendingProtocol))
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const mptokenKey = keylet::mptoken(mptIssuanceID, account);
auto mptoken = std::make_shared<SLE>(mptokenKey);
if (auto ter = dirLink(view, account, mptoken))
@@ -1401,6 +1608,14 @@ trustCreate(
auto const& uLowAccountID = !bSrcHigh ? uSrcAccountID : uDstAccountID;
auto const& uHighAccountID = bSrcHigh ? uSrcAccountID : uDstAccountID;
if (uLowAccountID == uHighAccountID)
{
// LCOV_EXCL_START
UNREACHABLE("ripple::trustCreate : trust line to self");
if (view.rules().enabled(featureLendingProtocol))
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const sleRippleState = std::make_shared<SLE>(ltRIPPLE_STATE, uIndex);
view.insert(sleRippleState);
@@ -1891,6 +2106,87 @@ rippleSendIOU(
return terResult;
}
// Send regardless of limits.
// --> receivers: Amount/currency/issuer to deliver to receivers.
// <-- saActual: Amount actually cost to sender. Sender pays fees.
static TER
rippleSendMultiIOU(
ApplyView& view,
AccountID const& senderID,
Issue const& issue,
MultiplePaymentDestinations const& receivers,
STAmount& actual,
beast::Journal j,
WaiveTransferFee waiveFee)
{
auto const issuer = issue.getIssuer();
XRPL_ASSERT(
!isXRP(senderID), "ripple::rippleSendMultiIOU : sender is not XRP");
// These may diverge
STAmount takeFromSender{issue};
actual = takeFromSender;
// Failures return immediately.
for (auto const& r : receivers)
{
auto const& receiverID = r.first;
STAmount amount{issue, r.second};
/* If we aren't sending anything or if the sender is the same as the
* receiver then we don't need to do anything.
*/
if (!amount || (senderID == receiverID))
continue;
XRPL_ASSERT(
!isXRP(receiverID),
"ripple::rippleSendMultiIOU : receiver is not XRP");
if (senderID == issuer || receiverID == issuer || issuer == noAccount())
{
// Direct send: redeeming IOUs and/or sending own IOUs.
if (auto const ter = rippleCreditIOU(
view, senderID, receiverID, amount, false, j))
return ter;
actual += amount;
// Do not add amount to takeFromSender, because rippleCreditIOU took
// it.
continue;
}
// Sending 3rd party IOUs: transit.
// Calculate the amount to transfer accounting
// for any transfer fees if the fee is not waived:
STAmount actualSend = (waiveFee == WaiveTransferFee::Yes)
? amount
: multiply(amount, transferRate(view, issuer));
actual += actualSend;
takeFromSender += actualSend;
JLOG(j.debug()) << "rippleSendMultiIOU> " << to_string(senderID)
<< " - > " << to_string(receiverID)
<< " : deliver=" << amount.getFullText()
<< " cost=" << actual.getFullText();
if (TER const terResult =
rippleCreditIOU(view, issuer, receiverID, amount, true, j))
return terResult;
}
if (senderID != issuer && takeFromSender)
{
if (TER const terResult = rippleCreditIOU(
view, senderID, issuer, takeFromSender, true, j))
return terResult;
}
return tesSUCCESS;
}
static TER
accountSendIOU(
ApplyView& view,
@@ -2015,6 +2311,144 @@ accountSendIOU(
return terResult;
}
static TER
accountSendMultiIOU(
ApplyView& view,
AccountID const& senderID,
Issue const& issue,
MultiplePaymentDestinations const& receivers,
beast::Journal j,
WaiveTransferFee waiveFee)
{
XRPL_ASSERT_PARTS(
receivers.size() > 1,
"ripple::accountSendMultiIOU",
"multiple recipients provided");
if (!issue.native())
{
STAmount actual;
JLOG(j.trace()) << "accountSendMultiIOU: " << to_string(senderID)
<< " sending " << receivers.size() << " IOUs";
return rippleSendMultiIOU(
view, senderID, issue, receivers, actual, j, waiveFee);
}
/* XRP send which does not check reserve and can do pure adjustment.
* Note that sender or receiver may be null and this not a mistake; this
* setup could be used during pathfinding and it is carefully controlled to
* ensure that transfers are balanced.
*/
SLE::pointer sender = senderID != beast::zero
? view.peek(keylet::account(senderID))
: SLE::pointer();
if (auto stream = j.trace())
{
std::string sender_bal("-");
if (sender)
sender_bal = sender->getFieldAmount(sfBalance).getFullText();
stream << "accountSendMultiIOU> " << to_string(senderID) << " ("
<< sender_bal << ") -> " << receivers.size() << " receivers.";
}
// Failures return immediately.
STAmount takeFromSender{issue};
for (auto const& r : receivers)
{
auto const& receiverID = r.first;
STAmount amount{issue, r.second};
if (amount < beast::zero)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
/* If we aren't sending anything or if the sender is the same as the
* receiver then we don't need to do anything.
*/
if (!amount || (senderID == receiverID))
continue;
SLE::pointer receiver = receiverID != beast::zero
? view.peek(keylet::account(receiverID))
: SLE::pointer();
if (auto stream = j.trace())
{
std::string receiver_bal("-");
if (receiver)
receiver_bal =
receiver->getFieldAmount(sfBalance).getFullText();
stream << "accountSendMultiIOU> " << to_string(senderID) << " -> "
<< to_string(receiverID) << " (" << receiver_bal
<< ") : " << amount.getFullText();
}
if (receiver)
{
// Increment XRP balance.
auto const rcvBal = receiver->getFieldAmount(sfBalance);
receiver->setFieldAmount(sfBalance, rcvBal + amount);
view.creditHook(xrpAccount(), receiverID, amount, -rcvBal);
view.update(receiver);
// Take what is actually sent
takeFromSender += amount;
}
if (auto stream = j.trace())
{
std::string receiver_bal("-");
if (receiver)
receiver_bal =
receiver->getFieldAmount(sfBalance).getFullText();
stream << "accountSendMultiIOU< " << to_string(senderID) << " -> "
<< to_string(receiverID) << " (" << receiver_bal
<< ") : " << amount.getFullText();
}
}
if (sender)
{
if (sender->getFieldAmount(sfBalance) < takeFromSender)
{
return TER{tecFAILED_PROCESSING};
}
else
{
auto const sndBal = sender->getFieldAmount(sfBalance);
view.creditHook(senderID, xrpAccount(), takeFromSender, sndBal);
// Decrement XRP balance.
sender->setFieldAmount(sfBalance, sndBal - takeFromSender);
view.update(sender);
}
}
if (auto stream = j.trace())
{
std::string sender_bal("-");
std::string receiver_bal("-");
if (sender)
sender_bal = sender->getFieldAmount(sfBalance).getFullText();
stream << "accountSendMultiIOU< " << to_string(senderID) << " ("
<< sender_bal << ") -> " << receivers.size() << " receivers.";
}
return tesSUCCESS;
}
static TER
rippleCreditMPT(
ApplyView& view,
@@ -2143,6 +2577,98 @@ rippleSendMPT(
return rippleCreditMPT(view, uSenderID, issuer, saActual, j);
}
static TER
rippleSendMultiMPT(
ApplyView& view,
AccountID const& senderID,
MPTIssue const& mptIssue,
MultiplePaymentDestinations const& receivers,
STAmount& actual,
beast::Journal j,
WaiveTransferFee waiveFee)
{
// Safe to get MPT since rippleSendMultiMPT is only called by
// accountSendMultiMPT
auto const issuer = mptIssue.getIssuer();
auto const sle = view.read(keylet::mptIssuance(mptIssue.getMptID()));
if (!sle)
return tecOBJECT_NOT_FOUND;
// These may diverge
STAmount takeFromSender{mptIssue};
actual = takeFromSender;
for (auto const& r : receivers)
{
auto const& receiverID = r.first;
STAmount amount{mptIssue, r.second};
if (amount < beast::zero)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
/* If we aren't sending anything or if the sender is the same as the
* receiver then we don't need to do anything.
*/
if (!amount || (senderID == receiverID))
continue;
if (senderID == issuer || receiverID == issuer)
{
// if sender is issuer, check that the new OutstandingAmount will
// not exceed MaximumAmount
if (senderID == issuer)
{
auto const sendAmount = amount.mpt().value();
auto const maximumAmount =
sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
if (sendAmount > maximumAmount - takeFromSender ||
sle->getFieldU64(sfOutstandingAmount) >
maximumAmount - sendAmount - takeFromSender)
return tecPATH_DRY;
}
// Direct send: redeeming MPTs and/or sending own MPTs.
if (auto const ter =
rippleCreditMPT(view, senderID, receiverID, amount, j))
return ter;
actual += amount;
// Do not add amount to takeFromSender, because rippleCreditMPT took
// it
continue;
}
// Sending 3rd party MPTs: transit.
STAmount actualSend = (waiveFee == WaiveTransferFee::Yes)
? amount
: multiply(
amount,
transferRate(view, amount.get<MPTIssue>().getMptID()));
actual += actualSend;
takeFromSender += actualSend;
JLOG(j.debug()) << "rippleSendMultiMPT> " << to_string(senderID)
<< " - > " << to_string(receiverID)
<< " : deliver=" << amount.getFullText()
<< " cost=" << actualSend.getFullText();
if (auto const terResult =
rippleCreditMPT(view, issuer, receiverID, amount, j))
return terResult;
}
if (senderID != issuer && takeFromSender)
{
if (TER const terResult =
rippleCreditMPT(view, senderID, issuer, takeFromSender, j))
return terResult;
}
return tesSUCCESS;
}
static TER
accountSendMPT(
ApplyView& view,
@@ -2168,6 +2694,21 @@ accountSendMPT(
view, uSenderID, uReceiverID, saAmount, saActual, j, waiveFee);
}
static TER
accountSendMultiMPT(
ApplyView& view,
AccountID const& senderID,
MPTIssue const& mptIssue,
MultiplePaymentDestinations const& receivers,
beast::Journal j,
WaiveTransferFee waiveFee)
{
STAmount actual;
return rippleSendMultiMPT(
view, senderID, mptIssue, receivers, actual, j, waiveFee);
}
TER
accountSend(
ApplyView& view,
@@ -2189,6 +2730,31 @@ accountSend(
saAmount.asset().value());
}
TER
accountSendMulti(
ApplyView& view,
AccountID const& senderID,
Asset const& asset,
MultiplePaymentDestinations const& receivers,
beast::Journal j,
WaiveTransferFee waiveFee)
{
XRPL_ASSERT_PARTS(
receivers.size() > 1,
"ripple::accountSendMulti",
"multiple recipients provided");
return std::visit(
[&]<ValidIssueType TIss>(TIss const& issue) {
if constexpr (std::is_same_v<TIss, Issue>)
return accountSendMultiIOU(
view, senderID, issue, receivers, j, waiveFee);
else
return accountSendMultiMPT(
view, senderID, issue, receivers, j, waiveFee);
},
asset.value());
}
static bool
updateTrustLine(
ApplyView& view,
@@ -2513,7 +3079,10 @@ requireAuth(
if (mptIssuer == account) // Issuer won't have MPToken
return tesSUCCESS;
if (view.rules().enabled(featureSingleAssetVault))
bool const featureSAVEnabled =
view.rules().enabled(featureSingleAssetVault);
if (featureSAVEnabled)
{
if (depth >= maxAssetCheckDepth)
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -2572,6 +3141,13 @@ requireAuth(
// belong to someone who is explicitly authorized e.g. a vault owner.
}
if (featureSAVEnabled)
{
// Implicitly authorize Vault and LoanBroker pseudo-accounts
if (isPseudoAccount(view, account, {&sfVaultID, &sfLoanBrokerID}))
return tesSUCCESS;
}
// mptoken must be authorized if issuance enabled requireAuth
if (sleIssuance->isFlag(lsfMPTRequireAuth) &&
(!sleToken || !sleToken->isFlag(lsfMPTAuthorized)))
+14
View File
@@ -77,6 +77,8 @@ enum class LedgerNameSpace : std::uint16_t {
PERMISSIONED_DOMAIN = 'm',
DELEGATE = 'E',
VAULT = 'V',
LOAN_BROKER = 'l', // lower-case L
LOAN = 'L',
// No longer used or supported. Left here to reserve the space
// to avoid accidental reuse.
@@ -547,6 +549,18 @@ vault(AccountID const& owner, std::uint32_t seq) noexcept
return vault(indexHash(LedgerNameSpace::VAULT, owner, seq));
}
Keylet
loanbroker(AccountID const& owner, std::uint32_t seq) noexcept
{
return loanbroker(indexHash(LedgerNameSpace::LOAN_BROKER, owner, seq));
}
Keylet
loan(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept
{
return loan(indexHash(LedgerNameSpace::LOAN, loanBrokerID, loanSeq));
}
Keylet
permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept
{
@@ -153,6 +153,14 @@ InnerObjectFormats::InnerObjectFormats()
{sfBookDirectory, soeREQUIRED},
{sfBookNode, soeREQUIRED},
});
add(sfCounterpartySignature.jsonName,
sfCounterpartySignature.getCode(),
{
{sfSigningPubKey, soeOPTIONAL},
{sfTxnSignature, soeOPTIONAL},
{sfSigners, soeOPTIONAL},
});
}
InnerObjectFormats const&
+26 -1
View File
@@ -279,7 +279,7 @@ STAmount::xrp() const
IOUAmount
STAmount::iou() const
{
if (native() || !holds<Issue>())
if (integral())
Throw<std::logic_error>("Cannot return non-IOU STAmount as IOUAmount");
auto mantissa = static_cast<std::int64_t>(mValue);
@@ -1461,6 +1461,31 @@ canonicalizeRoundStrict(
}
}
STAmount
roundToScale(STAmount value, std::int32_t scale, Number::rounding_mode rounding)
{
// Nothing to do for intgral types.
if (value.integral())
return value;
// If the value's exponent is greater than or equal to the scale, then
// rounding will do nothing, and might even lose precision, so just return
// the value.
if (value.exponent() >= scale)
return value;
STAmount referenceValue{
value.asset(), STAmount::cMinValue, scale, value.negative()};
NumberRoundModeGuard mg(rounding);
// With an IOU, the total will be truncated to the precision of the
// larger value: referenceValue
value += referenceValue;
// Remove the reference value, and we're left with the rounded value.
value -= referenceValue;
return value;
}
namespace {
// We need a class that has an interface similar to NumberRoundModeGuard
-2
View File
@@ -267,7 +267,6 @@ STTx::checkSign(
if (auto const ret = checkSign(requireCanonicalSig, rules, *this); !ret)
return ret;
/* Placeholder for field that will be added by Lending Protocol
if (isFieldPresent(sfCounterpartySignature))
{
auto const counterSig = getFieldObject(sfCounterpartySignature);
@@ -275,7 +274,6 @@ STTx::checkSign(
!ret)
return Unexpected("Counterparty: " + ret.error());
}
*/
return {};
}
+208 -3
View File
@@ -2295,9 +2295,12 @@ class Batch_test : public beast::unit_test::suite
Serializer s;
parsed.object->add(s);
auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result];
BEAST_EXPECT(
jrr[jss::status] == "success" &&
jrr[jss::engine_result] == "temINVALID_FLAG");
BEAST_EXPECTS(
jrr[jss::status] == "error" &&
jrr[jss::error] == "invalidTransaction" &&
jrr[jss::error_exception] ==
"fails local checks: Empty SigningPubKey.",
to_string(jrr));
env.close();
}
@@ -2535,6 +2538,207 @@ class Batch_test : public beast::unit_test::suite
}
}
void
testLoan(FeatureBitset features)
{
testcase("loan");
bool const lendingBatchEnabled = !std::any_of(
Batch::disabledTxTypes.begin(),
Batch::disabledTxTypes.end(),
[](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; });
using namespace test::jtx;
test::jtx::Env env{
*this,
envconfig(),
features | featureSingleAssetVault | featureLendingProtocol |
featureMPTokensV1};
Account const issuer{"issuer"};
// For simplicity, lender will be the sole actor for the vault &
// brokers.
Account const lender{"lender"};
// Borrower only wants to borrow
Account const borrower{"borrower"};
// Fund the accounts and trust lines with the same amount so that tests
// can use the same values regardless of the asset.
env.fund(XRP(100'000), issuer, noripple(lender, borrower));
env.close();
// Just use an XRP asset
PrettyAsset const asset{xrpIssue(), 1'000'000};
Vault vault{env};
auto const deposit = asset(50'000);
auto const debtMaximumValue = asset(25'000).value();
auto const coverDepositValue = asset(1000).value();
auto [tx, vaultKeylet] =
vault.create({.owner = lender, .asset = asset});
env(tx);
env.close();
BEAST_EXPECT(env.le(vaultKeylet));
env(vault.deposit(
{.depositor = lender, .id = vaultKeylet.key, .amount = deposit}));
env.close();
auto const brokerKeylet =
keylet::loanbroker(lender.id(), env.seq(lender));
{
using namespace loanBroker;
env(set(lender, vaultKeylet.key),
managementFeeRate(TenthBips16(100)),
debtMaximum(debtMaximumValue),
coverRateMinimum(TenthBips32(percentageToTenthBips(10))),
coverRateLiquidation(TenthBips32(percentageToTenthBips(25))));
env(coverDeposit(lender, brokerKeylet.key, coverDepositValue));
env.close();
}
{
using namespace loan;
using namespace std::chrono_literals;
auto const lenderSeq = env.seq(lender);
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
{
auto const [txIDs, batchID] = submitBatch(
env,
lendingBatchEnabled ? temBAD_SIGNATURE
: temINVALID_INNER_BATCH,
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
// Not allowed to include the counterparty signature
sig(sfCounterpartySignature, borrower),
sig(none),
fee(none),
seq(none)),
lenderSeq + 1),
batch::inner(
pay(lender,
loanKeylet.key,
STAmount{asset, asset(500).value()}),
lenderSeq + 2));
}
{
auto const [txIDs, batchID] = submitBatch(
env,
temINVALID_INNER_BATCH,
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
// Counterparty must be set
sig(none),
fee(none),
seq(none)),
lenderSeq + 1),
batch::inner(
pay(lender,
loanKeylet.key,
STAmount{asset, asset(500).value()}),
lenderSeq + 2));
}
{
auto const [txIDs, batchID] = submitBatch(
env,
lendingBatchEnabled ? temBAD_SIGNER
: temINVALID_INNER_BATCH,
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
// Counterparty must sign the outer transaction
counterparty(borrower.id()),
sig(none),
fee(none),
seq(none)),
lenderSeq + 1),
batch::inner(
pay(lender,
loanKeylet.key,
STAmount{asset, asset(500).value()}),
lenderSeq + 2));
}
{
// LoanSet normally charges at least 2x base fee, but since the
// signature check is done by the batch, it only charges the
// base fee.
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
lendingBatchEnabled ? TER(tesSUCCESS)
: TER(temINVALID_INNER_BATCH),
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
counterparty(borrower.id()),
sig(none),
fee(none),
seq(none)),
lenderSeq + 1),
batch::inner(
pay(
// However, this inner transaction will fail,
// because the lender is not allowed to draw the
// transaction
lender,
loanKeylet.key,
STAmount{asset, asset(500).value()}),
lenderSeq + 2),
batch::sig(borrower));
}
env.close();
BEAST_EXPECT(env.le(brokerKeylet));
BEAST_EXPECT(!env.le(loanKeylet));
{
// LoanSet normally charges at least 2x base fee, but since the
// signature check is done by the batch, it only charges the
// base fee.
auto const lenderSeq = env.seq(lender);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
auto const [txIDs, batchID] = submitBatch(
env,
lendingBatchEnabled ? TER(tesSUCCESS)
: TER(temINVALID_INNER_BATCH),
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender, brokerKeylet.key, asset(1000).value()),
counterparty(borrower.id()),
sig(none),
fee(none),
seq(none)),
lenderSeq + 1),
batch::inner(
manage(lender, loanKeylet.key, tfLoanImpair),
lenderSeq + 2),
batch::sig(borrower));
}
env.close();
BEAST_EXPECT(env.le(brokerKeylet));
if (auto const sleLoan = env.le(loanKeylet); lendingBatchEnabled
? BEAST_EXPECT(sleLoan)
: !BEAST_EXPECT(!sleLoan))
{
BEAST_EXPECT(sleLoan->isFlag(lsfLoanImpaired));
}
}
}
void
testObjectCreateSequence(FeatureBitset features)
{
@@ -4128,6 +4332,7 @@ class Batch_test : public beast::unit_test::suite
testAccountActivation(features);
testAccountSet(features);
testAccountDelete(features);
testLoan(features);
testObjectCreateSequence(features);
testObjectCreateTicket(features);
testObjectCreate3rdParty(features);
+432 -9
View File
@@ -155,9 +155,13 @@ class Invariants_test : public beast::unit_test::suite
{{"an account root was deleted"}},
[](Account const& A1, Account const&, ApplyContext& ac) {
// remove an account from the view
auto const sle = ac.view().peek(keylet::account(A1.id()));
auto sle = ac.view().peek(keylet::account(A1.id()));
if (!sle)
return false;
// Clear the balance so the "account deletion left behind a
// non-zero balance" check doesn't trip earlier than the desired
// check.
sle->at(sfBalance) = beast::zero;
ac.view().erase(sle);
return true;
});
@@ -181,10 +185,15 @@ class Invariants_test : public beast::unit_test::suite
{{"account deletion succeeded but deleted multiple accounts"}},
[](Account const& A1, Account const& A2, ApplyContext& ac) {
// remove two accounts from the view
auto const sleA1 = ac.view().peek(keylet::account(A1.id()));
auto const sleA2 = ac.view().peek(keylet::account(A2.id()));
auto sleA1 = ac.view().peek(keylet::account(A1.id()));
auto sleA2 = ac.view().peek(keylet::account(A2.id()));
if (!sleA1 || !sleA2)
return false;
// Clear the balance so the "account deletion left behind a
// non-zero balance" check doesn't trip earlier than the desired
// check.
sleA1->at(sfBalance) = beast::zero;
sleA2->at(sfBalance) = beast::zero;
ac.view().erase(sleA1);
ac.view().erase(sleA2);
return true;
@@ -199,6 +208,46 @@ class Invariants_test : public beast::unit_test::suite
using namespace test::jtx;
testcase << "account root deletion left artifact";
doInvariantCheck(
{{"account deletion left behind a non-zero balance"}},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
// A1 has a balance. Delete A1
auto const a1 = A1.id();
auto const sleA1 = ac.view().peek(keylet::account(a1));
if (!sleA1)
return false;
if (!BEAST_EXPECT(*sleA1->at(sfBalance) != beast::zero))
return false;
ac.view().erase(sleA1);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
doInvariantCheck(
{{"account deletion left behind a non-zero owner count"}},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
// Increment A1's owner count, then delete A1
auto const a1 = A1.id();
auto const sleA1 = ac.view().peek(keylet::account(a1));
if (!sleA1)
return false;
// Clear the balance so the "account deletion left behind a
// non-zero balance" check doesn't trip earlier than the desired
// check.
sleA1->at(sfBalance) = beast::zero;
BEAST_EXPECT(sleA1->at(sfOwnerCount) == 0);
adjustOwnerCount(ac.view(), sleA1, 1, ac.journal);
ac.view().erase(sleA1);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
for (auto const& keyletInfo : directAccountKeylets)
{
// TODO: Use structured binding once LLVM 16 is the minimum
@@ -219,29 +268,38 @@ class Invariants_test : public beast::unit_test::suite
// Add an object to the ledger for account A1, then delete
// A1
auto const a1 = A1.id();
auto const sleA1 = ac.view().peek(keylet::account(a1));
auto sleA1 = ac.view().peek(keylet::account(a1));
if (!sleA1)
return false;
auto const key = std::invoke(keyletfunc, a1);
auto const newSLE = std::make_shared<SLE>(key);
ac.view().insert(newSLE);
// Clear the balance so the "account deletion left behind a
// non-zero balance" check doesn't trip earlier than the
// desired check.
sleA1->at(sfBalance) = beast::zero;
ac.view().erase(sleA1);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_DELETE, [](STObject& tx) {}});
};
}
// NFT special case
doInvariantCheck(
{{"account deletion left behind a NFTokenPage object"}},
[&](Account const& A1, Account const&, ApplyContext& ac) {
// remove an account from the view
auto const sle = ac.view().peek(keylet::account(A1.id()));
auto sle = ac.view().peek(keylet::account(A1.id()));
if (!sle)
return false;
// Clear the balance so the "account deletion left behind a
// non-zero balance" check doesn't trip earlier than the desired
// check.
sle->at(sfBalance) = beast::zero;
sle->at(sfOwnerCount) = 0;
ac.view().erase(sle);
return true;
},
@@ -265,13 +323,18 @@ class Invariants_test : public beast::unit_test::suite
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
// Delete the AMM account without cleaning up the directory or
// deleting the AMM object
auto const sle = ac.view().peek(keylet::account(ammAcctID));
auto sle = ac.view().peek(keylet::account(ammAcctID));
if (!sle)
return false;
BEAST_EXPECT(sle->at(~sfAMMID));
BEAST_EXPECT(sle->at(~sfAMMID) == ammKey);
// Clear the balance so the "account deletion left behind a
// non-zero balance" check doesn't trip earlier than the desired
// check.
sle->at(sfBalance) = beast::zero;
sle->at(sfOwnerCount) = 0;
ac.view().erase(sle);
return true;
@@ -294,7 +357,7 @@ class Invariants_test : public beast::unit_test::suite
// Delete all the AMM's trust lines, remove the AMM from the AMM
// account's directory (this deletes the directory), and delete
// the AMM account. Do not delete the AMM object.
auto const sle = ac.view().peek(keylet::account(ammAcctID));
auto sle = ac.view().peek(keylet::account(ammAcctID));
if (!sle)
return false;
@@ -334,6 +397,11 @@ class Invariants_test : public beast::unit_test::suite
!ac.view().exists(ownerDirKeylet) ||
ac.view().emptyDirDelete(ownerDirKeylet));
// Clear the balance so the "account deletion left behind a
// non-zero balance" check doesn't trip earlier than the desired
// check.
sle->at(sfBalance) = beast::zero;
sle->at(sfOwnerCount) = 0;
ac.view().erase(sle);
return true;
@@ -1722,6 +1790,359 @@ class Invariants_test : public beast::unit_test::suite
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
}
Keylet
createLoanBroker(
jtx::Account const& a,
jtx::Env& env,
jtx::PrettyAsset const& asset)
{
using namespace jtx;
// Create vault
uint256 vaultID;
Vault vault{env};
auto [tx, vKeylet] = vault.create({.owner = a, .asset = asset});
env(tx);
BEAST_EXPECT(env.le(vKeylet));
vaultID = vKeylet.key;
// Create Loan Broker
using namespace loanBroker;
auto const loanBrokerKeylet = keylet::loanbroker(a.id(), env.seq(a));
// Create a Loan Broker with all default values.
env(set(a, vaultID), fee(increment));
return loanBrokerKeylet;
};
void
testNoModifiedUnmodifiableFields()
{
testcase("no modified unmodifiable fields");
using namespace jtx;
// Initialize with a placeholder value because there's no default ctor
Keylet loanBrokerKeylet = keylet::amendments();
Preclose createLoanBroker =
[&, this](Account const& a, Account const& b, Env& env) {
PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
loanBrokerKeylet = this->createLoanBroker(a, env, xrpAsset);
return BEAST_EXPECT(env.le(loanBrokerKeylet));
};
{
auto const mods =
std::to_array<std::function<void(SLE::pointer&)>>({
[](SLE::pointer& sle) { sle->at(sfSequence) += 1; },
[](SLE::pointer& sle) { sle->at(sfOwnerNode) += 1; },
[](SLE::pointer& sle) { sle->at(sfVaultNode) += 1; },
[](SLE::pointer& sle) { sle->at(sfVaultID) = uint256(1u); },
[](SLE::pointer& sle) {
sle->at(sfAccount) = sle->at(sfOwner);
},
[](SLE::pointer& sle) {
sle->at(sfOwner) = sle->at(sfAccount);
},
[](SLE::pointer& sle) {
sle->at(sfManagementFeeRate) += 1;
},
[](SLE::pointer& sle) { sle->at(sfCoverRateMinimum) += 1; },
[](SLE::pointer& sle) {
sle->at(sfCoverRateLiquidation) += 1;
},
[](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
[](SLE::pointer& sle) {
sle->at(sfLedgerIndex) = sle->at(sfVaultID).value();
},
});
for (auto const& mod : mods)
{
doInvariantCheck(
{{"changed an unchangable field"}},
[&](Account const& A1, Account const&, ApplyContext& ac) {
auto sle = ac.view().peek(loanBrokerKeylet);
if (!sle)
return false;
mod(sle);
ac.view().update(sle);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
}
}
// TODO: Loan Object
{
auto const mods =
std::to_array<std::function<void(SLE::pointer&)>>({
[](SLE::pointer& sle) { sle->at(sfLedgerEntryType) += 1; },
[](SLE::pointer& sle) {
sle->at(sfLedgerIndex) = uint256(1u);
},
});
for (auto const& mod : mods)
{
doInvariantCheck(
{{"changed an unchangable field"}},
[&](Account const& A1, Account const&, ApplyContext& ac) {
auto sle = ac.view().peek(keylet::account(A1.id()));
if (!sle)
return false;
mod(sle);
ac.view().update(sle);
return true;
});
}
}
}
void
testValidLoanBroker()
{
testcase << "valid loan broker";
using namespace jtx;
enum class Asset { XRP, IOU, MPT };
auto const assetTypes =
std::to_array({Asset::XRP, Asset::IOU, Asset::MPT});
for (auto const assetType : assetTypes)
{
// Initialize with a placeholder value because there's no default
// ctor
Keylet loanBrokerKeylet = keylet::amendments();
Preclose createLoanBroker = [&, this](
Account const& alice,
Account const& issuer,
Env& env) {
PrettyAsset const asset = [&]() {
switch (assetType)
{
case Asset::IOU: {
PrettyAsset const iouAsset = issuer["IOU"];
env(trust(alice, iouAsset(1000)));
env(pay(issuer, alice, iouAsset(1000)));
env.close();
return iouAsset;
}
case Asset::MPT: {
MPTTester mptt{env, issuer, mptInitNoFund};
mptt.create(
{.flags = tfMPTCanClawback | tfMPTCanTransfer |
tfMPTCanLock});
PrettyAsset const mptAsset = mptt.issuanceID();
mptt.authorize({.account = alice});
env(pay(issuer, alice, mptAsset(1000)));
env.close();
return mptAsset;
}
case Asset::XRP:
default:
return PrettyAsset{xrpIssue(), 1'000'000};
}
}();
loanBrokerKeylet = this->createLoanBroker(alice, env, asset);
return BEAST_EXPECT(env.le(loanBrokerKeylet));
};
// Ensure the test scenarios are set up completely. The test cases
// will need to recompute any of these values it needs for itself
// rather than trying to return a bunch of items
auto setupTest =
[&, this](Account const& A1, Account const&, ApplyContext& ac)
-> std::optional<std::pair<SLE::pointer, SLE::pointer>> {
if (loanBrokerKeylet.type != ltLOAN_BROKER)
return {};
auto sleBroker = ac.view().peek(loanBrokerKeylet);
if (!sleBroker)
return {};
if (!BEAST_EXPECT(sleBroker->at(sfOwnerCount) == 0))
return {};
// Need to touch sleBroker so that it is included in the
// modified entries for the invariant to find
ac.view().update(sleBroker);
// The pseudo-account holds the directory, so get it
auto const pseudoAccountID = sleBroker->at(sfAccount);
auto const pseudoAccountKeylet =
keylet::account(pseudoAccountID);
// Strictly speaking, we don't need to load the
// ACCOUNT_ROOT, but check anyway
auto slePseudo = ac.view().peek(pseudoAccountKeylet);
if (!BEAST_EXPECT(slePseudo))
return {};
// Make sure the directory doesn't already exist
auto const dirKeylet = keylet::ownerDir(pseudoAccountID);
auto sleDir = ac.view().peek(dirKeylet);
auto const describe = describeOwnerDir(pseudoAccountID);
if (!sleDir)
{
// Create the directory
BEAST_EXPECT(
::ripple::directory::createRoot(
ac.view(),
dirKeylet,
loanBrokerKeylet.key,
describe) == 0);
sleDir = ac.view().peek(dirKeylet);
}
return std::make_pair(slePseudo, sleDir);
};
doInvariantCheck(
{{"Loan Broker with zero OwnerCount has multiple directory "
"pages"}},
[&setupTest, this](
Account const& A1, Account const& A2, ApplyContext& ac) {
auto test = setupTest(A1, A2, ac);
if (!test || !test->first || !test->second)
return false;
auto slePseudo = test->first;
auto sleDir = test->second;
auto const describe =
describeOwnerDir(slePseudo->at(sfAccount));
BEAST_EXPECT(
::ripple::directory::insertPage(
ac.view(),
0,
sleDir,
0,
sleDir,
slePseudo->key(),
keylet::page(sleDir->key(), 0),
describe) == 1);
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
doInvariantCheck(
{{"Loan Broker with zero OwnerCount has multiple indexes in "
"the Directory root"}},
[&setupTest](
Account const& A1, Account const& A2, ApplyContext& ac) {
auto test = setupTest(A1, A2, ac);
if (!test || !test->first || !test->second)
return false;
auto slePseudo = test->first;
auto sleDir = test->second;
auto indexes = sleDir->getFieldV256(sfIndexes);
// Put some extra garbage into the directory
for (auto const& key : {slePseudo->key(), sleDir->key()})
{
::ripple::directory::insertKey(
ac.view(), sleDir, 0, false, indexes, key);
}
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
doInvariantCheck(
{{"Loan Broker directory corrupt"}},
[&setupTest](
Account const& A1, Account const& A2, ApplyContext& ac) {
auto test = setupTest(A1, A2, ac);
if (!test || !test->first || !test->second)
return false;
auto slePseudo = test->first;
auto sleDir = test->second;
auto const describe =
describeOwnerDir(slePseudo->at(sfAccount));
// Empty vector will overwrite the existing entry for the
// holding, if any, avoiding the "has multiple indexes"
// failure.
STVector256 indexes;
// Put one meaningless key into the directory
auto const key =
keylet::account(Account("random").id()).key;
::ripple::directory::insertKey(
ac.view(), sleDir, 0, false, indexes, key);
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
doInvariantCheck(
{{"Loan Broker with zero OwnerCount has an unexpected entry in "
"the directory"}},
[&setupTest](
Account const& A1, Account const& A2, ApplyContext& ac) {
auto test = setupTest(A1, A2, ac);
if (!test || !test->first || !test->second)
return false;
auto slePseudo = test->first;
auto sleDir = test->second;
// Empty vector will overwrite the existing entry for the
// holding, if any, avoiding the "has multiple indexes"
// failure.
STVector256 indexes;
::ripple::directory::insertKey(
ac.view(), sleDir, 0, false, indexes, slePseudo->key());
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
doInvariantCheck(
{{"Loan Broker sequence number decreased"}},
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
if (loanBrokerKeylet.type != ltLOAN_BROKER)
return false;
auto sleBroker = ac.view().peek(loanBrokerKeylet);
if (!sleBroker)
return false;
if (!BEAST_EXPECT(sleBroker->at(sfLoanSequence) > 0))
return false;
// Need to touch sleBroker so that it is included in the
// modified entries for the invariant to find
ac.view().update(sleBroker);
sleBroker->at(sfLoanSequence) -= 1;
return true;
},
XRPAmount{},
STTx{ttLOAN_BROKER_SET, [](STObject& tx) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
createLoanBroker);
}
}
void
testVault()
{
@@ -3485,8 +3906,10 @@ public:
testValidNewAccountRoot();
testNFTokenPageInvariants();
testPermissionedDomainInvariants();
testValidPseudoAccounts();
testPermissionedDEX();
testNoModifiedUnmodifiableFields();
testValidPseudoAccounts();
testValidLoanBroker();
testVault();
}
};
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -1152,7 +1152,7 @@ class MPToken_test : public beast::unit_test::suite
env(credentials::accept(bob, credIssuer1, credType));
env.close();
MPTTester mptAlice(env, alice, {});
MPTTester mptAlice(env, alice);
env.close();
mptAlice.create({
@@ -1194,7 +1194,7 @@ class MPToken_test : public beast::unit_test::suite
env(credentials::accept(bob, credIssuer1, credType));
env.close();
MPTTester mptAlice(env, alice, {});
MPTTester mptAlice(env, alice);
env.close();
mptAlice.create({
@@ -1274,7 +1274,7 @@ class MPToken_test : public beast::unit_test::suite
env(credentials::accept(carol, credIssuer2, credType));
env.close();
MPTTester mptAlice(env, alice, {});
MPTTester mptAlice(env, alice);
env.close();
mptAlice.create({
+49 -14
View File
@@ -69,14 +69,14 @@ class Vault_test : public beast::unit_test::suite
this]() -> std::tuple<PrettyAsset, Account> {
auto const vault = env.le(keylet);
BEAST_EXPECT(vault != nullptr);
if (asset.raw().holds<Issue>() && !asset.raw().native())
if (!asset.integral())
BEAST_EXPECT(vault->at(sfScale) == 6);
else
BEAST_EXPECT(vault->at(sfScale) == 0);
auto const shares =
env.le(keylet::mptIssuance(vault->at(sfShareMPTID)));
BEAST_EXPECT(shares != nullptr);
if (asset.raw().holds<Issue>() && !asset.raw().native())
if (!asset.integral())
BEAST_EXPECT(shares->at(sfAssetScale) == 6);
else
BEAST_EXPECT(shares->at(sfAssetScale) == 0);
@@ -502,7 +502,7 @@ class Vault_test : public beast::unit_test::suite
}
}
if (!asset.raw().native() && asset.raw().holds<Issue>())
if (!asset.integral())
{
testcase(prefix + " temporary authorization for 3rd party");
env(trust(erin, asset(1000)));
@@ -670,12 +670,13 @@ class Vault_test : public beast::unit_test::suite
test(env, issuer, owner, asset, vault);
};
testCase(
[&](Env& env,
Account const& issuer,
Account const& owner,
Asset const& asset,
Vault& vault) {
auto testDisabled = [&](TER resultAfterCreate = temDISABLED) {
return [&, resultAfterCreate](
Env& env,
Account const& issuer,
Account const& owner,
Asset const& asset,
Vault& vault) {
testcase("disabled single asset vault");
auto [tx, keylet] =
@@ -684,7 +685,7 @@ class Vault_test : public beast::unit_test::suite
{
auto tx = vault.set({.owner = owner, .id = keylet.key});
env(tx, ter{temDISABLED});
env(tx, data("test"), ter{resultAfterCreate});
}
{
@@ -692,7 +693,7 @@ class Vault_test : public beast::unit_test::suite
{.depositor = owner,
.id = keylet.key,
.amount = asset(10)});
env(tx, ter{temDISABLED});
env(tx, ter{resultAfterCreate});
}
{
@@ -700,7 +701,7 @@ class Vault_test : public beast::unit_test::suite
{.depositor = owner,
.id = keylet.key,
.amount = asset(10)});
env(tx, ter{temDISABLED});
env(tx, ter{resultAfterCreate});
}
{
@@ -709,15 +710,49 @@ class Vault_test : public beast::unit_test::suite
.id = keylet.key,
.holder = owner,
.amount = asset(10)});
env(tx, ter{temDISABLED});
env(tx, ter{resultAfterCreate});
}
{
auto tx = vault.del({.owner = owner, .id = keylet.key});
env(tx, ter{resultAfterCreate});
}
};
};
testCase(
testDisabled(),
{.features = testable_amendments() - featureSingleAssetVault});
testCase(
testDisabled(tecNO_ENTRY),
{.features = testable_amendments() - featureMPTokensV1});
testCase(
[&](Env& env,
Account const& issuer,
Account const& owner,
Asset const& asset,
Vault& vault) {
testcase("disabled permissioned domains");
auto [tx, keylet] =
vault.create({.owner = owner, .asset = asset});
env(tx);
tx[sfFlags] = tx[sfFlags].asUInt() | tfVaultPrivate;
tx[sfDomainID] = to_string(base_uint<256>(42ul));
env(tx, ter{temDISABLED});
{
auto tx = vault.set({.owner = owner, .id = keylet.key});
env(tx, data("Test"));
tx[sfDomainID] = to_string(base_uint<256>(13ul));
env(tx, ter{temDISABLED});
}
},
{.features = testable_amendments() - featureSingleAssetVault});
{.features = testable_amendments() - featurePermissionedDomains});
testCase([&](Env& env,
Account const& issuer,
+110
View File
@@ -725,6 +725,115 @@ public:
BEAST_EXPECT(Number(-100, -30000).truncate() == Number(0, 0));
}
void
testRounding()
{
// Test that rounding works as expected.
testcase("Rounding");
using NumberRoundings = std::map<Number::rounding_mode, std::int64_t>;
std::map<Number, NumberRoundings> const expected{
// Positive numbers
{Number{13, -1},
{{Number::to_nearest, 1},
{Number::towards_zero, 1},
{Number::downward, 1},
{Number::upward, 2}}},
{Number{23, -1},
{{Number::to_nearest, 2},
{Number::towards_zero, 2},
{Number::downward, 2},
{Number::upward, 3}}},
{Number{15, -1},
{{Number::to_nearest, 2},
{Number::towards_zero, 1},
{Number::downward, 1},
{Number::upward, 2}}},
{Number{25, -1},
{{Number::to_nearest, 2},
{Number::towards_zero, 2},
{Number::downward, 2},
{Number::upward, 3}}},
{Number{152, -2},
{{Number::to_nearest, 2},
{Number::towards_zero, 1},
{Number::downward, 1},
{Number::upward, 2}}},
{Number{252, -2},
{{Number::to_nearest, 3},
{Number::towards_zero, 2},
{Number::downward, 2},
{Number::upward, 3}}},
{Number{17, -1},
{{Number::to_nearest, 2},
{Number::towards_zero, 1},
{Number::downward, 1},
{Number::upward, 2}}},
{Number{27, -1},
{{Number::to_nearest, 3},
{Number::towards_zero, 2},
{Number::downward, 2},
{Number::upward, 3}}},
// Negative numbers
{Number{-13, -1},
{{Number::to_nearest, -1},
{Number::towards_zero, -1},
{Number::downward, -2},
{Number::upward, -1}}},
{Number{-23, -1},
{{Number::to_nearest, -2},
{Number::towards_zero, -2},
{Number::downward, -3},
{Number::upward, -2}}},
{Number{-15, -1},
{{Number::to_nearest, -2},
{Number::towards_zero, -1},
{Number::downward, -2},
{Number::upward, -1}}},
{Number{-25, -1},
{{Number::to_nearest, -2},
{Number::towards_zero, -2},
{Number::downward, -3},
{Number::upward, -2}}},
{Number{-152, -2},
{{Number::to_nearest, -2},
{Number::towards_zero, -1},
{Number::downward, -2},
{Number::upward, -1}}},
{Number{-252, -2},
{{Number::to_nearest, -3},
{Number::towards_zero, -2},
{Number::downward, -3},
{Number::upward, -2}}},
{Number{-17, -1},
{{Number::to_nearest, -2},
{Number::towards_zero, -1},
{Number::downward, -2},
{Number::upward, -1}}},
{Number{-27, -1},
{{Number::to_nearest, -3},
{Number::towards_zero, -2},
{Number::downward, -3},
{Number::upward, -2}}},
};
for (auto const& [num, roundings] : expected)
{
for (auto const& [mode, val] : roundings)
{
NumberRoundModeGuard g{mode};
auto const res = static_cast<std::int64_t>(num);
BEAST_EXPECTS(
res == val,
to_string(num) + " with mode " + std::to_string(mode) +
" expected " + std::to_string(val) + " got " +
std::to_string(res));
}
}
}
void
run() override
{
@@ -746,6 +855,7 @@ public:
test_inc_dec();
test_toSTAmount();
test_truncate();
testRounding();
}
};
+3 -1
View File
@@ -232,7 +232,9 @@ public:
*
* @param suite_ the current unit_test::suite
*/
Env(beast::unit_test::suite& suite_) : Env(suite_, envconfig())
Env(beast::unit_test::suite& suite_,
beast::severities::Severity thresh = beast::severities::kError)
: Env(suite_, envconfig(), nullptr, thresh)
{
}
+129
View File
@@ -160,6 +160,29 @@ public:
}
};
struct stAmountField : public JTxField<SF_AMOUNT, STAmount, Json::Value>
{
using SF = SF_AMOUNT;
using SV = STAmount;
using OV = Json::Value;
using base = JTxField<SF, SV, OV>;
protected:
using base::value_;
public:
explicit stAmountField(SF const& sfield, SV const& value)
: JTxField(sfield, value)
{
}
OV
value() const override
{
return value_.getJson(JsonOptions::none);
}
};
struct blobField : public JTxField<SF_VL, std::string>
{
using SF = SF_VL;
@@ -275,6 +298,8 @@ using simpleField = JTxFieldWrapper<JTxField<SField, StoredValue>>;
*/
auto const data = JTxFieldWrapper<blobField>(sfData);
auto const amount = JTxFieldWrapper<stAmountField>(sfAmount);
// TODO We only need this long "requires" clause as polyfill, for C++20
// implementations which are missing <ranges> header. Replace with
// `std::ranges::range<Input>`, and accordingly use std::ranges::begin/end
@@ -697,6 +722,110 @@ checkMetrics(
line);
}
/* LoanBroker */
/******************************************************************************/
namespace loanBroker {
Json::Value
set(AccountID const& account, uint256 const& vaultId, std::uint32_t flags = 0);
// Use "del" because "delete" is a reserved word in C++.
Json::Value
del(AccountID const& account, uint256 const& brokerID, std::uint32_t flags = 0);
Json::Value
coverDeposit(
AccountID const& account,
uint256 const& brokerID,
STAmount const& amount,
std::uint32_t flags = 0);
Json::Value
coverWithdraw(
AccountID const& account,
uint256 const& brokerID,
STAmount const& amount,
std::uint32_t flags = 0);
// Must specify at least one of loanBrokerID or amount.
Json::Value
coverClawback(AccountID const& account, std::uint32_t flags = 0);
auto const loanBrokerID = JTxFieldWrapper<uint256Field>(sfLoanBrokerID);
auto const managementFeeRate =
valueUnitWrapper<SF_UINT16, unit::TenthBipsTag>(sfManagementFeeRate);
auto const debtMaximum = simpleField<SF_NUMBER>(sfDebtMaximum);
auto const coverRateMinimum =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfCoverRateMinimum);
auto const coverRateLiquidation =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfCoverRateLiquidation);
auto const destination = JTxFieldWrapper<accountIDField>(sfDestination);
} // namespace loanBroker
/* Loan */
/******************************************************************************/
namespace loan {
Json::Value
set(AccountID const& account,
uint256 const& loanBrokerID,
Number principalRequested,
std::uint32_t flags = 0);
auto const counterparty = JTxFieldWrapper<accountIDField>(sfCounterparty);
// For `CounterPartySignature`, use `sig(sfCounterpartySignature, ...)`
auto const loanOriginationFee = simpleField<SF_NUMBER>(sfLoanOriginationFee);
auto const loanServiceFee = simpleField<SF_NUMBER>(sfLoanServiceFee);
auto const latePaymentFee = simpleField<SF_NUMBER>(sfLatePaymentFee);
auto const closePaymentFee = simpleField<SF_NUMBER>(sfClosePaymentFee);
auto const overpaymentFee =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfOverpaymentFee);
auto const interestRate =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfInterestRate);
auto const lateInterestRate =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfLateInterestRate);
auto const closeInterestRate =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfCloseInterestRate);
auto const overpaymentInterestRate =
valueUnitWrapper<SF_UINT32, unit::TenthBipsTag>(sfOverpaymentInterestRate);
auto const paymentTotal = simpleField<SF_UINT32>(sfPaymentTotal);
auto const paymentInterval = simpleField<SF_UINT32>(sfPaymentInterval);
auto const gracePeriod = simpleField<SF_UINT32>(sfGracePeriod);
Json::Value
manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags);
Json::Value
del(AccountID const& account, uint256 const& loanID, std::uint32_t flags = 0);
Json::Value
pay(AccountID const& account,
uint256 const& loanID,
STAmount const& amount,
std::uint32_t flags = 0);
} // namespace loan
} // namespace jtx
} // namespace test
} // namespace ripple
+34
View File
@@ -213,6 +213,25 @@ public:
{
return {asset_};
}
bool
integral() const
{
return asset_.integral();
}
bool
native() const
{
return asset_.native();
}
template <ValidIssueType TIss>
bool
holds() const
{
return asset_.holds<TIss>();
}
};
//------------------------------------------------------------------------------
@@ -257,6 +276,21 @@ struct XRP_t
return {TOut{v} * dropsPerXRP};
}
/** Returns an amount of XRP as PrettyAmount,
which is trivially convertable to STAmount
@param v The Number of XRP (not drops). May be fractional.
*/
PrettyAmount
operator()(Number v) const
{
auto const c = dropsPerXRP.drops();
auto const d = std::int64_t(v * c);
if (Number(d) / c != v)
Throw<std::domain_error>("unrepresentable");
return {d};
}
PrettyAmount
operator()(double v) const
{
+1 -2
View File
@@ -49,8 +49,7 @@ public:
inner(
Json::Value const& txn,
std::uint32_t const& sequence,
std::optional<std::uint32_t> const& ticket = std::nullopt,
std::optional<std::uint32_t> const& fee = std::nullopt)
std::optional<std::uint32_t> const& ticket = std::nullopt)
: txn_(txn), seq_(sequence), ticket_(ticket)
{
txn_[jss::SigningPubKey] = "";
+128
View File
@@ -356,6 +356,134 @@ allpe(AccountID const& a, Issue const& iss)
iss.account);
};
/* LoanBroker */
/******************************************************************************/
namespace loanBroker {
Json::Value
set(AccountID const& account, uint256 const& vaultId, uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanBrokerSet;
jv[sfAccount] = to_string(account);
jv[sfVaultID] = to_string(vaultId);
jv[sfFlags] = flags;
return jv;
}
Json::Value
del(AccountID const& account, uint256 const& brokerID, uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanBrokerDelete;
jv[sfAccount] = to_string(account);
jv[sfLoanBrokerID] = to_string(brokerID);
jv[sfFlags] = flags;
return jv;
}
Json::Value
coverDeposit(
AccountID const& account,
uint256 const& brokerID,
STAmount const& amount,
uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanBrokerCoverDeposit;
jv[sfAccount] = to_string(account);
jv[sfLoanBrokerID] = to_string(brokerID);
jv[sfAmount] = amount.getJson(JsonOptions::none);
jv[sfFlags] = flags;
return jv;
}
Json::Value
coverWithdraw(
AccountID const& account,
uint256 const& brokerID,
STAmount const& amount,
uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanBrokerCoverWithdraw;
jv[sfAccount] = to_string(account);
jv[sfLoanBrokerID] = to_string(brokerID);
jv[sfAmount] = amount.getJson(JsonOptions::none);
jv[sfFlags] = flags;
return jv;
}
Json::Value
coverClawback(AccountID const& account, std::uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanBrokerCoverClawback;
jv[sfAccount] = to_string(account);
jv[sfFlags] = flags;
return jv;
}
} // namespace loanBroker
/* Loan */
/******************************************************************************/
namespace loan {
Json::Value
set(AccountID const& account,
uint256 const& loanBrokerID,
Number principalRequested,
std::uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanSet;
jv[sfAccount] = to_string(account);
jv[sfLoanBrokerID] = to_string(loanBrokerID);
jv[sfPrincipalRequested] = to_string(principalRequested);
jv[sfFlags] = flags;
return jv;
}
Json::Value
manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanManage;
jv[sfAccount] = to_string(account);
jv[sfLoanID] = to_string(loanID);
jv[sfFlags] = flags;
return jv;
}
Json::Value
del(AccountID const& account, uint256 const& loanID, std::uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanDelete;
jv[sfAccount] = to_string(account);
jv[sfLoanID] = to_string(loanID);
jv[sfFlags] = flags;
return jv;
}
Json::Value
pay(AccountID const& account,
uint256 const& loanID,
STAmount const& amount,
std::uint32_t flags)
{
Json::Value jv;
jv[sfTransactionType] = jss::LoanPay;
jv[sfAccount] = to_string(account);
jv[sfLoanID] = to_string(loanID);
jv[sfAmount] = amount.getJson();
jv[sfFlags] = flags;
return jv;
}
} // namespace loan
} // namespace jtx
} // namespace test
} // namespace ripple
+211 -55
View File
@@ -62,17 +62,67 @@ MPTTester::MPTTester(Env& env, Account const& issuer, MPTInit const& arg)
env_.require(owners(it.second, 0));
}
}
if (arg.create)
create(*arg.create);
}
void
MPTTester::create(MPTCreate const& arg)
MPTTester::MPTTester(
Env& env,
Account const& issuer,
MPTID const& id,
std::vector<Account> const& holders,
bool close)
: env_(env)
, issuer_(issuer)
, holders_(makeHolders(holders))
, id_(id)
, close_(close)
{
if (id_)
Throw<std::runtime_error>("MPT can't be reused");
id_ = makeMptID(env_.seq(issuer_), issuer_);
}
static MPTCreate
makeMPTCreate(MPTInitDef const& arg)
{
if (arg.pay)
return {
.maxAmt = arg.maxAmt,
.transferFee = arg.transferFee,
.pay = {{arg.holders, *arg.pay}},
.flags = arg.flags,
.authHolder = arg.authHolder};
return {
.maxAmt = arg.maxAmt,
.transferFee = arg.transferFee,
.authorize = arg.holders,
.flags = arg.flags,
.authHolder = arg.authHolder};
}
MPTTester::MPTTester(MPTInitDef const& arg)
: MPTTester{
arg.env,
arg.issuer,
MPTInit{
.fund = arg.fund,
.close = arg.close,
.create = makeMPTCreate(arg)}}
{
}
MPTTester::operator MPT() const
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
return MPT("", *id_);
}
Json::Value
MPTTester::createjv(MPTCreate const& arg)
{
if (!arg.issuer)
Throw<std::runtime_error>("MPTTester::createjv: issuer is not set");
Json::Value jv;
jv[sfAccount] = issuer_.human();
jv[sfTransactionType] = jss::MPTokenIssuanceCreate;
jv[sfAccount] = arg.issuer->human();
if (arg.assetScale)
jv[sfAssetScale] = *arg.assetScale;
if (arg.transferFee)
@@ -85,6 +135,25 @@ MPTTester::create(MPTCreate const& arg)
jv[sfDomainID] = to_string(*arg.domainID);
if (arg.mutableFlags)
jv[sfMutableFlags] = *arg.mutableFlags;
jv[sfTransactionType] = jss::MPTokenIssuanceCreate;
return jv;
}
void
MPTTester::create(MPTCreate const& arg)
{
if (id_)
Throw<std::runtime_error>("MPT can't be reused");
id_ = makeMptID(env_.seq(issuer_), issuer_);
Json::Value jv = createjv(
{.issuer = issuer_,
.maxAmt = arg.maxAmt,
.assetScale = arg.assetScale,
.transferFee = arg.transferFee,
.metadata = arg.metadata,
.mutableFlags = arg.mutableFlags,
.domainID = arg.domainID});
if (submit(arg, jv) != tesSUCCESS)
{
// Verify issuance doesn't exist
@@ -95,26 +164,62 @@ MPTTester::create(MPTCreate const& arg)
id_.reset();
}
else
{
env_.require(mptflags(*this, arg.flags.value_or(0)));
auto authAndPay = [&](auto const& accts, auto const&& getAcct) {
for (auto const& it : accts)
{
authorize({.account = getAcct(it)});
if ((arg.flags.value_or(0) & tfMPTRequireAuth) &&
arg.authHolder)
authorize({.account = issuer_, .holder = getAcct(it)});
if (arg.pay && arg.pay->first.empty())
pay(issuer_, getAcct(it), arg.pay->second);
}
if (arg.pay)
{
for (auto const& p : arg.pay->first)
pay(issuer_, p, arg.pay->second);
}
};
if (arg.authorize)
{
if (arg.authorize->empty())
authAndPay(holders_, [](auto const& it) { return it.second; });
else
authAndPay(*arg.authorize, [](auto const& it) { return it; });
}
else if (arg.pay)
{
if (arg.pay->first.empty())
authAndPay(holders_, [](auto const& it) { return it.second; });
else
authAndPay(arg.pay->first, [](auto const& it) { return it; });
}
}
}
Json::Value
MPTTester::destroyjv(MPTDestroy const& arg)
{
Json::Value jv;
if (!arg.issuer || !arg.id)
Throw<std::runtime_error>("MPTTester::destroyjv: issuer/id is not set");
jv[sfAccount] = arg.issuer->human();
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
jv[sfTransactionType] = jss::MPTokenIssuanceDestroy;
return jv;
}
void
MPTTester::destroy(MPTDestroy const& arg)
{
Json::Value jv;
if (arg.issuer)
jv[sfAccount] = arg.issuer->human();
else
jv[sfAccount] = issuer_.human();
if (arg.id)
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
else
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
jv[sfMPTokenIssuanceID] = to_string(*id_);
}
jv[sfTransactionType] = jss::MPTokenIssuanceDestroy;
if (!arg.id && !id_)
Throw<std::runtime_error>("MPT has not been created");
Json::Value jv = destroyjv(
{.issuer = arg.issuer ? arg.issuer : issuer_,
.id = arg.id ? arg.id : id_});
submit(arg, jv);
}
@@ -127,25 +232,32 @@ MPTTester::holder(std::string const& holder_) const
return it->second;
}
Json::Value
MPTTester::authorizejv(MPTAuthorize const& arg)
{
Json::Value jv;
if (!arg.account || !arg.id)
Throw<std::runtime_error>(
"MPTTester::authorizejv: issuer/id is not set");
jv[sfAccount] = arg.account->human();
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
if (arg.holder)
jv[sfHolder] = arg.holder->human();
jv[sfTransactionType] = jss::MPTokenAuthorize;
return jv;
}
void
MPTTester::authorize(MPTAuthorize const& arg)
{
Json::Value jv;
if (arg.account)
jv[sfAccount] = arg.account->human();
else
jv[sfAccount] = issuer_.human();
jv[sfTransactionType] = jss::MPTokenAuthorize;
if (arg.id)
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
else
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
jv[sfMPTokenIssuanceID] = to_string(*id_);
}
if (arg.holder)
jv[sfHolder] = arg.holder->human();
if (!arg.id && !id_)
Throw<std::runtime_error>("MPT has not been created");
Json::Value jv = authorizejv({
.account = arg.account ? arg.account : issuer_,
.holder = arg.holder,
.id = arg.id ? arg.id : id_,
});
if (auto const result = submit(arg, jv); result == tesSUCCESS)
{
// Issuer authorizes
@@ -201,24 +313,34 @@ MPTTester::authorize(MPTAuthorize const& arg)
}
void
MPTTester::set(MPTSet const& arg)
MPTTester::authorizeHolders(Holders const& holders)
{
for (auto const& holder : holders)
{
authorize({.account = holder});
}
}
Json::Value
MPTTester::setjv(MPTSet const& arg)
{
Json::Value jv;
if (arg.account)
jv[sfAccount] = arg.account->human();
else
jv[sfAccount] = issuer_.human();
jv[sfTransactionType] = jss::MPTokenIssuanceSet;
if (arg.id)
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
else
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
jv[sfMPTokenIssuanceID] = to_string(*id_);
}
if (!arg.account || !arg.id)
Throw<std::runtime_error>("MPTTester::setjv: issuer/id is not set");
jv[sfAccount] = arg.account->human();
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
if (arg.holder)
jv[sfHolder] = arg.holder->human();
{
std::visit(
[&jv]<typename T>(T const& holder) {
if constexpr (std::is_same_v<T, Account>)
jv[sfHolder] = holder.human();
else if constexpr (std::is_same_v<T, AccountID>)
jv[sfHolder] = toBase58(holder);
},
*arg.holder);
}
if (arg.delegate)
jv[sfDelegate] = arg.delegate->human();
if (arg.domainID)
@@ -229,7 +351,27 @@ MPTTester::set(MPTSet const& arg)
jv[sfTransferFee] = *arg.transferFee;
if (arg.metadata)
jv[sfMPTokenMetadata] = strHex(*arg.metadata);
if (submit(arg, jv) == tesSUCCESS && (arg.flags || arg.mutableFlags))
jv[sfTransactionType] = jss::MPTokenIssuanceSet;
return jv;
}
void
MPTTester::set(MPTSet const& arg)
{
if (!arg.id && !id_)
Throw<std::runtime_error>("MPT has not been created");
Json::Value jv = setjv(
{.account = arg.account ? arg.account : issuer_,
.holder = arg.holder,
.id = arg.id ? arg.id : id_,
.mutableFlags = arg.mutableFlags,
.transferFee = arg.transferFee,
.metadata = arg.metadata,
.delegate = arg.delegate,
.domainID = arg.domainID});
if (submit(arg, jv) == tesSUCCESS &&
(arg.flags.value_or(0) || arg.mutableFlags))
{
auto require = [&](std::optional<Account> const& holder,
bool unchanged) {
@@ -281,8 +423,9 @@ MPTTester::set(MPTSet const& arg)
};
if (arg.account)
require(std::nullopt, arg.holder.has_value());
if (arg.holder)
require(*arg.holder, false);
if (auto const account =
(arg.holder ? std::get_if<Account>(&(*arg.holder)) : nullptr))
require(*account, false);
}
}
@@ -455,6 +598,13 @@ MPTTester::mpt(std::int64_t amount) const
return ripple::test::jtx::MPT(issuer_.name(), *id_)(amount);
}
MPTTester::operator Asset() const
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
return Asset(*id_);
}
std::int64_t
MPTTester::getBalance(Account const& account) const
{
@@ -493,6 +643,12 @@ MPTTester::operator[](std::string const& name) const
return MPT(name, issuanceID());
}
PrettyAmount
MPTTester::operator()(std::uint64_t amount) const
{
return MPT("", issuanceID())(amount);
}
} // namespace jtx
} // namespace test
} // namespace ripple
+71 -13
View File
@@ -14,6 +14,8 @@ namespace jtx {
class MPTTester;
auto const MPTDEXFlags = tfMPTCanTrade | tfMPTCanTransfer;
// Check flags settings on MPT create
class mptflags
{
@@ -67,31 +69,59 @@ public:
operator()(Env& env) const;
};
struct MPTInit
{
std::vector<Account> holders = {};
PrettyAmount const xrp = XRP(10'000);
PrettyAmount const xrpHolders = XRP(10'000);
bool fund = true;
bool close = true;
};
static MPTInit const mptInitNoFund{.fund = false};
using Holders = std::vector<Account>;
struct MPTCreate
{
static inline std::vector<Account> AllHolders = {};
std::optional<Account> issuer = std::nullopt;
std::optional<std::uint64_t> maxAmt = std::nullopt;
std::optional<std::uint8_t> assetScale = std::nullopt;
std::optional<std::uint16_t> transferFee = std::nullopt;
std::optional<std::string> metadata = std::nullopt;
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
bool fund = true;
// authorize if seated.
// if empty vector then authorize all holders
std::optional<std::vector<Account>> authorize = std::nullopt;
// pay if seated. if authorize is not seated then authorize.
// if empty vector then pay to either authorize or all holders.
std::optional<std::pair<std::vector<Account>, std::uint64_t>> pay =
std::nullopt;
std::optional<std::uint32_t> flags = {0};
std::optional<std::uint32_t> mutableFlags = std::nullopt;
bool authHolder = false;
std::optional<uint256> domainID = std::nullopt;
std::optional<TER> err = std::nullopt;
};
struct MPTInit
{
Holders holders = {};
PrettyAmount const xrp = XRP(10'000);
PrettyAmount const xrpHolders = XRP(10'000);
bool fund = true;
bool close = true;
// create MPTIssuanceID if seated and follow rules for MPTCreate args
std::optional<MPTCreate> create = std::nullopt;
};
static MPTInit const mptInitNoFund{.fund = false};
struct MPTInitDef
{
Env& env;
Account issuer;
Holders holders = {};
std::uint16_t transferFee = 0;
std::optional<std::uint64_t> pay = std::nullopt;
std::uint32_t flags = MPTDEXFlags;
bool authHolder = false;
bool fund = false;
bool close = true;
std::optional<std::uint64_t> maxAmt = std::nullopt;
std::optional<TER> err = std::nullopt;
};
struct MPTDestroy
{
std::optional<Account> issuer = std::nullopt;
@@ -116,7 +146,7 @@ struct MPTAuthorize
struct MPTSet
{
std::optional<Account> account = std::nullopt;
std::optional<Account> holder = std::nullopt;
std::optional<std::variant<Account, AccountID>> holder = std::nullopt;
std::optional<MPTID> id = std::nullopt;
std::optional<std::uint32_t> ownerCount = std::nullopt;
std::optional<std::uint32_t> holderCount = std::nullopt;
@@ -132,26 +162,49 @@ struct MPTSet
class MPTTester
{
Env& env_;
Account const& issuer_;
Account const issuer_;
std::unordered_map<std::string, Account> const holders_;
std::optional<MPTID> id_;
bool close_;
public:
MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {});
MPTTester(MPTInitDef const& constr);
MPTTester(
Env& env,
Account const& issuer,
MPTID const& id,
std::vector<Account> const& holders = {},
bool close = true);
operator MPT() const;
void
create(MPTCreate const& arg = MPTCreate{});
static Json::Value
createjv(MPTCreate const& arg = MPTCreate{});
void
destroy(MPTDestroy const& arg = MPTDestroy{});
static Json::Value
destroyjv(MPTDestroy const& arg = MPTDestroy{});
void
authorize(MPTAuthorize const& arg = MPTAuthorize{});
static Json::Value
authorizejv(MPTAuthorize const& arg = MPTAuthorize{});
void
authorizeHolders(Holders const& holders);
void
set(MPTSet const& set = {});
static Json::Value
setjv(MPTSet const& set = {});
[[nodiscard]] bool
checkDomainID(std::optional<uint256> expected) const;
@@ -218,8 +271,13 @@ public:
MPT
operator[](std::string const& name) const;
PrettyAmount
operator()(std::uint64_t amount) const;
operator Asset() const;
private:
using SLEP = std::shared_ptr<SLE const>;
using SLEP = SLE::const_pointer;
bool
forObject(
std::function<bool(SLEP const& sle)> const& cb,
+20 -20
View File
@@ -724,63 +724,63 @@ class STParsedJSON_test : public beast::unit_test::suite
{
Json::Value j;
int const minInt32 = -2147483648;
j[sfDummyInt32] = minInt32;
j[sfLoanScale] = minInt32;
STParsedJSONObject obj("Test", j);
BEAST_EXPECT(obj.object.has_value());
if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32)))
BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == minInt32);
if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale)))
BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == minInt32);
}
// max value
{
Json::Value j;
int const maxInt32 = 2147483647;
j[sfDummyInt32] = maxInt32;
j[sfLoanScale] = maxInt32;
STParsedJSONObject obj("Test", j);
BEAST_EXPECT(obj.object.has_value());
if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32)))
BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == maxInt32);
if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale)))
BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == maxInt32);
}
// max uint value
{
Json::Value j;
unsigned int const maxUInt32 = 2147483647u;
j[sfDummyInt32] = maxUInt32;
j[sfLoanScale] = maxUInt32;
STParsedJSONObject obj("Test", j);
BEAST_EXPECT(obj.object.has_value());
if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32)))
if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale)))
BEAST_EXPECT(
obj.object->getFieldI32(sfDummyInt32) ==
obj.object->getFieldI32(sfLoanScale) ==
static_cast<int32_t>(maxUInt32));
}
// Test with string value
{
Json::Value j;
j[sfDummyInt32] = "2147483647";
j[sfLoanScale] = "2147483647";
STParsedJSONObject obj("Test", j);
BEAST_EXPECT(obj.object.has_value());
if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32)))
if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale)))
BEAST_EXPECT(
obj.object->getFieldI32(sfDummyInt32) == 2147483647u);
obj.object->getFieldI32(sfLoanScale) == 2147483647u);
}
// Test with string negative value
{
Json::Value j;
int value = -2147483648;
j[sfDummyInt32] = std::to_string(value);
j[sfLoanScale] = std::to_string(value);
STParsedJSONObject obj("Test", j);
BEAST_EXPECT(obj.object.has_value());
if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32)))
BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == value);
if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale)))
BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == value);
}
// Test out of range value for int32 (negative)
{
Json::Value j;
j[sfDummyInt32] = "-2147483649";
j[sfLoanScale] = "-2147483649";
STParsedJSONObject obj("Test", j);
BEAST_EXPECT(!obj.object.has_value());
}
@@ -788,7 +788,7 @@ class STParsedJSON_test : public beast::unit_test::suite
// Test out of range value for int32 (positive)
{
Json::Value j;
j[sfDummyInt32] = 2147483648u;
j[sfLoanScale] = 2147483648u;
STParsedJSONObject obj("Test", j);
BEAST_EXPECT(!obj.object.has_value());
}
@@ -796,7 +796,7 @@ class STParsedJSON_test : public beast::unit_test::suite
// Test string value out of range
{
Json::Value j;
j[sfDummyInt32] = "2147483648";
j[sfLoanScale] = "2147483648";
STParsedJSONObject obj("Test", j);
BEAST_EXPECT(!obj.object.has_value());
}
@@ -804,7 +804,7 @@ class STParsedJSON_test : public beast::unit_test::suite
// Test bad_type (arrayValue)
{
Json::Value j;
j[sfDummyInt32] = Json::Value(Json::arrayValue);
j[sfLoanScale] = Json::Value(Json::arrayValue);
STParsedJSONObject obj("Test", j);
BEAST_EXPECT(!obj.object.has_value());
}
@@ -812,7 +812,7 @@ class STParsedJSON_test : public beast::unit_test::suite
// Test bad_type (objectValue)
{
Json::Value j;
j[sfDummyInt32] = Json::Value(Json::objectValue);
j[sfLoanScale] = Json::Value(Json::objectValue);
STParsedJSONObject obj("Test", j);
BEAST_EXPECT(!obj.object.has_value());
}
@@ -280,7 +280,7 @@ LedgerReplayTask::addDelta(std::shared_ptr<LedgerDeltaAcquire> const& delta)
deltas_.empty() ||
deltas_.back()->ledgerSeq_ + 1 == delta->ledgerSeq_,
"ripple::LedgerReplayTask::addDelta : no deltas or consecutive "
"sequence", );
"sequence");
deltas_.push_back(delta);
}
}
+4 -1
View File
@@ -105,8 +105,11 @@ OpenLedger::accept(
auto const txId = tx->getTransactionID();
// skip batch txns
// The flag should only be settable if Batch feature is enabled. If
// Batch is not enabled, the flag is always invalid, so don't relay it
// regardless.
// LCOV_EXCL_START
if (tx->isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch))
if (tx->isFlag(tfInnerBatchTxn))
{
XRPL_ASSERT(
txpair.second && txpair.second->isFieldPresent(sfParentBatchID),
+264
View File
@@ -0,0 +1,264 @@
#ifndef XRPL_APP_MISC_LENDINGHELPERS_H_INCLUDED
#define XRPL_APP_MISC_LENDINGHELPERS_H_INCLUDED
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/st.h>
namespace ripple {
struct PreflightContext;
// Lending protocol has dependencies, so capture them here.
bool
checkLendingProtocolDependencies(PreflightContext const& ctx);
Number
loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval);
/// Ensure the periodic payment is always rounded consistently
inline Number
roundPeriodicPayment(
Asset const& asset,
Number const& periodicPayment,
std::int32_t scale)
{
return roundToAsset(asset, periodicPayment, scale, Number::upward);
}
/// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure
/// Conditions)
struct LoanPaymentParts
{
/// principal_paid is the amount of principal that the payment covered.
Number principalPaid = numZero;
/// interest_paid is the amount of interest that the payment covered.
Number interestPaid = numZero;
/**
* value_change is the amount by which the total value of the Loan changed.
* If value_change < 0, Loan value decreased.
* If value_change > 0, Loan value increased.
* This is 0 for regular payments.
*/
Number valueChange = numZero;
/// feePaid is amount of fee that is paid to the broker
Number feePaid = numZero;
LoanPaymentParts&
operator+=(LoanPaymentParts const& other);
bool
operator==(LoanPaymentParts const& other) const;
};
/** This structure describes the initial "computed" properties of a loan.
*
* It is used at loan creation and when the terms of a loan change, such as
* after an overpayment.
*/
struct LoanProperties
{
Number periodicPayment;
Number totalValueOutstanding;
Number managementFeeOwedToBroker;
std::int32_t loanScale;
Number firstPaymentPrincipal;
};
/** This structure captures the current state of a loan and all the
relevant parts.
Whether the values are raw (unrounded) or rounded will
depend on how it was computed.
Many of the fields can be derived from each other, but they're all provided
here to reduce code duplication and possible mistakes.
e.g.
* interestOutstanding = valueOutstanding - principalOutstanding
* interestDue = interestOutstanding - managementFeeDue
*/
struct LoanState
{
/// Total value still due to be paid by the borrower.
Number valueOutstanding;
/// Prinicipal still due to be paid by the borrower.
Number principalOutstanding;
/// Interest still due to be paid TO the Vault.
// This is a portion of interestOutstanding
Number interestDue;
/// Management fee still due to be paid TO the broker.
// This is a portion of interestOutstanding
Number managementFeeDue;
/// Interest still due to be paid by the borrower.
Number
interestOutstanding() const
{
XRPL_ASSERT_PARTS(
interestDue + managementFeeDue ==
valueOutstanding - principalOutstanding,
"ripple::LoanState::interestOutstanding",
"other values add up correctly");
return interestDue + managementFeeDue;
}
};
LoanState
calculateRawLoanState(
Number const& periodicPayment,
Number const& periodicRate,
std::uint32_t const paymentRemaining,
TenthBips32 const managementFeeRate);
LoanState
calculateRawLoanState(
Number const& periodicPayment,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t const paymentRemaining,
TenthBips32 const managementFeeRate);
LoanState
calculateRoundedLoanState(
Number const& totalValueOutstanding,
Number const& principalOutstanding,
Number const& managementFeeOutstanding);
LoanState
calculateRoundedLoanState(SLE::const_ref loan);
Number
computeFee(
Asset const& asset,
Number const& value,
TenthBips32 managementFeeRate,
std::int32_t scale);
Number
calculateFullPaymentInterest(
Number const& rawPrincipalOutstanding,
Number const& periodicRate,
NetClock::time_point parentCloseTime,
std::uint32_t paymentInterval,
std::uint32_t prevPaymentDate,
std::uint32_t startDate,
TenthBips32 closeInterestRate);
Number
calculateFullPaymentInterest(
Number const& periodicPayment,
Number const& periodicRate,
std::uint32_t paymentRemaining,
NetClock::time_point parentCloseTime,
std::uint32_t paymentInterval,
std::uint32_t prevPaymentDate,
std::uint32_t startDate,
TenthBips32 closeInterestRate);
namespace detail {
// These classes and functions should only be accessed by LendingHelper
// functions and unit tests
enum class PaymentSpecialCase { none, final, extra };
/// This structure is used internally to compute the breakdown of a
/// single loan payment
struct PaymentComponents
{
#if LOANCOMPLETE
// raw values are unrounded, and are based on pure math
Number rawInterest;
Number rawPrincipal;
Number rawManagementFee;
#endif
// tracked values are rounded to the asset and loan scale, and correspond to
// fields in the Loan ledger object.
// trackedValueDelta modifies sfTotalValueOutstanding.
Number trackedValueDelta;
// trackedPrincipalDelta modifies sfPrincipalOutstanding.
Number trackedPrincipalDelta;
// trackedManagementFeeDelta modifies sfManagementFeeOutstanding. It will
// not include any "extra" fees that go directly to the broker, such as late
// fees.
Number trackedManagementFeeDelta;
PaymentSpecialCase specialCase = PaymentSpecialCase::none;
Number
trackedInterestPart() const;
};
struct LoanDeltas
{
Number principalDelta;
Number interestDueDelta;
Number managementFeeDueDelta;
Number
valueDelta() const
{
return principalDelta + interestDueDelta + managementFeeDueDelta;
}
void
nonNegative();
};
PaymentComponents
computePaymentComponents(
Asset const& asset,
std::int32_t scale,
Number const& totalValueOutstanding,
Number const& principalOutstanding,
Number const& managementFeeOutstanding,
Number const& periodicPayment,
Number const& periodicRate,
std::uint32_t paymentRemaining,
TenthBips16 managementFeeRate);
} // namespace detail
detail::LoanDeltas
operator-(LoanState const& lhs, LoanState const& rhs);
LoanState
operator-(LoanState const& lhs, detail::LoanDeltas const& rhs);
Number
valueMinusFee(
Asset const& asset,
Number const& value,
TenthBips16 managementFeeRate,
std::int32_t scale);
LoanProperties
computeLoanProperties(
Asset const& asset,
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining,
TenthBips32 managementFeeRate,
std::int32_t minimumScale);
bool
isRounded(Asset const& asset, Number const& value, std::int32_t scale);
// Indicates what type of payment is being made.
// regular, late, and full are mutually exclusive.
// overpayment is an "add on" to a regular payment, and follows that path with
// potential extra work at the end.
enum class LoanPaymentType { regular = 0, late, full, overpayment };
Expected<LoanPaymentParts, TER>
loanMakePayment(
Asset const& asset,
ApplyView& view,
SLE::ref loan,
SLE::const_ref brokerSle,
STAmount const& amount,
LoanPaymentType const paymentType,
beast::Journal j);
} // namespace ripple
#endif // XRPL_APP_MISC_LENDINGHELPERS_H_INCLUDED
+10 -7
View File
@@ -1677,10 +1677,11 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
app_.getHashRouter().shouldRelay(e.transaction->getID());
if (auto const sttx = *(e.transaction->getSTransaction());
toSkip &&
// Skip relaying if it's an inner batch txn and batch
// feature is enabled
!(sttx.isFlag(tfInnerBatchTxn) &&
newOL->rules().enabled(featureBatch)))
// Skip relaying if it's an inner batch txn. The flag should
// only be set if the Batch feature is enabled. If Batch is
// not enabled, the flag is always invalid, so don't relay
// it regardless.
!sttx.isFlag(tfInnerBatchTxn))
{
protocol::TMTransaction tx;
Serializer s;
@@ -3041,9 +3042,11 @@ NetworkOPsImp::pubProposedTransaction(
std::shared_ptr<STTx const> const& transaction,
TER result)
{
// never publish an inner txn inside a batch txn
if (transaction->isFlag(tfInnerBatchTxn) &&
ledger->rules().enabled(featureBatch))
// never publish an inner txn inside a batch txn. The flag should
// only be set if the Batch feature is enabled. If Batch is not
// enabled, the flag is always invalid, so don't publish it
// regardless.
if (transaction->isFlag(tfInnerBatchTxn))
return;
MultiApiJson jvObj =
File diff suppressed because it is too large. Load diff
+10 -5
View File
@@ -263,7 +263,8 @@ Batch::preflight(PreflightContext const& ctx)
return temREDUNDANT;
}
if (stx.getFieldU16(sfTransactionType) == ttBATCH)
auto const txType = stx.getFieldU16(sfTransactionType);
if (txType == ttBATCH)
{
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
<< "batch cannot have an inner batch txn. "
@@ -271,6 +272,14 @@ Batch::preflight(PreflightContext const& ctx)
return temINVALID;
}
if (std::any_of(
disabledTxTypes.begin(),
disabledTxTypes.end(),
[txType](auto const& disabled) { return txType == disabled; }))
{
return temINVALID_INNER_BATCH;
}
if (!(stx.getFlags() & tfInnerBatchTxn))
{
JLOG(ctx.j.debug())
@@ -283,7 +292,6 @@ Batch::preflight(PreflightContext const& ctx)
if (auto const ret = checkSignatureFields(stx, hash))
return ret;
/* Placeholder for field that will be added by Lending Protocol
// Note that the CounterpartySignature is optional, and should not be
// included, but if it is, ensure it doesn't contain a signature.
if (stx.isFieldPresent(sfCounterpartySignature))
@@ -296,7 +304,6 @@ Batch::preflight(PreflightContext const& ctx)
return ret;
}
}
*/
auto const innerAccount = stx.getAccountID(sfAccount);
if (auto const preflightResult = ripple::preflight(
@@ -393,13 +400,11 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
// inner account to the required signers set.
if (innerAccount != outerAccount)
requiredSigners.insert(innerAccount);
/* Placeholder for field that will be added by Lending Protocol
// Some transactions have a Counterparty, who must also sign the
// transaction if they are not the outer account
if (auto const counterparty = rb.at(~sfCounterparty);
counterparty && counterparty != outerAccount)
requiredSigners.insert(*counterparty);
*/
}
// Validation Batch Signers
+18
View File
@@ -35,6 +35,24 @@ public:
TER
doApply() override;
static constexpr auto disabledTxTypes = std::to_array<TxType>({
ttVAULT_CREATE,
ttVAULT_SET,
ttVAULT_DELETE,
ttVAULT_DEPOSIT,
ttVAULT_WITHDRAW,
ttVAULT_CLAWBACK,
ttLOAN_BROKER_SET,
ttLOAN_BROKER_DELETE,
ttLOAN_BROKER_COVER_DEPOSIT,
ttLOAN_BROKER_COVER_WITHDRAW,
ttLOAN_BROKER_COVER_CLAWBACK,
ttLOAN_SET,
ttLOAN_DELETE,
ttLOAN_MANAGE,
ttLOAN_PAY,
});
};
} // namespace ripple
+5 -1
View File
@@ -19,7 +19,11 @@ NotTEC
Transactor::invokePreflight<Change>(PreflightContext const& ctx)
{
// 0 means "Allow any flags"
if (auto const ret = preflight0(ctx, 0))
// The check for tfChangeMask is gated by LendingProtocol because that
// feature introduced this parameter, and it's not worth adding another
// amendment just for this.
if (auto const ret = preflight0(
ctx, ctx.rules.enabled(featureLendingProtocol) ? tfChangeMask : 0))
return ret;
auto account = ctx.tx.getAccountID(sfAccount);
+476 -115
View File
@@ -70,6 +70,8 @@ enum Privilege {
0x0400, // The transaction MAY delete an MPT object. May not create.
mustModifyVault =
0x0800, // The transaction must modify, delete or create, a vault
mayModifyVault =
0x1000, // The transaction MAY modify, delete or create, a vault
};
constexpr Privilege
operator|(Privilege lhs, Privilege rhs)
@@ -479,10 +481,10 @@ void
AccountRootsDeletedClean::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const&)
std::shared_ptr<SLE const> const& after)
{
if (isDelete && before && before->getType() == ltACCOUNT_ROOT)
accountsDeleted_.emplace_back(before);
accountsDeleted_.emplace_back(before, after);
}
bool
@@ -499,7 +501,8 @@ AccountRootsDeletedClean::finalize(
// be logged
[[maybe_unused]] bool const enforce =
view.rules().enabled(featureInvariantsV1_1) ||
view.rules().enabled(featureSingleAssetVault);
view.rules().enabled(featureSingleAssetVault) ||
view.rules().enabled(featureLendingProtocol);
auto const objectExists = [&view, enforce, &j](auto const& keylet) {
(void)enforce;
@@ -529,9 +532,33 @@ AccountRootsDeletedClean::finalize(
return false;
};
for (auto const& accountSLE : accountsDeleted_)
for (auto const& [before, after] : accountsDeleted_)
{
auto const accountID = accountSLE->getAccountID(sfAccount);
auto const accountID = before->getAccountID(sfAccount);
// An account should not be deleted with a balance
if (after->at(sfBalance) != beast::zero)
{
JLOG(j.fatal()) << "Invariant failed: account deletion left "
"behind a non-zero balance";
XRPL_ASSERT(
enforce,
"ripple::AccountRootsDeletedClean::finalize : "
"deleted account has zero balance");
if (enforce)
return false;
}
// An account should not be deleted with a non-zero owner count
if (after->at(sfOwnerCount) != 0)
{
JLOG(j.fatal()) << "Invariant failed: account deletion left "
"behind a non-zero owner count";
XRPL_ASSERT(
enforce,
"ripple::AccountRootsDeletedClean::finalize : "
"deleted account has zero owner count");
if (enforce)
return false;
}
// Simple types
for (auto const& [keyletfunc, _, __] : directAccountKeylets)
{
@@ -558,9 +585,9 @@ AccountRootsDeletedClean::finalize(
// also be deleted. e.g. AMM, Vault, etc.
for (auto const& field : getPseudoAccountFields())
{
if (accountSLE->isFieldPresent(*field))
if (before->isFieldPresent(*field))
{
auto const key = accountSLE->getFieldH256(*field);
auto const key = before->getFieldH256(*field);
if (objectExists(keylet::unchecked(key)) && enforce)
return false;
}
@@ -1019,7 +1046,9 @@ ValidNewAccountRoot::finalize(
if (hasPrivilege(tx, createAcct | createPseudoAcct) && result == tesSUCCESS)
{
bool const pseudoAccount =
(pseudoAccount_ && view.rules().enabled(featureSingleAssetVault));
(pseudoAccount_ &&
(view.rules().enabled(featureSingleAssetVault) ||
view.rules().enabled(featureLendingProtocol)));
if (pseudoAccount && !hasPrivilege(tx, createPseudoAcct))
{
@@ -1431,7 +1460,12 @@ ValidMPTIssuance::visitEntry(
if (isDelete)
mptokensDeleted_++;
else if (!before)
{
mptokensCreated_++;
MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)};
if (mptIssue.getIssuer() == after->at(sfAccount))
mptCreatedByIssuer_ = true;
}
}
}
@@ -1445,6 +1479,25 @@ ValidMPTIssuance::finalize(
{
if (result == tesSUCCESS)
{
auto const& rules = view.rules();
[[maybe_unused]]
bool enforceCreatedByIssuer = rules.enabled(featureSingleAssetVault) ||
rules.enabled(featureLendingProtocol);
if (mptCreatedByIssuer_)
{
JLOG(j.fatal())
<< "Invariant failed: MPToken created for the MPT issuer";
// The comment above starting with "assert(enforce)" explains this
// assert.
XRPL_ASSERT_PARTS(
enforceCreatedByIssuer,
"ripple::ValidMPTIssuance::finalize",
"no issuer MPToken");
if (enforceCreatedByIssuer)
return false;
}
auto const txnType = tx.getTxnType();
if (hasPrivilege(tx, createMPTIssuance))
{
if (mptIssuancesCreated_ == 0)
@@ -1487,15 +1540,14 @@ ValidMPTIssuance::finalize(
return mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1;
}
bool const lendingProtocolEnabled =
view.rules().enabled(featureLendingProtocol);
// ttESCROW_FINISH may authorize an MPT, but it can't have the
// mayAuthorizeMPT privilege, because that may cause
// non-amendment-gated side effects.
bool const enforceEscrowFinish = (tx.getTxnType() == ttESCROW_FINISH) &&
(view.rules().enabled(featureSingleAssetVault)
/*
TODO: Uncomment when LendingProtocol is defined
|| view.rules().enabled(featureLendingProtocol)*/
);
bool const enforceEscrowFinish = (txnType == ttESCROW_FINISH) &&
(view.rules().enabled(featureSingleAssetVault) ||
lendingProtocolEnabled);
if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) ||
enforceEscrowFinish)
{
@@ -1513,6 +1565,14 @@ ValidMPTIssuance::finalize(
"succeeded but deleted issuances";
return false;
}
else if (
lendingProtocolEnabled &&
mptokensCreated_ + mptokensDeleted_ > 1)
{
JLOG(j.fatal()) << "Invariant failed: MPT authorize succeeded "
"but created/deleted bad number mptokens";
return false;
}
else if (
submittedByIssuer &&
(mptokensCreated_ > 0 || mptokensDeleted_ > 0))
@@ -1536,7 +1596,7 @@ ValidMPTIssuance::finalize(
return true;
}
if (tx.getTxnType() == ttESCROW_FINISH)
if (txnType == ttESCROW_FINISH)
{
// ttESCROW_FINISH may authorize an MPT, but it can't have the
// mayAuthorizeMPT privilege, because that may cause
@@ -1676,104 +1736,6 @@ ValidPermissionedDomain::finalize(
(sleStatus_[1] ? check(*sleStatus_[1], j) : true);
}
//------------------------------------------------------------------------------
void
ValidPseudoAccounts::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete)
// Deletion is ignored
return;
if (after && after->getType() == ltACCOUNT_ROOT)
{
bool const isPseudo = [&]() {
// isPseudoAccount checks that any of the pseudo-account fields are
// set.
if (isPseudoAccount(after))
return true;
// Not all pseudo-accounts have a zero sequence, but all accounts
// with a zero sequence had better be pseudo-accounts.
if (after->at(sfSequence) == 0)
return true;
return false;
}();
if (isPseudo)
{
// Pseudo accounts must have the following properties:
// 1. Exactly one of the pseudo-account fields is set.
// 2. The sequence number is not changed.
// 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth
// flags are set.
// 4. The RegularKey is not set.
{
std::vector<SField const*> const& fields =
getPseudoAccountFields();
auto const numFields = std::count_if(
fields.begin(),
fields.end(),
[&after](SField const* sf) -> bool {
return after->isFieldPresent(*sf);
});
if (numFields != 1)
{
std::stringstream error;
error << "pseudo-account has " << numFields
<< " pseudo-account fields set";
errors_.emplace_back(error.str());
}
}
if (before && before->at(sfSequence) != after->at(sfSequence))
{
errors_.emplace_back("pseudo-account sequence changed");
}
if (!after->isFlag(
lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth))
{
errors_.emplace_back("pseudo-account flags are not set");
}
if (after->isFieldPresent(sfRegularKey))
{
errors_.emplace_back("pseudo-account has a regular key");
}
}
}
}
bool
ValidPseudoAccounts::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
bool const enforce = view.rules().enabled(featureSingleAssetVault);
// The comment above starting with "assert(enforce)" explains this assert.
XRPL_ASSERT(
errors_.empty() || enforce,
"ripple::ValidPseudoAccounts::finalize : no bad "
"changes or enforce invariant");
if (!errors_.empty())
{
for (auto const& error : errors_)
{
JLOG(j.fatal()) << "Invariant failed: " << error;
}
if (enforce)
return false;
}
return true;
}
//------------------------------------------------------------------------------
void
ValidPermissionedDEX::visitEntry(
bool,
@@ -2164,6 +2126,396 @@ ValidAMM::finalize(
//------------------------------------------------------------------------------
void
NoModifiedUnmodifiableFields::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete || !before)
// Creation and deletion are ignored
return;
changedEntries_.emplace(before, after);
}
bool
NoModifiedUnmodifiableFields::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
static auto const fieldChanged =
[](auto const& before, auto const& after, auto const& field) {
bool const beforeField = before->isFieldPresent(field);
bool const afterField = after->isFieldPresent(field);
return beforeField != afterField ||
(afterField && before->at(field) != after->at(field));
};
for (auto const& slePair : changedEntries_)
{
auto const& before = slePair.first;
auto const& after = slePair.second;
auto const type = after->getType();
bool bad = false;
[[maybe_unused]] bool enforce = false;
switch (type)
{
case ltLOAN_BROKER:
/*
* We check this invariant regardless of lending protocol
* amendment status, allowing for detection and logging of
* potential issues even when the amendment is disabled.
*/
enforce = view.rules().enabled(featureLendingProtocol);
bad = fieldChanged(before, after, sfLedgerEntryType) ||
fieldChanged(before, after, sfLedgerIndex) ||
fieldChanged(before, after, sfSequence) ||
fieldChanged(before, after, sfOwnerNode) ||
fieldChanged(before, after, sfVaultNode) ||
fieldChanged(before, after, sfVaultID) ||
fieldChanged(before, after, sfAccount) ||
fieldChanged(before, after, sfOwner) ||
fieldChanged(before, after, sfManagementFeeRate) ||
fieldChanged(before, after, sfCoverRateMinimum) ||
fieldChanged(before, after, sfCoverRateLiquidation);
break;
case ltLOAN:
/*
* We check this invariant regardless of lending protocol
* amendment status, allowing for detection and logging of
* potential issues even when the amendment is disabled.
*/
enforce = view.rules().enabled(featureLendingProtocol);
bad = fieldChanged(before, after, sfLedgerEntryType) ||
fieldChanged(before, after, sfLedgerIndex) ||
fieldChanged(before, after, sfSequence) ||
fieldChanged(before, after, sfOwnerNode) ||
fieldChanged(before, after, sfLoanBrokerNode) ||
fieldChanged(before, after, sfLoanBrokerID) ||
fieldChanged(before, after, sfBorrower) ||
fieldChanged(before, after, sfLoanOriginationFee) ||
fieldChanged(before, after, sfLoanServiceFee) ||
fieldChanged(before, after, sfLatePaymentFee) ||
fieldChanged(before, after, sfClosePaymentFee) ||
fieldChanged(before, after, sfOverpaymentFee) ||
fieldChanged(before, after, sfInterestRate) ||
fieldChanged(before, after, sfLateInterestRate) ||
fieldChanged(before, after, sfCloseInterestRate) ||
fieldChanged(before, after, sfOverpaymentInterestRate) ||
fieldChanged(before, after, sfStartDate) ||
fieldChanged(before, after, sfPaymentInterval) ||
fieldChanged(before, after, sfGracePeriod) ||
fieldChanged(before, after, sfLoanScale);
break;
default:
/*
* We check this invariant regardless of lending protocol
* amendment status, allowing for detection and logging of
* potential issues even when the amendment is disabled.
*
* We use the lending protocol as a gate, even though
* all transactions are affected because that's when it
* was added.
*/
enforce = view.rules().enabled(featureLendingProtocol);
bad = fieldChanged(before, after, sfLedgerEntryType) ||
fieldChanged(before, after, sfLedgerIndex);
}
XRPL_ASSERT(
!bad || enforce,
"ripple::NoModifiedUnmodifiableFields::finalize : no bad "
"changes or enforce invariant");
if (bad)
{
JLOG(j.fatal())
<< "Invariant failed: changed an unchangable field for "
<< tx.getTransactionID();
if (enforce)
return false;
}
}
return true;
}
//------------------------------------------------------------------------------
void
ValidPseudoAccounts::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete)
// Deletion is ignored
return;
if (after && after->getType() == ltACCOUNT_ROOT)
{
bool const isPseudo = [&]() {
// isPseudoAccount checks that any of the pseudo-account fields are
// set.
if (isPseudoAccount(after))
return true;
// Not all pseudo-accounts have a zero sequence, but all accounts
// with a zero sequence had better be pseudo-accounts.
if (after->at(sfSequence) == 0)
return true;
return false;
}();
if (isPseudo)
{
// Pseudo accounts must have the following properties:
// 1. Exactly one of the pseudo-account fields is set.
// 2. The sequence number is not changed.
// 3. The lsfDisableMaster, lsfDefaultRipple, and lsfDepositAuth
// flags are set.
// 4. The RegularKey is not set.
{
std::vector<SField const*> const& fields =
getPseudoAccountFields();
auto const numFields = std::count_if(
fields.begin(),
fields.end(),
[&after](SField const* sf) -> bool {
return after->isFieldPresent(*sf);
});
if (numFields != 1)
{
std::stringstream error;
error << "pseudo-account has " << numFields
<< " pseudo-account fields set";
errors_.emplace_back(error.str());
}
}
if (before && before->at(sfSequence) != after->at(sfSequence))
{
errors_.emplace_back("pseudo-account sequence changed");
}
if (!after->isFlag(
lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth))
{
errors_.emplace_back("pseudo-account flags are not set");
}
if (after->isFieldPresent(sfRegularKey))
{
errors_.emplace_back("pseudo-account has a regular key");
}
}
}
}
bool
ValidPseudoAccounts::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
bool const enforce = view.rules().enabled(featureSingleAssetVault);
XRPL_ASSERT(
errors_.empty() || enforce,
"ripple::ValidPseudoAccounts::finalize : no bad "
"changes or enforce invariant");
if (!errors_.empty())
{
for (auto const& error : errors_)
{
JLOG(j.fatal()) << "Invariant failed: " << error;
}
if (enforce)
return false;
}
return true;
}
//------------------------------------------------------------------------------
void
ValidLoanBroker::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (after && after->getType() == ltLOAN_BROKER)
{
brokers_.emplace_back(before, after);
}
}
bool
ValidLoanBroker::goodZeroDirectory(
ReadView const& view,
SLE::const_ref dir,
beast::Journal const& j) const
{
auto const next = dir->at(~sfIndexNext);
auto const prev = dir->at(~sfIndexPrevious);
if ((prev && *prev) || (next && *next))
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker with zero "
"OwnerCount has multiple directory pages";
return false;
}
auto indexes = dir->getFieldV256(sfIndexes);
if (indexes.size() > 1)
{
JLOG(j.fatal())
<< "Invariant failed: Loan Broker with zero "
"OwnerCount has multiple indexes in the Directory root";
return false;
}
if (indexes.size() == 1)
{
auto const index = indexes.value().front();
auto const sle = view.read(keylet::unchecked(index));
if (!sle)
{
JLOG(j.fatal())
<< "Invariant failed: Loan Broker directory corrupt";
return false;
}
if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN)
{
JLOG(j.fatal())
<< "Invariant failed: Loan Broker with zero "
"OwnerCount has an unexpected entry in the directory";
return false;
}
}
return true;
}
bool
ValidLoanBroker::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
// Loan Brokers will not exist on ledger if the Lending Protocol amendment
// is not enabled, so there's no need to check it.
for (auto const& [before, after] : brokers_)
{
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3123-invariants
// If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most
// one node (the root), which will only hold entries for `RippleState`
// or `MPToken` objects.
if (after->at(sfOwnerCount) == 0)
{
auto const dir = view.read(keylet::ownerDir(after->at(sfAccount)));
if (dir)
{
if (!goodZeroDirectory(view, dir, j))
{
return false;
}
}
}
if (before && before->at(sfLoanSequence) > after->at(sfLoanSequence))
{
JLOG(j.fatal()) << "Invariant failed: Loan Broker sequence number "
"decreased";
return false;
}
if (after->at(sfDebtTotal) < 0)
{
JLOG(j.fatal())
<< "Invariant failed: Loan Broker debt total is negative";
return false;
}
if (after->at(sfCoverAvailable) < 0)
{
JLOG(j.fatal())
<< "Invariant failed: Loan Broker cover available is negative";
return false;
}
}
return true;
}
//------------------------------------------------------------------------------
void
ValidLoan::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (after && after->getType() == ltLOAN)
{
loans_.emplace_back(before, after);
}
}
bool
ValidLoan::finalize(
STTx const& tx,
TER const,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
// Loans will not exist on ledger if the Lending Protocol amendment
// is not enabled, so there's no need to check it.
for (auto const& [before, after] : loans_)
{
// https://github.com/Tapanito/XRPL-Standards/blob/xls-66-lending-protocol/XLS-0066d-lending-protocol/README.md#3223-invariants
// If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
if (after->at(sfPaymentRemaining) == 0 &&
after->at(sfPrincipalOutstanding) != 0)
{
return false;
}
if (before &&
(before->isFlag(lsfLoanOverpayment) !=
after->isFlag(lsfLoanOverpayment)))
{
JLOG(j.fatal())
<< "Invariant failed: Loan Overpayment flag changed";
return false;
}
// Must not be negative - STNumber
for (auto const field :
{&sfLoanServiceFee,
&sfLatePaymentFee,
&sfClosePaymentFee,
&sfPrincipalOutstanding,
&sfTotalValueOutstanding,
&sfManagementFeeOutstanding})
{
if (after->at(*field) < 0)
{
JLOG(j.fatal()) << "Invariant failed: " << field->getName()
<< " is negative ";
return false;
}
}
// Must be positive - STNumber
for (auto const field : {
&sfPeriodicPayment,
})
{
if (after->at(*field) <= 0)
{
JLOG(j.fatal()) << "Invariant failed: " << field->getName()
<< " is zero or negative ";
return false;
}
}
}
return true;
}
ValidVault::Vault
ValidVault::Vault::make(SLE const& from)
{
@@ -2313,7 +2665,8 @@ ValidVault::finalize(
return true; // Not a vault operation
}
else if (!hasPrivilege(tx, mustModifyVault)) // TODO: mayModifyVault
else if (!(hasPrivilege(tx, mustModifyVault) ||
hasPrivilege(tx, mayModifyVault)))
{
JLOG(j.fatal()) << //
"Invariant failed: vault updated by a wrong transaction type";
@@ -2528,7 +2881,8 @@ ValidVault::finalize(
}
if (!beforeVault_.empty() &&
afterVault.lossUnrealized != beforeVault_[0].lossUnrealized)
afterVault.lossUnrealized != beforeVault_[0].lossUnrealized &&
txnType != ttLOAN_MANAGE && txnType != ttLOAN_PAY)
{
JLOG(j.fatal()) << //
"Invariant failed: vault transaction must not change loss "
@@ -3076,6 +3430,13 @@ ValidVault::finalize(
return result;
}
case ttLOAN_SET:
case ttLOAN_MANAGE:
case ttLOAN_PAY: {
// TBD
return true;
}
default:
// LCOV_EXCL_START
UNREACHABLE(
+134 -29
View File
@@ -158,7 +158,14 @@ public:
*/
class AccountRootsDeletedClean
{
std::vector<std::shared_ptr<SLE const>> accountsDeleted_;
// Pair is <before, after>. Before is used for most of the checks, so that
// if, for example, an object ID field is cleared, but the object is not
// deleted, it can still be found. After is used specifically for any checks
// that are expected as part of the deletion, such as zeroing out the
// balance.
std::vector<
std::pair<std::shared_ptr<SLE const>, std::shared_ptr<SLE const>>>
accountsDeleted_;
public:
void
@@ -550,6 +557,9 @@ class ValidMPTIssuance
std::uint32_t mptokensCreated_ = 0;
std::uint32_t mptokensDeleted_ = 0;
// non-MPT transactions may attempt to create
// MPToken by an issuer
bool mptCreatedByIssuer_ = false;
public:
void
@@ -602,34 +612,6 @@ public:
beast::Journal const&);
};
/**
* @brief Invariants: Pseudo-accounts have valid and consisent properties
*
* Pseudo-accounts have certain properties, and some of those properties are
* unique to pseudo-accounts. Check that all pseudo-accounts are following the
* rules, and that only pseudo-accounts look like pseudo-accounts.
*
*/
class ValidPseudoAccounts
{
std::vector<std::string> errors_;
public:
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
};
class ValidPermissionedDEX
{
bool regularOffers_ = false;
@@ -716,6 +698,126 @@ private:
};
/**
* @brief Invariants: Some fields are unmodifiable
*
* Check that any fields specified as unmodifiable are not modified when the
* object is modified. Creation and deletion are ignored.
*
*/
class NoModifiedUnmodifiableFields
{
// Pair is <before, after>.
std::set<std::pair<SLE::const_pointer, SLE::const_pointer>> changedEntries_;
public:
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
};
/**
* @brief Invariants: Pseudo-accounts have valid and consisent properties
*
* Pseudo-accounts have certain properties, and some of those properties are
* unique to pseudo-accounts. Check that all pseudo-accounts are following the
* rules, and that only pseudo-accounts look like pseudo-accounts.
*
*/
class ValidPseudoAccounts
{
std::vector<std::string> errors_;
public:
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
};
/**
* @brief Invariants: Loan brokers are internally consistent
*
* 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one
* node (the root), which will only hold entries for `RippleState` or
* `MPToken` objects.
*
*/
class ValidLoanBroker
{
// Pair is <before, after>. After is used for most of the checks, except
// those that check changed values.
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> brokers_;
bool
goodZeroDirectory(
ReadView const& view,
SLE::const_ref dir,
beast::Journal const& j) const;
public:
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
};
/**
* @brief Invariants: Loans are internally consistent
*
* 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
*
*/
class ValidLoan
{
// Pair is <before, after>. After is used for most of the checks, except
// those that check changed values.
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> loans_;
public:
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
};
/*
* @brief Invariants: Vault object and MPTokenIssuance for vault shares
*
* - vault deleted and vault created is empty
@@ -805,7 +907,10 @@ using InvariantChecks = std::tuple<
ValidPermissionedDomain,
ValidPermissionedDEX,
ValidAMM,
NoModifiedUnmodifiableFields,
ValidPseudoAccounts,
ValidLoanBroker,
ValidLoan,
ValidVault>;
/**
@@ -0,0 +1,336 @@
#include <xrpld/app/tx/detail/LoanBrokerCoverClawback.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
namespace ripple {
bool
LoanBrokerCoverClawback::checkExtraFeatures(PreflightContext const& ctx)
{
return checkLendingProtocolDependencies(ctx);
}
NotTEC
LoanBrokerCoverClawback::preflight(PreflightContext const& ctx)
{
auto const brokerID = ctx.tx[~sfLoanBrokerID];
auto const amount = ctx.tx[~sfAmount];
if (!brokerID && !amount)
return temINVALID;
if (brokerID && *brokerID == beast::zero)
return temINVALID;
if (amount)
{
// XRP has no counterparty, and thus nobody can claw it back
if (amount->native())
return temBAD_AMOUNT;
// Zero is OK, and indicates "take it all" (down to the minimum cover)
if (*amount < beast::zero)
return temBAD_AMOUNT;
// This should be redundant
if (!isLegalNet(*amount))
return temBAD_AMOUNT; // LCOV_EXCL_LINE
if (!brokerID)
{
if (amount->holds<MPTIssue>())
return temINVALID;
auto const account = ctx.tx[sfAccount];
// Since we don't have a LoanBrokerID, holder _should_ be the loan
// broker's pseudo-account, but we don't know yet whether it is, so
// use a generic placeholder name.
auto const holder = amount->getIssuer();
if (holder == account || holder == beast::zero)
return temINVALID;
}
}
return tesSUCCESS;
}
Expected<uint256, TER>
determineBrokerID(ReadView const& view, STTx const& tx)
{
// If the broker ID was provided in the transaction, that's all we
// need.
if (auto const brokerID = tx[~sfLoanBrokerID])
return *brokerID;
// If the broker ID was not provided, and the amount is either
// absent or holds a non-IOU - including MPT, something went wrong,
// because that should have been rejected in preflight().
auto const dstAmount = tx[~sfAmount];
if (!dstAmount || !dstAmount->holds<Issue>())
return Unexpected{tecINTERNAL}; // LCOV_EXCL_LINE
// Every trust line is bidirectional. Both sides are simultaneously
// issuer and holder. For this transaction, the Account is acting as
// a holder, and clawing back funds from the LoanBroker
// Pseudo-account acting as holder. If the Amount is an IOU, and the
// `issuer` field specified in that Amount is a LoanBroker
// Pseudo-account, we can get the LoanBrokerID from there.
//
// Thus, Amount.issuer _should_ be the loan broker's
// pseudo-account, but we don't know yet whether it is.
auto const maybePseudo = dstAmount->getIssuer();
auto const sle = view.read(keylet::account(maybePseudo));
// If the account was not found, the transaction can't go further.
if (!sle)
return Unexpected{tecNO_ENTRY};
// If the account was found, and has a LoanBrokerID (and therefore
// is a pseudo-account), that's the
// answer we need.
if (auto const brokerID = sle->at(~sfLoanBrokerID))
return *brokerID;
// If the account does not have a LoanBrokerID, the transaction
// can't go further, even if it's a different type of Pseudo-account.
return Unexpected{tecOBJECT_NOT_FOUND};
// Or tecWRONG_ASSET?
}
Expected<Asset, TER>
determineAsset(
ReadView const& view,
AccountID const& account,
AccountID const& brokerPseudoAccountID,
STAmount const& amount)
{
if (amount.holds<MPTIssue>())
return amount.asset();
// An IOU has an issue, which could be either end of the trust line.
// This check only applies to IOUs
auto const holder = amount.getIssuer();
// holder can be the submitting account (the issuer of the asset) if a
// LoanBrokerID was provided in the transaction.
if (holder == account)
{
return amount.asset();
}
else if (holder == brokerPseudoAccountID)
{
// We want the asset to match the vault asset, so use the account as the
// issuer
return Issue{amount.getCurrency(), account};
}
else
return Unexpected(tecWRONG_ASSET);
}
Expected<STAmount, TER>
determineClawAmount(
SLE const& sleBroker,
Asset const& vaultAsset,
std::optional<STAmount> const& amount)
{
auto const maxClawAmount = [&]() {
// Always round the minimum required up
NumberRoundModeGuard mg1(Number::upward);
auto const minRequiredCover = tenthBipsOfValue(
sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]));
// The subtraction probably won't round, but round down if it does.
NumberRoundModeGuard mg2(Number::downward);
return sleBroker[sfCoverAvailable] - minRequiredCover;
}();
if (maxClawAmount <= beast::zero)
return Unexpected(tecINSUFFICIENT_FUNDS);
// Use the vaultAsset here, because it will be the right type in all
// circumstances. The amount may be an IOU indicating the pseudo-account's
// asset, which is correct, but not what is needed here.
if (!amount || *amount == beast::zero)
return STAmount{vaultAsset, maxClawAmount};
Number const magnitude{*amount};
if (magnitude > maxClawAmount)
return STAmount{vaultAsset, maxClawAmount};
return STAmount{vaultAsset, magnitude};
}
template <ValidIssueType T>
static TER
preclaimHelper(
PreclaimContext const& ctx,
SLE const& sleIssuer,
STAmount const& clawAmount);
template <>
TER
preclaimHelper<Issue>(
PreclaimContext const& ctx,
SLE const& sleIssuer,
STAmount const& clawAmount)
{
// If AllowTrustLineClawback is not set or NoFreeze is set, return no
// permission
if (!(sleIssuer.isFlag(lsfAllowTrustLineClawback)) ||
(sleIssuer.isFlag(lsfNoFreeze)))
return tecNO_PERMISSION;
return tesSUCCESS;
}
template <>
TER
preclaimHelper<MPTIssue>(
PreclaimContext const& ctx,
SLE const& sleIssuer,
STAmount const& clawAmount)
{
auto const issuanceKey =
keylet::mptIssuance(clawAmount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanClawback) ||
!sleIssuance->isFlag(lsfMPTCanLock))
return tecNO_PERMISSION;
// With all the checking already done, this should be impossible
if (sleIssuance->at(sfIssuer) != sleIssuer[sfAccount])
return tecINTERNAL; // LCOV_EXCL_LINE
return tesSUCCESS;
}
TER
LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const findBrokerID = determineBrokerID(ctx.view, tx);
if (!findBrokerID)
return findBrokerID.error();
auto const brokerID = *findBrokerID;
auto const amount = tx[~sfAmount];
auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
if (!sleBroker)
{
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
auto const brokerPseudoAccountID = sleBroker->at(sfAccount);
auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)));
if (!vault)
return tecINTERNAL;
auto const vaultAsset = vault->at(sfAsset);
if (vaultAsset.native())
{
JLOG(ctx.j.warn()) << "Cannot clawback native asset.";
return tecNO_PERMISSION;
}
// Only the issuer of the vault asset can claw it back from the broker's
// cover funds.
if (vaultAsset.getIssuer() != account)
{
JLOG(ctx.j.warn()) << "Account is not the issuer of the vault asset.";
return tecNO_PERMISSION;
}
if (amount)
{
auto const findAsset =
determineAsset(ctx.view, account, brokerPseudoAccountID, *amount);
if (!findAsset)
return findAsset.error();
auto const txAsset = *findAsset;
if (txAsset != vaultAsset)
{
JLOG(ctx.j.warn()) << "Account is the correct issuer, but trying "
"to clawback the wrong asset from LoanBroker";
return tecWRONG_ASSET;
}
}
auto const findClawAmount =
determineClawAmount(*sleBroker, vaultAsset, amount);
if (!findClawAmount)
{
JLOG(ctx.j.warn()) << "LoanBroker cover is already at minimum.";
return findClawAmount.error();
}
STAmount const clawAmount = *findClawAmount;
// Explicitly check the balance of the trust line / MPT to make sure the
// balance is actually there. It should always match `stCoverAvailable`, so
// if there isn't, this is an internal error.
if (accountHolds(
ctx.view,
brokerPseudoAccountID,
vaultAsset,
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
ctx.j) < clawAmount)
return tecINTERNAL; // tecINSUFFICIENT_FUNDS; LCOV_EXCL_LINE
// Check if the vault asset issuer has the correct flags
auto const sleIssuer =
ctx.view.read(keylet::account(vaultAsset.getIssuer()));
return std::visit(
[&]<typename T>(T const&) {
return preclaimHelper<T>(ctx, *sleIssuer, clawAmount);
},
vaultAsset.value());
}
TER
LoanBrokerCoverClawback::doApply()
{
auto const& tx = ctx_.tx;
auto const account = tx[sfAccount];
auto const findBrokerID = determineBrokerID(view(), tx);
if (!findBrokerID)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const brokerID = *findBrokerID;
auto const amount = tx[~sfAmount];
auto sleBroker = view().peek(keylet::loanbroker(brokerID));
if (!sleBroker)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const brokerPseudoID = *sleBroker->at(sfAccount);
auto const vault = view().read(keylet::vault(sleBroker->at(sfVaultID)));
if (!vault)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const vaultAsset = vault->at(sfAsset);
auto const findClawAmount =
determineClawAmount(*sleBroker, vaultAsset, amount);
if (!findClawAmount)
return tecINTERNAL; // LCOV_EXCL_LINE
STAmount const clawAmount = *findClawAmount;
// Just for paranoia's sake
if (clawAmount.native())
return tecINTERNAL; // LCOV_EXCL_LINE
// Decrease the LoanBroker's CoverAvailable by Amount
sleBroker->at(sfCoverAvailable) -= clawAmount;
view().update(sleBroker);
// Transfer assets from pseudo-account to depositor.
return accountSend(
view(), brokerPseudoID, account, clawAmount, j_, WaiveTransferFee::Yes);
}
//------------------------------------------------------------------------------
} // namespace ripple
@@ -0,0 +1,34 @@
#ifndef XRPL_TX_LOANBROKERCOVERCLAWBACK_H_INCLUDED
#define XRPL_TX_LOANBROKERCOVERCLAWBACK_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanBrokerCoverClawback : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanBrokerCoverClawback(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif
@@ -0,0 +1,118 @@
#include <xrpld/app/tx/detail/LoanBrokerCoverDeposit.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
namespace ripple {
bool
LoanBrokerCoverDeposit::checkExtraFeatures(PreflightContext const& ctx)
{
return checkLendingProtocolDependencies(ctx);
}
NotTEC
LoanBrokerCoverDeposit::preflight(PreflightContext const& ctx)
{
if (ctx.tx[sfLoanBrokerID] == beast::zero)
return temINVALID;
auto const dstAmount = ctx.tx[sfAmount];
if (dstAmount <= beast::zero)
return temBAD_AMOUNT;
if (!isLegalNet(dstAmount))
return temBAD_AMOUNT;
return tesSUCCESS;
}
TER
LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const brokerID = tx[sfLoanBrokerID];
auto const amount = tx[sfAmount];
auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
if (!sleBroker)
{
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
if (account != sleBroker->at(sfOwner))
{
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)));
if (!vault)
{
JLOG(ctx.j.fatal()) << "Vault is missing for Broker " << brokerID;
return tefBAD_LEDGER;
}
auto const vaultAsset = vault->at(sfAsset);
if (amount.asset() != vaultAsset)
return tecWRONG_ASSET;
auto const pseudoAccountID = sleBroker->at(sfAccount);
// Cannot transfer a frozen Asset
if (auto const ret = checkFrozen(ctx.view, account, vaultAsset))
return ret;
// Pseudo-account cannot receive if asset is deep frozen
if (auto const ret = checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset))
return ret;
// Cannot transfer unauthorized asset
if (auto const ret =
requireAuth(ctx.view, vaultAsset, account, AuthType::StrongAuth))
return ret;
if (accountHolds(
ctx.view,
account,
vaultAsset,
FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahZERO_IF_UNAUTHORIZED,
ctx.j) < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
TER
LoanBrokerCoverDeposit::doApply()
{
auto const& tx = ctx_.tx;
auto const brokerID = tx[sfLoanBrokerID];
auto const amount = tx[sfAmount];
auto broker = view().peek(keylet::loanbroker(brokerID));
if (!broker)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const brokerPseudoID = broker->at(sfAccount);
// Transfer assets from depositor to pseudo-account.
if (auto ter = accountSend(
view(),
account_,
brokerPseudoID,
amount,
j_,
WaiveTransferFee::Yes))
return ter;
// Increase the LoanBroker's CoverAvailable by Amount
broker->at(sfCoverAvailable) += amount;
view().update(broker);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple
@@ -0,0 +1,34 @@
#ifndef XRPL_TX_LOANBROKERCOVERDEPOSIT_H_INCLUDED
#define XRPL_TX_LOANBROKERCOVERDEPOSIT_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanBrokerCoverDeposit : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanBrokerCoverDeposit(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif
@@ -0,0 +1,208 @@
#include <xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/tx/detail/Payment.h>
namespace ripple {
bool
LoanBrokerCoverWithdraw::checkExtraFeatures(PreflightContext const& ctx)
{
return checkLendingProtocolDependencies(ctx);
}
NotTEC
LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx)
{
if (ctx.tx[sfLoanBrokerID] == beast::zero)
return temINVALID;
auto const dstAmount = ctx.tx[sfAmount];
if (dstAmount <= beast::zero)
return temBAD_AMOUNT;
if (!isLegalNet(dstAmount))
return temBAD_AMOUNT;
if (auto const destination = ctx.tx[~sfDestination])
{
if (*destination == beast::zero)
{
return temMALFORMED;
}
}
return tesSUCCESS;
}
TER
LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const brokerID = tx[sfLoanBrokerID];
auto const amount = tx[sfAmount];
auto const dstAcct = tx[~sfDestination].value_or(account);
auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
if (!sleBroker)
{
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
if (account != sleBroker->at(sfOwner))
{
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
auto const vault = ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)));
if (!vault)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultAsset = vault->at(sfAsset);
if (amount.asset() != vaultAsset)
return tecWRONG_ASSET;
// Withdrawal to a 3rd party destination account is essentially a transfer.
// Enforce all the usual asset transfer checks.
AuthType authType = AuthType::Legacy;
if (account != dstAcct)
{
if (auto const ret = canWithdraw(ctx.view, tx))
return ret;
// The destination account must have consented to receive the asset by
// creating a RippleState or MPToken
authType = AuthType::StrongAuth;
}
// Destination MPToken must exist (if asset is an MPT)
if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType))
return ter;
// The broker's pseudo-account is the source of funds.
auto const pseudoAccountID = sleBroker->at(sfAccount);
// Check for freezes, unless sending directly to the issuer
if (dstAcct != vaultAsset.getIssuer())
{
// Cannot send a frozen Asset
if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset))
return ret;
// Destination account cannot receive if asset is deep frozen
if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset))
return ret;
}
auto const coverAvail = sleBroker->at(sfCoverAvailable);
// Cover Rate is in 1/10 bips units
auto const currentDebtTotal = sleBroker->at(sfDebtTotal);
auto const minimumCover = [&]() {
// Always round the minimum required up.
// Applies to `tenthBipsOfValue` as well as `roundToAsset`.
NumberRoundModeGuard mg(Number::upward);
return roundToAsset(
vaultAsset,
tenthBipsOfValue(
currentDebtTotal,
TenthBips32(sleBroker->at(sfCoverRateMinimum))),
currentDebtTotal.exponent());
}();
if (coverAvail < amount)
return tecINSUFFICIENT_FUNDS;
if ((coverAvail - amount) < minimumCover)
return tecINSUFFICIENT_FUNDS;
if (accountHolds(
ctx.view,
pseudoAccountID,
vaultAsset,
FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahZERO_IF_UNAUTHORIZED,
ctx.j) < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
TER
LoanBrokerCoverWithdraw::doApply()
{
auto const& tx = ctx_.tx;
auto const brokerID = tx[sfLoanBrokerID];
auto const amount = tx[sfAmount];
auto const dstAcct = tx[~sfDestination].value_or(account_);
auto broker = view().peek(keylet::loanbroker(brokerID));
if (!broker)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const brokerPseudoID = *broker->at(sfAccount);
// Decrease the LoanBroker's CoverAvailable by Amount
broker->at(sfCoverAvailable) -= amount;
view().update(broker);
// Move the funds from the broker's pseudo-account to the dstAcct
if (dstAcct == account_ || amount.native())
{
// Transfer assets directly from pseudo-account to depositor.
// Because this is either a self-transfer or an XRP payment, there is no
// need to use the payment engine.
return accountSend(
view(), brokerPseudoID, dstAcct, amount, j_, WaiveTransferFee::Yes);
}
{
// If sending the Cover to a different account, then this is
// effectively a payment. Use the Payment transaction code to call
// the payment engine, though only a subset of the functionality is
// supported in this transaction. e.g. No paths, no partial
// payments.
bool const mptDirect = amount.holds<MPTIssue>();
STAmount const maxSourceAmount =
Payment::getMaxSourceAmount(brokerPseudoID, amount);
SLE::pointer sleDst = view().peek(keylet::account(dstAcct));
if (!sleDst)
return tecINTERNAL; // LCOV_EXCL_LINE
Payment::RipplePaymentParams paymentParams{
.ctx = ctx_,
.maxSourceAmount = maxSourceAmount,
.srcAccountID = brokerPseudoID,
.dstAccountID = dstAcct,
.sleDst = sleDst,
.dstAmount = amount,
.paths = STPathSet{},
.deliverMin = std::nullopt,
.j = j_};
TER ret;
if (mptDirect)
{
ret = Payment::makeMPTDirectPayment(paymentParams);
}
else
{
ret = Payment::makeRipplePayment(paymentParams);
}
// Always claim a fee
if (!isTesSuccess(ret) && !isTecClaim(ret))
{
JLOG(j_.info())
<< "LoanBrokerCoverWithdraw: changing result from "
<< transToken(ret)
<< " to tecPATH_DRY for IOU payment with Destination";
return tecPATH_DRY;
}
return ret;
}
}
//------------------------------------------------------------------------------
} // namespace ripple
@@ -0,0 +1,34 @@
#ifndef XRPL_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED
#define XRPL_TX_LOANBROKERCOVERWITHDRAW_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanBrokerCoverWithdraw : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanBrokerCoverWithdraw(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif
@@ -0,0 +1,144 @@
#include <xrpld/app/tx/detail/LoanBrokerDelete.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
namespace ripple {
bool
LoanBrokerDelete::checkExtraFeatures(PreflightContext const& ctx)
{
return checkLendingProtocolDependencies(ctx);
}
NotTEC
LoanBrokerDelete::preflight(PreflightContext const& ctx)
{
if (ctx.tx[sfLoanBrokerID] == beast::zero)
return temINVALID;
return tesSUCCESS;
}
TER
LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const brokerID = tx[sfLoanBrokerID];
auto const sleBroker = ctx.view.read(keylet::loanbroker(brokerID));
if (!sleBroker)
{
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
if (account != sleBroker->at(sfOwner))
{
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
if (auto const ownerCount = sleBroker->at(sfOwnerCount); ownerCount != 0)
{
JLOG(ctx.j.warn()) << "LoanBrokerDelete: Owner count is " << ownerCount;
return tecHAS_OBLIGATIONS;
}
return tesSUCCESS;
}
TER
LoanBrokerDelete::doApply()
{
auto const& tx = ctx_.tx;
auto const brokerID = tx[sfLoanBrokerID];
// Delete the loan broker
auto broker = view().peek(keylet::loanbroker(brokerID));
if (!broker)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultID = broker->at(sfVaultID);
auto const sleVault = view().read(keylet::vault(vaultID));
if (!sleVault)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultPseudoID = sleVault->at(sfAccount);
auto const vaultAsset = sleVault->at(sfAsset);
auto const brokerPseudoID = broker->at(sfAccount);
if (!view().dirRemove(
keylet::ownerDir(account_),
broker->at(sfOwnerNode),
broker->key(),
false))
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
if (!view().dirRemove(
keylet::ownerDir(vaultPseudoID),
broker->at(sfVaultNode),
broker->key(),
false))
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
{
auto const coverAvailable =
STAmount{vaultAsset, broker->at(sfCoverAvailable)};
if (auto const ter = accountSend(
view(),
brokerPseudoID,
account_,
coverAvailable,
j_,
WaiveTransferFee::Yes))
return ter;
}
if (auto ter = removeEmptyHolding(view(), brokerPseudoID, vaultAsset, j_))
return ter;
auto brokerPseudoSLE = view().peek(keylet::account(brokerPseudoID));
if (!brokerPseudoSLE)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Making the payment and removing the empty holding should have deleted any
// obligations associated with the broker or broker pseudo-account.
if (*brokerPseudoSLE->at(sfBalance))
{
JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account has a balance";
return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE
}
if (brokerPseudoSLE->at(sfOwnerCount) != 0)
{
JLOG(j_.warn())
<< "LoanBrokerDelete: Pseudo-account still owns objects";
return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE
}
if (auto const directory = keylet::ownerDir(brokerPseudoID);
view().read(directory))
{
JLOG(j_.warn()) << "LoanBrokerDelete: Pseudo-account has a directory";
return tecHAS_OBLIGATIONS; // LCOV_EXCL_LINE
}
view().erase(brokerPseudoSLE);
view().erase(broker);
{
auto owner = view().peek(keylet::account(account_));
if (!owner)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
adjustOwnerCount(view(), owner, -2, j_);
}
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple
@@ -0,0 +1,34 @@
#ifndef XRPL_TX_LOANBROKERDELETE_H_INCLUDED
#define XRPL_TX_LOANBROKERDELETE_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanBrokerDelete : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanBrokerDelete(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif
+190
View File
@@ -0,0 +1,190 @@
#include <xrpld/app/tx/detail/LoanBrokerSet.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
namespace ripple {
bool
LoanBrokerSet::checkExtraFeatures(PreflightContext const& ctx)
{
return checkLendingProtocolDependencies(ctx);
}
NotTEC
LoanBrokerSet::preflight(PreflightContext const& ctx)
{
auto const& tx = ctx.tx;
if (auto const data = tx[~sfData]; data && !data->empty() &&
!validDataLength(tx[~sfData], maxDataPayloadLength))
return temINVALID;
if (!validNumericRange(tx[~sfManagementFeeRate], maxManagementFeeRate))
return temINVALID;
if (!validNumericRange(tx[~sfCoverRateMinimum], maxCoverRate))
return temINVALID;
if (!validNumericRange(tx[~sfCoverRateLiquidation], maxCoverRate))
return temINVALID;
if (!validNumericRange(
tx[~sfDebtMaximum], Number(maxMPTokenAmount), Number(0)))
return temINVALID;
if (tx.isFieldPresent(sfLoanBrokerID))
{
// Fixed fields can not be specified if we're modifying an existing
// LoanBroker Object
if (tx.isFieldPresent(sfManagementFeeRate) ||
tx.isFieldPresent(sfCoverRateMinimum) ||
tx.isFieldPresent(sfCoverRateLiquidation))
return temINVALID;
if (tx[sfLoanBrokerID] == beast::zero)
return temINVALID;
}
if (auto const vaultID = tx.at(~sfVaultID))
{
if (*vaultID == beast::zero)
return temINVALID;
}
{
auto const minimumZero = tx[~sfCoverRateMinimum].value_or(0) == 0;
auto const liquidationZero =
tx[~sfCoverRateLiquidation].value_or(0) == 0;
// Both must be zero or non-zero.
if (minimumZero != liquidationZero)
{
return temINVALID;
}
}
return tesSUCCESS;
}
TER
LoanBrokerSet::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const vaultID = tx[sfVaultID];
if (auto const brokerID = tx[~sfLoanBrokerID])
{
auto const sleBroker = ctx.view.read(keylet::loanbroker(*brokerID));
if (!sleBroker)
{
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
if (vaultID != sleBroker->at(sfVaultID))
{
JLOG(ctx.j.warn())
<< "Can not change VaultID on an existing LoanBroker.";
return tecNO_PERMISSION;
}
if (account != sleBroker->at(sfOwner))
{
JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker.";
return tecNO_PERMISSION;
}
}
else
{
auto const sleVault = ctx.view.read(keylet::vault(vaultID));
if (!sleVault)
{
JLOG(ctx.j.warn()) << "Vault does not exist.";
return tecNO_ENTRY;
}
if (account != sleVault->at(sfOwner))
{
JLOG(ctx.j.warn()) << "Account is not the owner of the Vault.";
return tecNO_PERMISSION;
}
if (auto const ter = canAddHolding(ctx.view, sleVault->at(sfAsset)))
return ter;
}
return tesSUCCESS;
}
TER
LoanBrokerSet::doApply()
{
auto const& tx = ctx_.tx;
auto& view = ctx_.view();
if (auto const brokerID = tx[~sfLoanBrokerID])
{
// Modify an existing LoanBroker
auto broker = view.peek(keylet::loanbroker(*brokerID));
if (!broker)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
if (auto const data = tx[~sfData])
broker->at(sfData) = *data;
if (auto const debtMax = tx[~sfDebtMaximum])
broker->at(sfDebtMaximum) = *debtMax;
view.update(broker);
}
else
{
// Create a new LoanBroker pointing back to the given Vault
auto const vaultID = tx[sfVaultID];
auto const sleVault = view.read(keylet::vault(vaultID));
auto const vaultPseudoID = sleVault->at(sfAccount);
auto const sequence = tx.getSeqValue();
auto owner = view.peek(keylet::account(account_));
if (!owner)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto broker =
std::make_shared<SLE>(keylet::loanbroker(account_, sequence));
if (auto const ter = dirLink(view, account_, broker))
return ter;
if (auto const ter = dirLink(view, vaultPseudoID, broker, sfVaultNode))
return ter;
adjustOwnerCount(view, owner, 2, j_);
auto const ownerCount = owner->at(sfOwnerCount);
if (mPriorBalance < view.fees().accountReserve(ownerCount))
return tecINSUFFICIENT_RESERVE;
auto maybePseudo =
createPseudoAccount(view, broker->key(), sfLoanBrokerID);
if (!maybePseudo)
return maybePseudo.error();
auto& pseudo = *maybePseudo;
auto pseudoId = pseudo->at(sfAccount);
if (auto ter = addEmptyHolding(
view, pseudoId, mPriorBalance, sleVault->at(sfAsset), j_))
return ter;
// Initialize data fields:
broker->at(sfSequence) = sequence;
broker->at(sfVaultID) = vaultID;
broker->at(sfOwner) = account_;
broker->at(sfAccount) = pseudoId;
broker->at(sfLoanSequence) = 1;
if (auto const data = tx[~sfData])
broker->at(sfData) = *data;
if (auto const rate = tx[~sfManagementFeeRate])
broker->at(sfManagementFeeRate) = *rate;
if (auto const debtMax = tx[~sfDebtMaximum])
broker->at(sfDebtMaximum) = *debtMax;
if (auto const coverMin = tx[~sfCoverRateMinimum])
broker->at(sfCoverRateMinimum) = *coverMin;
if (auto const coverLiq = tx[~sfCoverRateLiquidation])
broker->at(sfCoverRateLiquidation) = *coverLiq;
view.insert(broker);
}
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple
+34
View File
@@ -0,0 +1,34 @@
#ifndef XRPL_TX_LOANBROKERSET_H_INCLUDED
#define XRPL_TX_LOANBROKERSET_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanBrokerSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanBrokerSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif
+113
View File
@@ -0,0 +1,113 @@
#include <xrpld/app/tx/detail/LoanDelete.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
namespace ripple {
bool
LoanDelete::checkExtraFeatures(PreflightContext const& ctx)
{
return checkLendingProtocolDependencies(ctx);
}
NotTEC
LoanDelete::preflight(PreflightContext const& ctx)
{
if (ctx.tx[sfLoanID] == beast::zero)
return temINVALID;
return tesSUCCESS;
}
TER
LoanDelete::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const loanID = tx[sfLoanID];
auto const loanSle = ctx.view.read(keylet::loan(loanID));
if (!loanSle)
{
JLOG(ctx.j.warn()) << "Loan does not exist.";
return tecNO_ENTRY;
}
if (loanSle->at(sfPaymentRemaining) > 0)
{
JLOG(ctx.j.warn()) << "Active loan can not be deleted.";
return tecHAS_OBLIGATIONS;
}
auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
if (!loanBrokerSle)
{
// should be impossible
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (loanBrokerSle->at(sfOwner) != account &&
loanSle->at(sfBorrower) != account)
{
JLOG(ctx.j.warn())
<< "Account is not Loan Broker Owner or Loan Borrower.";
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
TER
LoanDelete::doApply()
{
auto const& tx = ctx_.tx;
auto& view = ctx_.view();
auto const loanID = tx[sfLoanID];
auto const loanSle = view.peek(keylet::loan(loanID));
if (!loanSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const borrower = loanSle->at(sfBorrower);
auto const borrowerSle = view.peek(keylet::account(borrower));
if (!borrowerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerID = loanSle->at(sfLoanBrokerID);
auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerPseudoAccount = brokerSle->at(sfAccount);
auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID)));
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Remove LoanID from Directory of the LoanBroker pseudo-account.
if (!view.dirRemove(
keylet::ownerDir(brokerPseudoAccount),
loanSle->at(sfLoanBrokerNode),
loanID,
false))
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Remove LoanID from Directory of the Borrower.
if (!view.dirRemove(
keylet::ownerDir(borrower),
loanSle->at(sfOwnerNode),
loanID,
false))
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Delete the Loan object
view.erase(loanSle);
// Decrement the LoanBroker's owner count.
adjustOwnerCount(view, brokerSle, -1, j_);
// Decrement the borrower's owner count
adjustOwnerCount(view, borrowerSle, -1, j_);
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple
+34
View File
@@ -0,0 +1,34 @@
#ifndef XRPL_TX_LOANDELETE_H_INCLUDED
#define XRPL_TX_LOANDELETE_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanDelete : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanDelete(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif
+420
View File
@@ -0,0 +1,420 @@
#include <xrpld/app/tx/detail/LoanManage.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
bool
LoanManage::checkExtraFeatures(PreflightContext const& ctx)
{
return checkLendingProtocolDependencies(ctx);
}
std::uint32_t
LoanManage::getFlagsMask(PreflightContext const& ctx)
{
return tfLoanManageMask;
}
NotTEC
LoanManage::preflight(PreflightContext const& ctx)
{
if (ctx.tx[sfLoanID] == beast::zero)
return temINVALID;
// Flags are mutually exclusive
if (auto const flagField = ctx.tx[~sfFlags]; flagField && *flagField)
{
auto const flags = *flagField & tfUniversalMask;
if ((flags & (flags - 1)) != 0)
{
JLOG(ctx.j.warn())
<< "LoanManage: Only one of tfLoanDefault, tfLoanImpair, or "
"tfLoanUnimpair can be set.";
return temINVALID_FLAG;
}
}
return tesSUCCESS;
}
TER
LoanManage::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const loanID = tx[sfLoanID];
auto const loanSle = ctx.view.read(keylet::loan(loanID));
if (!loanSle)
{
JLOG(ctx.j.warn()) << "Loan does not exist.";
return tecNO_ENTRY;
}
// Impairment only allows certain transitions.
// 1. Once it's in default, it can't be changed.
// 2. It can get worse: unimpaired -> impaired -> default
// or unimpaired -> default
// 3. It can get better: impaired -> unimpaired
// 4. If it's in a state, it can't be put in that state again.
if (loanSle->isFlag(lsfLoanDefault))
{
JLOG(ctx.j.warn())
<< "Loan is in default. A defaulted loan can not be modified.";
return tecNO_PERMISSION;
}
if (loanSle->isFlag(lsfLoanImpaired) && tx.isFlag(tfLoanImpair))
{
JLOG(ctx.j.warn())
<< "Loan is impaired. A loan can not be impaired twice.";
return tecNO_PERMISSION;
}
if (!(loanSle->isFlag(lsfLoanImpaired) ||
loanSle->isFlag(lsfLoanDefault)) &&
(tx.isFlag(tfLoanUnimpair)))
{
JLOG(ctx.j.warn())
<< "Loan is unimpaired. Can not be unimpaired again.";
return tecNO_PERMISSION;
}
if (loanSle->at(sfPaymentRemaining) == 0)
{
JLOG(ctx.j.warn()) << "Loan is fully paid. A loan can not be modified "
"after it is fully paid.";
return tecNO_PERMISSION;
}
if (tx.isFlag(tfLoanDefault) &&
!hasExpired(
ctx.view,
loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod)))
{
JLOG(ctx.j.warn())
<< "A loan can not be defaulted before the next payment due date.";
return tecTOO_SOON;
}
auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
if (!loanBrokerSle)
{
// should be impossible
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (loanBrokerSle->at(sfOwner) != account)
{
JLOG(ctx.j.warn())
<< "LoanBroker for Loan does not belong to the account. LoanModify "
"can only be submitted by the Loan Broker.";
return tecNO_PERMISSION;
}
return tesSUCCESS;
}
Number
owedToVault(SLE::ref loanSle)
{
return loanSle->at(sfTotalValueOutstanding) -
loanSle->at(sfManagementFeeOutstanding);
}
TER
LoanManage::defaultLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref brokerSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j)
{
// Calculate the amount of the Default that First-Loss Capital covers:
std::int32_t const loanScale = loanSle->at(sfLoanScale);
auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal);
auto principalOutstandingProxy = loanSle->at(sfPrincipalOutstanding);
auto managementFeeOutstandingProxy =
loanSle->at(sfManagementFeeOutstanding);
auto totalValueOutstandingProxy = loanSle->at(sfTotalValueOutstanding);
auto paymentRemainingProxy = loanSle->at(sfPaymentRemaining);
Number const totalDefaultAmount = owedToVault(loanSle);
// Apply the First-Loss Capital to the Default Amount
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
TenthBips32 const coverRateLiquidation{
brokerSle->at(sfCoverRateLiquidation)};
auto const defaultCovered = [&]() {
// Always round the minimum required up.
NumberRoundModeGuard mg(Number::upward);
auto const minimumCover =
tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
// Round the liquidation amount up, too
return roundToAsset(
vaultAsset,
/*
* This formula is from the XLS-66 spec, section 3.2.3.2 (State
* Changes), specifically "if the `tfLoanDefault` flag is set" /
* "Apply the First-Loss Capital to the Default Amount"
*/
std::min(
tenthBipsOfValue(minimumCover, coverRateLiquidation),
totalDefaultAmount),
loanScale);
}();
auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered;
// Update the Vault object:
{
// Decrease the Total Value of the Vault:
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
// The vault may be at a different scale than the loan. Reduce rounding
// errors during the accounting by rounding some of the values to that
// scale.
auto const vaultScale = vaultTotalProxy.value().exponent();
if (vaultTotalProxy < vaultDefaultAmount)
{
// LCOV_EXCL_START
JLOG(j.warn())
<< "Vault total assets is less than the vault default amount";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const vaultDefaultRounded = roundToAsset(
vaultAsset, vaultDefaultAmount, vaultScale, Number::downward);
vaultTotalProxy -= vaultDefaultRounded;
// Increase the Asset Available of the Vault by liquidated First-Loss
// Capital and any unclaimed funds amount:
vaultAvailableProxy += defaultCovered;
if (*vaultAvailableProxy > *vaultTotalProxy && !vaultAsset.integral())
{
auto const difference = vaultAvailableProxy - vaultTotalProxy;
JLOG(j.debug())
<< "Vault assets available: " << *vaultAvailableProxy << "("
<< vaultAvailableProxy.value().exponent()
<< "), Total: " << *vaultTotalProxy << "("
<< vaultTotalProxy.value().exponent()
<< "), Difference: " << difference << "("
<< difference.exponent() << ")";
if (vaultAvailableProxy.value().exponent() - difference.exponent() >
13)
{
// If the difference is dust, bring the total up to match
// the available
JLOG(j.debug())
<< "Difference between vault assets available and total is "
"dust. Set both to the larger value.";
vaultTotalProxy = vaultAvailableProxy;
}
}
if (*vaultAvailableProxy > *vaultTotalProxy)
{
JLOG(j.warn()) << "Vault assets available must not be greater "
"than assets outstanding. Available: "
<< *vaultAvailableProxy
<< ", Total: " << *vaultTotalProxy;
return tecLIMIT_EXCEEDED;
}
// The loss has been realized
if (loanSle->isFlag(lsfLoanImpaired))
{
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
if (vaultLossUnrealizedProxy < totalDefaultAmount)
{
// LCOV_EXCL_START
JLOG(j.warn())
<< "Vault unrealized loss is less than the default amount";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
vaultLossUnrealizedProxy -= totalDefaultAmount;
}
view.update(vaultSle);
}
// Update the LoanBroker object:
{
// Decrease the Debt of the LoanBroker:
if (brokerDebtTotalProxy < totalDefaultAmount)
{
// LCOV_EXCL_START
JLOG(j.warn())
<< "LoanBroker debt total is less than the default amount";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
brokerDebtTotalProxy -= totalDefaultAmount;
// Decrease the First-Loss Capital Cover Available:
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
if (coverAvailableProxy < defaultCovered)
{
// LCOV_EXCL_START
JLOG(j.warn())
<< "LoanBroker cover available is less than amount covered";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
coverAvailableProxy -= defaultCovered;
view.update(brokerSle);
}
// Update the Loan object:
loanSle->setFlag(lsfLoanDefault);
totalValueOutstandingProxy = 0;
paymentRemainingProxy = 0;
principalOutstandingProxy = 0;
managementFeeOutstandingProxy = 0;
// Zero out the next due date. Since it's default, it'll be removed from
// the object.
loanSle->at(sfNextPaymentDueDate) = 0;
view.update(loanSle);
// Return funds from the LoanBroker pseudo-account to the
// Vault pseudo-account:
return accountSend(
view,
brokerSle->at(sfAccount),
vaultSle->at(sfAccount),
STAmount{vaultAsset, defaultCovered},
j,
WaiveTransferFee::Yes);
}
TER
LoanManage::impairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
beast::Journal j)
{
Number const lossUnrealized = owedToVault(loanSle);
// Update the Vault object(set "paper loss")
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
vaultLossUnrealizedProxy += lossUnrealized;
if (vaultLossUnrealizedProxy >
vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
{
// Having a loss greater than the vault's unavailable assets
// will leave the vault in an invalid / inconsistent state.
JLOG(j.warn()) << "Vault unrealized loss is too large, and will "
"corrupt the vault.";
return tecLIMIT_EXCEEDED;
}
view.update(vaultSle);
// Update the Loan object
loanSle->setFlag(lsfLoanImpaired);
auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate);
if (!hasExpired(view, loanNextDueProxy))
{
// loan payment is not yet late -
// move the next payment due date to now
loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
}
view.update(loanSle);
return tesSUCCESS;
}
TER
LoanManage::unimpairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
beast::Journal j)
{
// Update the Vault object(clear "paper loss")
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
Number const lossReversed = owedToVault(loanSle);
if (vaultLossUnrealizedProxy < lossReversed)
{
// LCOV_EXCL_START
JLOG(j.warn())
<< "Vault unrealized loss is less than the amount to be cleared";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
vaultLossUnrealizedProxy -= lossReversed;
view.update(vaultSle);
// Update the Loan object
loanSle->clearFlag(lsfLoanImpaired);
auto const paymentInterval = loanSle->at(sfPaymentInterval);
auto const normalPaymentDueDate =
std::max(loanSle->at(sfPreviousPaymentDate), loanSle->at(sfStartDate)) +
paymentInterval;
if (!hasExpired(view, normalPaymentDueDate))
{
// loan was unimpaired within the payment interval
loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate;
}
else
{
// loan was unimpaired after the original payment due date
loanSle->at(sfNextPaymentDueDate) =
view.parentCloseTime().time_since_epoch().count() + paymentInterval;
}
view.update(loanSle);
return tesSUCCESS;
}
TER
LoanManage::doApply()
{
auto const& tx = ctx_.tx;
auto& view = ctx_.view();
auto const loanID = tx[sfLoanID];
auto const loanSle = view.peek(keylet::loan(loanID));
if (!loanSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerID = loanSle->at(sfLoanBrokerID);
auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID)));
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultAsset = vaultSle->at(sfAsset);
// Valid flag combinations are checked in preflight. No flags is valid -
// just a noop.
if (tx.isFlag(tfLoanDefault))
{
if (auto const ter =
defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_))
return ter;
}
else if (tx.isFlag(tfLoanImpair))
{
if (auto const ter = impairLoan(view, loanSle, vaultSle, j_))
return ter;
}
else if (tx.isFlag(tfLoanUnimpair))
{
if (auto const ter = unimpairLoan(view, loanSle, vaultSle, j_))
return ter;
}
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple
+66
View File
@@ -0,0 +1,66 @@
#ifndef XRPL_TX_LOANMANAGE_H_INCLUDED
#define XRPL_TX_LOANMANAGE_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanManage : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanManage(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const& ctx);
/** Helper function that might be needed by other transactors
*/
static TER
defaultLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref brokerSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j);
/** Helper function that might be needed by other transactors
*/
static TER
impairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
beast::Journal j);
/** Helper function that might be needed by other transactors
*/
static TER
unimpairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
beast::Journal j);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif
+583
View File
@@ -0,0 +1,583 @@
#include <xrpld/app/tx/detail/LoanPay.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/tx/detail/LoanManage.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
bool
LoanPay::checkExtraFeatures(PreflightContext const& ctx)
{
return checkLendingProtocolDependencies(ctx);
}
std::uint32_t
LoanPay::getFlagsMask(PreflightContext const& ctx)
{
return tfLoanPayMask;
}
NotTEC
LoanPay::preflight(PreflightContext const& ctx)
{
if (ctx.tx[sfLoanID] == beast::zero)
return temINVALID;
if (ctx.tx[sfAmount] <= beast::zero)
return temBAD_AMOUNT;
// The loan payment flags are all mutually exclusive. If more than one is
// set, the tx is malformed.
int flagsSet = 0;
for (auto const flag :
{tfLoanLatePayment, tfLoanFullPayment, tfLoanOverpayment})
{
if (ctx.tx.isFlag(flag))
++flagsSet;
}
if (flagsSet > 1)
{
JLOG(ctx.j.warn()) << "Only one LoanPay flag can be set per tx. "
<< flagsSet << " is too many.";
return temINVALID_FLAG;
}
return tesSUCCESS;
}
XRPAmount
LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
{
auto const normalCost = Transactor::calculateBaseFee(view, tx);
if (tx.isFlag(tfLoanFullPayment) || tx.isFlag(tfLoanLatePayment))
// The loan will be making one set of calculations for one full or late
// payment
return normalCost;
// The fee is based on the potential number of payments, unless the loan is
// being fully paid off.
auto const amount = tx[sfAmount];
auto const loanID = tx[sfLoanID];
auto const loanSle = view.read(keylet::loan(loanID));
if (!loanSle)
// Let preclaim worry about the error for this
return normalCost;
if (loanSle->at(sfPaymentRemaining) <= loanPaymentsPerFeeIncrement)
{
// If there are fewer than loanPaymentsPerFeeIncrement payments left to
// pay, we can skip the computations.
return normalCost;
}
if (hasExpired(view, loanSle->at(sfNextPaymentDueDate)))
// If the payment is late, and the late payment flag is not set, it'll
// fail
return normalCost;
auto const brokerSle =
view.read(keylet::loanbroker(loanSle->at(sfLoanBrokerID)));
if (!brokerSle)
// Let preclaim worry about the error for this
return normalCost;
auto const vaultSle = view.read(keylet::vault(brokerSle->at(sfVaultID)));
if (!vaultSle)
// Let preclaim worry about the error for this
return normalCost;
auto const asset = vaultSle->at(sfAsset);
if (asset != amount.asset())
// Let preclaim worry about the error for this
return normalCost;
auto const scale = loanSle->at(sfLoanScale);
auto const regularPayment =
roundPeriodicPayment(asset, loanSle->at(sfPeriodicPayment), scale) +
loanSle->at(sfLoanServiceFee);
// If making an overpayment, count it as a full payment because it will do
// about the same amount of work, if not more.
NumberRoundModeGuard mg(
tx.isFlag(tfLoanOverpayment) ? Number::upward : Number::downward);
// Estimate how many payments will be made
Number const numPaymentEstimate =
static_cast<std::int64_t>(amount / regularPayment);
// Charge one base fee per paymentsPerFeeIncrement payments, rounding up.
Number::setround(Number::upward);
auto const feeIncrements = std::max(
std::int64_t(1),
static_cast<std::int64_t>(
numPaymentEstimate / loanPaymentsPerFeeIncrement));
return feeIncrements * normalCost;
}
TER
LoanPay::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
auto const account = tx[sfAccount];
auto const loanID = tx[sfLoanID];
auto const amount = tx[sfAmount];
auto const loanSle = ctx.view.read(keylet::loan(loanID));
if (!loanSle)
{
JLOG(ctx.j.warn()) << "Loan does not exist.";
return tecNO_ENTRY;
}
if (loanSle->at(sfBorrower) != account)
{
JLOG(ctx.j.warn()) << "Loan does not belong to the account.";
return tecNO_PERMISSION;
}
if (tx.isFlag(tfLoanOverpayment) && !loanSle->isFlag(lsfLoanOverpayment))
{
JLOG(ctx.j.warn())
<< "Requested overpayment on a loan that doesn't allow it";
return temINVALID_FLAG;
}
auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
TenthBips32 const interestRate{loanSle->at(sfInterestRate)};
auto const paymentRemaining = loanSle->at(sfPaymentRemaining);
TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)};
if (paymentRemaining == 0 || principalOutstanding == 0)
{
JLOG(ctx.j.warn()) << "Loan is already paid off.";
return tecKILLED;
}
auto const loanBrokerID = loanSle->at(sfLoanBrokerID);
auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID));
if (!loanBrokerSle)
{
// This should be impossible
// LCOV_EXCL_START
JLOG(ctx.j.fatal()) << "LoanBroker does not exist.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const vaultID = loanBrokerSle->at(sfVaultID);
auto const vaultSle = ctx.view.read(keylet::vault(vaultID));
if (!vaultSle)
{
// This should be impossible
// LCOV_EXCL_START
JLOG(ctx.j.fatal()) << "Vault does not exist.";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
auto const asset = vaultSle->at(sfAsset);
auto const vaultPseudoAccount = vaultSle->at(sfAccount);
if (amount.asset() != asset)
{
JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset.";
return tecWRONG_ASSET;
}
if (auto const ret = checkFrozen(ctx.view, account, asset))
{
JLOG(ctx.j.warn()) << "Borrower account is frozen.";
return ret;
}
if (auto const ret = checkDeepFrozen(ctx.view, vaultPseudoAccount, asset))
{
JLOG(ctx.j.warn())
<< "Vault pseudo-account can not receive funds (deep frozen).";
return ret;
}
if (auto const ret = requireAuth(ctx.view, asset, account))
{
JLOG(ctx.j.warn()) << "Borrower account is not authorized.";
return ret;
}
// Make sure the borrower has enough funds to make the payment!
// Do not support "partial payments" - if the transaction says to pay X,
// then the account must have X available, even if the loan payment takes
// less.
if (auto const balance = accountCanSend(
ctx.view,
account,
asset,
fhZERO_IF_FROZEN,
ahZERO_IF_UNAUTHORIZED,
ctx.j);
balance < amount)
{
JLOG(ctx.j.warn()) << "Payment amount too large. Amount: "
<< to_string(amount.getJson())
<< ". Balance: " << to_string(balance.getJson());
return tecINSUFFICIENT_FUNDS;
}
return tesSUCCESS;
}
TER
LoanPay::doApply()
{
auto const& tx = ctx_.tx;
auto& view = ctx_.view();
auto const amount = tx[sfAmount];
auto const loanID = tx[sfLoanID];
auto const loanSle = view.peek(keylet::loan(loanID));
if (!loanSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
std::int32_t const loanScale = loanSle->at(sfLoanScale);
auto const brokerID = loanSle->at(sfLoanBrokerID);
auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerOwner = brokerSle->at(sfOwner);
auto const brokerPseudoAccount = brokerSle->at(sfAccount);
auto const vaultID = brokerSle->at(sfVaultID);
auto const vaultSle = view.peek(keylet::vault(vaultID));
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultPseudoAccount = vaultSle->at(sfAccount);
auto const asset = *vaultSle->at(sfAsset);
// Determine where to send the broker's fee
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
auto debtTotalProxy = brokerSle->at(sfDebtTotal);
// Send the broker fee to the owner if they have sufficient cover available,
// _and_ if the owner can receive funds. If not, so as not to block the
// payment, add it to the cover balance (send it to the broker pseudo
// account).
//
// Normally freeze status is checked in preflight, but we do it here to
// avoid duplicating the check. It'll claim a fee either way.
bool const sendBrokerFeeToOwner = [&]() {
// Always round the minimum required up.
NumberRoundModeGuard mg(Number::upward);
return coverAvailableProxy >=
roundToAsset(
asset,
tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum),
loanScale) &&
!isDeepFrozen(view, brokerOwner, asset);
}();
auto const brokerPayee =
sendBrokerFeeToOwner ? brokerOwner : brokerPseudoAccount;
auto const brokerPayeeSle = view.peek(keylet::account(brokerPayee));
if (!sendBrokerFeeToOwner)
{
// If we can't send the fee to the owner, and the pseudo-account is
// frozen, then we have to fail the payment.
if (auto const ret = checkDeepFrozen(view, brokerPayee, asset))
{
JLOG(j_.warn())
<< "Both Loan Broker and Loan Broker pseudo-account "
"can not receive funds (deep frozen).";
return ret;
}
}
//------------------------------------------------------
// Loan object state changes
// Unimpair the loan if it was impaired. Do this before the payment is
// attempted, so the original values can be used. If the payment fails, this
// change will be discarded.
if (loanSle->isFlag(lsfLoanImpaired))
{
LoanManage::unimpairLoan(view, loanSle, vaultSle, j_);
}
LoanPaymentType const paymentType = [&tx]() {
// preflight already checked that at most one flag is set.
if (tx.isFlag(tfLoanLatePayment))
return LoanPaymentType::late;
if (tx.isFlag(tfLoanFullPayment))
return LoanPaymentType::full;
if (tx.isFlag(tfLoanOverpayment))
return LoanPaymentType::overpayment;
return LoanPaymentType::regular;
}();
Expected<LoanPaymentParts, TER> const paymentParts = loanMakePayment(
asset, view, loanSle, brokerSle, amount, paymentType, j_);
if (!paymentParts)
{
XRPL_ASSERT_PARTS(
paymentParts.error(),
"ripple::LoanPay::doApply",
"payment error is an error");
return paymentParts.error();
}
// If the payment computation completed without error, the loanSle object
// has been modified.
view.update(loanSle);
XRPL_ASSERT_PARTS(
// It is possible to pay 0 principal
paymentParts->principalPaid >= 0,
"ripple::LoanPay::doApply",
"valid principal paid");
XRPL_ASSERT_PARTS(
// It is possible to pay 0 interest
paymentParts->interestPaid >= 0,
"ripple::LoanPay::doApply",
"valid interest paid");
XRPL_ASSERT_PARTS(
// It should not be possible to pay 0 total
paymentParts->principalPaid + paymentParts->interestPaid > 0,
"ripple::LoanPay::doApply",
"valid total paid");
XRPL_ASSERT_PARTS(
paymentParts->feePaid >= 0,
"ripple::LoanPay::doApply",
"valid fee paid");
if (paymentParts->principalPaid < 0 || paymentParts->interestPaid < 0 ||
paymentParts->feePaid < 0)
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Loan payment computation returned invalid values.";
return tecLIMIT_EXCEEDED;
// LCOV_EXCL_STOP
}
//------------------------------------------------------
// LoanBroker object state changes
view.update(brokerSle);
auto assetsAvailableProxy = vaultSle->at(sfAssetsAvailable);
auto assetsTotalProxy = vaultSle->at(sfAssetsTotal);
// The vault may be at a different scale than the loan. Reduce rounding
// errors during the payment by rounding some of the values to that scale.
auto const vaultScale = assetsTotalProxy.value().exponent();
auto const totalPaidToVaultRaw =
paymentParts->principalPaid + paymentParts->interestPaid;
auto const totalPaidToVaultRounded =
roundToAsset(asset, totalPaidToVaultRaw, vaultScale, Number::downward);
XRPL_ASSERT_PARTS(
!asset.integral() || totalPaidToVaultRaw == totalPaidToVaultRounded,
"ripple::LoanPay::doApply",
"rounding does nothing for integral asset");
auto const totalPaidToVaultForDebt =
totalPaidToVaultRaw - paymentParts->valueChange;
auto const totalPaidToBroker = paymentParts->feePaid;
XRPL_ASSERT_PARTS(
(totalPaidToVaultRaw + totalPaidToBroker) ==
(paymentParts->principalPaid + paymentParts->interestPaid +
paymentParts->feePaid),
"ripple::LoanPay::doApply",
"payments add up");
// Decrease LoanBroker Debt by the amount paid, add the Loan value change
// (which might be negative). totalPaidToVaultForDebt may be negative,
// increasing the debt
XRPL_ASSERT_PARTS(
isRounded(asset, totalPaidToVaultForDebt, loanScale),
"ripple::LoanPay::doApply",
"totalPaidToVaultForDebt rounding good");
// Despite our best efforts, it's possible for rounding errors to accumulate
// in the loan broker's debt total. This is because the broker may have more
// that one loan with significantly different scales.
if (totalPaidToVaultForDebt >= debtTotalProxy)
debtTotalProxy = 0;
else
debtTotalProxy -= totalPaidToVaultForDebt;
//------------------------------------------------------
// Vault object state changes
view.update(vaultSle);
Number const assetsAvailableBefore = *assetsAvailableProxy;
Number const pseudoAccountBalanceBefore = accountHolds(
view,
vaultPseudoAccount,
asset,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_);
{
XRPL_ASSERT_PARTS(
assetsAvailableBefore == pseudoAccountBalanceBefore,
"ripple::LoanPay::doApply",
"vault pseudo balance agrees before");
assetsAvailableProxy += totalPaidToVaultRounded;
assetsTotalProxy += paymentParts->valueChange;
XRPL_ASSERT_PARTS(
*assetsAvailableProxy <= *assetsTotalProxy,
"ripple::LoanPay::doApply",
"assets available must not be greater than assets outstanding");
if (*assetsAvailableProxy > *assetsTotalProxy)
{
// LCOV_EXCL_START
return tecINTERNAL;
// LCOV_EXCL_STOP
}
}
// Move funds
XRPL_ASSERT_PARTS(
totalPaidToVaultRounded + totalPaidToBroker <= amount,
"ripple::LoanPay::doApply",
"amount is sufficient");
if (!sendBrokerFeeToOwner)
{
// If there is not enough first-loss capital, add the fee to First Loss
// Cover Pool. Note that this moves the entire fee - it does not attempt
// to split it. The broker can Withdraw it later if they want, or leave
// it for future needs.
coverAvailableProxy += totalPaidToBroker;
}
#if !NDEBUG
auto const accountBalanceBefore = accountCanSend(
view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const vaultBalanceBefore = account_ == vaultPseudoAccount
? STAmount{asset, 0}
: accountCanSend(
view,
vaultPseudoAccount,
asset,
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
j_);
auto const brokerBalanceBefore = account_ == brokerPayee
? STAmount{asset, 0}
: accountCanSend(
view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
#endif
if (totalPaidToVaultRounded != beast::zero)
{
if (auto const ter = requireAuth(
view, asset, vaultPseudoAccount, AuthType::StrongAuth))
return ter;
}
if (totalPaidToBroker != beast::zero)
{
if (brokerPayee == account_)
{
// The broker may have deleted their holding. Recreate it if needed
if (auto const ter = addEmptyHolding(
view,
brokerPayee,
brokerPayeeSle->at(sfBalance).value().xrp(),
asset,
j_);
ter && ter != tecDUPLICATE)
// ignore tecDUPLICATE. That means the holding already exists,
// and is fine here
return ter;
}
if (auto const ter =
requireAuth(view, asset, brokerPayee, AuthType::StrongAuth))
return ter;
}
if (auto const ter = accountSendMulti(
view,
account_,
asset,
{{vaultPseudoAccount, totalPaidToVaultRounded},
{brokerPayee, totalPaidToBroker}},
j_,
WaiveTransferFee::Yes))
return ter;
Number const assetsAvailableAfter = *assetsAvailableProxy;
Number const pseudoAccountBalanceAfter = accountHolds(
view,
vaultPseudoAccount,
asset,
FreezeHandling::fhIGNORE_FREEZE,
AuthHandling::ahIGNORE_AUTH,
j_);
XRPL_ASSERT_PARTS(
assetsAvailableAfter == pseudoAccountBalanceAfter,
"ripple::LoanPay::doApply",
"vault pseudo balance agrees after");
#if !NDEBUG
auto const accountBalanceAfter = accountCanSend(
view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
auto const vaultBalanceAfter = account_ == vaultPseudoAccount
? STAmount{asset, 0}
: accountCanSend(
view,
vaultPseudoAccount,
asset,
fhIGNORE_FREEZE,
ahIGNORE_AUTH,
j_);
auto const brokerBalanceAfter = account_ == brokerPayee
? STAmount{asset, 0}
: accountCanSend(
view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_);
XRPL_ASSERT_PARTS(
accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore ==
accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter,
"ripple::LoanPay::doApply",
"funds are conserved (with rounding)");
XRPL_ASSERT_PARTS(
accountBalanceAfter >= beast::zero,
"ripple::LoanPay::doApply",
"positive account balance");
XRPL_ASSERT_PARTS(
accountBalanceAfter < accountBalanceBefore,
"ripple::LoanPay::doApply",
"account balance decreased");
XRPL_ASSERT_PARTS(
vaultBalanceAfter >= beast::zero && brokerBalanceAfter >= beast::zero,
"ripple::LoanPay::doApply",
"positive vault and broker balances");
XRPL_ASSERT_PARTS(
vaultBalanceAfter >= vaultBalanceBefore,
"ripple::LoanPay::doApply",
"vault balance did not decrease");
XRPL_ASSERT_PARTS(
brokerBalanceAfter >= brokerBalanceBefore,
"ripple::LoanPay::doApply",
"broker balance did not decrease");
XRPL_ASSERT_PARTS(
vaultBalanceAfter > vaultBalanceBefore ||
brokerBalanceAfter > brokerBalanceBefore,
"ripple::LoanPay::doApply",
"vault and/or broker balance increased");
#endif
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple
+40
View File
@@ -0,0 +1,40 @@
#ifndef XRPL_TX_LOANPAY_H_INCLUDED
#define XRPL_TX_LOANPAY_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanPay : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanPay(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER
preclaim(PreclaimContext const& ctx);
TER
doApply() override;
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif
+675
View File
@@ -0,0 +1,675 @@
#include <xrpld/app/tx/detail/LoanSet.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
bool
LoanSet::checkExtraFeatures(PreflightContext const& ctx)
{
return checkLendingProtocolDependencies(ctx);
}
std::uint32_t
LoanSet::getFlagsMask(PreflightContext const& ctx)
{
return tfLoanSetMask;
}
NotTEC
LoanSet::preflight(PreflightContext const& ctx)
{
auto const& tx = ctx.tx;
// Special case for Batch inner transactions
if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch) &&
!tx.isFieldPresent(sfCounterparty))
{
auto const parentBatchId = ctx.parentBatchId.value_or(uint256{0});
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
<< "no Counterparty for inner LoanSet transaction.";
return temBAD_SIGNER;
}
// These extra hoops are because STObjects cannot be Proxy'd from STObject.
auto const counterPartySig = [&tx]() -> std::optional<STObject const> {
if (tx.isFieldPresent(sfCounterpartySignature))
return tx.getFieldObject(sfCounterpartySignature);
return std::nullopt;
}();
if (!tx.isFlag(tfInnerBatchTxn) && !counterPartySig)
{
JLOG(ctx.j.warn())
<< "LoanSet transaction must have a CounterpartySignature.";
return temBAD_SIGNER;
}
if (counterPartySig)
{
if (auto const ret = ripple::detail::preflightCheckSigningKey(
*counterPartySig, ctx.j))
return ret;
}
if (auto const data = tx[~sfData]; data && !data->empty() &&
!validDataLength(tx[~sfData], maxDataPayloadLength))
return temINVALID;
for (auto const& field :
{&sfLoanServiceFee, &sfLatePaymentFee, &sfClosePaymentFee})
{
if (!validNumericMinimum(tx[~*field]))
return temINVALID;
}
// Principal Requested is required
if (auto const p = tx[sfPrincipalRequested]; p <= 0)
return temINVALID;
else if (!validNumericRange(tx[~sfLoanOriginationFee], p))
return temINVALID;
if (!validNumericRange(tx[~sfInterestRate], maxInterestRate))
return temINVALID;
if (!validNumericRange(tx[~sfOverpaymentFee], maxOverpaymentFee))
return temINVALID;
if (!validNumericRange(tx[~sfLateInterestRate], maxLateInterestRate))
return temINVALID;
if (!validNumericRange(tx[~sfCloseInterestRate], maxCloseInterestRate))
return temINVALID;
if (!validNumericRange(
tx[~sfOverpaymentInterestRate], maxOverpaymentInterestRate))
return temINVALID;
if (auto const paymentTotal = tx[~sfPaymentTotal];
paymentTotal && *paymentTotal <= 0)
return temINVALID;
if (auto const paymentInterval = tx[~sfPaymentInterval];
!validNumericMinimum(paymentInterval, LoanSet::minPaymentInterval))
return temINVALID;
else if (!validNumericRange(
tx[~sfGracePeriod],
paymentInterval.value_or(LoanSet::defaultPaymentInterval)))
return temINVALID;
// Copied from preflight2
if (counterPartySig)
{
if (auto const ret = ripple::detail::preflightCheckSimulateKeys(
ctx.flags, *counterPartySig, ctx.j))
return *ret;
}
if (auto const brokerID = ctx.tx[~sfLoanBrokerID];
brokerID && *brokerID == beast::zero)
return temINVALID;
return tesSUCCESS;
}
NotTEC
LoanSet::checkSign(PreclaimContext const& ctx)
{
if (auto ret = Transactor::checkSign(ctx))
return ret;
// Counter signer is optional. If it's not specified, it's assumed to be
// `LoanBroker.Owner`. Note that we have not checked whether the
// loanbroker exists at this point.
auto const counterSigner = [&]() -> std::optional<AccountID> {
if (auto const c = ctx.tx.at(~sfCounterparty))
return c;
if (auto const broker =
ctx.view.read(keylet::loanbroker(ctx.tx[sfLoanBrokerID])))
return broker->at(sfOwner);
return std::nullopt;
}();
if (!counterSigner)
return temBAD_SIGNER;
// Counterparty signature is optional. Presence is checked in preflight.
if (!ctx.tx.isFieldPresent(sfCounterpartySignature))
return tesSUCCESS;
auto const counterSig = ctx.tx.getFieldObject(sfCounterpartySignature);
return Transactor::checkSign(
ctx.view,
ctx.flags,
ctx.parentBatchId,
*counterSigner,
counterSig,
ctx.j);
}
XRPAmount
LoanSet::calculateBaseFee(ReadView const& view, STTx const& tx)
{
auto const normalCost = Transactor::calculateBaseFee(view, tx);
// Compute the additional cost of each signature in the
// CounterpartySignature, whether a single signature or a multisignature
XRPAmount const baseFee = view.fees().base;
// Counterparty signature is optional, but getFieldObject will return an
// empty object if it's not present.
auto const counterSig = tx.getFieldObject(sfCounterpartySignature);
// Each signer adds one more baseFee to the minimum required fee
// for the transaction. Note that unlike the base class, the single signer
// is counted if present. It will only be absent in a batch inner
// transaction.
std::size_t const signerCount = counterSig.isFieldPresent(sfSigners)
? counterSig.getFieldArray(sfSigners).size()
: (counterSig.isFieldPresent(sfTxnSignature) ? 1 : 0);
return normalCost + (signerCount * baseFee);
}
std::vector<OptionaledField<STNumber>> const&
LoanSet::getValueFields()
{
static std::vector<OptionaledField<STNumber>> const valueFields{
~sfPrincipalRequested,
~sfLoanOriginationFee,
~sfLoanServiceFee,
~sfLatePaymentFee,
~sfClosePaymentFee
// Overpayment fee is really a rate. Don't check it here.
};
return valueFields;
}
std::uint32_t
getStartDate(ReadView const& view)
{
return view.info().closeTime.time_since_epoch().count();
}
TER
LoanSet::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
{
// Check for numeric overflow of the schedule before we load any
// objects. The Grace Period for the last payment ends at:
// startDate + (paymentInterval * paymentTotal) + gracePeriod.
// If that value is larger than "maxTime", the value
// overflows, and we kill the transaction.
using timeType = decltype(sfNextPaymentDueDate)::type::value_type;
static_assert(std::is_same_v<timeType, std::uint32_t>);
timeType constexpr maxTime = std::numeric_limits<timeType>::max();
static_assert(maxTime == 4'294'967'295);
auto const timeAvailable = maxTime - getStartDate(ctx.view);
auto const interval =
ctx.tx.at(~sfPaymentInterval).value_or(defaultPaymentInterval);
auto const total =
ctx.tx.at(~sfPaymentTotal).value_or(defaultPaymentTotal);
auto const grace =
ctx.tx.at(~sfGracePeriod).value_or(defaultGracePeriod);
// The grace period can't be larger than the interval. Check it first,
// mostly so that unit tests can test that specific case.
if (grace > timeAvailable)
return tecKILLED;
if (interval > timeAvailable)
return tecKILLED;
if (total > timeAvailable)
return tecKILLED;
auto const timeLastPayment = timeAvailable - grace;
if (timeLastPayment / interval < total)
return tecKILLED;
}
auto const account = tx[sfAccount];
auto const brokerID = tx[sfLoanBrokerID];
auto const brokerSle = ctx.view.read(keylet::loanbroker(brokerID));
if (!brokerSle)
{
// This can only be hit if there's a counterparty specified, otherwise
// it'll fail in the signature check
JLOG(ctx.j.warn()) << "LoanBroker does not exist.";
return tecNO_ENTRY;
}
auto const brokerOwner = brokerSle->at(sfOwner);
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
if (account != brokerOwner && counterparty != brokerOwner)
{
JLOG(ctx.j.warn()) << "Neither Account nor Counterparty are the owner "
"of the LoanBroker.";
return tecNO_PERMISSION;
}
auto const borrower = counterparty == brokerOwner ? account : counterparty;
if (auto const borrowerSle = ctx.view.read(keylet::account(borrower));
!borrowerSle)
{
// It may not be possible to hit this case, because it'll fail the
// signature check with terNO_ACCOUNT.
JLOG(ctx.j.warn()) << "Borrower does not exist.";
return terNO_ACCOUNT;
}
auto const vault = ctx.view.read(keylet::vault(brokerSle->at(sfVaultID)));
if (!vault)
// Should be impossible
return tefBAD_LEDGER; // LCOV_EXCL_LINE
Asset const asset = vault->at(sfAsset);
auto const vaultPseudo = vault->at(sfAccount);
// Check that relevant values can be represented as the vault asset type.
// This check is almost duplicated in doApply, but that check is done after
// the overall loan scale is known. This is mostly only relevant for
// integral (non-IOU) types
{
for (auto const& field : getValueFields())
{
if (auto const value = tx[field];
value && STAmount{asset, *value} != *value)
{
JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value
<< ") can not be represented as a(n) "
<< to_string(asset) << ".";
return tecPRECISION_LOSS;
}
}
}
if (auto const ter = canAddHolding(ctx.view, asset))
return ter;
// vaultPseudo is going to send funds, so it can't be frozen.
if (auto const ret = checkFrozen(ctx.view, vaultPseudo, asset))
{
JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen.";
return ret;
}
// borrower is eventually going to have to pay back the loan, so it can't be
// frozen now. It is also going to receive funds, so it can't be deep
// frozen, but being frozen is a prerequisite for being deep frozen, so
// checking the one is sufficient.
if (auto const ret = checkFrozen(ctx.view, borrower, asset))
{
JLOG(ctx.j.warn()) << "Borrower account is frozen.";
return ret;
}
// brokerOwner is going to receive funds if there's an origination fee, so
// it can't be deep frozen
if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset))
{
JLOG(ctx.j.warn()) << "Broker owner account is frozen.";
return ret;
}
return tesSUCCESS;
}
TER
LoanSet::checkGuards(
Asset const& vaultAsset,
Number const& principalRequested,
TenthBips32 interestRate,
std::uint32_t paymentTotal,
LoanProperties const& properties,
beast::Journal j)
{
auto const totalInterestOutstanding =
properties.totalValueOutstanding - principalRequested;
// Guard 1: if there is no computed total interest over the life of the
// loan for a non-zero interest rate, we cannot properly amortize the
// loan
if (interestRate > TenthBips32{0} && totalInterestOutstanding <= 0)
{
// Unless this is a zero-interest loan, there must be some interest
// due on the loan, even if it's (measurable) dust
JLOG(j.warn()) << "Loan for " << principalRequested << " with "
<< interestRate << "% interest has no interest due";
return tecPRECISION_LOSS;
}
// Guard 1a: If there is any interest computed over the life of the
// loan, for a zero interest rate, something went sideways.
if (interestRate == TenthBips32{0} && totalInterestOutstanding > 0)
{
// LCOV_EXCL_START
JLOG(j.warn()) << "Loan for " << principalRequested
<< " with 0% interest has interest due";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
// Guard 2: if the principal portion of the first periodic payment is
// too small to be accurately represented with the given rounding mode,
// raise an error
if (properties.firstPaymentPrincipal <= 0)
{
// Check that some true (unrounded) principal is paid each period.
// Since the first payment pays the least principal, if it's good,
// they'll all be good. Note that the outstanding principal is
// rounded, and may not change right away.
JLOG(j.warn()) << "Loan is unable to pay principal.";
return tecPRECISION_LOSS;
}
// Guard 3: If the periodic payment is so small that it can't even be
// rounded to a representable value, then the loan can't be paid. Also,
// avoids dividing by 0.
auto const roundedPayment = roundPeriodicPayment(
vaultAsset, properties.periodicPayment, properties.loanScale);
if (roundedPayment == beast::zero)
{
JLOG(j.warn()) << "Loan Periodic payment ("
<< properties.periodicPayment << ") rounds to 0. ";
return tecPRECISION_LOSS;
}
// Guard 4: if the rounded periodic payment is large enough that the
// loan can't be amortized in the specified number of payments, raise an
// error
{
NumberRoundModeGuard mg(Number::upward);
if (std::int64_t const computedPayments{
properties.totalValueOutstanding / roundedPayment};
computedPayments != paymentTotal)
{
JLOG(j.warn()) << "Loan Periodic payment ("
<< properties.periodicPayment << ") rounding ("
<< roundedPayment << ") on a total value of "
<< properties.totalValueOutstanding
<< " can not complete the loan in the specified "
"number of payments ("
<< computedPayments << " != " << paymentTotal << ")";
return tecPRECISION_LOSS;
}
}
return tesSUCCESS;
}
TER
LoanSet::doApply()
{
auto const& tx = ctx_.tx;
auto& view = ctx_.view();
auto const brokerID = tx[sfLoanBrokerID];
auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
if (!brokerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerOwner = brokerSle->at(sfOwner);
auto const brokerOwnerSle = view.peek(keylet::account(brokerOwner));
if (!brokerOwnerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID)));
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultPseudo = vaultSle->at(sfAccount);
Asset const vaultAsset = vaultSle->at(sfAsset);
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
auto const borrower = counterparty == brokerOwner ? account_ : counterparty;
auto const borrowerSle = view.peek(keylet::account(borrower));
if (!borrowerSle)
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
auto const brokerPseudo = brokerSle->at(sfAccount);
auto const brokerPseudoSle = view.peek(keylet::account(brokerPseudo));
if (!brokerPseudoSle)
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
auto const principalRequested = tx[sfPrincipalRequested];
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
auto const vaultScale = vaultTotalProxy.value().exponent();
if (vaultAvailableProxy < principalRequested)
{
JLOG(j_.warn())
<< "Insufficient assets available in the Vault to fund the loan.";
return tecINSUFFICIENT_FUNDS;
}
TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)};
auto const paymentInterval =
tx[~sfPaymentInterval].value_or(defaultPaymentInterval);
auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal);
auto const properties = computeLoanProperties(
vaultAsset,
principalRequested,
interestRate,
paymentInterval,
paymentTotal,
TenthBips16{brokerSle->at(sfManagementFeeRate)},
vaultScale);
// Check that relevant values won't lose precision. This is mostly only
// relevant for IOU assets.
{
for (auto const& field : getValueFields())
{
if (auto const value = tx[field];
value && !isRounded(vaultAsset, *value, properties.loanScale))
{
JLOG(j_.warn())
<< field.f->getName() << " (" << *value
<< ") has too much precision. Total loan value is "
<< properties.totalValueOutstanding << " with a scale of "
<< properties.loanScale;
return tecPRECISION_LOSS;
}
}
}
if (auto const ret = checkGuards(
vaultAsset,
principalRequested,
interestRate,
paymentTotal,
properties,
j_))
return ret;
// Check that the other computed values are valid
if (properties.managementFeeOwedToBroker < 0 ||
properties.totalValueOutstanding <= 0 ||
properties.periodicPayment <= 0)
{
// LCOV_EXCL_START
JLOG(j_.warn())
<< "Computed loan properties are invalid. Does not compute.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}
LoanState const state = calculateRoundedLoanState(
properties.totalValueOutstanding,
principalRequested,
properties.managementFeeOwedToBroker);
auto const originationFee = tx[~sfLoanOriginationFee].value_or(Number{});
auto const loanAssetsToBorrower = principalRequested - originationFee;
auto const newDebtDelta = principalRequested + state.interestDue;
auto const newDebtTotal = brokerSle->at(sfDebtTotal) + newDebtDelta;
if (auto const debtMaximum = brokerSle->at(sfDebtMaximum);
debtMaximum != 0 && debtMaximum < newDebtTotal)
{
JLOG(j_.warn())
<< "Loan would exceed the maximum debt limit of the LoanBroker.";
return tecLIMIT_EXCEEDED;
}
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
{
// Always round the minimum required up.
NumberRoundModeGuard mg(Number::upward);
if (brokerSle->at(sfCoverAvailable) <
tenthBipsOfValue(newDebtTotal, coverRateMinimum))
{
JLOG(j_.warn())
<< "Insufficient first-loss capital to cover the loan.";
return tecINSUFFICIENT_FUNDS;
}
}
adjustOwnerCount(view, borrowerSle, 1, j_);
{
auto const ownerCount = borrowerSle->at(sfOwnerCount);
auto const balance = account_ == borrower
? mPriorBalance
: borrowerSle->at(sfBalance).value().xrp();
if (balance < view.fees().accountReserve(ownerCount))
return tecINSUFFICIENT_RESERVE;
}
// Account for the origination fee using two payments
//
// 1. Transfer loanAssetsAvailable (principalRequested - originationFee)
// from vault pseudo-account to the borrower.
// Create a holding for the borrower if one does not already exist.
XRPL_ASSERT_PARTS(
borrower == account_ || borrower == counterparty,
"ripple::LoanSet::doApply",
"borrower signed transaction");
if (auto const ter = addEmptyHolding(
view,
borrower,
borrowerSle->at(sfBalance).value().xrp(),
vaultAsset,
j_);
ter && ter != tecDUPLICATE)
// ignore tecDUPLICATE. That means the holding already exists, and
// is fine here
return ter;
if (auto const ter =
requireAuth(view, vaultAsset, borrower, AuthType::StrongAuth))
return ter;
// 2. Transfer originationFee, if any, from vault pseudo-account to
// LoanBroker owner.
if (originationFee != beast::zero)
{
// Create the holding if it doesn't already exist (necessary for MPTs).
// The owner may have deleted their MPT / line at some point.
XRPL_ASSERT_PARTS(
brokerOwner == account_ || brokerOwner == counterparty,
"ripple::LoanSet::doApply",
"broker owner signed transaction");
if (auto const ter = addEmptyHolding(
view,
brokerOwner,
brokerOwnerSle->at(sfBalance).value().xrp(),
vaultAsset,
j_);
ter && ter != tecDUPLICATE)
// ignore tecDUPLICATE. That means the holding already exists,
// and is fine here
return ter;
if (auto const ter = requireAuth(
view, vaultAsset, brokerOwner, AuthType::StrongAuth))
return ter;
}
if (auto const ter = accountSendMulti(
view,
vaultPseudo,
vaultAsset,
{{borrower, loanAssetsToBorrower}, {brokerOwner, originationFee}},
j_,
WaiveTransferFee::Yes))
return ter;
// Get shortcuts to the loan property values
auto const startDate = getStartDate(view);
auto loanSequenceProxy = brokerSle->at(sfLoanSequence);
// Create the loan
auto loan =
std::make_shared<SLE>(keylet::loan(brokerID, *loanSequenceProxy));
// Prevent copy/paste errors
auto setLoanField =
[&loan, &tx](auto const& field, std::uint32_t const defValue = 0) {
// at() is smart enough to unseat a default field set to the default
// value
loan->at(field) = tx[field].value_or(defValue);
};
// Set required and fixed tx fields
loan->at(sfLoanScale) = properties.loanScale;
loan->at(sfStartDate) = startDate;
loan->at(sfPaymentInterval) = paymentInterval;
loan->at(sfLoanSequence) = *loanSequenceProxy;
loan->at(sfLoanBrokerID) = brokerID;
loan->at(sfBorrower) = borrower;
// Set all other transaction fields directly from the transaction
if (tx.isFlag(tfLoanOverpayment))
loan->setFlag(lsfLoanOverpayment);
setLoanField(~sfLoanOriginationFee);
setLoanField(~sfLoanServiceFee);
setLoanField(~sfLatePaymentFee);
setLoanField(~sfClosePaymentFee);
setLoanField(~sfOverpaymentFee);
setLoanField(~sfInterestRate);
setLoanField(~sfLateInterestRate);
setLoanField(~sfCloseInterestRate);
setLoanField(~sfOverpaymentInterestRate);
setLoanField(~sfGracePeriod, defaultGracePeriod);
// Set dynamic / computed fields to their initial values
loan->at(sfPrincipalOutstanding) = principalRequested;
loan->at(sfPeriodicPayment) = properties.periodicPayment;
loan->at(sfTotalValueOutstanding) = properties.totalValueOutstanding;
loan->at(sfManagementFeeOutstanding) = properties.managementFeeOwedToBroker;
loan->at(sfPreviousPaymentDate) = 0;
loan->at(sfNextPaymentDueDate) = startDate + paymentInterval;
loan->at(sfPaymentRemaining) = paymentTotal;
view.insert(loan);
// Update the balances in the vault
vaultAvailableProxy -= principalRequested;
vaultTotalProxy += state.interestDue;
XRPL_ASSERT_PARTS(
*vaultAvailableProxy <= *vaultTotalProxy,
"ripple::LoanSet::doApply",
"assets available must not be greater than assets outstanding");
view.update(vaultSle);
// Update the balances in the loan broker
brokerSle->at(sfDebtTotal) += newDebtDelta;
// The broker's owner count is solely for the number of outstanding loans,
// and is distinct from the broker's pseudo-account's owner count
adjustOwnerCount(view, brokerSle, 1, j_);
loanSequenceProxy += 1;
view.update(brokerSle);
// Put the loan into the pseudo-account's directory
if (auto const ter = dirLink(view, brokerPseudo, loan, sfLoanBrokerNode))
return ter;
// Borrower is the owner of the loan
if (auto const ter = dirLink(view, borrower, loan, sfOwnerNode))
return ter;
return tesSUCCESS;
}
//------------------------------------------------------------------------------
} // namespace ripple
+68
View File
@@ -0,0 +1,68 @@
#ifndef XRPL_TX_LOANSET_H_INCLUDED
#define XRPL_TX_LOANSET_H_INCLUDED
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/tx/detail/Transactor.h>
namespace ripple {
class LoanSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit LoanSet(ApplyContext& ctx) : Transactor(ctx)
{
}
static bool
checkExtraFeatures(PreflightContext const& ctx);
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static NotTEC
checkSign(PreclaimContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static std::vector<OptionaledField<STNumber>> const&
getValueFields();
static TER
preclaim(PreclaimContext const& ctx);
static TER
checkGuards(
Asset const& vaultAsset,
Number const& principalRequested,
TenthBips32 interestRate,
std::uint32_t paymentTotal,
LoanProperties const& properties,
beast::Journal j);
TER
doApply() override;
public:
static std::uint32_t constexpr minPaymentTotal = 1;
static std::uint32_t constexpr defaultPaymentTotal = 1;
static_assert(defaultPaymentTotal >= minPaymentTotal);
static std::uint32_t constexpr minPaymentInterval = 60;
static std::uint32_t constexpr defaultPaymentInterval = 60;
static_assert(defaultPaymentInterval >= minPaymentInterval);
static std::uint32_t constexpr defaultGracePeriod = 60;
static_assert(defaultGracePeriod >= minPaymentInterval);
};
//------------------------------------------------------------------------------
} // namespace ripple
#endif
+8 -1
View File
@@ -94,7 +94,8 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
return tesSUCCESS;
}
if (!ctx.view.exists(keylet::account(*holderID)))
auto const sleHolder = ctx.view.read(keylet::account(*holderID));
if (!sleHolder)
return tecNO_DST;
auto const sleMptIssuance =
@@ -124,6 +125,12 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], *holderID)))
return tecOBJECT_NOT_FOUND;
// Can't unauthorize the pseudo-accounts because they are implicitly
// always authorized. No need to amendment gate since Vault and LoanBroker
// can only be created if the Vault amendment is enabled.
if (isPseudoAccount(ctx.view, *holderID, {&sfVaultID, &sfLoanBrokerID}))
return tecNO_PERMISSION;
return tesSUCCESS;
}
+227 -149
View File
@@ -29,8 +29,8 @@ Payment::makeTxConsequences(PreflightContext const& ctx)
}
STAmount
getMaxSourceAmount(
AccountID const& account,
Payment::getMaxSourceAmount(
AccountID const& senderAccount,
STAmount const& dstAmount,
std::optional<STAmount> const& sendMax)
{
@@ -40,7 +40,7 @@ getMaxSourceAmount(
return dstAmount;
else
return STAmount(
Issue{dstAmount.get<Issue>().currency, account},
Issue{dstAmount.get<Issue>().currency, senderAccount},
dstAmount.mantissa(),
dstAmount.exponent(),
dstAmount < beast::zero);
@@ -422,155 +422,35 @@ Payment::doApply()
if (ripple)
{
// Ripple payment with at least one intermediate step and uses
// transitive balances.
// An account that requires authorization has two ways to get an
// IOU Payment in:
// 1. If Account == Destination, or
// 2. If Account is deposit preauthorized by destination.
if (auto err = verifyDepositPreauth(
ctx_.tx,
ctx_.view(),
account_,
dstAccountID,
sleDst,
ctx_.journal);
!isTesSuccess(err))
return err;
path::RippleCalc::Input rcInput;
rcInput.partialPaymentAllowed = partialPaymentAllowed;
rcInput.defaultPathsAllowed = defaultPathsAllowed;
rcInput.limitQuality = limitQuality;
rcInput.isLedgerOpen = view().open();
path::RippleCalc::Output rc;
{
PaymentSandbox pv(&view());
JLOG(j_.debug()) << "Entering RippleCalc in payment: "
<< ctx_.tx.getTransactionID();
rc = path::RippleCalc::rippleCalculate(
pv,
maxSourceAmount,
dstAmount,
dstAccountID,
account_,
ctx_.tx.getFieldPathSet(sfPaths),
ctx_.tx[~sfDomainID],
ctx_.app.logs(),
&rcInput);
// VFALCO NOTE We might not need to apply, depending
// on the TER. But always applying *should*
// be safe.
pv.apply(ctx_.rawView());
}
// TODO: is this right? If the amount is the correct amount, was
// the delivered amount previously set?
if (rc.result() == tesSUCCESS && rc.actualAmountOut != dstAmount)
{
if (deliverMin && rc.actualAmountOut < *deliverMin)
rc.setResult(tecPATH_PARTIAL);
else
ctx_.deliver(rc.actualAmountOut);
}
auto terResult = rc.result();
// Because of its overhead, if RippleCalc
// fails with a retry code, claim a fee
// instead. Maybe the user will be more
// careful with their path spec next time.
if (isTerRetry(terResult))
terResult = tecPATH_DRY;
return terResult;
return makeRipplePayment(RipplePaymentParams{
.ctx = ctx_,
.maxSourceAmount = maxSourceAmount,
.srcAccountID = account_,
.dstAccountID = dstAccountID,
.sleDst = sleDst,
.dstAmount = dstAmount,
.paths = ctx_.tx.getFieldPathSet(sfPaths),
.deliverMin = deliverMin,
.partialPaymentAllowed = partialPaymentAllowed,
.defaultPathsAllowed = defaultPathsAllowed,
.limitQuality = limitQuality,
.j = j_});
}
else if (mptDirect)
{
JLOG(j_.trace()) << " dstAmount=" << dstAmount.getFullText();
auto const& mptIssue = dstAmount.get<MPTIssue>();
if (auto const ter = requireAuth(view(), mptIssue, account_);
ter != tesSUCCESS)
return ter;
if (auto const ter = requireAuth(view(), mptIssue, dstAccountID);
ter != tesSUCCESS)
return ter;
if (auto const ter =
canTransfer(view(), mptIssue, account_, dstAccountID);
ter != tesSUCCESS)
return ter;
if (auto err = verifyDepositPreauth(
ctx_.tx,
ctx_.view(),
account_,
dstAccountID,
sleDst,
ctx_.journal);
!isTesSuccess(err))
return err;
auto const& issuer = mptIssue.getIssuer();
// Transfer rate
Rate rate{QUALITY_ONE};
// Payment between the holders
if (account_ != issuer && dstAccountID != issuer)
{
// If globally/individually locked then
// - can't send between holders
// - holder can send back to issuer
// - issuer can send to holder
if (isAnyFrozen(view(), {account_, dstAccountID}, mptIssue))
return tecLOCKED;
// Get the rate for a payment between the holders.
rate = transferRate(view(), mptIssue.getMptID());
}
// Amount to deliver.
STAmount amountDeliver = dstAmount;
// Factor in the transfer rate.
// No rounding. It'll change once MPT integrated into DEX.
STAmount requiredMaxSourceAmount = multiply(dstAmount, rate);
// Send more than the account wants to pay or less than
// the account wants to deliver (if no SendMax).
// Adjust the amount to deliver.
if (partialPaymentAllowed && requiredMaxSourceAmount > maxSourceAmount)
{
requiredMaxSourceAmount = maxSourceAmount;
// No rounding. It'll change once MPT integrated into DEX.
amountDeliver = divide(maxSourceAmount, rate);
}
if (requiredMaxSourceAmount > maxSourceAmount ||
(deliverMin && amountDeliver < *deliverMin))
return tecPATH_PARTIAL;
PaymentSandbox pv(&view());
auto res = accountSend(
pv, account_, dstAccountID, amountDeliver, ctx_.journal);
if (res == tesSUCCESS)
{
pv.apply(ctx_.rawView());
// If the actual amount delivered is different from the original
// amount due to partial payment or transfer fee, we need to update
// DelieveredAmount using the actual delivered amount
if (view().rules().enabled(fixMPTDeliveredAmount) &&
amountDeliver != dstAmount)
ctx_.deliver(amountDeliver);
}
else if (res == tecINSUFFICIENT_FUNDS || res == tecPATH_DRY)
res = tecPATH_PARTIAL;
return res;
return makeMPTDirectPayment(RipplePaymentParams{
.ctx = ctx_,
.maxSourceAmount = maxSourceAmount,
.srcAccountID = account_,
.dstAccountID = dstAccountID,
.sleDst = sleDst,
.dstAmount = dstAmount,
.paths = ctx_.tx.getFieldPathSet(sfPaths),
.deliverMin = deliverMin,
.partialPaymentAllowed = partialPaymentAllowed,
.defaultPathsAllowed = defaultPathsAllowed,
.limitQuality = limitQuality,
.j = j_});
}
XRPL_ASSERT(dstAmount.native(), "ripple::Payment::doApply : amount is XRP");
@@ -663,4 +543,202 @@ Payment::doApply()
return tesSUCCESS;
}
// Reusable helpers
TER
Payment::makeRipplePayment(Payment::RipplePaymentParams const& p)
{
// Set up some copies/references so the code can be moved from
// Payment::doApply otherwise unmodified
//
// Note that some of these variable names use trailing '_', which is
// usually reserved for member variables. After, or just before these
// changes are merged, a follow-up can clean up the names for consistency.
ApplyContext& ctx_ = p.ctx;
STAmount const& maxSourceAmount = p.maxSourceAmount;
AccountID const& account_ = p.srcAccountID;
AccountID const& dstAccountID = p.dstAccountID;
SLE::pointer sleDst = p.sleDst;
STAmount const& dstAmount = p.dstAmount;
STPathSet const& paths = p.paths;
std::optional<STAmount> const& deliverMin = p.deliverMin;
bool partialPaymentAllowed = p.partialPaymentAllowed;
bool defaultPathsAllowed = p.defaultPathsAllowed;
bool limitQuality = p.limitQuality;
beast::Journal j_ = p.j;
auto view = [&p]() -> ApplyView& { return p.ctx.view(); };
bool const depositPreauth =
p.ctx.view().rules().enabled(featureDepositPreauth);
bool const depositAuth = p.ctx.view().rules().enabled(featureDepositAuth);
// Below this line, copied straight from Payment::doApply
// except `ctx_.tx.getFieldPathSet(sfPaths)` replaced with `paths`,
// because not all transactions have that field available, and using
// it will throw
//-------------------------------------------------------
// Ripple payment with at least one intermediate step and uses
// transitive balances.
// An account that requires authorization has two ways to get an
// IOU Payment in:
// 1. If Account == Destination, or
// 2. If Account is deposit preauthorized by destination.
if (auto err = verifyDepositPreauth(
ctx_.tx, ctx_.view(), account_, dstAccountID, sleDst, ctx_.journal);
!isTesSuccess(err))
return err;
path::RippleCalc::Input rcInput;
rcInput.partialPaymentAllowed = partialPaymentAllowed;
rcInput.defaultPathsAllowed = defaultPathsAllowed;
rcInput.limitQuality = limitQuality;
rcInput.isLedgerOpen = view().open();
path::RippleCalc::Output rc;
{
PaymentSandbox pv(&view());
JLOG(j_.debug()) << "Entering RippleCalc in payment: "
<< ctx_.tx.getTransactionID();
rc = path::RippleCalc::rippleCalculate(
pv,
maxSourceAmount,
dstAmount,
dstAccountID,
account_,
paths,
ctx_.tx[~sfDomainID],
ctx_.app.logs(),
&rcInput);
// VFALCO NOTE We might not need to apply, depending
// on the TER. But always applying *should*
// be safe.
pv.apply(ctx_.rawView());
}
// TODO: is this right? If the amount is the correct amount, was
// the delivered amount previously set?
if (rc.result() == tesSUCCESS && rc.actualAmountOut != dstAmount)
{
if (deliverMin && rc.actualAmountOut < *deliverMin)
rc.setResult(tecPATH_PARTIAL);
else
ctx_.deliver(rc.actualAmountOut);
}
auto terResult = rc.result();
// Because of its overhead, if RippleCalc
// fails with a retry code, claim a fee
// instead. Maybe the user will be more
// careful with their path spec next time.
if (isTerRetry(terResult))
terResult = tecPATH_DRY;
return terResult;
}
TER
Payment::makeMPTDirectPayment(Payment::RipplePaymentParams const& p)
{
// Set up some copies/references so the code can be moved from
// Payment::doApply otherwise unmodified
//
// Note that some of these variable names use trailing '_', which is
// usually reserved for member variables. After, or just before these
// changes are merged, a follow-up can clean up the names for consistency.
ApplyContext& ctx_ = p.ctx;
STAmount const& maxSourceAmount = p.maxSourceAmount;
AccountID const& account_ = p.srcAccountID;
AccountID const& dstAccountID = p.dstAccountID;
SLE::pointer sleDst = p.sleDst;
STAmount const& dstAmount = p.dstAmount;
// STPathSet const& paths = p.paths;
std::optional<STAmount> const& deliverMin = p.deliverMin;
bool partialPaymentAllowed = p.partialPaymentAllowed;
// bool defaultPathsAllowed = p.defaultPathsAllowed;
// bool limitQuality = p.limitQuality;
beast::Journal j_ = p.j;
auto view = [&p]() -> ApplyView& { return p.ctx.view(); };
// Below this line, copied straight from Payment::doApply
//-------------------------------------------------------
JLOG(j_.trace()) << " dstAmount=" << dstAmount.getFullText();
auto const& mptIssue = dstAmount.get<MPTIssue>();
if (auto const ter = requireAuth(view(), mptIssue, account_);
ter != tesSUCCESS)
return ter;
if (auto const ter = requireAuth(view(), mptIssue, dstAccountID);
ter != tesSUCCESS)
return ter;
if (auto const ter = canTransfer(view(), mptIssue, account_, dstAccountID);
ter != tesSUCCESS)
return ter;
if (auto err = verifyDepositPreauth(
ctx_.tx, ctx_.view(), account_, dstAccountID, sleDst, ctx_.journal);
!isTesSuccess(err))
return err;
auto const& issuer = mptIssue.getIssuer();
// Transfer rate
Rate rate{QUALITY_ONE};
// Payment between the holders
if (account_ != issuer && dstAccountID != issuer)
{
// If globally/individually locked then
// - can't send between holders
// - holder can send back to issuer
// - issuer can send to holder
if (isAnyFrozen(view(), {account_, dstAccountID}, mptIssue))
return tecLOCKED;
// Get the rate for a payment between the holders.
rate = transferRate(view(), mptIssue.getMptID());
}
// Amount to deliver.
STAmount amountDeliver = dstAmount;
// Factor in the transfer rate.
// No rounding. It'll change once MPT integrated into DEX.
STAmount requiredMaxSourceAmount = multiply(dstAmount, rate);
// Send more than the account wants to pay or less than
// the account wants to deliver (if no SendMax).
// Adjust the amount to deliver.
if (partialPaymentAllowed && requiredMaxSourceAmount > maxSourceAmount)
{
requiredMaxSourceAmount = maxSourceAmount;
// No rounding. It'll change once MPT integrated into DEX.
amountDeliver = divide(maxSourceAmount, rate);
}
if (requiredMaxSourceAmount > maxSourceAmount ||
(deliverMin && amountDeliver < *deliverMin))
return tecPATH_PARTIAL;
PaymentSandbox pv(&view());
auto res =
accountSend(pv, account_, dstAccountID, amountDeliver, ctx_.journal);
if (res == tesSUCCESS)
{
pv.apply(ctx_.rawView());
// If the actual amount delivered is different from the original
// amount due to partial payment or transfer fee, we need to update
// DelieveredAmount using the actual delivered amount
if (view().rules().enabled(fixMPTDeliveredAmount) &&
amountDeliver != dstAmount)
ctx_.deliver(amountDeliver);
}
else if (res == tecINSUFFICIENT_FUNDS || res == tecPATH_DRY)
res = tecPATH_PARTIAL;
return res;
}
} // namespace ripple
+31
View File
@@ -40,6 +40,37 @@ public:
TER
doApply() override;
// Helpers for this class and other transactors that make "Payments"
struct RipplePaymentParams
{
ApplyContext& ctx;
STAmount const& maxSourceAmount;
AccountID const& srcAccountID;
AccountID const& dstAccountID;
SLE::pointer sleDst;
STAmount const& dstAmount;
// Paths need to be explicitly included because other transactions don't
// have them defined
STPathSet const& paths;
std::optional<STAmount> const& deliverMin;
bool partialPaymentAllowed = false;
bool defaultPathsAllowed = true;
bool limitQuality = false;
beast::Journal j;
};
static STAmount
getMaxSourceAmount(
AccountID const& senderAccount,
STAmount const& dstAmount,
std::optional<STAmount> const& sendMax = {});
static TER
makeRipplePayment(RipplePaymentParams const& p);
static TER
makeMPTDirectPayment(RipplePaymentParams const& p);
};
} // namespace ripple
+3 -1
View File
@@ -255,7 +255,9 @@ SetTrust::preclaim(PreclaimContext const& ctx)
else
return tecINTERNAL; // LCOV_EXCL_LINE
}
else if (sleDst->isFieldPresent(sfVaultID))
else if (
sleDst->isFieldPresent(sfVaultID) ||
sleDst->isFieldPresent(sfLoanBrokerID))
{
if (!ctx.view.exists(keylet::line(id, uDstAccountID, currency)))
return tecNO_PERMISSION;
+25 -7
View File
@@ -212,12 +212,17 @@ Transactor::preflight2(PreflightContext const& ctx)
// regardless of success or failure
return *ret;
auto const sigValid = checkValidity(
ctx.app.getHashRouter(), ctx.tx, ctx.rules, ctx.app.config());
if (sigValid.first == Validity::SigBad)
// Skip signature check on batch inner transactions
if (!ctx.tx.isFlag(tfInnerBatchTxn) || !ctx.rules.enabled(featureBatch))
{
JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second;
return temINVALID; // LCOV_EXCL_LINE
auto const sigValid = checkValidity(
ctx.app.getHashRouter(), ctx.tx, ctx.rules, ctx.app.config());
if (sigValid.first == Validity::SigBad)
{ // LCOV_EXCL_START
JLOG(ctx.j.debug())
<< "preflight2: bad signature. " << sigValid.second;
return temINVALID;
} // LCOV_EXCL_STOP
}
return tesSUCCESS;
}
@@ -651,13 +656,25 @@ NotTEC
Transactor::checkSign(
ReadView const& view,
ApplyFlags flags,
std::optional<uint256 const> const& parentBatchId,
AccountID const& idAccount,
STObject const& sigObject,
beast::Journal const j)
{
{
auto const sle = view.read(keylet::account(idAccount));
if (view.rules().enabled(featureLendingProtocol) &&
isPseudoAccount(sle))
// Pseudo-accounts can't sign transactions. This check is gated on
// the Lending Protocol amendment because that's the project it was
// added under, and it doesn't justify another amendment
return tefBAD_AUTH;
}
auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey);
// Ignore signature check on batch inner transactions
if (sigObject.isFlag(tfInnerBatchTxn) && view.rules().enabled(featureBatch))
if (parentBatchId && view.rules().enabled(featureBatch))
{
// Defensive Check: These values are also checked in Batch::preflight
if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() ||
@@ -710,7 +727,8 @@ Transactor::checkSign(PreclaimContext const& ctx)
auto const idAccount = ctx.tx.isFieldPresent(sfDelegate)
? ctx.tx.getAccountID(sfDelegate)
: ctx.tx.getAccountID(sfAccount);
return checkSign(ctx.view, ctx.flags, idAccount, ctx.tx, ctx.j);
return checkSign(
ctx.view, ctx.flags, ctx.parentBatchId, idAccount, ctx.tx, ctx.j);
}
NotTEC
+33 -2
View File
@@ -266,6 +266,7 @@ protected:
checkSign(
ReadView const& view,
ApplyFlags flags,
std::optional<uint256 const> const& parentBatchId,
AccountID const& idAccount,
STObject const& sigObject,
beast::Journal const j);
@@ -287,14 +288,26 @@ protected:
template <class T>
static bool
validNumericRange(std::optional<T> value, T max, T min = {});
validNumericRange(std::optional<T> value, T max, T min = T{});
template <class T, class Unit>
static bool
validNumericRange(
std::optional<T> value,
unit::ValueUnit<Unit, T> max,
unit::ValueUnit<Unit, T> min = {});
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});
/// Minimum will usually be zero.
template <class T>
static bool
validNumericMinimum(std::optional<T> value, T min = T{});
/// Minimum will usually be zero.
template <class T, class Unit>
static bool
validNumericMinimum(
std::optional<T> value,
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});
private:
std::pair<TER, XRPAmount>
@@ -421,6 +434,24 @@ Transactor::validNumericRange(
return validNumericRange(value, max.value(), min.value());
}
template <class T>
bool
Transactor::validNumericMinimum(std::optional<T> value, T min)
{
if (!value)
return true;
return value >= min;
}
template <class T, class Unit>
bool
Transactor::validNumericMinimum(
std::optional<T> value,
unit::ValueUnit<Unit, T> min)
{
return validNumericMinimum(value, min.value());
}
} // namespace ripple
#endif
+5 -2
View File
@@ -22,8 +22,11 @@ VaultCreate::checkExtraFeatures(PreflightContext const& ctx)
if (!ctx.rules.enabled(featureMPTokensV1))
return false;
return !ctx.tx.isFieldPresent(sfDomainID) ||
ctx.rules.enabled(featurePermissionedDomains);
if (ctx.tx.isFieldPresent(sfDomainID) &&
!ctx.rules.enabled(featurePermissionedDomains))
return false;
return true;
}
std::uint32_t
+5 -2
View File
@@ -14,8 +14,11 @@ namespace ripple {
bool
VaultSet::checkExtraFeatures(PreflightContext const& ctx)
{
return !ctx.tx.isFieldPresent(sfDomainID) ||
ctx.rules.enabled(featurePermissionedDomains);
if (ctx.tx.isFieldPresent(sfDomainID) &&
!ctx.rules.enabled(featurePermissionedDomains))
return false;
return true;
}
NotTEC
+3 -18
View File
@@ -23,13 +23,10 @@ VaultWithdraw::preflight(PreflightContext const& ctx)
if (ctx.tx[sfAmount] <= beast::zero)
return temBAD_AMOUNT;
if (auto const destination = ctx.tx[~sfDestination];
destination.has_value())
if (auto const destination = ctx.tx[~sfDestination])
{
if (*destination == beast::zero)
{
JLOG(ctx.j.debug())
<< "VaultWithdraw: zero/empty destination account.";
return temMALFORMED;
}
}
@@ -92,21 +89,9 @@ VaultWithdraw::preclaim(PreclaimContext const& ctx)
auto const account = ctx.tx[sfAccount];
auto const dstAcct = ctx.tx[~sfDestination].value_or(account);
auto const sleDst = ctx.view.read(keylet::account(dstAcct));
if (sleDst == nullptr)
return account == dstAcct ? tecINTERNAL : tecNO_DST;
if (sleDst->isFlag(lsfRequireDestTag) &&
!ctx.tx.isFieldPresent(sfDestinationTag))
return tecDST_TAG_NEEDED; // Cannot send without a tag
// Withdrawal to a 3rd party destination account is essentially a transfer,
// via shares in the vault. Enforce all the usual asset transfer checks.
if (account != dstAcct && sleDst->isFlag(lsfDepositAuth))
{
if (!ctx.view.exists(keylet::depositPreauth(dstAcct, account)))
return tecNO_PERMISSION;
}
if (auto const ret = canWithdraw(ctx.view, ctx.tx))
return ret;
// If sending to Account (i.e. not a transfer), we will also create (only
// if authorized) a trust line or MPToken as needed, in doApply().
-10
View File
@@ -40,16 +40,6 @@ checkValidity(
return {
Validity::SigBad,
"Malformed: Invalid inner batch transaction."};
std::string reason;
if (!passesLocalChecks(tx, reason))
{
router.setFlags(id, SF_LOCALBAD);
return {Validity::SigGoodOnly, reason};
}
router.setFlags(id, SF_SIGGOOD);
return {Validity::Valid, ""};
}
if (any(flags & SF_SIGBAD))
+34 -4
View File
@@ -1365,8 +1365,23 @@ PeerImp::handleTransaction(
// Charge strongly for attempting to relay a txn with tfInnerBatchTxn
// LCOV_EXCL_START
if (stx->isFlag(tfInnerBatchTxn) &&
getCurrentTransactionRules()->enabled(featureBatch))
/*
There is no need to check whether the featureBatch amendment is
enabled.
* If the `tfInnerBatchTxn` flag is set, and the amendment is
enabled, then it's an invalid transaction because inner batch
transactions should not be relayed.
* If the `tfInnerBatchTxn` flag is set, and the amendment is *not*
enabled, then the transaction is malformed because it's using an
"unknown" flag. There's no need to waste the resources to send it
to the transaction engine.
We don't normally check transaction validity at this level, but
since we _need_ to check it when the amendment is enabled, we may as
well drop it if the flag is set regardless.
*/
if (stx->isFlag(tfInnerBatchTxn))
{
JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing "
"tfInnerBatchTxn (handleTransaction).";
@@ -2924,8 +2939,23 @@ PeerImp::checkTransaction(
{
// charge strongly for relaying batch txns
// LCOV_EXCL_START
if (stx->isFlag(tfInnerBatchTxn) &&
getCurrentTransactionRules()->enabled(featureBatch))
/*
There is no need to check whether the featureBatch amendment is
enabled.
* If the `tfInnerBatchTxn` flag is set, and the amendment is
enabled, then it's an invalid transaction because inner batch
transactions should not be relayed.
* If the `tfInnerBatchTxn` flag is set, and the amendment is *not*
enabled, then the transaction is malformed because it's using an
"unknown" flag. There's no need to waste the resources to send it
to the transaction engine.
We don't normally check transaction validity at this level, but
since we _need_ to check it when the amendment is enabled, we may as
well drop it if the flag is set regardless.
*/
if (stx->isFlag(tfInnerBatchTxn))
{
JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing "
"tfInnerBatchTxn (checkSignature).";
+5 -1
View File
@@ -1078,7 +1078,11 @@ checkMultiSignFields(Json::Value const& jvRequest)
if (!tx_json.isMember(sfSigningPubKey.getJsonName()))
return RPC::missing_field_error("tx_json.SigningPubKey");
if (!tx_json[sfSigningPubKey.getJsonName()].asString().empty())
// Multi-signing into a signature_target object field is fine,
// because it means the signature is not for the transaction
// Account.
if (!jvRequest.isMember(jss::signature_target) &&
!tx_json[sfSigningPubKey.getJsonName()].asString().empty())
return RPC::make_error(
rpcINVALID_PARAMS,
"When multi-signing 'tx_json.SigningPubKey' must be empty.");
+22
View File
@@ -131,6 +131,28 @@ doAccountInfo(RPC::JsonContext& context)
result[jss::account_flags] = std::move(acctFlags);
auto const pseudoFields = getPseudoAccountFields();
for (auto const& pseudoField : pseudoFields)
{
if (sleAccepted->isFieldPresent(*pseudoField))
{
std::string name = pseudoField->fieldName;
if (name.ends_with("ID"))
{
// Remove the ID suffix from the field name.
name = name.substr(0, name.size() - 2);
XRPL_ASSERT_PARTS(
!name.empty(),
"ripple::doAccountInfo",
"name is not empty");
}
// ValidPseudoAccounts invariant guarantees that only one field
// can be set
result[jss::pseudo_account][jss::type] = name;
break;
}
}
// The document[https://xrpl.org/account_info.html#account_info] states
// that signer_lists is a bool, however assigning any string value
// works. Do not allow this. This check is for api Version 2 onwards
+40
View File
@@ -381,6 +381,46 @@ parseLedgerHashes(Json::Value const& params, Json::StaticString const fieldName)
return parseObjectID(params, fieldName, "hex string");
}
static Expected<uint256, Json::Value>
parseLoanBroker(Json::Value const& params, Json::StaticString const fieldName)
{
if (!params.isObject())
{
return parseObjectID(params, fieldName, "hex string");
}
auto const id = LedgerEntryHelpers::requiredAccountID(
params, jss::owner, "malformedOwner");
if (!id)
return Unexpected(id.error());
auto const seq =
LedgerEntryHelpers::requiredUInt32(params, jss::seq, "malformedSeq");
if (!seq)
return Unexpected(seq.error());
return keylet::loanbroker(*id, *seq).key;
}
static Expected<uint256, Json::Value>
parseLoan(Json::Value const& params, Json::StaticString const fieldName)
{
if (!params.isObject())
{
return parseObjectID(params, fieldName, "hex string");
}
auto const id = LedgerEntryHelpers::requiredUInt256(
params, jss::loan_broker_id, "malformedOwner");
if (!id)
return Unexpected(id.error());
auto const seq = LedgerEntryHelpers::requiredUInt32(
params, jss::loan_seq, "malformedSeq");
if (!seq)
return Unexpected(seq.error());
return keylet::loan(*id, *seq).key;
}
static Expected<uint256, Json::Value>
parseMPToken(Json::Value const& params, Json::StaticString const fieldName)
{