Compare commits

..

8 Commits

Author SHA1 Message Date
Bart
a07399d2f1 Merge branch 'develop' into bthomee/ccache 2025-12-04 09:45:00 -05:00
Bart
73ef462422 Merge branch 'develop' into bthomee/ccache 2025-12-04 08:33:02 -05:00
Bart
ac71a5fcc8 Update name of step 2025-12-03 18:16:04 -05:00
Bart
efd2a33ecb Use env var for the ccache dir 2025-12-03 17:54:47 -05:00
Bart
5ca0577739 Cache the ccache dir 2025-12-03 17:41:42 -05:00
Bart
89ea91af5d Update CI images 2025-12-03 17:18:49 -05:00
Bart
30d2f70744 Print ccache statistics 2025-12-03 17:00:52 -05:00
Bart
aec131bcbb ci: Use ccache to cache build objects for speeding up building 2025-12-03 16:50:35 -05:00
21 changed files with 273 additions and 916 deletions

View File

@@ -15,196 +15,196 @@
"distro_version": "bookworm",
"compiler_name": "gcc",
"compiler_version": "12",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "debian",
"distro_version": "bookworm",
"compiler_name": "gcc",
"compiler_version": "13",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "debian",
"distro_version": "bookworm",
"compiler_name": "gcc",
"compiler_version": "14",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "debian",
"distro_version": "bookworm",
"compiler_name": "gcc",
"compiler_version": "15",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "debian",
"distro_version": "bookworm",
"compiler_name": "clang",
"compiler_version": "16",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "debian",
"distro_version": "bookworm",
"compiler_name": "clang",
"compiler_version": "17",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "debian",
"distro_version": "bookworm",
"compiler_name": "clang",
"compiler_version": "18",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "debian",
"distro_version": "bookworm",
"compiler_name": "clang",
"compiler_version": "19",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "debian",
"distro_version": "bookworm",
"compiler_name": "clang",
"compiler_version": "20",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "debian",
"distro_version": "trixie",
"compiler_name": "gcc",
"compiler_version": "14",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "debian",
"distro_version": "trixie",
"compiler_name": "gcc",
"compiler_version": "15",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "debian",
"distro_version": "trixie",
"compiler_name": "clang",
"compiler_version": "20",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "debian",
"distro_version": "trixie",
"compiler_name": "clang",
"compiler_version": "21",
"image_sha": "0525eae"
"image_sha": "ca4517d"
},
{
"distro_name": "rhel",
"distro_version": "8",
"compiler_name": "gcc",
"compiler_version": "14",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "rhel",
"distro_version": "8",
"compiler_name": "clang",
"compiler_version": "any",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "rhel",
"distro_version": "9",
"compiler_name": "gcc",
"compiler_version": "12",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "rhel",
"distro_version": "9",
"compiler_name": "gcc",
"compiler_version": "13",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "rhel",
"distro_version": "9",
"compiler_name": "gcc",
"compiler_version": "14",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "rhel",
"distro_version": "9",
"compiler_name": "clang",
"compiler_version": "any",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "rhel",
"distro_version": "10",
"compiler_name": "gcc",
"compiler_version": "14",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "rhel",
"distro_version": "10",
"compiler_name": "clang",
"compiler_version": "any",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "ubuntu",
"distro_version": "jammy",
"compiler_name": "gcc",
"compiler_version": "12",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "ubuntu",
"distro_version": "noble",
"compiler_name": "gcc",
"compiler_version": "13",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "ubuntu",
"distro_version": "noble",
"compiler_name": "gcc",
"compiler_version": "14",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "ubuntu",
"distro_version": "noble",
"compiler_name": "clang",
"compiler_version": "16",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "ubuntu",
"distro_version": "noble",
"compiler_name": "clang",
"compiler_version": "17",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "ubuntu",
"distro_version": "noble",
"compiler_name": "clang",
"compiler_version": "18",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
},
{
"distro_name": "ubuntu",
"distro_version": "noble",
"compiler_name": "clang",
"compiler_version": "19",
"image_sha": "e1782cd"
"image_sha": "ca4517d"
}
],
"build_type": ["Debug", "Release"],

View File

