mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-22 11:37:07 +00:00
Compare commits
482
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03ea1deed1 | ||
|
|
bdffe99022 | ||
|
|
9582f75ce2 | ||
|
|
d786035fe0 | ||
|
|
ba19d7f32f | ||
|
|
c438852b41 | ||
|
|
fb941ed1af | ||
|
|
b50d728461 | ||
|
|
fa93ca7128 | ||
|
|
7e94916d08 | ||
|
|
f3677c9bed | ||
|
|
1fa366576e | ||
|
|
6e4f6eab3b | ||
|
|
348e7f7cfc | ||
|
|
43174d31d1 | ||
|
|
4fbf687166 | ||
|
|
cc46e94308 | ||
|
|
e485d5d6ad | ||
|
|
54d738b93e | ||
|
|
3fdd3cb495 | ||
|
|
2a9eec6c4f | ||
|
|
3a45fc3364 | ||
|
|
8428255866 | ||
|
|
e001681ada | ||
|
|
d2f8e3817e | ||
|
|
a38e09e327 | ||
|
|
165478b929 | ||
|
|
95fdbe520f | ||
|
|
34db04ffc1 | ||
|
|
65e6746b5c | ||
|
|
aed8e2b166 | ||
|
|
7925cc4052 | ||
|
|
245bac1769 | ||
|
|
cdd6ad4ba4 | ||
|
|
89979022f2 | ||
|
|
4920e65b14 | ||
|
|
b472cc3493 | ||
|
|
27389cd7e8 | ||
|
|
2b9f70b0bd | ||
|
|
e8dddcbb7f | ||
|
|
7a758a42f9 | ||
|
|
298f6deac9 | ||
|
|
71565a909e | ||
|
|
52490730b2 | ||
|
|
64e0ee4be9 | ||
|
|
d0c60cdf59 | ||
|
|
0febc60cbe | ||
|
|
122d8f96b3 | ||
|
|
ec60dcf90d | ||
|
|
310852ba2d | ||
|
|
f4404eafbd | ||
|
|
fe5bd82ed8 | ||
|
|
da14c6f018 | ||
|
|
5ceefdfbd5 | ||
|
|
b8b9e7a51e | ||
|
|
269fb44cd7 | ||
|
|
40ef91e7e0 | ||
|
|
fe4269cf8b | ||
|
|
a3b82023d6 | ||
|
|
0950d41fce | ||
|
|
45819477d3 | ||
|
|
83e249eb8a | ||
|
|
07fcbd5498 | ||
|
|
88a770c71b | ||
|
|
78ef800e30 | ||
|
|
7f2f6b8791 | ||
|
|
7c45f6a673 | ||
|
|
66b547f6fc | ||
|
|
6adb2eca76 | ||
|
|
f60e298627 | ||
|
|
9f5bc8f0da | ||
|
|
e306a7c75f | ||
|
|
59364be5f8 | ||
|
|
aa696ed61a | ||
|
|
044b9b8217 | ||
|
|
f78c5f65bc | ||
|
|
6ad7d1c076 | ||
|
|
9814ec0309 | ||
|
|
b3593fac1c | ||
|
|
30e2650ff9 | ||
|
|
4aa6baef26 | ||
|
|
dd938c4e7b | ||
|
|
92144efbdf | ||
|
|
5da586bffc | ||
|
|
70ae693821 | ||
|
|
f6098fd80b | ||
|
|
8757022312 | ||
|
|
587cb2533c | ||
|
|
92431e11c8 | ||
|
|
dfc4933c3b | ||
|
|
1bb306baf4 | ||
|
|
98d1cf07ed | ||
|
|
baabf142e4 | ||
|
|
f9bd9f390a | ||
|
|
d7196a8e56 | ||
|
|
bda6bb46d1 | ||
|
|
ec20974cae | ||
|
|
1efc532b21 | ||
|
|
2dd239c59f | ||
|
|
fc2163708a | ||
|
|
eeec90ee74 | ||
|
|
8d982758cb | ||
|
|
1b31dbc4c9 | ||
|
|
f8b63228b6 | ||
|
|
0b8cd2d7ca | ||
|
|
a8de91c57d | ||
|
|
97e2c10359 | ||
|
|
96d0258f51 | ||
|
|
fb8dafa6a8 | ||
|
|
3cc8e5564a | ||
|
|
e1939d0d20 | ||
|
|
2509293943 | ||
|
|
63edf035a6 | ||
|
|
d353f4a2e6 | ||
|
|
9f1ed7ed61 | ||
|
|
d700f6876d | ||
|
|
9c28578262 | ||
|
|
134998d86f | ||
|
|
307d5474b4 | ||
|
|
1212902be6 | ||
|
|
48d458afc3 | ||
|
|
e6e9003821 | ||
|
|
66dbc05a9f | ||
|
|
7f4cdefc24 | ||
|
|
8152bcb1f3 | ||
|
|
53e82a1c11 | ||
|
|
c179135e15 | ||
|
|
5117ef831e | ||
|
|
f35d0dc829 | ||
|
|
311da8fdcf | ||
|
|
7120cce996 | ||
|
|
23045fcbef | ||
|
|
d576e9d214 | ||
|
|
5863f05343 | ||
|
|
ce23d0aa54 | ||
|
|
aac25e8c79 | ||
|
|
1888784e8e | ||
|
|
681c179174 | ||
|
|
05739c59cf | ||
|
|
b127b24dae | ||
|
|
c2a0824da0 | ||
|
|
e1b234cc51 | ||
|
|
03d8cf2c12 | ||
|
|
7a3f7aebcf | ||
|
|
f78d27e9b3 | ||
|
|
e38e911190 | ||
|
|
44313d9a30 | ||
|
|
a3365545df | ||
|
|
1d85727675 | ||
|
|
da3c2beae0 | ||
|
|
6b7136906f | ||
|
|
7612fd0ba6 | ||
|
|
0bd6b1ae49 | ||
|
|
66216a3d41 | ||
|
|
4b90f97e52 | ||
|
|
f1a2fe3d0a | ||
|
|
d76f31cb7c | ||
|
|
b5caa9c63d | ||
|
|
3931eb39d5 | ||
|
|
322308f7d1 | ||
|
|
4c8f7968c4 | ||
|
|
6e2965f01a | ||
|
|
dba3af9318 | ||
|
|
7304ee68d5 | ||
|
|
759d114ab8 | ||
|
|
3c93dbbc7c | ||
|
|
97819b23ba | ||
|
|
9bfa88cd86 | ||
|
|
de5af71cb6 | ||
|
|
67578b8f50 | ||
|
|
e72a9d4bf2 | ||
|
|
5b576b841c | ||
|
|
267b537a13 | ||
|
|
543f1e702f | ||
|
|
f9dcce2d7e | ||
|
|
928af9633f | ||
|
|
9e84c37306 | ||
|
|
2aa626c205 | ||
|
|
36c2fa1e56 | ||
|
|
886b0a487c | ||
|
|
7849df5feb | ||
|
|
55baf35d9c | ||
|
|
90ca9ea71a | ||
|
|
47eb1e8db2 | ||
|
|
540b76fe42 | ||
|
|
d4135a9115 | ||
|
|
4c6a34d3c1 | ||
|
|
be5390b8c7 | ||
|
|
b5ddc812ea | ||
|
|
b9a2eb3399 | ||
|
|
2c0c4567f4 | ||
|
|
d1c708ca01 | ||
|
|
ae27376863 | ||
|
|
e237f968c7 | ||
|
|
b02b58e3b5 | ||
|
|
d10ac3b169 | ||
|
|
c37f863201 | ||
|
|
1fbd3fcfa4 | ||
|
|
70755ed1d3 | ||
|
|
6926c511cb | ||
|
|
e01dd6d5b0 | ||
|
|
0bfd404111 | ||
|
|
5c072579d7 | ||
|
|
c118121337 | ||
|
|
8a4c607af9 | ||
|
|
f85ce9c742 | ||
|
|
49e1f97476 | ||
|
|
47aa95fcc4 | ||
|
|
dc5b0918da | ||
|
|
cb991523f8 | ||
|
|
9683344940 | ||
|
|
5ac905be01 | ||
|
|
4d5afbea3a | ||
|
|
9d08fe9a03 | ||
|
|
84e97f0ef5 | ||
|
|
4d24b5eac2 | ||
|
|
935749fe1f | ||
|
|
49eb1cc54e | ||
|
|
dee972e9cd | ||
|
|
bdeed43304 | ||
|
|
7eff303160 | ||
|
|
aa1341f42e | ||
|
|
91031dbec4 | ||
|
|
03da5da815 | ||
|
|
fc860c1a1d | ||
|
|
2545bc5936 | ||
|
|
9a9588eaf6 | ||
|
|
abee3dd55c | ||
|
|
22d9ade2b0 | ||
|
|
3ea0fa67cb | ||
|
|
1b295c7c00 | ||
|
|
dd52a50acc | ||
|
|
80a2d8f789 | ||
|
|
e5f99e8ee7 | ||
|
|
b925ed44a0 | ||
|
|
14ff03821c | ||
|
|
5ff6bc496a | ||
|
|
7aaad4a8ef | ||
|
|
1897ac5ee7 | ||
|
|
5894cd2103 | ||
|
|
2f89182fd3 | ||
|
|
4191ddd702 | ||
|
|
37888b97ba | ||
|
|
10f7e4a02a | ||
|
|
e5941d5b23 | ||
|
|
3f1a0f3a3d | ||
|
|
fa2c595a59 | ||
|
|
fc6fe9802e | ||
|
|
b3c40ad408 | ||
|
|
779e94bc03 | ||
|
|
f653ca03d9 | ||
|
|
d46e477092 | ||
|
|
24f58a2e94 | ||
|
|
e3358ad124 | ||
|
|
5102147709 | ||
|
|
0a54c09de1 | ||
|
|
d594ca1401 | ||
|
|
5335c04616 | ||
|
|
6db14ccb13 | ||
|
|
582f56487d | ||
|
|
d47c039941 | ||
|
|
bf4ae57da4 | ||
|
|
83702e79bf | ||
|
|
6379363bad | ||
|
|
f0c96ccfe6 | ||
|
|
4c726838d1 | ||
|
|
47bcc67d56 | ||
|
|
4b48f9612b | ||
|
|
7bba34832b | ||
|
|
76ae61feb5 | ||
|
|
adb260fe17 | ||
|
|
8444a944af | ||
|
|
6d20e59212 | ||
|
|
c49cf6cec6 | ||
|
|
e24f034b17 | ||
|
|
72733b57e4 | ||
|
|
7a78449f6d | ||
|
|
0d500343ed | ||
|
|
f15e88c009 | ||
|
|
0069c0c5fa | ||
|
|
450d38293d | ||
|
|
6d9001edac | ||
|
|
3dc14e7430 | ||
|
|
9a8c2523ae | ||
|
|
115161f7d2 | ||
|
|
77fcb9b9b3 | ||
|
|
d2e8144c53 | ||
|
|
9d45fd1873 | ||
|
|
4c43a0bca4 | ||
|
|
bb491431bd | ||
|
|
f090434c2d | ||
|
|
5f7b4a5e13 | ||
|
|
11b8f9978e | ||
|
|
58551da698 | ||
|
|
5ade8756f6 | ||
|
|
ac4c4905df | ||
|
|
a5f1879afb | ||
|
|
8a77ac71ed | ||
|
|
a72c237373 | ||
|
|
faae2514b9 | ||
|
|
bcbbf71f29 | ||
|
|
12994d4251 | ||
|
|
22679673f6 | ||
|
|
a14cbf117c | ||
|
|
5ce07e769f | ||
|
|
08a5e8428a | ||
|
|
a8e35f8399 | ||
|
|
1f85888ae8 | ||
|
|
183450db46 | ||
|
|
64959b980f | ||
|
|
0b833e17ae | ||
|
|
2e3c5543f2 | ||
|
|
0475398a17 | ||
|
|
bf483a2e94 | ||
|
|
5e3539d6bb | ||
|
|
a90265dd2a | ||
|
|
8498ed9df4 | ||
|
|
4169b0a6b7 | ||
|
|
024339c99b | ||
|
|
044697c438 | ||
|
|
2aebeeb966 | ||
|
|
4c79fe46c9 | ||
|
|
51abafea26 | ||
|
|
f1f1117b08 | ||
|
|
5bb12ac1db | ||
|
|
dbd9a05e10 | ||
|
|
8981885196 | ||
|
|
a7390c070f | ||
|
|
4d0f895cda | ||
|
|
48162219ac | ||
|
|
1cb4a49ad3 | ||
|
|
18ffbbd42d | ||
|
|
7f1c0e55d6 | ||
|
|
1876ffcd71 | ||
|
|
3ef1e4269b | ||
|
|
ae25e0f058 | ||
|
|
7d634c1e62 | ||
|
|
f713208d89 | ||
|
|
5d98799205 | ||
|
|
9086c678a9 | ||
|
|
e434563e2e | ||
|
|
29300fc972 | ||
|
|
a88aceb846 | ||
|
|
9986346d60 | ||
|
|
7c642783f4 | ||
|
|
0f1a2324bc | ||
|
|
2d570267de | ||
|
|
97849b6e70 | ||
|
|
5d2a7d651e | ||
|
|
b26477a21e | ||
|
|
422a99bb26 | ||
|
|
b447fc54c8 | ||
|
|
9920037d13 | ||
|
|
714e12f44f | ||
|
|
fba91487ec | ||
|
|
49ea5cc152 | ||
|
|
87e0c25a34 | ||
|
|
a648249b90 | ||
|
|
083ab7b054 | ||
|
|
b6ef337e45 | ||
|
|
e62efa2a90 | ||
|
|
3867945b46 | ||
|
|
084896189d | ||
|
|
e9033898da | ||
|
|
d952e9de1a | ||
|
|
bc3c9e1534 | ||
|
|
6d137e44dc | ||
|
|
6778521f12 | ||
|
|
b0f3ef4969 | ||
|
|
c01ef3f155 | ||
|
|
241f22bab7 | ||
|
|
a387a7aadf | ||
|
|
d5e137de82 | ||
|
|
e0cdd65352 | ||
|
|
2c1ccdbc84 | ||
|
|
36204d2ce9 | ||
|
|
4639c1c351 | ||
|
|
c863bd25e0 | ||
|
|
7dcc682e29 | ||
|
|
162875616d | ||
|
|
9847025099 | ||
|
|
bb183ea09a | ||
|
|
9fd45e7aa7 | ||
|
|
924b05ea9f | ||
|
|
a0a0916108 | ||
|
|
c78b9aedac | ||
|
|
b07087f10b | ||
|
|
f89c88dbb8 | ||
|
|
0c76153b2c | ||
|
|
70a23bcca3 | ||
|
|
a66ef800bc | ||
|
|
5f5f60a024 | ||
|
|
d58399d417 | ||
|
|
a1e9091f1e | ||
|
|
84acebeb7f | ||
|
|
83ba11d505 | ||
|
|
97469a1c02 | ||
|
|
c74dc6baf2 | ||
|
|
1d90ae7c25 | ||
|
|
39db7381b2 | ||
|
|
ec8eaf0d73 | ||
|
|
9d1a23a811 | ||
|
|
421cbb9abd | ||
|
|
bba9119a75 | ||
|
|
6f00f296c8 | ||
|
|
aee4719df4 | ||
|
|
5f2402479d | ||
|
|
c3fa4fb71c | ||
|
|
a8d0d763b0 | ||
|
|
6310b2544c | ||
|
|
3c0ef8f8d3 | ||
|
|
ebe39e9320 | ||
|
|
1b8da176bf | ||
|
|
66dd0de019 | ||
|
|
a2be55fbc9 | ||
|
|
41c24094eb | ||
|
|
3b446f144f | ||
|
|
760a9aad83 | ||
|
|
c73372297c | ||
|
|
1f331bf8d8 | ||
|
|
57c78c56ba | ||
|
|
1adb410e8e | ||
|
|
6444ad4393 | ||
|
|
1e7462606c | ||
|
|
68aecfe8a2 | ||
|
|
8ffe1838d4 | ||
|
|
30becab0b6 | ||
|
|
e037ae3d3a | ||
|
|
2f3c79837e | ||
|
|
4550d2bfba | ||
|
|
5edaad41af | ||
|
|
24447969be | ||
|
|
c76e485d01 | ||
|
|
021bee8dd4 | ||
|
|
6c5945f9e8 | ||
|
|
96ad348fb0 | ||
|
|
72f33d8f3b | ||
|
|
96f2a65f64 | ||
|
|
43fe49e756 | ||
|
|
e14ca4c438 | ||
|
|
ed00018adc | ||
|
|
f7aef49509 | ||
|
|
e1edd43205 | ||
|
|
03d253ae20 | ||
|
|
577b047cc1 | ||
|
|
c46d894192 | ||
|
|
e4480569f7 | ||
|
|
e6e6360c1a | ||
|
|
6b4236343a | ||
|
|
45abab3a78 | ||
|
|
4d7492a00d | ||
|
|
b83af2d54a | ||
|
|
10b73525be | ||
|
|
89dbb48860 | ||
|
|
16854fff1a | ||
|
|
b34f59eafc | ||
|
|
38cb371c72 | ||
|
|
5199c5e073 | ||
|
|
3ef72d28bd | ||
|
|
06906a8bd3 | ||
|
|
0a3c728a96 | ||
|
|
37745cb5b2 | ||
|
|
269eac9a15 | ||
|
|
d82461ea70 | ||
|
|
e9d2dfe329 | ||
|
|
9d052dc86c | ||
|
|
ad5d28bc48 | ||
|
|
2b5a8ebf2f | ||
|
|
28999a242c | ||
|
|
186821f971 | ||
|
|
1c8e4c346b | ||
|
|
6bcd037264 | ||
|
|
439e2d4934 | ||
|
|
f812774637 | ||
|
|
d82693c2d3 | ||
|
|
907cc19a41 | ||
|
|
cc83ea8eb6 | ||
|
|
527e0c916f | ||
|
|
fb5d94bbef | ||
|
|
4fe3ec8a08 | ||
|
|
937b67cbc0 | ||
|
|
4e50087612 |
No files matched your search
@@ -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}
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -186,6 +186,11 @@ enum LedgerSpecificFlags {
|
||||
|
||||
// ltVAULT
|
||||
lsfVaultPrivate = 0x00010000,
|
||||
|
||||
// ltLOAN
|
||||
lsfLoanDefault = 0x00010000,
|
||||
lsfLoanImpaired = 0x00020000,
|
||||
lsfLoanOverpayment = 0x00040000, // True, loan allows overpayments
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
@@ -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)))
|
||||
|
||||
@@ -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&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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] = "";
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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).";
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user