@@ -116,6 +116,16 @@ jobs:
${CMAKE_ARGS} \
..
- name: Restore build objects (Linux)
if: ${{ runner.os == 'Linux' }}
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ inputs.config_name }}
- name: Print out ccache statistics before building
run: ccache -s
- name: Build the binary
working-directory: ${{ inputs.build_dir }}
env:
@@ -129,14 +139,22 @@ jobs:
--parallel "${BUILD_NPROC}" \
--target "${CMAKE_TARGET}"
- name: Print out ccache statistics after building
run: ccache -s
- name: Save build objects (Linux)
if: ${{ runner.os == 'Linux' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ inputs.config_name }}
- name: Upload rippled artifact (Linux)
if: ${{ github.repository_owner == 'XRPLF' && runner.os == 'Linux' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
env:
BUILD_DIR: ${{ inputs.build_dir }}
with:
name: rippled-${{ inputs.config_name }}
path: ${{ env.BUILD_DIR }}/rippled
path: ${{ inputs.build_dir }}/rippled
retention-days: 3
if-no-files-found: error

3
.gitignore vendored
View File

@@ -111,6 +111,3 @@ bld.rippled/
# Suggested in-tree build directory
/.build*/
# Locally patched Conan recipes
external/conan-center-index/

View File

@@ -141,37 +141,21 @@ Alternatively, you can pull the patched recipes into the repository and use them
locally:
```bash
# Extract the version number from the lockfile.
function extract_version {
version=$(cat conan.lock | sed -nE "s@.+${1}/(.+)#.+@\1@p" | head -n1)
echo ${version}
}
# Define which recipes to export.
recipes=(ed25519 grpc secp256k1 snappy soci)
# Selectively check out the recipes from our CCI fork.
cd external
mkdir -p conan-center-index
cd conan-center-index
git init
git remote add origin git@github.com:XRPLF/conan-center-index.git
git sparse-checkout init
for recipe in ${recipes[@]}; do
echo "Checking out ${recipe}..."
git sparse-checkout add recipes/${recipe}/all
done
git sparse-checkout set recipes/ed25519
git sparse-checkout add recipes/secp256k1
git sparse-checkout add recipes/snappy
git sparse-checkout add recipes/soci
git fetch origin master
git checkout master
cd ../..
# Export the recipes into the local cache.
for recipe in ${recipes[@]}; do
version=$(extract_version ${recipe})
echo "Exporting ${recipe}/${version}..."
conan export --version $(extract_version ${recipe}) \
external/conan-center-index/recipes/${recipe}/all
done
conan export --version 2015.03 recipes/ed25519/all
conan export --version 0.7.0 recipes/secp256k1/all
conan export --version 1.1.10 recipes/snappy/all
conan export --version 4.0.3 recipes/soci/all
rm -rf .git
```
In the case we switch to a newer version of a dependency that still requires a
@@ -401,6 +385,19 @@ tools.build:cxxflags=['-DBOOST_ASIO_DISABLE_CONCEPTS']
`--settings build_type=$BUILD_TYPE` or in the profile itself,
under the section `[settings]` with the key `build_type`.
If you are using a Microsoft Visual C++ compiler,
then you will need to ensure consistency between the `build_type` setting
and the `compiler.runtime` setting.
When `build_type` is `Release`, `compiler.runtime` should be `MT`.
When `build_type` is `Debug`, `compiler.runtime` should be `MTd`.
```
conan install .. --output-folder . --build missing --settings build_type=Release --settings compiler.runtime=MT
conan install .. --output-folder . --build missing --settings build_type=Debug --settings compiler.runtime=MTd
```
3. Configure CMake and pass the toolchain file generated by Conan, located at
`$OUTPUT_FOLDER/build/generators/conan_toolchain.cmake`.

View File

@@ -2,6 +2,12 @@
setup project-wide compiler settings
#]===================================================================]
find_program(CCACHE_PATH "ccache")
if (CCACHE_PATH)
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PATH}")
message(STATUS "Using ccache: ${CCACHE_PATH}")
endif ()
#[=========================================================[
TODO some/most of these common settings belong in a
toolchain file, especially the ABI-impacting ones

View File

@@ -9,7 +9,7 @@
"secp256k1/0.7.0#9210e4c655d0a665400378a663960d17%1764261276.764",
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1762797952.535",
"re2/20230301#ca3b241baec15bd31ea9187150e0b333%1764175362.029",
"protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1759134873.45",
"protobuf/3.21.12#44ee56c0a6eea0c19aeeaca680370b88%1764175361.456",
"openssl/3.5.4#a1d5835cc6ed5c5b8f3cd5b9b5d24205%1760106486.594",
"nudb/2.0.9#fb8dfd1a5557f5e0528114c2da17721e%1763150366.909",
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1756234228.999",
@@ -17,19 +17,19 @@
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1756230911.03",
"libarchive/3.8.1#ffee18995c706e02bf96e7a2f7042e0d%1764175360.142",
"jemalloc/5.3.0#e951da9cf599e956cebc117880d2d9f8%1729241615.244",
"grpc/1.72.0#f244a57bff01e708c55a1100b12e1589%1763158050.628",
"ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1764270189.893",
"grpc/1.50.1#02291451d1e17200293a409410d1c4e1%1756234248.958",
"ed25519/2015.03#17c1f1910e769f368025267a53c23c13%1764259445.491",
"doctest/2.4.12#eb9fb352fb2fdfc8abb17ec270945165%1762797941.757",
"date/3.0.4#862e11e80030356b53c2c38599ceb32b%1763584497.32",
"c-ares/1.34.5#5581c2b62a608b40bb85d965ab3ec7c8%1764175359.429",
"bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1764175359.429",
"boost/1.88.0#8852c0b72ce8271fb8ff7c53456d4983%1756223752.326",
"abseil/20250127.0#9e8e8cfc89a1324139fc0ee3bd4d8c8c%1753819045.301"
"abseil/20230802.1#90ba607d4ee8fb5fb157c3db540671fc%1764175359.429"
],
"build_requires": [
"zlib/1.3.1#b8bc2603263cf7eccbd6e17e66b0ed76%1756234269.497",
"strawberryperl/5.32.1.1#707032463aa0620fa17ec0d887f5fe41%1756234281.733",
"protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1759134873.45",
"protobuf/3.21.12#44ee56c0a6eea0c19aeeaca680370b88%1764175361.456",
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1756234232.901",
"msys2/cci.latest#5b73b10144f73cc5bfe0572ed9be39e1%1751977009.857",
"m4/1.4.19#70dc8bbb33e981d119d2acc0175cf381%1763158052.846",
@@ -37,13 +37,13 @@
"cmake/3.31.10#313d16a1aa16bbdb2ca0792467214b76%1764175359.429",
"b2/5.3.3#107c15377719889654eb9a162a673975%1756234226.28",
"automake/1.16.5#b91b7c384c3deaa9d535be02da14d04f%1755524470.56",
"autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86",
"abseil/20250127.0#9e8e8cfc89a1324139fc0ee3bd4d8c8c%1753819045.301"
"autoconf/2.71#51077f068e61700d65bb05541ea1e4b0%1731054366.86"
],
"python_requires": [],
"overrides": {
"protobuf/5.27.0": [
"protobuf/6.32.1"
"protobuf/3.21.12": [
null,
"protobuf/3.21.12"
],
"lz4/1.9.4": [
"lz4/1.10.0"

View File

@@ -20,6 +20,12 @@ compiler.libcxx={{detect_api.detect_libcxx(compiler, version, compiler_exe)}}
{% endif %}
[conf]
{% if compiler == "clang" and compiler_version >= 19 %}
grpc/1.50.1:tools.build:cxxflags+=['-Wno-missing-template-arg-list-after-template-kw']
{% endif %}
{% if compiler == "apple-clang" and compiler_version >= 17 %}
grpc/1.50.1:tools.build:cxxflags+=['-Wno-missing-template-arg-list-after-template-kw']
{% endif %}
{% if compiler == "gcc" and compiler_version < 13 %}
tools.build:cxxflags+=['-Wno-restrict']
{% endif %}

View File

@@ -29,7 +29,7 @@ class Xrpl(ConanFile):
requires = [
"ed25519/2015.03",
"grpc/1.72.0",
"grpc/1.50.1",
"libarchive/3.8.1",
"nudb/2.0.9",
"openssl/3.5.4",
@@ -43,7 +43,7 @@ class Xrpl(ConanFile):
]
tool_requires = [
"protobuf/6.32.1",
"protobuf/3.21.12",
]
default_options = {
@@ -61,16 +61,6 @@ class Xrpl(ConanFile):
"ed25519/*:shared": False,
"grpc/*:shared": False,
"grpc/*:secure": True,
"grpc/*:codegen": True,
"grpc/*:cpp_plugin": True,
"grpc/*:csharp_ext": False,
"grpc/*:csharp_plugin": False,
"grpc/*:node_plugin": False,
"grpc/*:objective_c_plugin": False,
"grpc/*:php_plugin": False,
"grpc/*:python_plugin": False,
"grpc/*:ruby_plugin": False,
"grpc/*:otel_plugin": False,
"libarchive/*:shared": False,
"libarchive/*:with_acl": False,
"libarchive/*:with_bzip2": False,
@@ -128,7 +118,7 @@ class Xrpl(ConanFile):
self.requires("boost/1.88.0", force=True, **transitive_headers_opt)
self.requires("date/3.0.4", **transitive_headers_opt)
self.requires("lz4/1.10.0", force=True)
self.requires("protobuf/6.32.1", force=True)
self.requires("protobuf/3.21.12", force=True)
self.requires("sqlite3/3.49.1", force=True)
if self.options.jemalloc:
self.requires("jemalloc/5.3.0")

View File

@@ -16,7 +16,7 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo)
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)
@@ -30,7 +30,7 @@ XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo
XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(SingleAssetVault, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo)
// Check flags in Credential transactions
XRPL_FIX (InvalidTxFlags, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -1,642 +0,0 @@
#include <xrpl/beast/unit_test/suite.h>
// DO NOT REMOVE
#include <test/jtx.h>
#include <test/jtx/Account.h>
#include <test/jtx/amount.h>
#include <test/jtx/mpt.h>
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/misc/LoadFeeTrack.h>
#include <xrpld/app/tx/detail/Batch.h>
#include <xrpld/app/tx/detail/LoanSet.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/protocol/SField.h>
#include <string>
#include <vector>
namespace ripple {
namespace test {
class LendingHelpers_test : public beast::unit_test::suite
{
void
testComputeRaisedRate()
{
using namespace jtx;
using namespace ripple::detail;
struct TestCase
{
std::string name;
Number periodicRate;
std::uint32_t paymentsRemaining;
Number expectedRaisedRate;
};
auto const testCases = std::vector<TestCase>{
{
.name = "Zero payments remaining",
.periodicRate = Number{5, -2},
.paymentsRemaining = 0,
.expectedRaisedRate = Number{1}, // (1 + r)^0 = 1
},
{
.name = "One payment remaining",
.periodicRate = Number{5, -2},
.paymentsRemaining = 1,
.expectedRaisedRate = Number{105, -2},
}, // 1.05^1
{
.name = "Multiple payments remaining",
.periodicRate = Number{5, -2},
.paymentsRemaining = 3,
.expectedRaisedRate = Number{1157625, -6},
}, // 1.05^3
{
.name = "Zero periodic rate",
.periodicRate = Number{0},
.paymentsRemaining = 5,
.expectedRaisedRate = Number{1}, // (1 + 0)^5 = 1
}};
for (auto const& tc : testCases)
{
testcase("computeRaisedRate: " + tc.name);
auto const computedRaisedRate =
computeRaisedRate(tc.periodicRate, tc.paymentsRemaining);
BEAST_EXPECTS(
computedRaisedRate == tc.expectedRaisedRate,
"Raised rate mismatch: expected " +
to_string(tc.expectedRaisedRate) + ", got " +
to_string(computedRaisedRate));
}
}
void
testComputePaymentFactor()
{
using namespace jtx;
using namespace ripple::detail;
struct TestCase
{
std::string name;
Number periodicRate;
std::uint32_t paymentsRemaining;
Number expectedPaymentFactor;
};
auto const testCases = std::vector<TestCase>{
{
.name = "Zero periodic rate",
.periodicRate = Number{0},
.paymentsRemaining = 4,
.expectedPaymentFactor = Number{25, -2},
}, // 1/4 = 0.25
{
.name = "One payment remaining",
.periodicRate = Number{5, -2},
.paymentsRemaining = 1,
.expectedPaymentFactor = Number{105, -2},
}, // 0.05/1 = 1.05
{
.name = "Multiple payments remaining",
.periodicRate = Number{5, -2},
.paymentsRemaining = 3,
.expectedPaymentFactor = Number{367208564631245, -15},
}, // from calc
{
.name = "Zero payments remaining",
.periodicRate = Number{5, -2},
.paymentsRemaining = 0,
.expectedPaymentFactor = Number{0},
} // edge case
};
for (auto const& tc : testCases)
{
testcase("computePaymentFactor: " + tc.name);
auto const computedPaymentFactor =
computePaymentFactor(tc.periodicRate, tc.paymentsRemaining);
BEAST_EXPECTS(
computedPaymentFactor == tc.expectedPaymentFactor,
"Payment factor mismatch: expected " +
to_string(tc.expectedPaymentFactor) + ", got " +
to_string(computedPaymentFactor));
}
}
void
testLoanPeriodicPayment()
{
using namespace jtx;
using namespace ripple::detail;
struct TestCase
{
std::string name;
Number principalOutstanding;
Number periodicRate;
std::uint32_t paymentsRemaining;
Number expectedPeriodicPayment;
};
auto const testCases = std::vector<TestCase>{
{
.name = "Zero principal outstanding",
.principalOutstanding = Number{0},
.periodicRate = Number{5, -2},
.paymentsRemaining = 5,
.expectedPeriodicPayment = Number{0},
},
{
.name = "Zero payments remaining",
.principalOutstanding = Number{1'000},
.periodicRate = Number{5, -2},
.paymentsRemaining = 0,
.expectedPeriodicPayment = Number{0},
},
{
.name = "Zero periodic rate",
.principalOutstanding = Number{1'000},
.periodicRate = Number{0},
.paymentsRemaining = 4,
.expectedPeriodicPayment = Number{250},
},
{
.name = "Standard case",
.principalOutstanding = Number{1'000},
.periodicRate =
loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60),
.paymentsRemaining = 3,
.expectedPeriodicPayment =
Number{3895690663961231, -13}, // from calc
},
};
for (auto const& tc : testCases)
{
testcase("loanPeriodicPayment: " + tc.name);
auto const computedPeriodicPayment = loanPeriodicPayment(
tc.principalOutstanding, tc.periodicRate, tc.paymentsRemaining);
BEAST_EXPECTS(
computedPeriodicPayment == tc.expectedPeriodicPayment,
"Periodic payment mismatch: expected " +
to_string(tc.expectedPeriodicPayment) + ", got " +
to_string(computedPeriodicPayment));
}
}
void
testLoanPrincipalFromPeriodicPayment()
{
using namespace jtx;
using namespace ripple::detail;
struct TestCase
{
std::string name;
Number periodicPayment;
Number periodicRate;
std::uint32_t paymentsRemaining;
Number expectedPrincipalOutstanding;
};
auto const testCases = std::vector<TestCase>{
{
.name = "Zero periodic payment",
.periodicPayment = Number{0},
.periodicRate = Number{5, -2},
.paymentsRemaining = 5,
.expectedPrincipalOutstanding = Number{0},
},
{
.name = "Zero payments remaining",
.periodicPayment = Number{1'000},
.periodicRate = Number{5, -2},
.paymentsRemaining = 0,
.expectedPrincipalOutstanding = Number{0},
},
{
.name = "Zero periodic rate",
.periodicPayment = Number{250},
.periodicRate = Number{0},
.paymentsRemaining = 4,
.expectedPrincipalOutstanding = Number{1'000},
},
{
.name = "Standard case",
.periodicPayment = Number{3895690663961231, -13}, // from calc
.periodicRate =
loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60),
.paymentsRemaining = 3,
.expectedPrincipalOutstanding = Number{1'000},
},
};
for (auto const& tc : testCases)
{
testcase("loanPrincipalFromPeriodicPayment: " + tc.name);
auto const computedPrincipalOutstanding =
loanPrincipalFromPeriodicPayment(
tc.periodicPayment, tc.periodicRate, tc.paymentsRemaining);
BEAST_EXPECTS(
computedPrincipalOutstanding == tc.expectedPrincipalOutstanding,
"Principal outstanding mismatch: expected " +
to_string(tc.expectedPrincipalOutstanding) + ", got " +
to_string(computedPrincipalOutstanding));
}
}
void
testComputeOverpaymentComponents()
{
testcase("computeOverpaymentComponents");
using namespace jtx;
using namespace ripple::detail;
Account const issuer{"issuer"};
PrettyAsset const IOU = issuer["IOU"];
int32_t const loanScale = 1;
auto const overpayment = Number{1'000};
auto const overpaymentInterestRate = TenthBips32{10'000}; // 10%
auto const overpaymentFeeRate = TenthBips32{50'000}; // 50%
auto const managementFeeRate = TenthBips16{10'000}; // 10%
auto const expectedOverpaymentFee = Number{500}; // 50% of 1,000
auto const expectedOverpaymentInterestGross =
Number{100}; // 10% of 1,000
auto const expectedOverpaymentInterestNet =
Number{90}; // 100 - 10% of 100
auto const expectedOverpaymentManagementFee = Number{10}; // 10% of 100
auto const expectedPrincipalPortion = Number{400}; // 1,000 - 100 - 500
auto const components = detail::computeOverpaymentComponents(
IOU,
loanScale,
overpayment,
overpaymentInterestRate,
overpaymentFeeRate,
managementFeeRate);
BEAST_EXPECT(
components.untrackedManagementFee == expectedOverpaymentFee);
BEAST_EXPECT(
components.untrackedInterest == expectedOverpaymentInterestNet);
BEAST_EXPECT(
components.trackedManagementFeeDelta ==
expectedOverpaymentManagementFee);
BEAST_EXPECT(
components.trackedPrincipalDelta == expectedPrincipalPortion);
BEAST_EXPECT(
components.trackedManagementFeeDelta +
components.untrackedInterest ==
expectedOverpaymentInterestGross);
BEAST_EXPECT(
components.trackedManagementFeeDelta +
components.untrackedInterest +
components.trackedPrincipalDelta +
components.untrackedManagementFee ==
overpayment);
}
void
testComputeInterestAndFeeParts()
{
using namespace jtx;
using namespace ripple::detail;
struct TestCase
{
std::string name;
Number interest;
TenthBips16 managementFeeRate;
Number expectedInterestPart;
Number expectedFeePart;
};
Account const issuer{"issuer"};
PrettyAsset const IOU = issuer["IOU"];
std::int32_t const loanScale = 1;
auto const testCases = std::vector<TestCase>{
{.name = "Zero interest",
.interest = Number{0},
.managementFeeRate = TenthBips16{10'000},
.expectedInterestPart = Number{0},
.expectedFeePart = Number{0}},
{.name = "Zero fee rate",
.interest = Number{1'000},
.managementFeeRate = TenthBips16{0},
.expectedInterestPart = Number{1'000},
.expectedFeePart = Number{0}},
{.name = "10% fee rate",
.interest = Number{1'000},
.managementFeeRate = TenthBips16{10'000},
.expectedInterestPart = Number{900},
.expectedFeePart = Number{100}},
};
for (auto const& tc : testCases)
{
testcase("computeInterestAndFeeParts: " + tc.name);
auto const [computedInterestPart, computedFeePart] =
computeInterestAndFeeParts(
IOU, tc.interest, tc.managementFeeRate, loanScale);
BEAST_EXPECTS(
computedInterestPart == tc.expectedInterestPart,
"Interest part mismatch: expected " +
to_string(tc.expectedInterestPart) + ", got " +
to_string(computedInterestPart));
BEAST_EXPECTS(
computedFeePart == tc.expectedFeePart,
"Fee part mismatch: expected " + to_string(tc.expectedFeePart) +
", got " + to_string(computedFeePart));
}
}
void
testLoanLatePaymentInterest()
{
using namespace jtx;
using namespace ripple::detail;
struct TestCase
{
std::string name;
Number principalOutstanding;
TenthBips32 lateInterestRate;
NetClock::time_point parentCloseTime;
std::uint32_t nextPaymentDueDate;
Number expectedLateInterest;
};
auto const testCases = std::vector<TestCase>{
{
.name = "On-time payment",
.principalOutstanding = Number{1'000},
.lateInterestRate = TenthBips32{10'000}, // 10%
.parentCloseTime =
NetClock::time_point{NetClock::duration{3'000}},
.nextPaymentDueDate = 3'000,
.expectedLateInterest = Number{0},
},
{
.name = "Early payment",
.principalOutstanding = Number{1'000},
.lateInterestRate = TenthBips32{10'000}, // 10%
.parentCloseTime =
NetClock::time_point{NetClock::duration{3'000}},
.nextPaymentDueDate = 4'000,
.expectedLateInterest = Number{0},
},
{
.name = "No principal outstanding",
.principalOutstanding = Number{0},
.lateInterestRate = TenthBips32{10'000}, // 10%
.parentCloseTime =
NetClock::time_point{NetClock::duration{3'000}},
.nextPaymentDueDate = 2'000,
.expectedLateInterest = Number{0},
},
{
.name = "No late interest rate",
.principalOutstanding = Number{1'000},
.lateInterestRate = TenthBips32{0}, // 0%
.parentCloseTime =
NetClock::time_point{NetClock::duration{3'000}},
.nextPaymentDueDate = 2'000,
.expectedLateInterest = Number{0},
},
{
.name = "Late payment",
.principalOutstanding = Number{1'000},
.lateInterestRate = TenthBips32{100'000}, // 100%
.parentCloseTime =
NetClock::time_point{NetClock::duration{3'000}},
.nextPaymentDueDate = 2'000,
.expectedLateInterest =
Number{3170979198376459, -17}, // from calc
},
};
for (auto const& tc : testCases)
{
testcase("loanLatePaymentInterest: " + tc.name);
auto const computedLateInterest = loanLatePaymentInterest(
tc.principalOutstanding,
tc.lateInterestRate,
tc.parentCloseTime,
tc.nextPaymentDueDate);
BEAST_EXPECTS(
computedLateInterest == tc.expectedLateInterest,
"Late interest mismatch: expected " +
to_string(tc.expectedLateInterest) + ", got " +
to_string(computedLateInterest));
}
}
void
testLoanAccruedInterest()
{
using namespace jtx;
using namespace ripple::detail;
struct TestCase
{
std::string name;
Number principalOutstanding;
Number periodicRate;
NetClock::time_point parentCloseTime;
std::uint32_t startDate;
std::uint32_t prevPaymentDate;
std::uint32_t paymentInterval;
Number expectedAccruedInterest;
};
auto const testCases = std::vector<TestCase>{
{
.name = "Zero principal outstanding",
.principalOutstanding = Number{0},
.periodicRate = Number{5, -2},
.parentCloseTime =
NetClock::time_point{NetClock::duration{3'000}},
.startDate = 2'000,
.prevPaymentDate = 2'500,
.paymentInterval = 30 * 24 * 60 * 60,
.expectedAccruedInterest = Number{0},
},
{
.name = "Before start date",
.principalOutstanding = Number{1'000},
.periodicRate = Number{5, -2},
.parentCloseTime =
NetClock::time_point{NetClock::duration{1'000}},
.startDate = 2'000,
.prevPaymentDate = 1'500,
.paymentInterval = 30 * 24 * 60 * 60,
.expectedAccruedInterest = Number{0},
},
{
.name = "Zero periodic rate",
.principalOutstanding = Number{1'000},
.periodicRate = Number{0},
.parentCloseTime =
NetClock::time_point{NetClock::duration{3'000}},
.startDate = 2'000,
.prevPaymentDate = 2'500,
.paymentInterval = 30 * 24 * 60 * 60,
.expectedAccruedInterest = Number{0},
},
{
.name = "Zero payment interval",
.principalOutstanding = Number{1'000},
.periodicRate = Number{5, -2},
.parentCloseTime =
NetClock::time_point{NetClock::duration{3'000}},
.startDate = 2'000,
.prevPaymentDate = 2'500,
.paymentInterval = 0,
.expectedAccruedInterest = Number{0},
},
{
.name = "Standard case",
.principalOutstanding = Number{1'000},
.periodicRate = Number{5, -2},
.parentCloseTime =
NetClock::time_point{NetClock::duration{3'000}},
.startDate = 1'000,
.prevPaymentDate = 2'000,
.paymentInterval = 30 * 24 * 60 * 60,
.expectedAccruedInterest =
Number{1929012345679012, -17}, // from calc
},
};
for (auto const& tc : testCases)
{
testcase("loanAccruedInterest: " + tc.name);
auto const computedAccruedInterest = loanAccruedInterest(
tc.principalOutstanding,
tc.periodicRate,
tc.parentCloseTime,
tc.startDate,
tc.prevPaymentDate,
tc.paymentInterval);
BEAST_EXPECTS(
computedAccruedInterest == tc.expectedAccruedInterest,
"Accrued interest mismatch: expected " +
to_string(tc.expectedAccruedInterest) + ", got " +
to_string(computedAccruedInterest));
}
}
// This test overlaps with testLoanAccruedInterest, the test cases only
// exercise the computeFullPaymentInterest parts unique to it.
void
testComputeFullPaymentInterest()
{
using namespace jtx;
using namespace ripple::detail;
struct TestCase
{
std::string name;
Number rawPrincipalOutstanding;
Number periodicRate;
NetClock::time_point parentCloseTime;
std::uint32_t paymentInterval;
std::uint32_t prevPaymentDate;
std::uint32_t startDate;
TenthBips32 closeInterestRate;
Number expectedFullPaymentInterest;
};
auto const testCases = std::vector<TestCase>{
{
.name = "Zero principal outstanding",
.rawPrincipalOutstanding = Number{0},
.periodicRate = Number{5, -2},
.parentCloseTime =
NetClock::time_point{NetClock::duration{3'000}},
.paymentInterval = 30 * 24 * 60 * 60,
.prevPaymentDate = 2'000,
.startDate = 1'000,
.closeInterestRate = TenthBips32{10'000},
.expectedFullPaymentInterest = Number{0},
},
{
.name = "Zero close interest rate",
.rawPrincipalOutstanding = Number{1'000},
.periodicRate = Number{5, -2},
.parentCloseTime =
NetClock::time_point{NetClock::duration{3'000}},
.paymentInterval = 30 * 24 * 60 * 60,
.prevPaymentDate = 2'000,
.startDate = 1'000,
.closeInterestRate = TenthBips32{0},
.expectedFullPaymentInterest =
Number{1929012345679012, -17}, // from calc
},
{
.name = "Standard case",
.rawPrincipalOutstanding = Number{1'000},
.periodicRate = Number{5, -2},
.parentCloseTime =
NetClock::time_point{NetClock::duration{3'000}},
.paymentInterval = 30 * 24 * 60 * 60,
.prevPaymentDate = 2'000,
.startDate = 1'000,
.closeInterestRate = TenthBips32{10'000},
.expectedFullPaymentInterest =
Number{1000192901234568, -13}, // from calc
},
};
for (auto const& tc : testCases)
{
testcase("computeFullPaymentInterest: " + tc.name);
auto const computedFullPaymentInterest = computeFullPaymentInterest(
tc.rawPrincipalOutstanding,
tc.periodicRate,
tc.parentCloseTime,
tc.paymentInterval,
tc.prevPaymentDate,
tc.startDate,
tc.closeInterestRate);
BEAST_EXPECTS(
computedFullPaymentInterest == tc.expectedFullPaymentInterest,
"Full payment interest mismatch: expected " +
to_string(tc.expectedFullPaymentInterest) + ", got " +
to_string(computedFullPaymentInterest));
}
}
public:
void
run() override
{
testComputeFullPaymentInterest();
testLoanAccruedInterest();
testLoanLatePaymentInterest();
testLoanPeriodicPayment();
testLoanPrincipalFromPeriodicPayment();
testComputeRaisedRate();
testComputePaymentFactor();
testComputeOverpaymentComponents();
testComputeInterestAndFeeParts();
}
};
BEAST_DEFINE_TESTSUITE(LendingHelpers, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -141,7 +141,7 @@ protected:
using namespace jtx;
auto const vaultSle = env.le(keylet::vault(vaultID));
return getAssetsTotalScale(vaultSle);
return getVaultScale(vaultSle);
}
};
@@ -551,15 +551,12 @@ protected:
broker.vaultScale(env),
state.principalOutstanding.exponent())));
BEAST_EXPECT(state.paymentInterval == 600);
{
NumberRoundModeGuard mg(Number::upward);
BEAST_EXPECT(
state.totalValue ==
roundToAsset(
broker.asset,
state.periodicPayment * state.paymentRemaining,
state.loanScale));
}
BEAST_EXPECT(
state.totalValue ==
roundToAsset(
broker.asset,
state.periodicPayment * state.paymentRemaining,
state.loanScale));
BEAST_EXPECT(
state.managementFeeOutstanding ==
computeManagementFee(
@@ -700,8 +697,7 @@ protected:
interval,
total,
feeRate,
asset(brokerParams.vaultDeposit).number().exponent(),
env.journal);
asset(brokerParams.vaultDeposit).number().exponent());
log << "Loan properties:\n"
<< "\tPrincipal: " << principal << std::endl
<< "\tInterest rate: " << interest << std::endl
@@ -1275,8 +1271,7 @@ protected:
verifyLoanStatus,
issuer,
lender,
borrower,
PaymentParameters{.showStepBalances = true});
borrower);
}
/** Runs through the complete lifecycle of a loan
@@ -1482,8 +1477,7 @@ protected:
state.paymentInterval,
state.paymentRemaining,
broker.params.managementFeeRate,
state.loanScale,
env.journal);
state.loanScale);
verifyLoanStatus(
0,
@@ -2454,18 +2448,13 @@ protected:
// Make all the payments in one transaction
// service fee is 2
auto const startingPayments = state.paymentRemaining;
STAmount const payoffAmount = [&]() {
NumberRoundModeGuard mg(Number::upward);
auto const rawPayoff = startingPayments *
(state.periodicPayment + broker.asset(2).value());
STAmount const payoffAmount{broker.asset, rawPayoff};
BEAST_EXPECTS(
payoffAmount ==
broker.asset(Number(1024014840139457, -12)),
to_string(payoffAmount));
BEAST_EXPECT(payoffAmount > state.principalOutstanding);
return payoffAmount;
}();
auto const rawPayoff = startingPayments *
(state.periodicPayment + broker.asset(2).value());
STAmount const payoffAmount{broker.asset, rawPayoff};
BEAST_EXPECT(
payoffAmount ==
broker.asset(Number(1024014840139457, -12)));
BEAST_EXPECT(payoffAmount > state.principalOutstanding);
singlePayment(
loanKeylet,
@@ -4020,7 +4009,7 @@ protected:
createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
// Fails in preclaim because principal requested can't be
// represented as XRP
env(createJson, ter(tecPRECISION_LOSS), THISLINE);
env(createJson, ter(tecPRECISION_LOSS));
env.close();
BEAST_EXPECT(!env.le(keylet));
@@ -4032,7 +4021,7 @@ protected:
createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
// Fails in doApply because the payment is too small to be
// represented as XRP.
env(createJson, ter(tecPRECISION_LOSS), THISLINE);
env(createJson, ter(tecPRECISION_LOSS));
env.close();
}
@@ -5007,7 +4996,7 @@ protected:
auto const keylet = keylet::loan(broker.brokerID, loanSequence);
createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
env(createJson, ter(tecPRECISION_LOSS), THISLINE);
env(createJson, ter(tecPRECISION_LOSS));
env.close(startDate);
auto loanPayTx = env.json(
@@ -6155,16 +6144,15 @@ protected:
// Accrued + prepayment-penalty interest based on current periodic
// schedule
auto const fullPaymentInterest = computeFullPaymentInterest(
detail::loanPrincipalFromPeriodicPayment(
after.periodicPayment, periodicRate2, after.paymentRemaining),
after.periodicPayment,
periodicRate2,
after.paymentRemaining,
env.current()->parentCloseTime(),
after.paymentInterval,
after.previousPaymentDate,
static_cast<std::uint32_t>(
after.startDate.time_since_epoch().count()),
closeInterestRate);
// Round to asset scale and split interest/fee parts
auto const roundedInterest =
roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale);
@@ -6192,9 +6180,9 @@ protected:
// window by clamping prevPaymentDate to 'now' for the full-pay path.
auto const prevClamped = std::min(after.previousPaymentDate, nowSecs);
auto const fullPaymentInterestClamped = computeFullPaymentInterest(
detail::loanPrincipalFromPeriodicPayment(
after.periodicPayment, periodicRate2, after.paymentRemaining),
after.periodicPayment,
periodicRate2,
after.paymentRemaining,
env.current()->parentCloseTime(),
after.paymentInterval,
prevClamped,
@@ -7205,15 +7193,15 @@ class LoanArbitrary_test : public LoanBatch_test
.vaultDeposit = 10000,
.debtMax = 0,
.coverRateMin = TenthBips32{0},
.managementFeeRate = TenthBips16{0},
// .managementFeeRate = TenthBips16{5919},
.coverRateLiquidation = TenthBips32{0}};
LoanParameters const loanParams{
.account = Account("lender"),
.counter = Account("borrower"),
.principalRequest = Number{200000, -6},
.interest = TenthBips32{50000},
.payTotal = 2,
.payInterval = 200};
.principalRequest = Number{10000, 0},
// .interest = TenthBips32{0},
// .payTotal = 5816,
.payInterval = 150};
runLoan(AssetType::XRP, brokerParams, loanParams);
}

View File

@@ -644,7 +644,7 @@ MPTTester::operator[](std::string const& name) const
}
PrettyAmount
MPTTester::operator()(std::int64_t amount) const
MPTTester::operator()(std::uint64_t amount) const
{
return MPT("", issuanceID())(amount);
}

View File

@@ -272,7 +272,7 @@ public:
operator[](std::string const& name) const;
PrettyAmount
operator()(std::int64_t amount) const;
operator()(std::uint64_t amount) const;
operator Asset() const;

View File

@@ -179,12 +179,11 @@ adjustImpreciseNumber(
}
inline int
getAssetsTotalScale(SLE::const_ref vaultSle)
getVaultScale(SLE::const_ref vaultSle)
{
if (!vaultSle)
return Number::minExponent - 1; // LCOV_EXCL_LINE
return STAmount{vaultSle->at(sfAsset), vaultSle->at(sfAssetsTotal)}
.exponent();
return vaultSle->at(sfAssetsTotal).exponent();
}
TER
@@ -203,6 +202,14 @@ computeRawLoanState(
std::uint32_t const paymentRemaining,
TenthBips32 const managementFeeRate);
LoanState
computeRawLoanState(
Number const& periodicPayment,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t const paymentRemaining,
TenthBips32 const managementFeeRate);
// Constructs a valid LoanState object from arbitrary inputs
LoanState
constructLoanState(
@@ -232,6 +239,17 @@ computeFullPaymentInterest(
std::uint32_t startDate,
TenthBips32 closeInterestRate);
Number
computeFullPaymentInterest(
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
@@ -369,58 +387,6 @@ struct LoanStateDeltas
nonNegative();
};
Number
computeRaisedRate(Number const& periodicRate, std::uint32_t paymentsRemaining);
Number
computePaymentFactor(
Number const& periodicRate,
std::uint32_t paymentsRemaining);
std::pair<Number, Number>
computeInterestAndFeeParts(
Asset const& asset,
Number const& interest,
TenthBips16 managementFeeRate,
std::int32_t loanScale);
Number
loanPeriodicPayment(
Number const& principalOutstanding,
Number const& periodicRate,
std::uint32_t paymentsRemaining);
Number
loanPrincipalFromPeriodicPayment(
Number const& periodicPayment,
Number const& periodicRate,
std::uint32_t paymentsRemaining);
Number
loanLatePaymentInterest(
Number const& principalOutstanding,
TenthBips32 lateInterestRate,
NetClock::time_point parentCloseTime,
std::uint32_t nextPaymentDueDate);
Number
loanAccruedInterest(
Number const& principalOutstanding,
Number const& periodicRate,
NetClock::time_point parentCloseTime,
std::uint32_t startDate,
std::uint32_t prevPaymentDate,
std::uint32_t paymentInterval);
ExtendedPaymentComponents
computeOverpaymentComponents(
Asset const& asset,
int32_t const loanScale,
Number const& overpayment,
TenthBips32 const overpaymentInterestRate,
TenthBips32 const overpaymentFeeRate,
TenthBips16 const managementFeeRate);
PaymentComponents
computePaymentComponents(
Asset const& asset,
@@ -452,8 +418,7 @@ computeLoanProperties(
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining,
TenthBips32 managementFeeRate,
std::int32_t minimumScale,
beast::Journal j);
std::int32_t minimumScale);
bool
isRounded(Asset const& asset, Number const& value, std::int32_t scale);

View File

@@ -100,9 +100,6 @@ computePaymentFactor(
Number const& periodicRate,
std::uint32_t paymentsRemaining)
{
if (paymentsRemaining == 0)
return numZero;
// For zero interest, payment factor is simply 1/paymentsRemaining
if (periodicRate == beast::zero)
return Number{1} / paymentsRemaining;
@@ -135,6 +132,27 @@ loanPeriodicPayment(
computePaymentFactor(periodicRate, paymentsRemaining);
}
/* Calculates the periodic payment amount from annualized interest rate.
* Converts the annual rate to periodic rate before computing payment.
*
* Equation (7) from XLS-66 spec, Section A-2 Equation Glossary
*/
Number
loanPeriodicPayment(
Number const& principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining)
{
if (principalOutstanding == 0 || paymentsRemaining == 0)
return 0;
Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
return loanPeriodicPayment(
principalOutstanding, periodicRate, paymentsRemaining);
}
/* Reverse-calculates principal from periodic payment amount.
* Used to determine theoretical principal at any point in the schedule.
*
@@ -146,9 +164,6 @@ loanPrincipalFromPeriodicPayment(
Number const& periodicRate,
std::uint32_t paymentsRemaining)
{
if (paymentsRemaining == 0)
return numZero;
if (periodicRate == 0)
return periodicPayment * paymentsRemaining;
@@ -156,6 +171,21 @@ loanPrincipalFromPeriodicPayment(
computePaymentFactor(periodicRate, paymentsRemaining);
}
/* Splits gross interest into net interest (to vault) and management fee (to
* broker). Returns pair of (net interest, management fee).
*
* Equation (33) from XLS-66 spec, Section A-2 Equation Glossary
*/
std::pair<Number, Number>
computeInterestAndFeeParts(
Number const& interest,
TenthBips16 managementFeeRate)
{
auto const fee = tenthBipsOfValue(interest, managementFeeRate);
return std::make_pair(interest - fee, fee);
}
/*
* Computes the interest and management fee parts from interest amount.
*
@@ -186,12 +216,6 @@ loanLatePaymentInterest(
NetClock::time_point parentCloseTime,
std::uint32_t nextPaymentDueDate)
{
if (principalOutstanding == beast::zero)
return numZero;
if (lateInterestRate == TenthBips32{0})
return numZero;
auto const now = parentCloseTime.time_since_epoch().count();
// If the payment is not late by any amount of time, then there's no late
@@ -224,9 +248,6 @@ loanAccruedInterest(
if (periodicRate == beast::zero)
return numZero;
if (paymentInterval == 0)
return numZero;
auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
auto const now = parentCloseTime.time_since_epoch().count();
@@ -430,8 +451,7 @@ tryOverpayment(
paymentInterval,
paymentRemaining,
managementFeeRate,
loanScale,
j);
loanScale);
JLOG(j.debug()) << "new periodic payment: "
<< newLoanProperties.periodicPayment
@@ -527,28 +547,21 @@ tryOverpayment(
auto const deltas = rounded - newRounded;
// The change in loan management fee is equal to the change between the old
// and the new outstanding management fees
XRPL_ASSERT_PARTS(
deltas.managementFee ==
rounded.managementFeeDue - managementFeeOutstanding,
"ripple::detail::tryOverpayment",
"no fee change");
auto const hypotheticalValueOutstanding =
rounded.valueOutstanding - deltas.principal;
// Calculate how the loan's value changed due to the overpayment.
// This should be negative (value decreased) or zero. A principal
// overpayment should never increase the loan's value.
auto const valueChange = newRounded.valueOutstanding -
hypotheticalValueOutstanding - deltas.managementFee;
auto const valueChange =
newRounded.valueOutstanding - hypotheticalValueOutstanding;
if (valueChange > 0)
{
JLOG(j.warn()) << "Principal overpayment would increase the value of "
"the loan. Ignore the overpayment";
return Unexpected(tesSUCCESS);
}
return LoanPaymentParts{
// Principal paid is the reduction in principal outstanding
.principalPaid = deltas.principal,
@@ -663,6 +676,12 @@ doOverpayment(
"ripple::detail::doOverpayment",
"principal change agrees");
XRPL_ASSERT_PARTS(
overpaymentComponents.trackedManagementFeeDelta ==
managementFeeOutstandingProxy - managementFeeOutstanding,
"ripple::detail::doOverpayment",
"no fee change");
// I'm not 100% sure the following asserts are correct. If in doubt, and
// everything else works, remove any that cause trouble.
@@ -693,6 +712,13 @@ doOverpayment(
"ripple::detail::doOverpayment",
"principal payment matches");
XRPL_ASSERT_PARTS(
loanPaymentParts.feePaid ==
overpaymentComponents.untrackedManagementFee +
overpaymentComponents.trackedManagementFeeDelta,
"ripple::detail::doOverpayment",
"fee payment matches");
// All validations passed, so update the proxy objects (which will
// modify the actual Loan ledger object)
totalValueOutstandingProxy = totalValueOutstanding;
@@ -1205,12 +1231,17 @@ computeOverpaymentComponents(
// This interest doesn't follow the normal amortization schedule - it's
// a one-time charge for paying early.
// Equation (20) and (21) from XLS-66 spec, Section A-2 Equation Glossary
auto const [rawOverpaymentInterest, _] = [&]() {
Number const interest =
tenthBipsOfValue(overpayment, overpaymentInterestRate);
return detail::computeInterestAndFeeParts(interest, managementFeeRate);
}();
// Round the penalty interest components to the loan scale
auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] =
[&]() {
auto const interest = roundToAsset(
asset,
tenthBipsOfValue(overpayment, overpaymentInterestRate),
loanScale);
Number const interest =
roundToAsset(asset, rawOverpaymentInterest, loanScale);
return detail::computeInterestAndFeeParts(
asset, interest, managementFeeRate, loanScale);
}();
@@ -1404,6 +1435,31 @@ computeFullPaymentInterest(
return accruedInterest + prepaymentPenalty;
}
Number
computeFullPaymentInterest(
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)
{
Number const rawPrincipalOutstanding =
detail::loanPrincipalFromPeriodicPayment(
periodicPayment, periodicRate, paymentRemaining);
return computeFullPaymentInterest(
rawPrincipalOutstanding,
periodicRate,
parentCloseTime,
paymentInterval,
prevPaymentDate,
startDate,
closeInterestRate);
}
/* Calculates the theoretical loan state at maximum precision for a given point
* in the amortization schedule.
*
@@ -1465,6 +1521,21 @@ computeRawLoanState(
.managementFeeDue = rawManagementFeeOutstanding};
};
LoanState
computeRawLoanState(
Number const& periodicPayment,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t const paymentRemaining,
TenthBips32 const managementFeeRate)
{
return computeRawLoanState(
periodicPayment,
loanPeriodicRate(interestRate, paymentInterval),
paymentRemaining,
managementFeeRate);
}
/* Constructs a LoanState from rounded Loan ledger object values.
*
* This function creates a LoanState structure from the three tracked values
@@ -1540,8 +1611,7 @@ computeLoanProperties(
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining,
TenthBips32 managementFeeRate,
std::int32_t minimumScale,
beast::Journal j)
std::int32_t minimumScale)
{
auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
XRPL_ASSERT(
@@ -1552,22 +1622,13 @@ computeLoanProperties(
principalOutstanding, periodicRate, paymentsRemaining);
auto const [totalValueOutstanding, loanScale] = [&]() {
// only round up if there should be interest
NumberRoundModeGuard mg(
periodicRate == 0 ? Number::to_nearest : Number::upward);
NumberRoundModeGuard mg(Number::to_nearest);
// Use STAmount's internal rounding instead of roundToAsset, because
// we're going to use this result to determine the scale for all the
// other rounding.
// Equation (30) from XLS-66 spec, Section A-2 Equation Glossary
STAmount amount{asset, periodicPayment * paymentsRemaining};
JLOG(j.debug()) << "computeLoanProperties:" << " Principal requested: "
<< principalOutstanding
<< ". Periodic payment: " << periodicPayment
<< ". Payments remaining: " << paymentsRemaining
<< ". Raw total value: "
<< periodicPayment * paymentsRemaining
<< ". Candidate total value: " << amount << std::endl;
// Base the loan scale on the total value, since that's going to be
// the biggest number involved (barring unusual parameters for late,
@@ -1582,10 +1643,7 @@ computeLoanProperties(
// We may need to truncate the total value because of the minimum
// scale
amount = roundToAsset(asset, amount, loanScale);
JLOG(j.debug()) << "computeLoanProperties: Loan scale:" << loanScale
<< ". Actual total value: " << amount << std::endl;
amount = roundToAsset(asset, amount, loanScale, Number::to_nearest);
return std::make_pair(amount, loanScale);
}();

View File

@@ -56,7 +56,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
if (!vault)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const asset = vault->at(sfAsset);
auto const scale = getAssetsTotalScale(vault);
auto const scale = getVaultScale(vault);
auto const rounded =
roundToAsset(asset, debtTotal, scale, Number::towards_zero);
@@ -67,7 +67,7 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
JLOG(ctx.j.warn()) << "LoanBrokerDelete: Debt total is "
<< debtTotal << ", which rounds to " << rounded;
return tecHAS_OBLIGATIONS;
// LCOV_EXCL_STOP
// LCOV_EXCL_START
}
}

View File

@@ -115,7 +115,7 @@ LoanDelete::doApply()
roundToAsset(
vaultSle->at(sfAsset),
debtTotalProxy,
getAssetsTotalScale(vaultSle),
getVaultScale(vaultSle),
Number::towards_zero) == beast::zero,
"ripple::LoanDelete::doApply",
"last loan, remaining debt rounds to zero");

View File

@@ -106,7 +106,7 @@ LoanManage::preclaim(PreclaimContext const& ctx)
if (loanBrokerSle->at(sfOwner) != account)
{
JLOG(ctx.j.warn())
<< "LoanBroker for Loan does not belong to the account. LoanManage "
<< "LoanBroker for Loan does not belong to the account. LoanModify "
"can only be submitted by the Loan Broker.";
return tecNO_PERMISSION;
}
@@ -178,7 +178,7 @@ LoanManage::defaultLoan(
// 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 = getAssetsTotalScale(vaultSle);
auto const vaultScale = getVaultScale(vaultSle);
{
// Decrease the Total Value of the Vault:
@@ -242,11 +242,7 @@ LoanManage::defaultLoan(
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
adjustImpreciseNumber(
vaultLossUnrealizedProxy,
-totalDefaultAmount,
vaultAsset,
vaultScale);
vaultLossUnrealizedProxy -= totalDefaultAmount;
}
view.update(vaultSle);
}
@@ -254,9 +250,11 @@ LoanManage::defaultLoan(
// Update the LoanBroker object:
{
auto const asset = *vaultSle->at(sfAsset);
// Decrease the Debt of the LoanBroker:
adjustImpreciseNumber(
brokerDebtTotalProxy, -totalDefaultAmount, vaultAsset, vaultScale);
brokerDebtTotalProxy, -totalDefaultAmount, asset, vaultScale);
// Decrease the First-Loss Capital Cover Available:
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
if (coverAvailableProxy < defaultCovered)
@@ -299,20 +297,13 @@ LoanManage::impairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j)
{
Number const lossUnrealized = owedToVault(loanSle);
// 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 = getAssetsTotalScale(vaultSle);
// Update the Vault object(set "paper loss")
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
adjustImpreciseNumber(
vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale);
vaultLossUnrealizedProxy += lossUnrealized;
if (vaultLossUnrealizedProxy >
vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
{
@@ -343,14 +334,8 @@ LoanManage::unimpairLoan(
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j)
{
// 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 = getAssetsTotalScale(vaultSle);
// Update the Vault object(clear "paper loss")
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
Number const lossReversed = owedToVault(loanSle);
@@ -362,10 +347,7 @@ LoanManage::unimpairLoan(
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
// Reverse the "paper loss"
adjustImpreciseNumber(
vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
vaultLossUnrealizedProxy -= lossReversed;
view.update(vaultSle);
// Update the Loan object
@@ -421,14 +403,12 @@ LoanManage::doApply()
}
else if (tx.isFlag(tfLoanImpair))
{
if (auto const ter =
impairLoan(view, loanSle, vaultSle, vaultAsset, j_))
if (auto const ter = impairLoan(view, loanSle, vaultSle, j_))
return ter;
}
else if (tx.isFlag(tfLoanUnimpair))
{
if (auto const ter =
unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_))
if (auto const ter = unimpairLoan(view, loanSle, vaultSle, j_))
return ter;
}

View File

@@ -44,7 +44,6 @@ public:
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j);
/** Helper function that might be needed by other transactors
@@ -54,7 +53,6 @@ public:
ApplyView& view,
SLE::ref loanSle,
SLE::ref vaultSle,
Asset const& vaultAsset,
beast::Journal j);
TER

View File

@@ -305,7 +305,7 @@ LoanPay::doApply()
// change will be discarded.
if (loanSle->isFlag(lsfLoanImpaired))
{
LoanManage::unimpairLoan(view, loanSle, vaultSle, asset, j_);
LoanManage::unimpairLoan(view, loanSle, vaultSle, j_);
}
LoanPaymentType const paymentType = [&tx]() {
@@ -379,7 +379,7 @@ LoanPay::doApply()
// 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 = getAssetsTotalScale(vaultSle);
auto const vaultScale = assetsTotalProxy.value().exponent();
auto const totalPaidToVaultRaw =
paymentParts->principalPaid + paymentParts->interestPaid;

View File

@@ -383,7 +383,7 @@ LoanSet::doApply()
auto vaultAvailableProxy = vaultSle->at(sfAssetsAvailable);
auto vaultTotalProxy = vaultSle->at(sfAssetsTotal);
auto const vaultScale = getAssetsTotalScale(vaultSle);
auto const vaultScale = getVaultScale(vaultSle);
if (vaultAvailableProxy < principalRequested)
{
JLOG(j_.warn())
@@ -404,8 +404,7 @@ LoanSet::doApply()
paymentInterval,
paymentTotal,
TenthBips16{brokerSle->at(sfManagementFeeRate)},
vaultScale,
j_);
vaultScale);
// Check that relevant values won't lose precision. This is mostly only
// relevant for IOU assets.
@@ -441,10 +440,7 @@ LoanSet::doApply()
{
// LCOV_EXCL_START
JLOG(j_.warn())
<< "Computed loan properties are invalid. Does not compute."
<< " Management fee: " << properties.managementFeeOwedToBroker
<< ". Total Value: " << properties.totalValueOutstanding
<< ". PeriodicPayment: " << properties.periodicPayment;
<< "Computed loan properties are invalid. Does not compute.";
return tecINTERNAL;
// LCOV_EXCL_STOP
}