diff --git a/.github/workflows/levelization.yml b/.github/workflows/levelization.yml index f99c1ca568..029672e181 100644 --- a/.github/workflows/levelization.yml +++ b/.github/workflows/levelization.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Check levelization - run: Builds/levelization/levelization.sh + run: python Builds/levelization/levelization.py - name: Check for differences id: assert run: | @@ -40,7 +40,7 @@ jobs: To fix it, you can do one of two things: 1. Download and apply the patch generated as an artifact of this job to your repo, commit, and push. - 2. Run './Builds/levelization/levelization.sh' in your repo, + 2. Run 'python Builds/levelization/levelization.py' in your repo, commit, and push. See Builds/levelization/README.md for more info. diff --git a/.gitignore b/.gitignore index 585a69efb2..121cba965c 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,9 @@ Builds/levelization/results/paths.txt Builds/levelization/results/includes/ Builds/levelization/results/includedby/ +# Python +__pycache__ + # Ignore tmp directory. tmp diff --git a/Builds/levelization/README.md b/Builds/levelization/README.md index 4ff3a54236..430247558b 100644 --- a/Builds/levelization/README.md +++ b/Builds/levelization/README.md @@ -50,7 +50,7 @@ that `test` code should *never* be included in `ripple` code.) ## Validation -The [levelization.sh](levelization.sh) script takes no parameters, +The [levelization.py](levelization.py) script takes no parameters, reads no environment variables, and can be run from any directory, as long as it is in the expected location in the rippled repo. It can be run at any time from within a checked out repo, and will @@ -84,7 +84,7 @@ It generates many files of [results](results): Github Actions workflow to test that levelization loops haven't changed. Unfortunately, if changes are detected, it can't tell if they are improvements or not, so if you have resolved any issues or - done anything else to improve levelization, run `levelization.sh`, + done anything else to improve levelization, run `levelization.py`, and commit the updated results. The `loops.txt` and `ordering.txt` files relate the modules @@ -108,7 +108,7 @@ The committed files hide the detailed values intentionally, to prevent false alarms and merging issues, and because it's easy to get those details locally. -1. Run `levelization.sh` +1. Run `levelization.py` 2. Grep the modules in `paths.txt`. * For example, if a cycle is found `A ~= B`, simply `grep -w A Builds/levelization/results/paths.txt | grep -w B` diff --git a/Builds/levelization/levelization.py b/Builds/levelization/levelization.py new file mode 100755 index 0000000000..043c9e00d1 --- /dev/null +++ b/Builds/levelization/levelization.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 + +""" +Usage: levelization.py +This script takes no parameters, and can be called from any directory in the file system. +""" + +import os +import re +import sys +from collections import defaultdict +from pathlib import Path + +# Compile regex patterns once at module level +INCLUDE_PATTERN = re.compile(r"^\s*#include.*/.*\.h") +INCLUDE_PATH_PATTERN = re.compile(r'[<"]([^>"]+)[>"]') + + +def dictionary_sort_key(s): + """ + Create a sort key that mimics 'sort -d' (dictionary order). + Dictionary order only considers blanks and alphanumeric characters. + """ + return "".join(c for c in s if c.isalnum() or c.isspace()) + + +def get_level(file_path): + """ + Extract the level from a file path (second and third directory components). + Equivalent to bash: cut -d/ -f 2,3 + + Examples: + src/ripple/app/main.cpp -> ripple.app + src/test/app/Import_test.cpp -> test.app + """ + parts = file_path.split("/") + + if len(parts) >= 3: + level = f"{parts[1]}/{parts[2]}" + elif len(parts) >= 2: + level = f"{parts[1]}/toplevel" + else: + level = file_path + + # If the "level" indicates a file, cut off the filename + if "." in level.split("/")[-1]: + # Use the "toplevel" label as a workaround for `sort` + # inconsistencies between different utility versions + level = level.rsplit("/", 1)[0] + "/toplevel" + + return level.replace("/", ".") + + +def extract_include_level(include_line): + """ + Extract the include path from an #include directive. + Gets the first two directory components from the include path. + Equivalent to bash: cut -d/ -f 1,2 + + Examples: + #include -> ripple.basics + #include "ripple/app/main/Application.h" -> ripple.app + """ + match = INCLUDE_PATH_PATTERN.search(include_line) + if not match: + return None + + include_path = match.group(1) + parts = include_path.split("/") + + if len(parts) >= 2: + include_level = f"{parts[0]}/{parts[1]}" + else: + include_level = include_path + + # If the "includelevel" indicates a file, cut off the filename + if "." in include_level.split("/")[-1]: + include_level = include_level.rsplit("/", 1)[0] + "/toplevel" + + return include_level.replace("/", ".") + + +def find_repository_directories(start_path, depth_limit=10): + """ + Find the repository root by looking for src or include folders. + Walks up the directory tree from the start path. + """ + current = start_path.resolve() + + for _ in range(depth_limit): + src_path = current / "src" + include_path = current / "include" + has_src = src_path.exists() + has_include = include_path.exists() + + if has_src or has_include: + dirs = [] + if has_src: + dirs.append(src_path) + if has_include: + dirs.append(include_path) + return current, dirs + + parent = current.parent + if parent == current: + break + current = parent + + raise RuntimeError( + "Could not find repository root. " + "Expected to find a directory containing 'src' and/or 'include' folders." + ) + + +def main(): + script_dir = Path(__file__).parent.resolve() + os.chdir(script_dir) + + # Clean up and create results directory. + results_dir = script_dir / "results" + if results_dir.exists(): + import shutil + + shutil.rmtree(results_dir) + results_dir.mkdir() + + # Find the repository root. + try: + repo_root, scan_dirs = find_repository_directories(script_dir) + print(f"Found repository root: {repo_root}") + for scan_dir in scan_dirs: + print(f" Scanning: {scan_dir.relative_to(repo_root)}") + except RuntimeError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + # Find all #include directives. + print("\nScanning for raw includes...") + raw_includes = [] + rawincludes_file = results_dir / "rawincludes.txt" + + with open(rawincludes_file, "w", buffering=8192) as raw_f: + for dir_path in scan_dirs: + for file_path in dir_path.rglob("*"): + if not file_path.is_file(): + continue + try: + rel_path_str = str(file_path.relative_to(repo_root)) + with open( + file_path, "r", encoding="utf-8", errors="ignore", buffering=8192 + ) as f: + for line in f: + if "#include" not in line or "boost" in line: + continue + if INCLUDE_PATTERN.match(line): + line_stripped = line.strip() + entry = f"{rel_path_str}:{line_stripped}\n" + print(entry, end="") + raw_f.write(entry) + raw_includes.append((rel_path_str, line_stripped)) + except Exception as e: + print(f"Error reading {file_path}: {e}", file=sys.stderr) + + # Build levelization paths and count directly. + print("Build levelization paths") + path_counts = defaultdict(int) + + for file_path, include_line in raw_includes: + include_level = extract_include_level(include_line) + if not include_level: + continue + level = get_level(file_path) + if level != include_level: + path_counts[(level, include_level)] += 1 + + # Sort and deduplicate paths. + print("Sort and deduplicate paths") + sorted_items = sorted( + path_counts.items(), + key=lambda x: (dictionary_sort_key(x[0][0]), dictionary_sort_key(x[0][1])), + ) + + paths_file = results_dir / "paths.txt" + with open(paths_file, "w") as f: + for (level, include_level), count in sorted_items: + line = f"{count:7} {level} {include_level}\n" + print(line.rstrip()) + f.write(line) + + # Split into flat-file database. + print("Split into flat-file database") + includes_dir = results_dir / "includes" + includedby_dir = results_dir / "includedby" + includes_dir.mkdir() + includedby_dir.mkdir() + + includes_data = defaultdict(list) + includedby_data = defaultdict(list) + + for (level, include_level), count in sorted_items: + includes_data[level].append((include_level, count)) + includedby_data[include_level].append((level, count)) + + for level in sorted(includes_data.keys(), key=dictionary_sort_key): + with open(includes_dir / level, "w") as f: + for include_level, count in includes_data[level]: + line = f"{include_level} {count}\n" + print(line.rstrip()) + f.write(line) + + for include_level in sorted(includedby_data.keys(), key=dictionary_sort_key): + with open(includedby_dir / include_level, "w") as f: + for level, count in includedby_data[include_level]: + line = f"{level} {count}\n" + print(line.rstrip()) + f.write(line) + + # Search for loops. + print("Search for loops") + loops_file = results_dir / "loops.txt" + ordering_file = results_dir / "ordering.txt" + + # Pre-load all include files into memory for fast lookup. + includes_cache = {} + includes_lookup = {} + + for include_file in sorted(includes_dir.iterdir(), key=lambda p: p.name): + if not include_file.is_file(): + continue + includes_cache[include_file.name] = [] + includes_lookup[include_file.name] = {} + with open(include_file, "r") as f: + for line in f: + parts = line.strip().split() + if len(parts) >= 2: + name, count = parts[0], int(parts[1]) + includes_cache[include_file.name].append((name, count)) + includes_lookup[include_file.name][name] = count + + loops_found = set() + + with open(loops_file, "w", buffering=8192) as loops_f, open( + ordering_file, "w", buffering=8192 + ) as ordering_f: + for source in sorted(includes_cache.keys()): + for include, include_freq in includes_cache[source]: + if include not in includes_lookup: + continue + + source_freq = includes_lookup[include].get(source) + + if source_freq is not None: + loop_key = tuple(sorted([source, include])) + if loop_key in loops_found: + continue + loops_found.add(loop_key) + + loops_f.write(f"Loop: {source} {include}\n") + + diff = include_freq - source_freq + if diff > 3: + loops_f.write(f" {source} > {include}\n\n") + elif diff < -3: + loops_f.write(f" {include} > {source}\n\n") + elif source_freq == include_freq: + loops_f.write(f" {include} == {source}\n\n") + else: + loops_f.write(f" {include} ~= {source}\n\n") + else: + ordering_f.write(f"{source} > {include}\n") + + # Print results. + print("\nOrdering:") + with open(ordering_file, "r") as f: + print(f.read(), end="") + + print("\nLoops:") + with open(loops_file, "r") as f: + print(f.read(), end="") + + +if __name__ == "__main__": + main() diff --git a/Builds/levelization/levelization.sh b/Builds/levelization/levelization.sh deleted file mode 100755 index c18ca703f7..0000000000 --- a/Builds/levelization/levelization.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/bin/bash - -# Usage: levelization.sh -# This script takes no parameters, reads no environment variables, -# and can be run from any directory, as long as it is in the expected -# location in the repo. - -pushd $( dirname $0 ) - -if [ -v PS1 ] -then - # if the shell is interactive, clean up any flotsam before analyzing - git clean -ix -fi - -# Ensure all sorting is ASCII-order consistently across platforms. -export LANG=C - -rm -rfv results -mkdir results -includes="$( pwd )/results/rawincludes.txt" -pushd ../.. -echo Raw includes: -grep -r '^[ ]*#include.*/.*\.h' include src | \ - grep -v boost | tee ${includes} -popd -pushd results - -oldifs=${IFS} -IFS=: -mkdir includes -mkdir includedby -echo Build levelization paths -exec 3< ${includes} # open rawincludes.txt for input -while read -r -u 3 file include -do - level=$( echo ${file} | cut -d/ -f 2,3 ) - # If the "level" indicates a file, cut off the filename - if [[ "${level##*.}" != "${level}" ]] - then - # Use the "toplevel" label as a workaround for `sort` - # inconsistencies between different utility versions - level="$( dirname ${level} )/toplevel" - fi - level=$( echo ${level} | tr '/' '.' ) - - includelevel=$( echo ${include} | sed 's/.*["<]//; s/[">].*//' | \ - cut -d/ -f 1,2 ) - if [[ "${includelevel##*.}" != "${includelevel}" ]] - then - # Use the "toplevel" label as a workaround for `sort` - # inconsistencies between different utility versions - includelevel="$( dirname ${includelevel} )/toplevel" - fi - includelevel=$( echo ${includelevel} | tr '/' '.' ) - - if [[ "$level" != "$includelevel" ]] - then - echo $level $includelevel | tee -a paths.txt - fi -done -echo Sort and dedup paths -sort -ds paths.txt | uniq -c | tee sortedpaths.txt -mv sortedpaths.txt paths.txt -exec 3>&- #close fd 3 -IFS=${oldifs} -unset oldifs - -echo Split into flat-file database -exec 4&- #close fd 4 - -loops="$( pwd )/loops.txt" -ordering="$( pwd )/ordering.txt" -pushd includes -echo Search for loops -# Redirect stdout to a file -exec 4>&1 -exec 1>"${loops}" -for source in * -do - if [[ -f "$source" ]] - then - exec 5<"${source}" # open for input - while read -r -u 5 include includefreq - do - if [[ -f $include ]] - then - if grep -q -w $source $include - then - if grep -q -w "Loop: $include $source" "${loops}" - then - continue - fi - sourcefreq=$( grep -w $source $include | cut -d\ -f2 ) - echo "Loop: $source $include" - # If the counts are close, indicate that the two modules are - # on the same level, though they shouldn't be - if [[ $(( $includefreq - $sourcefreq )) -gt 3 ]] - then - echo -e " $source > $include\n" - elif [[ $(( $sourcefreq - $includefreq )) -gt 3 ]] - then - echo -e " $include > $source\n" - elif [[ $sourcefreq -eq $includefreq ]] - then - echo -e " $include == $source\n" - else - echo -e " $include ~= $source\n" - fi - else - echo "$source > $include" >> "${ordering}" - fi - fi - done - exec 5>&- #close fd 5 - fi -done -exec 1>&4 #close fd 1 -exec 4>&- #close fd 4 -cat "${ordering}" -cat "${loops}" -popd -popd -popd diff --git a/README.md b/README.md index 30efdb776d..b34fa83946 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The server software that powers Xahau is called `xahaud` and is available in thi ### Build from Source -* [Read the build instructions in our documentation](https://xahau.network/infrastructure/building-xahau) +* [Read the build instructions in our documentation](https://xahau.network/docs/infrastructure/build-xahaud/) * If you encounter any issues, please [open an issue](https://github.com/xahau/xahaud/issues) ## Highlights of Xahau diff --git a/build-core.sh b/build-core.sh index 7054ce4ed1..e6867d5e24 100755 --- a/build-core.sh +++ b/build-core.sh @@ -71,6 +71,7 @@ cmake .. -G Ninja \ -Dxrpld=TRUE \ -Dtests=TRUE && ccache -z && +ccache -p && ninja -j $3 && echo "=== Re-running final link with verbose output ===" && rm -f rippled && ninja -v rippled && ccache -s && strip -s rippled && diff --git a/cmake/RippledCore.cmake b/cmake/RippledCore.cmake index c96c42b6c0..f4b070e0fc 100644 --- a/cmake/RippledCore.cmake +++ b/cmake/RippledCore.cmake @@ -68,6 +68,17 @@ target_link_libraries(xrpl.imports.main $<$:antithesis-sdk-cpp> ) +# date-tz for enhanced logging (always linked, code is #ifdef guarded) +if(TARGET date::date-tz) + target_link_libraries(xrpl.imports.main INTERFACE date::date-tz) +endif() + +# BEAST_ENHANCED_LOGGING: enable for Debug builds OR when explicitly requested +# Uses generator expression so it works with multi-config generators (Xcode, VS, Ninja Multi-Config) +target_compile_definitions(xrpl.imports.main INTERFACE + $<$,$>:BEAST_ENHANCED_LOGGING=1> +) + include(add_module) include(target_link_modules) diff --git a/include/xrpl/protocol/IOUAmount.h b/include/xrpl/protocol/IOUAmount.h index e89feb123d..d057b2b742 100644 --- a/include/xrpl/protocol/IOUAmount.h +++ b/include/xrpl/protocol/IOUAmount.h @@ -97,6 +97,12 @@ class IOUAmount : private boost::totally_ordered, static IOUAmount minPositiveAmount(); + + friend std::ostream& + operator<<(std::ostream& os, IOUAmount const& x) + { + return os << to_string(x); + } }; inline IOUAmount::IOUAmount(beast::Zero) diff --git a/include/xrpl/protocol/Rules.h b/include/xrpl/protocol/Rules.h index 7d6d9b0a9c..a6632c23ac 100644 --- a/include/xrpl/protocol/Rules.h +++ b/include/xrpl/protocol/Rules.h @@ -28,6 +28,9 @@ namespace ripple { +bool +isFeatureEnabled(uint256 const& feature); + class DigestAwareReadView; /** Rules controlling protocol behavior. */ diff --git a/release-builder.sh b/release-builder.sh index 436c8cd4ab..ac9c469818 100755 --- a/release-builder.sh +++ b/release-builder.sh @@ -196,6 +196,7 @@ ENV PATH=/usr/local/bin:$PATH RUN /hbb_exe/activate-exec bash -c "ccache -M 100G && \ ccache -o cache_dir=/cache/ccache && \ ccache -o compiler_check=content && \ + ccache -o direct_mode=true && \ mkdir -p ~/.conan2 /cache/conan2 /cache/conan2_download /cache/conan2_sources && \ echo 'core.cache:storage_path=/cache/conan2' > ~/.conan2/global.conf && \ echo 'core.download:download_cache=/cache/conan2_download' >> ~/.conan2/global.conf && \ diff --git a/src/libxrpl/protocol/Rules.cpp b/src/libxrpl/protocol/Rules.cpp index 7043acff96..89cb91a0a7 100644 --- a/src/libxrpl/protocol/Rules.cpp +++ b/src/libxrpl/protocol/Rules.cpp @@ -153,4 +153,12 @@ Rules::operator!=(Rules const& other) const { return !(*this == other); } + +bool +isFeatureEnabled(uint256 const& feature) +{ + auto const& rules = getCurrentTransactionRules(); + return rules && rules->enabled(feature); +} + } // namespace ripple diff --git a/src/test/app/AMMClawback_test.cpp b/src/test/app/AMMClawback_test.cpp index c547a537bf..d2ad29003f 100644 --- a/src/test/app/AMMClawback_test.cpp +++ b/src/test/app/AMMClawback_test.cpp @@ -16,27 +16,25 @@ //============================================================================== #include #include -#include -#include -#include -#include -#include +#include + +#include + #include -#include -#include + namespace ripple { namespace test { -class AMMClawback_test : public jtx::AMMTest +class AMMClawback_test : public beast::unit_test::suite { void - testInvalidRequest(FeatureBitset features) + testInvalidRequest() { testcase("test invalid request"); using namespace jtx; // Test if holder does not exist. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(100000), gw, alice); @@ -47,8 +45,9 @@ class AMMClawback_test : public jtx::AMMTest env.close(); env.require(flags(gw, asfAllowTrustLineClawback)); + auto const USD = gw["USD"]; env.trust(USD(10000), alice); - env(pay(gw, alice, gw["USD"](100))); + env(pay(gw, alice, USD(100))); AMM amm(env, alice, XRP(100), USD(100)); env.close(); @@ -61,7 +60,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if asset pair provided does not exist. This should // return terNO_AMM error. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(100000), gw, alice); @@ -87,14 +86,14 @@ class AMMClawback_test : public jtx::AMMTest // The AMM account does not exist at all now. // It should return terNO_AMM error. - env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + env(amm::ammClawback(gw, alice, USD, gw["EUR"], std::nullopt), ter(terNO_AMM)); } // Test if the issuer field and holder field is the same. This should // return temMALFORMED error. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -124,7 +123,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if the Asset field matches the Account field. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -156,7 +155,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if the Amount field matches the Asset field. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -189,7 +188,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if the Amount is invalid, which is less than zero. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -230,7 +229,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if the issuer did not set asfAllowTrustLineClawback, AMMClawback // transaction is prohibited. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -241,7 +240,7 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(1000), alice); env(pay(gw, alice, USD(100))); env.close(); - env.require(balance(alice, gw["USD"](100))); + env.require(balance(alice, USD(100))); env.require(balance(gw, alice["USD"](-100))); // gw creates AMM pool of XRP/USD. @@ -255,7 +254,7 @@ class AMMClawback_test : public jtx::AMMTest // Test invalid flag. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -283,7 +282,7 @@ class AMMClawback_test : public jtx::AMMTest // Test if tfClawTwoAssets is set when the two assets in the AMM pool // are not issued by the same issuer. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(10000), gw, alice); @@ -314,7 +313,7 @@ class AMMClawback_test : public jtx::AMMTest // Test clawing back XRP is being prohibited. { - Env env(*this, features); + Env env(*this); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(1000000), gw, alice); @@ -400,7 +399,7 @@ class AMMClawback_test : public jtx::AMMTest env(pay(gw, alice, USD(3000))); env.close(); env.require(balance(gw, alice["USD"](-3000))); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // gw2 issues 3000 EUR to Alice. auto const EUR = gw2["EUR"]; @@ -408,7 +407,7 @@ class AMMClawback_test : public jtx::AMMTest env(pay(gw2, alice, EUR(3000))); env.close(); env.require(balance(gw2, alice["EUR"](-3000))); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // Alice creates AMM pool of EUR/USD. AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); @@ -426,13 +425,13 @@ class AMMClawback_test : public jtx::AMMTest // USD into the pool, then she has 1000 USD. And 1000 USD was clawed // back from the AMM pool, so she still has 1000 USD. env.require(balance(gw, alice["USD"](-1000))); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); // Alice's initial balance for EUR is 3000 EUR. Alice deposited 1000 // EUR into the pool, 500 EUR was withdrawn proportionally. So she // has 2500 EUR now. env.require(balance(gw2, alice["EUR"](-2500))); - env.require(balance(alice, gw2["EUR"](2500))); + env.require(balance(alice, EUR(2500))); // 1000 USD and 500 EUR was withdrawn from the AMM pool, so the // current balance is 1000 USD and 500 EUR. @@ -452,12 +451,12 @@ class AMMClawback_test : public jtx::AMMTest // Alice should still has 1000 USD because gw clawed back from the // AMM pool. env.require(balance(gw, alice["USD"](-1000))); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); // Alice should has 3000 EUR now because another 500 EUR was // withdrawn. env.require(balance(gw2, alice["EUR"](-3000))); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // amm is automatically deleted. BEAST_EXPECT(!amm.ammExists()); @@ -483,7 +482,7 @@ class AMMClawback_test : public jtx::AMMTest env(pay(gw, alice, USD(3000))); env.close(); env.require(balance(gw, alice["USD"](-3000))); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // Alice creates AMM pool of XRP/USD. AMM amm(env, alice, XRP(1000), USD(2000), ter(tesSUCCESS)); @@ -503,11 +502,12 @@ class AMMClawback_test : public jtx::AMMTest // USD into the pool, then she has 1000 USD. And 1000 USD was clawed // back from the AMM pool, so she still has 1000 USD. env.require(balance(gw, alice["USD"](-1000))); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); // Alice will get 500 XRP back. BEAST_EXPECT( expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(500))); + aliceXrpBalance = env.balance(alice, XRP); // 1000 USD and 500 XRP was withdrawn from the AMM pool, so the // current balance is 1000 USD and 500 XRP. @@ -527,11 +527,11 @@ class AMMClawback_test : public jtx::AMMTest // Alice should still has 1000 USD because gw clawed back from the // AMM pool. env.require(balance(gw, alice["USD"](-1000))); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); - // Alice will get another 1000 XRP back. + // Alice will get another 500 XRP back. BEAST_EXPECT( - expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000))); + expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(500))); // amm is automatically deleted. BEAST_EXPECT(!amm.ammExists()); @@ -568,21 +568,21 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(6000))); env.close(); - env.require(balance(alice, gw["USD"](6000))); + env.require(balance(alice, USD(6000))); // gw2 issues 6000 EUR to Alice. auto const EUR = gw2["EUR"]; env.trust(EUR(100000), alice); env(pay(gw2, alice, EUR(6000))); env.close(); - env.require(balance(alice, gw2["EUR"](6000))); + env.require(balance(alice, EUR(6000))); // Alice creates AMM pool of EUR/USD AMM amm(env, alice, EUR(5000), USD(4000), ter(tesSUCCESS)); env.close(); BEAST_EXPECT(amm.expectBalances( - USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + USD(4000), EUR(5000), IOUAmount{4472135954999579, -12})); // gw clawback 1000 USD from the AMM pool env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)), @@ -592,21 +592,21 @@ class AMMClawback_test : public jtx::AMMTest // Alice's initial balance for USD is 6000 USD. Alice deposited 4000 // USD into the pool, then she has 2000 USD. And 1000 USD was clawed // back from the AMM pool, so she still has 2000 USD. - env.require(balance(alice, gw["USD"](2000))); + env.require(balance(alice, USD(2000))); // Alice's initial balance for EUR is 6000 EUR. Alice deposited 5000 // EUR into the pool, 1250 EUR was withdrawn proportionally. So she // has 2500 EUR now. - env.require(balance(alice, gw2["EUR"](2250))); + env.require(balance(alice, EUR(2250))); // 1000 USD and 1250 EUR was withdrawn from the AMM pool, so the // current balance is 3000 USD and 3750 EUR. BEAST_EXPECT(amm.expectBalances( - USD(3000), EUR(3750), IOUAmount{3354101966249685, -12})); + USD(3000), EUR(3750), IOUAmount{3354101966249684, -12})); // Alice has 3/4 of its initial lptokens Left. BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{3354101966249685, -12})); + amm.expectLPTokens(alice, IOUAmount{3354101966249684, -12})); // gw clawback another 500 USD from the AMM pool. env(amm::ammClawback(gw, alice, USD, EUR, USD(500)), @@ -615,32 +615,28 @@ class AMMClawback_test : public jtx::AMMTest // Alice should still has 2000 USD because gw clawed back from the // AMM pool. - env.require(balance(alice, gw["USD"](2000))); + env.require(balance(alice, USD(2000))); BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2500000000000001), -12}, - STAmount{EUR, UINT64_C(3125000000000001), -12}, - IOUAmount{2795084971874738, -12})); + USD(2500), EUR(3125), IOUAmount{2795084971874737, -12})); - BEAST_EXPECT( - env.balance(alice, EUR) == - STAmount(EUR, UINT64_C(2874999999999999), -12)); + BEAST_EXPECT(env.balance(alice, EUR) == EUR(2875)); // gw clawback small amount, 1 USD. env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), ter(tesSUCCESS)); env.close(); // Another 1 USD / 1.25 EUR was withdrawn. - env.require(balance(alice, gw["USD"](2000))); + env.require(balance(alice, USD(2000))); BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(2499000000000002), -12}, - STAmount{EUR, UINT64_C(3123750000000002), -12}, - IOUAmount{2793966937885989, -12})); + STAmount{USD, UINT64_C(2499000000000001), -12}, + STAmount{EUR, UINT64_C(3123750000000001), -12}, + IOUAmount{2793966937885988, -12})); BEAST_EXPECT( env.balance(alice, EUR) == - STAmount(EUR, UINT64_C(2876249999999998), -12)); + STAmount(EUR, UINT64_C(2876'249999999999), -12)); // gw clawback 4000 USD, exceeding the current balance. We // will clawback all. @@ -648,7 +644,7 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](2000))); + env.require(balance(alice, USD(2000))); // All alice's EUR in the pool goes back to alice. BEAST_EXPECT( @@ -714,13 +710,14 @@ class AMMClawback_test : public jtx::AMMTest // gw2 creates AMM pool of XRP/EUR, alice and bob deposit XRP/EUR. AMM amm2(env, gw2, XRP(3000), EUR(1000), ter(tesSUCCESS)); BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9})); + amm2.deposit(alice, EUR(1000), XRP(3000)); BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9})); amm2.deposit(bob, EUR(1000), XRP(3000)); BEAST_EXPECT(amm2.expectBalances( - EUR(3000), XRP(9000), IOUAmount{5196152422706634, -9})); + EUR(3000), XRP(9000), IOUAmount{5196152422706631, -9})); env.close(); auto aliceXrpBalance = env.balance(alice, XRP); @@ -734,19 +731,24 @@ class AMMClawback_test : public jtx::AMMTest // Alice's initial balance for USD is 6000 USD. Alice deposited 1000 // USD into the pool, then she has 5000 USD. And 500 USD was clawed // back from the AMM pool, so she still has 5000 USD. - env.require(balance(alice, gw["USD"](5000))); + env.require(balance(alice, USD(5000))); // Bob's balance is not changed. - env.require(balance(bob, gw["USD"](4000))); + env.require(balance(bob, USD(4000))); // Alice gets 1000 XRP back. - BEAST_EXPECT( - expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000))); + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(1000) - XRPAmount(1))); + aliceXrpBalance = env.balance(alice, XRP); BEAST_EXPECT(amm.expectBalances( - USD(2500), XRP(5000), IOUAmount{3535533905932738, -9})); + USD(2500), + XRPAmount(5000000001), + IOUAmount{3'535'533'905932738, -9})); + BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{7071067811865480, -10})); + amm.expectLPTokens(alice, IOUAmount{707106781186548, -9})); + BEAST_EXPECT( amm.expectLPTokens(bob, IOUAmount{1414213562373095, -9})); @@ -754,18 +756,22 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw, bob, USD, XRP, USD(10)), ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](5000))); - env.require(balance(bob, gw["USD"](4000))); + env.require(balance(alice, USD(5000))); + env.require(balance(bob, USD(4000))); // Bob gets 20 XRP back. BEAST_EXPECT( expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(20))); + bobXrpBalance = env.balance(bob, XRP); + BEAST_EXPECT(amm.expectBalances( STAmount{USD, UINT64_C(2490000000000001), -12}, - XRP(4980), - IOUAmount{3521391770309008, -9})); + XRPAmount(4980000001), + IOUAmount{3521391'770309008, -9})); + BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{7071067811865480, -10})); + amm.expectLPTokens(alice, IOUAmount{707106781186548, -9})); + BEAST_EXPECT( amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); @@ -774,18 +780,22 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw2["EUR"](4000))); - env.require(balance(bob, gw2["EUR"](3000))); + env.require(balance(alice, EUR(4000))); + env.require(balance(bob, EUR(3000))); - // Alice gets 600 XRP back. BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(1000) + XRP(600))); + env, alice, aliceXrpBalance + XRP(600) - XRPAmount{1})); + aliceXrpBalance = env.balance(alice, XRP); + BEAST_EXPECT(amm2.expectBalances( - EUR(2800), XRP(8400), IOUAmount{4849742261192859, -9})); + EUR(2800), + XRPAmount(8400000001), + IOUAmount{4849742261192856, -9})); + BEAST_EXPECT( - amm2.expectLPTokens(alice, IOUAmount{1385640646055103, -9})); + amm2.expectLPTokens(alice, IOUAmount{1385640646055102, -9})); BEAST_EXPECT( - amm2.expectLPTokens(bob, IOUAmount{1732050807568878, -9})); + amm2.expectLPTokens(bob, IOUAmount{1732050807568877, -9})); // gw claw back 1000 USD from alice in amm, which exceeds alice's // balance. This will clawback all the remaining LP tokens of alice @@ -794,20 +804,21 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](5000))); - env.require(balance(bob, gw["USD"](4000))); + env.require(balance(alice, USD(5000))); + env.require(balance(bob, USD(4000))); // Alice gets 1000 XRP back. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); + BEAST_EXPECT( + expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000))); + aliceXrpBalance = env.balance(alice, XRP); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); BEAST_EXPECT( amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9})); + BEAST_EXPECT(amm.expectBalances( STAmount{USD, UINT64_C(1990000000000001), -12}, - XRP(3980), + XRPAmount{3'980'000'001}, IOUAmount{2814284989122460, -9})); // gw clawback 1000 USD from bob in amm, which also exceeds bob's @@ -817,15 +828,14 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](5000))); - env.require(balance(bob, gw["USD"](4000))); + env.require(balance(alice, USD(5000))); + env.require(balance(bob, USD(4000))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, bob, bobXrpBalance + XRP(20) + XRP(1980))); + BEAST_EXPECT(expectLedgerEntryRoot(env, alice, aliceXrpBalance)); + + BEAST_EXPECT( + expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(1980))); + bobXrpBalance = env.balance(bob, XRP); // Now neither alice nor bob has any lptoken in amm. BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); @@ -838,51 +848,51 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw2["EUR"](4000))); - env.require(balance(bob, gw2["EUR"](3000))); + env.require(balance(alice, EUR(4000))); + env.require(balance(bob, EUR(3000))); // Alice gets another 2400 XRP back, bob's XRP balance remains the // same. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + - XRP(2400))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, bob, bobXrpBalance + XRP(20) + XRP(1980))); + BEAST_EXPECT( + expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(2400))); + + BEAST_EXPECT(expectLedgerEntryRoot(env, bob, bobXrpBalance)); + aliceXrpBalance = env.balance(alice, XRP); // Alice now does not have any lptoken in amm2 BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); BEAST_EXPECT(amm2.expectBalances( - EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9})); + EUR(2000), + XRPAmount(6000000001), + IOUAmount{3464101615137754, -9})); - // gw2 claw back 2000 EUR from bib in amm2, which exceeds bob's + // gw2 claw back 2000 EUR from bob in amm2, which exceeds bob's // balance. All bob's lptokens will be consumed, which corresponds // to 1000EUR / 3000 XRP. env(amm::ammClawback(gw2, bob, EUR, XRP, EUR(2000)), ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw2["EUR"](4000))); - env.require(balance(bob, gw2["EUR"](3000))); + env.require(balance(alice, EUR(4000))); + env.require(balance(bob, EUR(3000))); // Bob gets another 3000 XRP back. Alice's XRP balance remains the // same. - BEAST_EXPECT(expectLedgerEntryRoot( - env, - alice, - aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + - XRP(2400))); - BEAST_EXPECT(expectLedgerEntryRoot( - env, bob, bobXrpBalance + XRP(20) + XRP(1980) + XRP(3000))); + BEAST_EXPECT(expectLedgerEntryRoot(env, alice, aliceXrpBalance)); + + BEAST_EXPECT( + expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(3000))); + bobXrpBalance = env.balance(bob, XRP); // Neither alice nor bob has any lptoken in amm2 BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0))); BEAST_EXPECT(amm2.expectLPTokens(bob, IOUAmount(0))); BEAST_EXPECT(amm2.expectBalances( - EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9})); + EUR(1000), + XRPAmount(3000000001), + IOUAmount{1732050807568877, -9})); } } @@ -941,27 +951,27 @@ class AMMClawback_test : public jtx::AMMTest env.close(); BEAST_EXPECT(amm.expectBalances( - USD(4000), EUR(5000), IOUAmount{4472135954999580, -12})); + USD(4000), EUR(5000), IOUAmount{4472135954999579, -12})); amm.deposit(bob, USD(2000), EUR(2500)); BEAST_EXPECT(amm.expectBalances( - USD(6000), EUR(7500), IOUAmount{6708203932499370, -12})); + USD(6000), EUR(7500), IOUAmount{6708203932499368, -12})); amm.deposit(carol, USD(1000), EUR(1250)); BEAST_EXPECT(amm.expectBalances( - USD(7000), EUR(8750), IOUAmount{7826237921249265, -12})); + USD(7000), EUR(8750), IOUAmount{7826237921249262, -12})); BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12})); + amm.expectLPTokens(alice, IOUAmount{4472135954999579, -12})); BEAST_EXPECT( - amm.expectLPTokens(bob, IOUAmount{2236067977499790, -12})); + amm.expectLPTokens(bob, IOUAmount{2236067977499789, -12})); BEAST_EXPECT( - amm.expectLPTokens(carol, IOUAmount{1118033988749895, -12})); + amm.expectLPTokens(carol, IOUAmount{1118033988749894, -12})); - env.require(balance(alice, gw["USD"](2000))); - env.require(balance(alice, gw2["EUR"](1000))); - env.require(balance(bob, gw["USD"](3000))); - env.require(balance(bob, gw2["EUR"](2500))); - env.require(balance(carol, gw["USD"](3000))); - env.require(balance(carol, gw2["EUR"](2750))); + env.require(balance(alice, USD(2000))); + env.require(balance(alice, EUR(1000))); + env.require(balance(bob, USD(3000))); + env.require(balance(bob, EUR(2500))); + env.require(balance(carol, USD(3000))); + env.require(balance(carol, EUR(2750))); // gw clawback all the bob's USD in amm. (2000 USD / 2500 EUR) env(amm::ammClawback(gw, bob, USD, EUR, std::nullopt), @@ -969,40 +979,40 @@ class AMMClawback_test : public jtx::AMMTest env.close(); BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(4999999999999999), -12}, - STAmount{EUR, UINT64_C(6249999999999999), -12}, - IOUAmount{5590169943749475, -12})); + STAmount{USD, UINT64_C(5000000000000001), -12}, + STAmount{EUR, UINT64_C(6250000000000001), -12}, + IOUAmount{5590169943749473, -12})); BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12})); + amm.expectLPTokens(alice, IOUAmount{4472135954999579, -12})); BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); BEAST_EXPECT( - amm.expectLPTokens(carol, IOUAmount{1118033988749895, -12})); + amm.expectLPTokens(carol, IOUAmount{1118033988749894, -12})); // Bob will get 2500 EUR back. - env.require(balance(alice, gw["USD"](2000))); - env.require(balance(alice, gw2["EUR"](1000))); + env.require(balance(alice, USD(2000))); + env.require(balance(alice, EUR(1000))); BEAST_EXPECT( env.balance(bob, USD) == STAmount(USD, UINT64_C(3000000000000000), -12)); BEAST_EXPECT( env.balance(bob, EUR) == - STAmount(EUR, UINT64_C(5000000000000001), -12)); - env.require(balance(carol, gw["USD"](3000))); - env.require(balance(carol, gw2["EUR"](2750))); + STAmount(EUR, UINT64_C(4999999999999999), -12)); + env.require(balance(carol, USD(3000))); + env.require(balance(carol, EUR(2750))); // gw2 clawback all carol's EUR in amm. (1000 USD / 1250 EUR) env(amm::ammClawback(gw2, carol, EUR, USD, std::nullopt), ter(tesSUCCESS)); env.close(); BEAST_EXPECT(amm.expectBalances( - STAmount{USD, UINT64_C(3999999999999999), -12}, - STAmount{EUR, UINT64_C(4999999999999999), -12}, - IOUAmount{4472135954999580, -12})); + STAmount{USD, UINT64_C(4000000000000001), -12}, + STAmount{EUR, UINT64_C(5000000000000002), -12}, + IOUAmount{4472135954999579, -12})); BEAST_EXPECT( - amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12})); + amm.expectLPTokens(alice, IOUAmount{4472135954999579, -12})); BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0))); BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(0))); @@ -1011,8 +1021,8 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(carol, gw2["EUR"](2750))); - env.require(balance(carol, gw["USD"](4000))); + env.require(balance(carol, EUR(2750))); + env.require(balance(carol, USD(4000))); BEAST_EXPECT(!amm.ammExists()); } @@ -1042,13 +1052,13 @@ class AMMClawback_test : public jtx::AMMTest // gw creates AMM pool of XRP/USD, alice and bob deposit XRP/USD. AMM amm(env, gw, XRP(2000), USD(10000), ter(tesSUCCESS)); BEAST_EXPECT(amm.expectBalances( - USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + USD(10000), XRP(2000), IOUAmount{4472135954999579, -9})); amm.deposit(alice, USD(1000), XRP(200)); BEAST_EXPECT(amm.expectBalances( - USD(11000), XRP(2200), IOUAmount{4919349550499538, -9})); + USD(11000), XRP(2200), IOUAmount{4919349550499536, -9})); amm.deposit(bob, USD(2000), XRP(400)); BEAST_EXPECT(amm.expectBalances( - USD(13000), XRP(2600), IOUAmount{5813776741499453, -9})); + USD(13000), XRP(2600), IOUAmount{5813776741499451, -9})); env.close(); auto aliceXrpBalance = env.balance(alice, XRP); @@ -1059,9 +1069,11 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); BEAST_EXPECT(amm.expectBalances( - USD(12000), XRP(2400), IOUAmount{5366563145999495, -9})); - BEAST_EXPECT( - expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(200))); + USD(12000), + XRPAmount(2400000001), + IOUAmount{5366563145999494, -9})); + BEAST_EXPECT(expectLedgerEntryRoot( + env, alice, aliceXrpBalance + XRP(200) - XRPAmount{1})); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); // gw clawback all bob's USD in amm. (2000 USD / 400 XRP) @@ -1069,7 +1081,9 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); BEAST_EXPECT(amm.expectBalances( - USD(10000), XRP(2000), IOUAmount{4472135954999580, -9})); + USD(10000), + XRPAmount(2000000001), + IOUAmount{4472135954999579, -9})); BEAST_EXPECT( expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(400))); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); @@ -1125,10 +1139,9 @@ class AMMClawback_test : public jtx::AMMTest amm.deposit(bob, USD(4000), EUR(1000)); BEAST_EXPECT( amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); - amm.deposit(carol, USD(2000), EUR(500)); + amm.deposit(carol, USD(2000.25), EUR(500)); BEAST_EXPECT( amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000))); - // gw clawback 1000 USD from carol. env(amm::ammClawback(gw, carol, USD, EUR, USD(1000)), ter(tesSUCCESS)); env.close(); @@ -1142,7 +1155,9 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); // 250 EUR goes back to carol. BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); @@ -1164,7 +1179,9 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); // 250 EUR did not go back to bob because tfClawTwoAssets is set. BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); // gw clawback all USD from alice and set tfClawTwoAssets. @@ -1181,7 +1198,9 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); } @@ -1351,10 +1370,10 @@ class AMMClawback_test : public jtx::AMMTest env(amm::ammClawback(gw, gw2, USD, EUR, USD(1000)), ter(tesSUCCESS)); env.close(); BEAST_EXPECT(amm.expectBalances( - USD(5000), EUR(10000), IOUAmount{7071067811865475, -12})); + USD(5000), EUR(10000), IOUAmount{7071067811865474, -12})); BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{1414213562373095, -12})); - BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); @@ -1369,10 +1388,12 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(amm.expectBalances( USD(4500), STAmount(EUR, UINT64_C(9000000000000001), -12), - IOUAmount{6363961030678928, -12})); + IOUAmount{6363961030678927, -12})); BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); - BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + + BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); + BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12})); @@ -1387,10 +1408,11 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(amm.expectBalances( USD(2500), STAmount(EUR, UINT64_C(5000000000000001), -12), - IOUAmount{3535533905932738, -12})); + IOUAmount{3535533905932737, -12})); BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13})); - BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12})); + + BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373094, -12})); BEAST_EXPECT( amm.expectLPTokens(alice, IOUAmount{1414213562373095, -12})); @@ -1457,14 +1479,14 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(3000))); env.close(); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // gw2 issues 3000 EUR to Alice. auto const EUR = gw2["EUR"]; env.trust(EUR(100000), alice); env(pay(gw2, alice, EUR(3000))); env.close(); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // Alice creates AMM pool of EUR/USD. AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); @@ -1482,8 +1504,8 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](1000))); - env.require(balance(alice, gw2["EUR"](2500))); + env.require(balance(alice, USD(1000))); + env.require(balance(alice, EUR(2500))); BEAST_EXPECT(amm.expectBalances( USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); @@ -1499,8 +1521,8 @@ class AMMClawback_test : public jtx::AMMTest // Alice should still has 1000 USD because gw clawed back from the // AMM pool. - env.require(balance(alice, gw["USD"](1000))); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, USD(1000))); + env.require(balance(alice, EUR(3000))); // amm is automatically deleted. BEAST_EXPECT(!amm.ammExists()); @@ -1525,14 +1547,14 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(3000))); env.close(); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // gw2 issues 3000 EUR to Alice. auto const EUR = gw2["EUR"]; env.trust(EUR(100000), alice); env(pay(gw2, alice, EUR(3000))); env.close(); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // Alice creates AMM pool of EUR/USD. AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); @@ -1551,8 +1573,8 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](1000))); - env.require(balance(alice, gw2["EUR"](2500))); + env.require(balance(alice, USD(1000))); + env.require(balance(alice, EUR(2500))); BEAST_EXPECT(amm.expectBalances( USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); BEAST_EXPECT( @@ -1578,14 +1600,14 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(3000))); env.close(); - env.require(balance(alice, gw["USD"](3000))); + env.require(balance(alice, USD(3000))); // gw2 issues 3000 EUR to Alice. auto const EUR = gw2["EUR"]; env.trust(EUR(100000), alice); env(pay(gw2, alice, EUR(3000))); env.close(); - env.require(balance(alice, gw2["EUR"](3000))); + env.require(balance(alice, EUR(3000))); // Alice creates AMM pool of EUR/USD. AMM amm(env, alice, EUR(1000), USD(2000), ter(tesSUCCESS)); @@ -1603,8 +1625,8 @@ class AMMClawback_test : public jtx::AMMTest ter(tesSUCCESS)); env.close(); - env.require(balance(alice, gw["USD"](1000))); - env.require(balance(alice, gw2["EUR"](2500))); + env.require(balance(alice, USD(1000))); + env.require(balance(alice, EUR(2500))); BEAST_EXPECT(amm.expectBalances( USD(1000), EUR(500), IOUAmount{7071067811865475, -13})); BEAST_EXPECT( @@ -1653,7 +1675,7 @@ class AMMClawback_test : public jtx::AMMTest amm.deposit(bob, USD(4000), EUR(1000)); BEAST_EXPECT( amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000))); - amm.deposit(carol, USD(2000), EUR(500)); + amm.deposit(carol, USD(2000.25), EUR(500)); BEAST_EXPECT( amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000))); @@ -1675,7 +1697,9 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); // 250 EUR goes back to carol. BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); @@ -1697,7 +1721,9 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); // 250 EUR did not go back to bob because tfClawTwoAssets is set. BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); // gw clawback all USD from alice and set tfClawTwoAssets. @@ -1715,7 +1741,9 @@ class AMMClawback_test : public jtx::AMMTest BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000)); BEAST_EXPECT(env.balance(bob, USD) == USD(5000)); BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000)); - BEAST_EXPECT(env.balance(carol, USD) == USD(6000)); + BEAST_EXPECT( + env.balance(carol, USD) == + STAmount(USD, UINT64_C(5999'999999999999), -12)); BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750)); } } @@ -1725,10 +1753,17 @@ class AMMClawback_test : public jtx::AMMTest { testcase("test single depoit and clawback"); using namespace jtx; + std::string logs; // Test AMMClawback for USD/XRP pool. Claw back USD, and XRP goes back // to the holder. - Env env(*this, features); + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); Account gw{"gateway"}; Account alice{"alice"}; env.fund(XRP(1000000000), gw, alice); @@ -1744,7 +1779,7 @@ class AMMClawback_test : public jtx::AMMTest env.trust(USD(100000), alice); env(pay(gw, alice, USD(1000))); env.close(); - env.require(balance(alice, gw["USD"](1000))); + env.require(balance(alice, USD(1000))); // gw creates AMM pool of XRP/USD. AMM amm(env, gw, XRP(100), USD(400), ter(tesSUCCESS)); @@ -1764,29 +1799,309 @@ class AMMClawback_test : public jtx::AMMTest env.close(); BEAST_EXPECT(amm.expectBalances( - STAmount(USD, UINT64_C(5656854249492380), -13), - XRP(70.710678), + STAmount(USD, UINT64_C(565'685424949238), -12), + XRP(70.710679), IOUAmount(200000))); BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0))); BEAST_EXPECT(expectLedgerEntryRoot( - env, alice, aliceXrpBalance + XRP(29.289322))); + env, alice, aliceXrpBalance + XRP(29.289321))); + } + + void + testLastHolderLPTokenBalance(FeatureBitset features) + { + testcase( + "test last holder's lptoken balance not equal to AMM's lptoken " + "balance before clawback"); + using namespace jtx; + std::string logs; + + auto setupAccounts = + [&](Env& env, Account& gw, Account& alice, Account& bob) { + env.fund(XRP(100000), gw, alice, bob); + env.close(); + env(fset(gw, asfAllowTrustLineClawback)); + env.close(); + + auto const USD = gw["USD"]; + env.trust(USD(100000), alice); + env(pay(gw, alice, USD(50000))); + env.trust(USD(100000), bob); + env(pay(gw, bob, USD(40000))); + env.close(); + + return USD; + }; + + auto getLPTokenBalances = + [&](auto& env, + auto const& amm, + auto const& account) -> std::pair { + auto const lpToken = + getAccountLines( + env, account, amm.lptIssue())[jss::lines][0u][jss::balance] + .asString(); + auto const lpTokenBalance = + amm.ammRpcInfo()[jss::amm][jss::lp_token][jss::value] + .asString(); + return {lpToken, lpTokenBalance}; + }; + + // IOU/XRP pool. AMMClawback almost last holder's USD balance + { + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + AMM amm(env, alice, XRP(2), USD(1)); + amm.deposit(alice, IOUAmount{1'876123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000'000}); + amm.withdraw(alice, IOUAmount{1'876123487565916, -15}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1414.21356237366" && + lpTokenBalance == "1414.213562374"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + auto const lpBalance = IOUAmount{989, -12}; + env(amm::ammClawback(gw, alice, USD, XRP, USD(1))); + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(7000000000000000), -28), + XRPAmount(1), + lpBalance)); + BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); + } + + // IOU/XRP pool. AMMClawback part of last holder's USD balance + { + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + AMM amm(env, alice, XRP(2), USD(1)); + amm.deposit(alice, IOUAmount{1'876123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000'000}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1416.08968586066" && + lpTokenBalance == "1416.089685861"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + env(amm::ammClawback(gw, alice, USD, XRP, USD(0.5))); + + auto const lpBalance = IOUAmount{708'9829046743238, -13}; + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(5013266196406999), -16), + XRPAmount(1002655), + lpBalance)); + BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); + } + + // IOU/XRP pool. AMMClawback all of last holder's USD balance + { + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + AMM amm(env, alice, XRP(2), USD(1)); + amm.deposit(alice, IOUAmount{1'876123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000'000}); + amm.withdraw(alice, IOUAmount{1'876123487565916, -15}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1414.21356237366" && + lpTokenBalance == "1414.213562374"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + env(amm::ammClawback(gw, alice, USD, XRP, std::nullopt)); + BEAST_EXPECT(!amm.ammExists()); + } + + // IOU/IOU pool, different issuers + { + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + Account gw2{"gateway2"}; + env.fund(XRP(100000), gw2); + env.close(); + auto const EUR = gw2["EUR"]; + env.trust(EUR(100000), alice); + env(pay(gw2, alice, EUR(50000))); + env.trust(EUR(100000), bob); + env(pay(gw2, bob, EUR(50000))); + env.close(); + + AMM amm(env, alice, USD(2), EUR(1)); + amm.deposit(alice, IOUAmount{1'576123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000}); + amm.withdraw(alice, IOUAmount{1'576123487565916, -15}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1.414213562374011" && + lpTokenBalance == "1.414213562374"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt)); + BEAST_EXPECT(!amm.ammExists()); + } + + // IOU/IOU pool, same issuer + { + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + auto const EUR = gw["EUR"]; + env.trust(EUR(100000), alice); + env(pay(gw, alice, EUR(50000))); + env.trust(EUR(100000), bob); + env(pay(gw, bob, EUR(50000))); + env.close(); + + AMM amm(env, alice, USD(1), EUR(2)); + amm.deposit(alice, IOUAmount{1'076123487565916, -15}); + amm.deposit(bob, IOUAmount{1'000}); + amm.withdraw(alice, IOUAmount{1'076123487565916, -15}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + BEAST_EXPECT( + lpToken == "1.414213562374011" && + lpTokenBalance == "1.414213562374"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + env(amm::ammClawback(gw, alice, USD, EUR, std::nullopt), + txflags(tfClawTwoAssets)); + BEAST_EXPECT(!amm.ammExists()); + } + + // IOU/IOU pool, larger asset ratio + { + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + Account gw{"gateway"}, alice{"alice"}, bob{"bob"}; + auto const USD = setupAccounts(env, gw, alice, bob); + + auto const EUR = gw["EUR"]; + env.trust(EUR(1000000000), alice); + env(pay(gw, alice, EUR(500000000))); + env.trust(EUR(1000000000), bob); + env(pay(gw, bob, EUR(500000000))); + env.close(); + + AMM amm(env, alice, USD(1), EUR(2000000)); + amm.deposit(alice, IOUAmount{1'076123487565916, -12}); + amm.deposit(bob, IOUAmount{10000}); + amm.withdraw(alice, IOUAmount{1'076123487565916, -12}); + amm.withdrawAll(bob); + + auto [lpToken, lpTokenBalance] = + getLPTokenBalances(env, amm, alice); + + BEAST_EXPECT( + lpToken == "1414.213562373101" && + lpTokenBalance == "1414.2135623731"); + + auto res = + isOnlyLiquidityProvider(*env.current(), amm.lptIssue(), alice); + BEAST_EXPECT(res && res.value()); + + env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), + txflags(tfClawTwoAssets)); + auto const lpBalance = IOUAmount{5, -12}; + BEAST_EXPECT(amm.expectBalances( + STAmount(USD, UINT64_C(4), -15), + STAmount(EUR, UINT64_C(8), -9), + lpBalance)); + BEAST_EXPECT(amm.expectLPTokens(alice, lpBalance)); + } } void run() override { FeatureBitset const all{jtx::supported_amendments()}; - testInvalidRequest(all); + + testInvalidRequest(); testFeatureDisabled(all - featureAMMClawback); - testAMMClawbackSpecificAmount(all); - testAMMClawbackExceedBalance(all); - testAMMClawbackAll(all); - testAMMClawbackSameIssuerAssets(all); - testAMMClawbackSameCurrency(all); - testAMMClawbackIssuesEachOther(all); - testNotHoldingLptoken(all); - testAssetFrozen(all); - testSingleDepositAndClawback(all); + for (auto const& features : {all}) + { + testAMMClawbackSpecificAmount(features); + testAMMClawbackExceedBalance(features); + testAMMClawbackAll(features); + testAMMClawbackSameIssuerAssets(features); + testAMMClawbackSameCurrency(features); + testAMMClawbackIssuesEachOther(features); + testNotHoldingLptoken(features); + testAssetFrozen(features); + testSingleDepositAndClawback(features); + testLastHolderLPTokenBalance(features); + } } }; BEAST_DEFINE_TESTSUITE(AMMClawback, app, ripple); diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index ad1e761d98..f5f21fb1f6 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -821,21 +820,6 @@ struct AMM_test : public jtx::AMMTest std::nullopt, ter(tecAMM_FAILED)); - // Tiny deposit - ammAlice.deposit( - carol, - IOUAmount{1, -4}, - std::nullopt, - std::nullopt, - ter(temBAD_AMOUNT)); - ammAlice.deposit( - carol, - STAmount{USD, 1, -12}, - std::nullopt, - std::nullopt, - std::nullopt, - ter(tecAMM_INVALID_TOKENS)); - // Deposit non-empty AMM ammAlice.deposit( carol, @@ -846,6 +830,31 @@ struct AMM_test : public jtx::AMMTest ter(tecAMM_NOT_EMPTY)); }); + // Tiny deposit + testAMM( + [&](AMM& ammAlice, Env& env) { + auto const err = ter(tesSUCCESS); + // Pre-amendment XRP deposit side is rounded to 0 + // and deposit fails. + // Post-amendment XRP deposit side is rounded to 1 + // and deposit succeeds. + ammAlice.deposit( + carol, IOUAmount{1, -4}, std::nullopt, std::nullopt, err); + // Pre/post-amendment LPTokens is rounded to 0 and deposit + // fails with tecAMM_INVALID_TOKENS. + ammAlice.deposit( + carol, + STAmount{USD, 1, -12}, + std::nullopt, + std::nullopt, + std::nullopt, + ter(tecAMM_INVALID_TOKENS)); + }, + std::nullopt, + 0, + std::nullopt, + {features}); + // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice); @@ -1301,6 +1310,44 @@ struct AMM_test : public jtx::AMMTest std::nullopt, ter(tecAMM_FAILED)); }); + + // Equal deposit, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .tokens = IOUAmount{1, -12}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Equal deposit limit, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .asset2In = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Single deposit by asset, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Single deposit by tokens, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .tokens = IOUAmount{1, -10}, + .asset1In = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); + + // Single deposit with eprice, tokens rounded to 0 + testAMM([&](AMM& amm, Env& env) { + amm.deposit(DepositArg{ + .asset1In = STAmount{USD, 1, -15}, + .maxEP = STAmount{USD, 1, -1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + }); } void @@ -1309,6 +1356,7 @@ struct AMM_test : public jtx::AMMTest testcase("Deposit"); using namespace jtx; + auto const all = supported_amendments(); // Equal deposit: 1000000 tokens, 10% of the current pool testAMM([&](AMM& ammAlice, Env& env) { @@ -1513,8 +1561,9 @@ struct AMM_test : public jtx::AMMTest }); // Issuer create/deposit + for (auto const& feat : {all}) { - Env env(*this); + Env env(*this, feat); env.fund(XRP(30000), gw); AMM ammGw(env, gw, XRP(10'000), USD(10'000)); BEAST_EXPECT( @@ -1608,6 +1657,7 @@ struct AMM_test : public jtx::AMMTest testcase("Invalid Withdraw"); using namespace jtx; + auto const all = supported_amendments(); testAMM( [&](AMM& ammAlice, Env& env) { @@ -1901,16 +1951,6 @@ struct AMM_test : public jtx::AMMTest ammAlice.withdraw( carol, 10'000, std::nullopt, std::nullopt, ter(tecAMM_BALANCE)); - // Withdraw entire one side of the pool. - // Equal withdraw but due to XRP precision limit, - // this results in full withdraw of XRP pool only, - // while leaving a tiny amount in USD pool. - ammAlice.withdraw( - alice, - IOUAmount{9'999'999'9999, -4}, - std::nullopt, - std::nullopt, - ter(tecAMM_BALANCE)); // Withdrawing from one side. // XRP by tokens ammAlice.withdraw( @@ -1942,6 +1982,53 @@ struct AMM_test : public jtx::AMMTest ter(tecAMM_BALANCE)); }); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Withdraw entire one side of the pool. + // Pre-amendment: + // Equal withdraw but due to XRP rounding + // this results in full withdraw of XRP pool only, + // while leaving a tiny amount in USD pool. + // Post-amendment: + // Most of the pool is withdrawn with remaining tiny amounts + auto err = ter(tesSUCCESS); + ammAlice.withdraw( + alice, + IOUAmount{9'999'999'9999, -4}, + std::nullopt, + std::nullopt, + err); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(1), STAmount{USD, 1, -7}, IOUAmount{1, -4})); + }, + std::nullopt, + 0, + std::nullopt, + {all}); + + testAMM( + [&](AMM& ammAlice, Env& env) { + // Similar to above with even smaller remaining amount + // is it ok that the pool is unbalanced? + // Withdraw entire one side of the pool. + // Equal withdraw but due to XRP precision limit, + // this results in full withdraw of XRP pool only, + // while leaving a tiny amount in USD pool. + auto err = ter(tesSUCCESS); + ammAlice.withdraw( + alice, + IOUAmount{9'999'999'999999999, -9}, + std::nullopt, + std::nullopt, + err); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(1), STAmount{USD, 1, -11}, IOUAmount{1, -8})); + }, + std::nullopt, + 0, + std::nullopt, + {all}); + // Invalid AMM testAMM([&](AMM& ammAlice, Env& env) { ammAlice.withdrawAll(alice); @@ -2005,15 +2092,17 @@ struct AMM_test : public jtx::AMMTest // Withdraw with EPrice limit. Fails to withdraw, calculated tokens // to withdraw are 0. - testAMM([&](AMM& ammAlice, Env&) { - ammAlice.deposit(carol, 1'000'000); - ammAlice.withdraw( - carol, - USD(100), - std::nullopt, - IOUAmount{500, 0}, - ter(tecAMM_FAILED)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.deposit(carol, 1'000'000); + auto const err = ter(tecAMM_INVALID_TOKENS); + ammAlice.withdraw( + carol, USD(100), std::nullopt, IOUAmount{500, 0}, err); + }, + std::nullopt, + 0, + std::nullopt, + {all}); // Withdraw with EPrice limit. Fails to withdraw, calculated tokens // to withdraw are greater than the LP shares. @@ -2078,14 +2167,17 @@ struct AMM_test : public jtx::AMMTest // Withdraw close to one side of the pool. Account's LP tokens // are rounded to all LP tokens. - testAMM([&](AMM& ammAlice, Env&) { - ammAlice.withdraw( - alice, - STAmount{USD, UINT64_C(9'999'999999999999), -12}, - std::nullopt, - std::nullopt, - ter(tecAMM_BALANCE)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + auto const err = ter(tecINVARIANT_FAILED); + ammAlice.withdraw( + alice, + STAmount{USD, UINT64_C(9'999'999999999999), -12}, + std::nullopt, + std::nullopt, + err); + }, + {.features = {all}, .noLog = true}); // Tiny withdraw testAMM([&](AMM& ammAlice, Env&) { @@ -2116,6 +2208,17 @@ struct AMM_test : public jtx::AMMTest XRPAmount{1}, std::nullopt, ter(tecAMM_INVALID_TOKENS)); + ammAlice.withdraw(WithdrawArg{ + .tokens = IOUAmount{1, -10}, + .err = ter(tecAMM_INVALID_TOKENS)}); + ammAlice.withdraw(WithdrawArg{ + .asset1Out = STAmount{USD, 1, -15}, + .asset2Out = XRPAmount{1}, + .err = ter(tecAMM_INVALID_TOKENS)}); + ammAlice.withdraw(WithdrawArg{ + .tokens = IOUAmount{1, -10}, + .asset1Out = STAmount{USD, 1, -15}, + .err = ter(tecAMM_INVALID_TOKENS)}); }); } @@ -2125,6 +2228,7 @@ struct AMM_test : public jtx::AMMTest testcase("Withdraw"); using namespace jtx; + auto const all = supported_amendments(); // Equal withdrawal by Carol: 1000000 of tokens, 10% of the current // pool @@ -2178,11 +2282,18 @@ struct AMM_test : public jtx::AMMTest }); // Single withdrawal by amount XRP1000 - testAMM([&](AMM& ammAlice, Env&) { - ammAlice.withdraw(alice, XRP(1'000)); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(9'000), USD(10'000), IOUAmount{9'486'832'98050514, -8})); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(alice, XRP(1'000)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount{9'000'000'001}, + USD(10'000), + IOUAmount{9'486'832'98050514, -8})); + }, + std::nullopt, + 0, + std::nullopt, + {all}); // Single withdrawal by tokens 10000. testAMM([&](AMM& ammAlice, Env&) { @@ -2233,20 +2344,25 @@ struct AMM_test : public jtx::AMMTest }); // Single deposit/withdraw by the same account - testAMM([&](AMM& ammAlice, Env&) { - // Since a smaller amount might be deposited due to - // the lp tokens adjustment, withdrawing by tokens - // is generally preferred to withdrawing by amount. - auto lpTokens = ammAlice.deposit(carol, USD(1'000)); - ammAlice.withdraw(carol, lpTokens, USD(0)); - lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6)); - ammAlice.withdraw(carol, lpTokens, USD(0)); - lpTokens = ammAlice.deposit(carol, XRPAmount(1)); - ammAlice.withdraw(carol, lpTokens, XRPAmount(0)); - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), ammAlice.tokens())); - BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Since a smaller amount might be deposited due to + // the lp tokens adjustment, withdrawing by tokens + // is generally preferred to withdrawing by amount. + auto lpTokens = ammAlice.deposit(carol, USD(1'000)); + ammAlice.withdraw(carol, lpTokens, USD(0)); + lpTokens = ammAlice.deposit(carol, STAmount(USD, 1, -6)); + ammAlice.withdraw(carol, lpTokens, USD(0)); + lpTokens = ammAlice.deposit(carol, XRPAmount(1)); + ammAlice.withdraw(carol, lpTokens, XRPAmount(0)); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(10'000'000'001), USD(10'000), ammAlice.tokens())); + BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); + }, + std::nullopt, + 0, + std::nullopt, + {all}); // Single deposit by different accounts and then withdraw // in reverse. @@ -2289,27 +2405,22 @@ struct AMM_test : public jtx::AMMTest IOUAmount{10'000'000, 0})); }); - auto const all = supported_amendments(); // Withdraw with EPrice limit. testAMM( [&](AMM& ammAlice, Env& env) { ammAlice.deposit(carol, 1'000'000); ammAlice.withdraw( carol, USD(100), std::nullopt, IOUAmount{520, 0}); - BEAST_EXPECT( - ammAlice.expectBalances( - XRPAmount(11'000'000'000), - STAmount{USD, UINT64_C(9'372'781065088769), -12}, - IOUAmount{10'153'846'15384616, -8}) && - ammAlice.expectLPTokens( - carol, IOUAmount{153'846'15384616, -8})); + BEAST_EXPECT(ammAlice.expectLPTokens( + carol, IOUAmount{153'846'15384616, -8})); + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(11'000'000'000), + STAmount{USD, UINT64_C(9'372'78106508877), -11}, + IOUAmount{10'153'846'15384616, -8})); ammAlice.withdrawAll(carol); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{0})); }, - std::nullopt, - 0, - std::nullopt, - {all}); + {.features = {all}, .noLog = true}); // Withdraw with EPrice limit. AssetOut is 0. testAMM( @@ -2317,13 +2428,12 @@ struct AMM_test : public jtx::AMMTest ammAlice.deposit(carol, 1'000'000); ammAlice.withdraw( carol, USD(0), std::nullopt, IOUAmount{520, 0}); - BEAST_EXPECT( - ammAlice.expectBalances( - XRPAmount(11'000'000'000), - STAmount{USD, UINT64_C(9'372'781065088769), -12}, - IOUAmount{10'153'846'15384616, -8}) && - ammAlice.expectLPTokens( - carol, IOUAmount{153'846'15384616, -8})); + BEAST_EXPECT(ammAlice.expectLPTokens( + carol, IOUAmount{153'846'15384616, -8})); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(11'000), + STAmount{USD, UINT64_C(9'372'78106508877), -11}, + IOUAmount{10'153'846'15384616, -8})); }, std::nullopt, 0, @@ -2367,14 +2477,17 @@ struct AMM_test : public jtx::AMMTest STAmount{USD, UINT64_C(9'999'999999), -6}, IOUAmount{9'999'999'999, -3})); }); - testAMM([&](AMM& ammAlice, Env&) { - // Single XRP pool - ammAlice.withdraw(alice, std::nullopt, XRPAmount{1}); - BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{9'999'999'999}, - USD(10'000), - IOUAmount{9'999'999'9995, -4})); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + // Single XRP pool + ammAlice.withdraw(alice, std::nullopt, XRPAmount{1}); + BEAST_EXPECT(ammAlice.expectBalances( + XRP(10'000), USD(10'000), IOUAmount{9'999'999'9995, -4})); + }, + std::nullopt, + 0, + std::nullopt, + {all}); testAMM([&](AMM& ammAlice, Env&) { // Single USD pool ammAlice.withdraw(alice, std::nullopt, STAmount{USD, 1, -10}); @@ -2492,6 +2605,7 @@ struct AMM_test : public jtx::AMMTest { testcase("Fee Vote"); using namespace jtx; + auto const all = supported_amendments(); // One vote sets fee to 1%. testAMM([&](AMM& ammAlice, Env& env) { @@ -2509,6 +2623,11 @@ struct AMM_test : public jtx::AMMTest std::uint32_t tokens = 10'000'000, std::vector* accounts = nullptr) { Account a(std::to_string(i)); + // post-amendment the amount to deposit is slightly higher + // in order to ensure AMM invariant sqrt(asset1 * asset2) >= tokens + // fund just one USD higher in this case, which is enough for + // deposit to succeed + ++fundUSD; fund(env, gw, {a}, {USD(fundUSD)}, Fund::Acct); ammAlice.deposit(a, tokens); ammAlice.vote(a, 50 * (i + 1)); @@ -2517,11 +2636,16 @@ struct AMM_test : public jtx::AMMTest }; // Eight votes fill all voting slots, set fee 0.175%. - testAMM([&](AMM& ammAlice, Env& env) { - for (int i = 0; i < 7; ++i) - vote(ammAlice, env, i, 10'000); - BEAST_EXPECT(ammAlice.expectTradingFee(175)); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + for (int i = 0; i < 7; ++i) + vote(ammAlice, env, i, 10'000); + BEAST_EXPECT(ammAlice.expectTradingFee(175)); + }, + std::nullopt, + 0, + std::nullopt, + {all}); // Eight votes fill all voting slots, set fee 0.175%. // New vote, same account, sets fee 0.225% @@ -2916,7 +3040,9 @@ struct AMM_test : public jtx::AMMTest fund(env, gw, {bob}, {USD(10'000)}, Fund::Acct); ammAlice.deposit(bob, 1'000'000); BEAST_EXPECT(ammAlice.expectBalances( - XRP(12'000), USD(12'000), IOUAmount{12'000'000, 0})); + XRPAmount{12'000'000'001}, + USD(12'000), + IOUAmount{12'000'000, 0})); // Initial state. Pay bidMin. env(ammAlice.bid({.account = carol, .bidMin = 110})).close(); @@ -2949,7 +3075,9 @@ struct AMM_test : public jtx::AMMTest 0, std::nullopt, IOUAmount{110})); // ~321.09 tokens burnt on bidding fees. BEAST_EXPECT(ammAlice.expectBalances( - XRP(12'000), USD(12'000), IOUAmount{11'999'678'91, -2})); + XRPAmount{12'000'000'001}, + USD(12'000), + IOUAmount{11'999'678'91, -2})); }, std::nullopt, 0, @@ -2979,7 +3107,7 @@ struct AMM_test : public jtx::AMMTest ammTokens -= slotPrice; BEAST_EXPECT(ammAlice.expectAuctionSlot(100, 0, slotPrice)); BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), USD(13'000), ammTokens)); + XRPAmount{13'000'000'003}, USD(13'000), ammTokens)); // Discounted trade for (int i = 0; i < 10; ++i) { @@ -3002,10 +3130,9 @@ struct AMM_test : public jtx::AMMTest STAmount(USD, UINT64_C(18'999'0057261184), -10)); // USD pool is slightly higher because of the fees. BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), + XRPAmount{13'000'000'003}, STAmount(USD, UINT64_C(13'002'98282151422), -11), ammTokens)); - ammTokens = ammAlice.getLPTokensBalance(); // Trade with the fee for (int i = 0; i < 10; ++i) @@ -3016,30 +3143,30 @@ struct AMM_test : public jtx::AMMTest // dan pays ~9.94USD, which is ~10 times more in fees than // carol, bob, ed. the discounted fee is 10 times less // than the trading fee. + BEAST_EXPECT( env.balance(dan, USD) == - STAmount(USD, UINT64_C(19'490'05672274399), -11)); + STAmount(USD, UINT64_C(19'490'05672274398), -11)); // USD pool gains more in dan's fees. BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount{USD, UINT64_C(13'012'92609877023), -11}, + XRPAmount{13'000'000'003}, + STAmount{USD, UINT64_C(13'012'92609877024), -11}, ammTokens)); // Discounted fee payment ammAlice.deposit(carol, USD(100)); ammTokens = ammAlice.getLPTokensBalance(); BEAST_EXPECT(ammAlice.expectBalances( - XRP(13'000), - STAmount{USD, UINT64_C(13'112'92609877023), -11}, + XRPAmount{13'000'000'003}, + STAmount{USD, UINT64_C(13'112'92609877024), -11}, ammTokens)); env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); env.close(); // carol pays 100000 drops in fees // 99900668XRP swapped in for 100USD BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'100'000'668}, - STAmount{USD, UINT64_C(13'012'92609877023), -11}, + XRPAmount{13'100'000'671}, + STAmount{USD, UINT64_C(13'012'92609877024), -11}, ammTokens)); - // Payment with the trading fee env(pay(alice, carol, XRP(100)), path(~XRP), sendmax(USD(110))); env.close(); @@ -3047,17 +3174,14 @@ struct AMM_test : public jtx::AMMTest // than carol's fee // 100.099431529USD swapped in for 100XRP BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'000'000'668}, - STAmount{USD, UINT64_C(13'114'03663047269), -11}, + XRPAmount{13'000'000'671}, + STAmount{USD, UINT64_C(13'114'03663044937), -11}, ammTokens)); // Auction slot expired, no discounted fee env.close(seconds(TOTAL_TIME_SLOT_SECS + 1)); // clock is parent's based env.close(); - BEAST_EXPECT( - env.balance(carol, USD) == - STAmount(USD, UINT64_C(29'399'00572620544), -11)); ammTokens = ammAlice.getLPTokensBalance(); for (int i = 0; i < 10; ++i) { @@ -3068,10 +3192,10 @@ struct AMM_test : public jtx::AMMTest // trading fees vs discounted fee. BEAST_EXPECT( env.balance(carol, USD) == - STAmount(USD, UINT64_C(29'389'06197177124), -11)); + STAmount(USD, UINT64_C(29'389'06197177129), -11)); BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount{13'000'000'668}, - STAmount{USD, UINT64_C(13'123'98038490689), -11}, + XRPAmount{13'000'000'671}, + STAmount{USD, UINT64_C(13'123'98038488352), -11}, ammTokens)); env(pay(carol, bob, USD(100)), path(~USD), sendmax(XRP(110))); @@ -3080,8 +3204,8 @@ struct AMM_test : public jtx::AMMTest // ~10 times more than the discounted fee. // 99.815876XRP is swapped in for 100USD BEAST_EXPECT(ammAlice.expectBalances( - XRPAmount(13'100'824'790), - STAmount{USD, UINT64_C(13'023'98038490689), -11}, + XRPAmount(13'100'824'793), + STAmount{USD, UINT64_C(13'023'98038488352), -11}, ammTokens)); }, std::nullopt, @@ -4187,8 +4311,9 @@ struct AMM_test : public jtx::AMMTest // Offer crossing with AMM LPTokens and XRP. testAMM([&](AMM& ammAlice, Env& env) { + auto const baseFee = env.current()->fees().base.drops(); auto const token1 = ammAlice.lptIssue(); - auto priceXRP = withdrawByTokens( + auto priceXRP = ammAssetOut( STAmount{XRPAmount{10'000'000'000}}, STAmount{token1, 10'000'000}, STAmount{token1, 5'000'000}, @@ -4212,8 +4337,10 @@ struct AMM_test : public jtx::AMMTest env(ammAlice.bid({.account = carol, .bidMin = 100})); BEAST_EXPECT(ammAlice.expectLPTokens(carol, IOUAmount{4'999'900})); BEAST_EXPECT(ammAlice.expectAuctionSlot(0, 0, IOUAmount{100})); - BEAST_EXPECT(accountBalance(env, carol) == "22499999960"); - priceXRP = withdrawByTokens( + BEAST_EXPECT( + accountBalance(env, carol) == + std::to_string(22500000000 - 4 * baseFee)); + priceXRP = ammAssetOut( STAmount{XRPAmount{10'000'000'000}}, STAmount{token1, 9'999'900}, STAmount{token1, 4'999'900}, @@ -4561,7 +4688,7 @@ struct AMM_test : public jtx::AMMTest carol, USD(100), std::nullopt, IOUAmount{520, 0}); // carol withdraws ~1,443.44USD auto const balanceAfterWithdraw = [&]() { - return STAmount(USD, UINT64_C(30'443'43891402714), -11); + return STAmount(USD, UINT64_C(30'443'43891402713), -11); }(); BEAST_EXPECT(env.balance(carol, USD) == balanceAfterWithdraw); // Set to original pool size @@ -4573,10 +4700,10 @@ struct AMM_test : public jtx::AMMTest auto const tokensNoFee = ammAlice.withdraw(carol, deposit); BEAST_EXPECT( env.balance(carol, USD) == - STAmount(USD, UINT64_C(30'443'43891402716), -11)); + STAmount(USD, UINT64_C(30'443'43891402713), -11)); // carol pays ~4008 LPTokens in fees or ~0.5% of the no-fee // LPTokens - BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779912, -8)); + BEAST_EXPECT(tokensNoFee == IOUAmount(746'579'80779911, -8)); BEAST_EXPECT(tokensFee == IOUAmount(750'588'23529411, -8)); }, std::nullopt, @@ -4839,7 +4966,9 @@ struct AMM_test : public jtx::AMMTest // other have a tiny loss. The last account to withdraw // gets everything in the pool. BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), IOUAmount{10'000'000})); + XRP(10'000), + STAmount{USD, UINT64_C(10'000'0000000003), -10}, + IOUAmount{10'000'000})); BEAST_EXPECT(expectLine(env, ben, USD(1'500'000))); BEAST_EXPECT(expectLine(env, simon, USD(1'500'000))); BEAST_EXPECT(expectLine(env, chris, USD(1'500'000))); @@ -4847,13 +4976,13 @@ struct AMM_test : public jtx::AMMTest BEAST_EXPECT(expectLine(env, carol, USD(30'000))); BEAST_EXPECT(expectLine(env, ed, USD(1'500'000))); BEAST_EXPECT(expectLine(env, paul, USD(1'500'000))); - BEAST_EXPECT(expectLine( - env, - nataly, - STAmount{USD, UINT64_C(1'500'000'000000005), -9})); + BEAST_EXPECT(expectLine(env, nataly, USD(1'500'000))); ammAlice.withdrawAll(alice); BEAST_EXPECT(!ammAlice.ammExists()); - BEAST_EXPECT(expectLine(env, alice, USD(30'000))); + BEAST_EXPECT(expectLine( + env, + alice, + STAmount{USD, UINT64_C(30'000'0000000003), -10})); // alice XRP balance is 30,000initial - 50 ammcreate fee - // 10drops fee BEAST_EXPECT(accountBalance(env, alice) == "29949999990"); @@ -4864,62 +4993,80 @@ struct AMM_test : public jtx::AMMTest {features}); // Same as above but deposit/withdraw in XRP - testAMM([&](AMM& ammAlice, Env& env) { - Account const bob("bob"); - Account const ed("ed"); - Account const paul("paul"); - Account const dan("dan"); - Account const chris("chris"); - Account const simon("simon"); - Account const ben("ben"); - Account const nataly("nataly"); - fund( - env, - gw, - {bob, ed, paul, dan, chris, simon, ben, nataly}, - XRP(2'000'000), - {}, - Fund::Acct); - for (int i = 0; i < 10; ++i) - { - ammAlice.deposit(ben, XRPAmount{1}); - ammAlice.withdrawAll(ben, XRP(0)); - ammAlice.deposit(simon, XRPAmount(1'000)); - ammAlice.withdrawAll(simon, XRP(0)); - ammAlice.deposit(chris, XRP(1)); - ammAlice.withdrawAll(chris, XRP(0)); - ammAlice.deposit(dan, XRP(10)); - ammAlice.withdrawAll(dan, XRP(0)); - ammAlice.deposit(bob, XRP(100)); - ammAlice.withdrawAll(bob, XRP(0)); - ammAlice.deposit(carol, XRP(1'000)); - ammAlice.withdrawAll(carol, XRP(0)); - ammAlice.deposit(ed, XRP(10'000)); - ammAlice.withdrawAll(ed, XRP(0)); - ammAlice.deposit(paul, XRP(100'000)); - ammAlice.withdrawAll(paul, XRP(0)); - ammAlice.deposit(nataly, XRP(1'000'000)); - ammAlice.withdrawAll(nataly, XRP(0)); - } - // No round off with XRP in this test - BEAST_EXPECT(ammAlice.expectBalances( - XRP(10'000), USD(10'000), IOUAmount{10'000'000})); - ammAlice.withdrawAll(alice); - BEAST_EXPECT(!ammAlice.ammExists()); - // 20,000 initial - (deposit+withdraw) * 10 - auto const xrpBalance = (XRP(2'000'000) - txfee(env, 20)).getText(); - BEAST_EXPECT(accountBalance(env, ben) == xrpBalance); - BEAST_EXPECT(accountBalance(env, simon) == xrpBalance); - BEAST_EXPECT(accountBalance(env, chris) == xrpBalance); - BEAST_EXPECT(accountBalance(env, dan) == xrpBalance); - // 30,000 initial - (deposit+withdraw) * 10 - BEAST_EXPECT(accountBalance(env, carol) == "29999999800"); - BEAST_EXPECT(accountBalance(env, ed) == xrpBalance); - BEAST_EXPECT(accountBalance(env, paul) == xrpBalance); - BEAST_EXPECT(accountBalance(env, nataly) == xrpBalance); - // 30,000 initial - 50 ammcreate fee - 10drops withdraw fee - BEAST_EXPECT(accountBalance(env, alice) == "29949999990"); - }); + testAMM( + [&](AMM& ammAlice, Env& env) { + Account const bob("bob"); + Account const ed("ed"); + Account const paul("paul"); + Account const dan("dan"); + Account const chris("chris"); + Account const simon("simon"); + Account const ben("ben"); + Account const nataly("nataly"); + fund( + env, + gw, + {bob, ed, paul, dan, chris, simon, ben, nataly}, + XRP(2'000'000), + {}, + Fund::Acct); + for (int i = 0; i < 10; ++i) + { + ammAlice.deposit(ben, XRPAmount{1}); + ammAlice.withdrawAll(ben, XRP(0)); + ammAlice.deposit(simon, XRPAmount(1'000)); + ammAlice.withdrawAll(simon, XRP(0)); + ammAlice.deposit(chris, XRP(1)); + ammAlice.withdrawAll(chris, XRP(0)); + ammAlice.deposit(dan, XRP(10)); + ammAlice.withdrawAll(dan, XRP(0)); + ammAlice.deposit(bob, XRP(100)); + ammAlice.withdrawAll(bob, XRP(0)); + ammAlice.deposit(carol, XRP(1'000)); + ammAlice.withdrawAll(carol, XRP(0)); + ammAlice.deposit(ed, XRP(10'000)); + ammAlice.withdrawAll(ed, XRP(0)); + ammAlice.deposit(paul, XRP(100'000)); + ammAlice.withdrawAll(paul, XRP(0)); + ammAlice.deposit(nataly, XRP(1'000'000)); + ammAlice.withdrawAll(nataly, XRP(0)); + } + auto const baseFee = env.current()->fees().base.drops(); + // post-amendment the rounding takes place to ensure + // AMM invariant + BEAST_EXPECT(ammAlice.expectBalances( + XRPAmount(10'000'000'080), + USD(10'000), + IOUAmount{10'000'000})); + ammAlice.withdrawAll(alice); + BEAST_EXPECT(!ammAlice.ammExists()); + auto const xrpBalance = + XRP(2'000'000) - txfee(env, 20) - drops(10); + auto const xrpBalanceText = xrpBalance.getText(); + BEAST_EXPECT(accountBalance(env, ben) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, simon) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, chris) == xrpBalanceText); + BEAST_EXPECT(accountBalance(env, dan) == xrpBalanceText); + BEAST_EXPECT( + accountBalance(env, carol) == + std::to_string(30'000'000'000 - 20 * baseFee - 10)); + BEAST_EXPECT( + accountBalance(env, ed) == + (xrpBalance + drops(2)).getText()); + BEAST_EXPECT( + accountBalance(env, paul) == + (xrpBalance + drops(3)).getText()); + BEAST_EXPECT( + accountBalance(env, nataly) == + (xrpBalance + drops(5)).getText()); + BEAST_EXPECT( + accountBalance(env, alice) == + std::to_string(29'950'000'000 - baseFee + 80)); + }, + std::nullopt, + 0, + std::nullopt, + {features}); } void @@ -5804,11 +5951,11 @@ struct AMM_test : public jtx::AMMTest } void - testFixOverflowOffer(FeatureBitset features) + testFixOverflowOffer(FeatureBitset featuresInitial) { using namespace jtx; using namespace std::chrono; - FeatureBitset const all{features}; + FeatureBitset const all{featuresInitial}; Account const gatehub{"gatehub"}; Account const bitstamp{"bitstamp"}; @@ -5833,6 +5980,7 @@ struct AMM_test : public jtx::AMMTest STAmount const goodUsdBIT; STAmount const goodUsdBITr; IOUAmount const lpTokenBalance; + std::optional const lpTokenBalanceAlt = {}; double const offer1BtcGH = 0.1; double const offer2BtcGH = 0.1; double const offer2UsdGH = 1; @@ -5858,6 +6006,7 @@ struct AMM_test : public jtx::AMMTest .goodUsdBIT{usdBIT, uint64_t(8'464739069120721), -15}, // .goodUsdBITr{usdBIT, uint64_t(8'464739069098152), -15}, // .lpTokenBalance = {28'61817604250837, -14}, // + .lpTokenBalanceAlt = IOUAmount{28'61817604250836, -14}, // .offer1BtcGH = 0.1, // .offer2BtcGH = 0.1, // .offer2UsdGH = 1, // @@ -6035,72 +6184,90 @@ struct AMM_test : public jtx::AMMTest }) { testcase(input.testCase); + for (auto const& features : {all}) + { + // Env env(*this, features, + // std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + + env.fund(XRP(5'000), gatehub, bitstamp, trader); + env.close(); - Env env( - *this, envconfig(), all, nullptr, beast::severities::kDisabled); + if (input.rateGH != 0.0) + env(rate(gatehub, input.rateGH)); + if (input.rateBIT != 0.0) + env(rate(bitstamp, input.rateBIT)); - env.fund(XRP(5'000), gatehub, bitstamp, trader); - env.close(); - - if (input.rateGH != 0.0) - env(rate(gatehub, input.rateGH)); - if (input.rateBIT != 0.0) - env(rate(bitstamp, input.rateBIT)); + env(trust(trader, usdGH(10'000'000))); + env(trust(trader, usdBIT(10'000'000))); + env(trust(trader, btcGH(10'000'000))); + env.close(); - env(trust(trader, usdGH(10'000'000))); - env(trust(trader, usdBIT(10'000'000))); - env(trust(trader, btcGH(10'000'000))); - env.close(); + env(pay(gatehub, trader, usdGH(100'000))); + env(pay(gatehub, trader, btcGH(100'000))); + env(pay(bitstamp, trader, usdBIT(100'000))); + env.close(); - env(pay(gatehub, trader, usdGH(100'000))); - env(pay(gatehub, trader, btcGH(100'000))); - env(pay(bitstamp, trader, usdBIT(100'000))); - env.close(); + AMM amm{ + env, + trader, + usdGH(input.poolUsdGH), + usdBIT(input.poolUsdBIT)}; + env.close(); - AMM amm{ - env, trader, usdGH(input.poolUsdGH), usdBIT(input.poolUsdBIT)}; - env.close(); + IOUAmount const preSwapLPTokenBalance = + amm.getLPTokensBalance(); - IOUAmount const preSwapLPTokenBalance = amm.getLPTokensBalance(); + env(offer(trader, usdBIT(1), btcGH(input.offer1BtcGH))); + env(offer( + trader, + btcGH(input.offer2BtcGH), + usdGH(input.offer2UsdGH))); + env.close(); - env(offer(trader, usdBIT(1), btcGH(input.offer1BtcGH))); - env(offer( - trader, btcGH(input.offer2BtcGH), usdGH(input.offer2UsdGH))); - env.close(); + env(pay(trader, trader, input.sendUsdGH), + path(~usdGH), + path(~btcGH, ~usdGH), + sendmax(input.sendMaxUsdBIT), + txflags(tfPartialPayment)); + env.close(); - env(pay(trader, trader, input.sendUsdGH), - path(~usdGH), - path(~btcGH, ~usdGH), - sendmax(input.sendMaxUsdBIT), - txflags(tfPartialPayment)); - env.close(); + auto const failUsdGH = input.failUsdGHr; + auto const failUsdBIT = input.failUsdBITr; + auto const goodUsdGH = input.goodUsdGHr; + auto const goodUsdBIT = input.goodUsdBITr; - auto const failUsdGH = input.failUsdGHr; - auto const failUsdBIT = input.failUsdBITr; - auto const goodUsdGH = input.goodUsdGHr; - auto const goodUsdBIT = input.goodUsdBITr; + auto const lpTokenBalance = input.lpTokenBalanceAlt + ? *input.lpTokenBalanceAlt + : input.lpTokenBalance; - BEAST_EXPECT(amm.expectBalances( - goodUsdGH, goodUsdBIT, input.lpTokenBalance)); - - // Invariant: LPToken balance must not change in a - // payment or a swap transaction - BEAST_EXPECT(amm.getLPTokensBalance() == preSwapLPTokenBalance); - - // Invariant: The square root of (product of the pool - // balances) must be at least the LPTokenBalance - Number const sqrtPoolProduct = root2(goodUsdGH * goodUsdBIT); - - // Include a tiny tolerance for the test cases using - // .goodUsdGH{usdGH, uint64_t(35'44113971506987), - // -14}, .goodUsdBIT{usdBIT, - // uint64_t(2'821579689703915), -15}, - // These two values multiply - // to 99.99999999999994227040383754105 which gets - // internally rounded to 100, due to representation - // error. - BEAST_EXPECT( - (sqrtPoolProduct + Number{1, -14} >= input.lpTokenBalance)); + BEAST_EXPECT( + amm.expectBalances(goodUsdGH, goodUsdBIT, lpTokenBalance)); + + // Invariant: LPToken balance must not change in a + // payment or a swap transaction + BEAST_EXPECT(amm.getLPTokensBalance() == preSwapLPTokenBalance); + + // Invariant: The square root of (product of the pool + // balances) must be at least the LPTokenBalance + Number const sqrtPoolProduct = root2(goodUsdGH * goodUsdBIT); + + // Include a tiny tolerance for the test cases using + // .goodUsdGH{usdGH, uint64_t(35'44113971506987), + // -14}, .goodUsdBIT{usdBIT, + // uint64_t(2'821579689703915), -15}, + // These two values multiply + // to 99.99999999999994227040383754105 which gets + // internally rounded to 100, due to representation + // error. + BEAST_EXPECT( + (sqrtPoolProduct + Number{1, -14} >= input.lpTokenBalance)); + } } } @@ -6246,11 +6413,19 @@ struct AMM_test : public jtx::AMMTest void testLPTokenBalance(FeatureBitset features) { + testcase("LPToken Balance"); using namespace jtx; // Last Liquidity Provider is the issuer of one token { - Env env(*this, features); + std::string logs; + // Env env(*this, features, std::make_unique(&logs)); + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); fund( env, gw, @@ -6261,7 +6436,9 @@ struct AMM_test : public jtx::AMMTest amm.deposit(alice, IOUAmount{1'876123487565916, -15}); amm.deposit(carol, IOUAmount{1'000'000}); amm.withdrawAll(alice); + BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount{0})); amm.withdrawAll(carol); + BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount{0})); auto const lpToken = getAccountLines( env, gw, amm.lptIssue())[jss::lines][0u][jss::balance]; auto const lpTokenBalance = @@ -6522,6 +6699,425 @@ struct AMM_test : public jtx::AMMTest }); } + // void + // testFailedPseudoAccount() + // { + // using namespace test::jtx; + + // auto const testCase = [&](std::string suffix, + // FeatureBitset features) { + // testcase("Failed pseudo-account allocation " + suffix); + // std::string logs; + // Env env{ + // *this, features, std::make_unique(&logs)}; + // env.fund(XRP(30'000), gw, alice); + // env.close(); + // env(trust(alice, gw["USD"](30'000), 0)); + // env(pay(gw, alice, USD(10'000))); + // env.close(); + + // STAmount amount = XRP(10'000); + // STAmount amount2 = USD(10'000); + // auto const keylet = + // keylet::amm(amount.issue(), amount2.issue()); + // for (int i = 0; i < 256; ++i) + // { + // AccountID const accountId = + // ripple::pseudoAccountAddress( + // *env.current(), keylet.key); + + // env(pay(env.master.id(), accountId, XRP(1000)), + // seq(autofill), + // fee(autofill), + // sig(autofill)); + // } + + // AMM ammAlice( + // env, + // alice, + // amount, + // amount2, + // features[featureSingleAssetVault] + // ? ter{terADDRESS_COLLISION} + // : ter{tecDUPLICATE}); + // }; + + // testCase( + // "tecDUPLICATE", + // supported_amendments() - featureSingleAssetVault); + // testCase( + // "terADDRESS_COLLISION", + // supported_amendments() | featureSingleAssetVault); + // } + + void + testDepositAndWithdrawRounding(FeatureBitset features) + { + testcase("Deposit and Withdraw Rounding V2"); + using namespace jtx; + + auto const XPM = gw["XPM"]; + STAmount xrpBalance{XRPAmount(692'614'492'126)}; + STAmount xpmBalance{XPM, UINT64_C(18'610'359'80246901), -8}; + STAmount amount{XPM, UINT64_C(6'566'496939465400), -12}; + std::uint16_t tfee = 941; + + auto test = [&](auto&& cb, std::uint16_t tfee_) { + Env env(*this, features); + env.fund(XRP(1'000'000), gw); + env.fund(XRP(1'000), alice); + env(trust(alice, XPM(7'000))); + env(pay(gw, alice, amount)); + + AMM amm(env, gw, xrpBalance, xpmBalance, CreateArg{.tfee = tfee_}); + // AMM LPToken balance required to replicate single deposit + // failure + STAmount lptAMMBalance{ + amm.lptIssue(), UINT64_C(3'234'987'266'485968), -6}; + auto const burn = + IOUAmount{amm.getLPTokensBalance() - lptAMMBalance}; + // burn tokens to get to the required AMM state + env(amm.bid(BidArg{.account = gw, .bidMin = burn, .bidMax = burn})); + cb(amm, env); + }; + test( + [&](AMM& amm, Env& env) { + auto const err = ter(tesSUCCESS); + amm.deposit(DepositArg{ + .account = alice, .asset1In = amount, .err = err}); + }, + tfee); + test( + [&](AMM& amm, Env& env) { + auto const [amount, amount2, lptAMM] = amm.balances(XRP, XPM); + auto const withdraw = STAmount{XPM, 1, -5}; + amm.withdraw(WithdrawArg{.asset1Out = STAmount{XPM, 1, -5}}); + auto const [amount_, amount2_, lptAMM_] = + amm.balances(XRP, XPM); + BEAST_EXPECT((amount2 - amount2_) <= withdraw); + }, + 0); + } + + void + invariant( + jtx::AMM& amm, + jtx::Env& env, + std::string const& msg, + bool shouldFail) + { + auto const [amount, amount2, lptBalance] = amm.balances(GBP, EUR); + + NumberRoundModeGuard g(Number::upward); + auto const res = root2(amount * amount2); + + if (shouldFail) + BEAST_EXPECT(res < lptBalance); + else + BEAST_EXPECT(res >= lptBalance); + } + + void + testDepositRounding(FeatureBitset all) + { + testcase("Deposit Rounding"); + using namespace jtx; + + // Single asset deposit + for (auto const& deposit : + {STAmount(EUR, 1, 1), + STAmount(EUR, 1, 2), + STAmount(EUR, 1, 5), + STAmount(EUR, 1, -3), // fail + STAmount(EUR, 1, -6), + STAmount(EUR, 1, -9)}) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit( + DepositArg{.account = bob, .asset1In = deposit}); + invariant( + ammAlice, + env, + "dep1", + deposit == STAmount{EUR, 1, -3} && + !true /*env.enabled(fixAMMv1_3)*/); + }, + {{GBP(30'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + // Two-asset proportional deposit (1:1 pool ratio) + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + STAmount const depositEuro{ + EUR, UINT64_C(10'1234567890123456), -16}; + STAmount const depositGBP{ + GBP, UINT64_C(10'1234567890123456), -16}; + + ammAlice.deposit(DepositArg{ + .account = bob, + .asset1In = depositEuro, + .asset2In = depositGBP}); + invariant(ammAlice, env, "dep2", false); + }, + {{GBP(30'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // Two-asset proportional deposit (1:3 pool ratio) + for (auto const& exponent : {1, 2, 3, 4, -3 /*fail*/, -6, -9}) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + STAmount const depositEuro{EUR, 1, exponent}; + STAmount const depositGBP{GBP, 1, exponent}; + + ammAlice.deposit(DepositArg{ + .account = bob, + .asset1In = depositEuro, + .asset2In = depositGBP}); + invariant( + ammAlice, + env, + "dep3", + exponent != -3 && !true /*env.enabled(fixAMMv1_3)*/); + }, + {{GBP(10'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + // tfLPToken deposit + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit(DepositArg{ + .account = bob, + .tokens = IOUAmount{10'1234567890123456, -16}}); + invariant(ammAlice, env, "dep4", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfOneAssetLPToken deposit + for (auto const& tokens : + {IOUAmount{1, -3}, + IOUAmount{1, -2}, + IOUAmount{1, -1}, + IOUAmount{1}, + IOUAmount{10}, + IOUAmount{100}, + IOUAmount{1'000}, + IOUAmount{10'000}}) + { + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(1'000'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit(DepositArg{ + .account = bob, + .tokens = tokens, + .asset1In = STAmount{EUR, 1, 6}}); + invariant(ammAlice, env, "dep5", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + // Single deposit with EP not exceeding specified: + // 1'000 GBP with EP not to exceed 5 (GBP/TokensOut) + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit( + bob, GBP(1'000), std::nullopt, STAmount{GBP, 5}); + invariant(ammAlice, env, "dep6", false); + }, + {{GBP(30'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + + void + testWithdrawRounding(FeatureBitset all) + { + testcase("Withdraw Rounding"); + + using namespace jtx; + + // tfLPToken mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(alice, 1'000); + invariant(ammAlice, env, "with1", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfWithdrawAll mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw( + WithdrawArg{.account = alice, .flags = tfWithdrawAll}); + invariant(ammAlice, env, "with2", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfTwoAsset withdraw mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 3'500}, + .asset2Out = STAmount{EUR, 15'000}, + .flags = tfTwoAsset}); + invariant(ammAlice, env, "with3", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfSingleAsset withdraw mode + // Note: This test fails with 0 trading fees, but doesn't fail + // if trading fees is set to 1'000 -- I suspect the compound + // operations in AMMHelpers.cpp:withdrawByTokens compensate for + // the rounding errors + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 1'234}, + .flags = tfSingleAsset}); + invariant(ammAlice, env, "with4", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfOneAssetWithdrawAll mode + testAMM( + [&](AMM& ammAlice, Env& env) { + fund( + env, + gw, + {bob}, + XRP(10'000'000), + {GBP(100'000), EUR(100'000)}, + Fund::Acct); + env.close(); + + ammAlice.deposit(DepositArg{ + .account = bob, .asset1In = STAmount{GBP, 3'456}}); + + ammAlice.withdraw(WithdrawArg{ + .account = bob, + .asset1Out = STAmount{GBP, 1'000}, + .flags = tfOneAssetWithdrawAll}); + invariant(ammAlice, env, "with5", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfOneAssetLPToken mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .tokens = 1'000, + .asset1Out = STAmount{GBP, 100}, + .flags = tfOneAssetLPToken}); + invariant(ammAlice, env, "with6", false); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + + // tfLimitLPToken mode + testAMM( + [&](AMM& ammAlice, Env& env) { + ammAlice.withdraw(WithdrawArg{ + .account = alice, + .asset1Out = STAmount{GBP, 100}, + .maxEP = IOUAmount{2}, + .flags = tfLimitLPToken}); + invariant(ammAlice, env, "with7", true); + }, + {{GBP(7'000), EUR(30'000)}}, + 0, + std::nullopt, + {all}); + } + void run() override { @@ -6563,6 +7159,10 @@ struct AMM_test : public jtx::AMMTest testAMMDepositWithFrozenAssets(all); testAMMDepositWithFrozenAssets(all - featureAMMClawback); testFixReserveCheckOnWithdrawal(all); + testDepositAndWithdrawRounding(all); + testDepositRounding(all); + testWithdrawRounding(all); + // testFailedPseudoAccount(); } }; diff --git a/src/test/app/BaseFee_test.cpp b/src/test/app/BaseFee_test.cpp index b6600e40f0..97de936315 100644 --- a/src/test/app/BaseFee_test.cpp +++ b/src/test/app/BaseFee_test.cpp @@ -582,9 +582,9 @@ class BaseFee_test : public beast::unit_test::suite } void - testSignersListSet(FeatureBitset features) + testSignerListSet(FeatureBitset features) { - testcase("signers list set w/ hook params"); + testcase("signer list set w/ hook params"); using namespace test::jtx; using namespace std::literals; @@ -810,7 +810,7 @@ class BaseFee_test : public beast::unit_test::suite testPaymentChannelFund(features); testSetHook(features); testSetRegularKey(features); - testSignersListSet(features); + testSignerListSet(features); testTicketCreate(features); testTrustSet(features); testURITokenBurnFee(features); diff --git a/src/test/app/Import_json.h b/src/test/app/Import_json.h index 37cd6363ff..22982ea01b 100644 --- a/src/test/app/Import_json.h +++ b/src/test/app/Import_json.h @@ -879,7 +879,7 @@ inline std::string ImportTCSetRegularKey::w_signers = R"json({ } })json"; -class ImportTCSignersListSet +class ImportTCSignerListSet { public: static std::string w_seed_bad_fee; @@ -891,7 +891,7 @@ class ImportTCSignersListSet static std::string w_signers_empty; }; -inline std::string ImportTCSignersListSet::w_seed_bad_fee = R"json({ +inline std::string ImportTCSignerListSet::w_seed_bad_fee = R"json({ "ledger": { "acroot": "64F75A08037D9F8ED8A103893401EB2AD726E7D6AAC3EAA249005916A9354892", "close": 743008501, @@ -952,7 +952,7 @@ inline std::string ImportTCSignersListSet::w_seed_bad_fee = R"json({ } } })json"; -inline std::string ImportTCSignersListSet::w_seed = R"json({ +inline std::string ImportTCSignerListSet::w_seed = R"json({ "ledger": { "acroot": "8112FF5F3FEEA34894A16CCCD64A24D552521F2E699780A587A9E6F5F5117CE5", "close": 743008510, @@ -993,7 +993,7 @@ inline std::string ImportTCSignersListSet::w_seed = R"json({ } } })json"; -inline std::string ImportTCSignersListSet::w_regular_key = R"json({ +inline std::string ImportTCSignerListSet::w_regular_key = R"json({ "ledger": { "acroot": "2A25CA219781A3144C72FD5FB6EB62763214E050050DA6176624A046C51EECBD", "close": 743015350, @@ -1034,7 +1034,7 @@ inline std::string ImportTCSignersListSet::w_regular_key = R"json({ } } })json"; -inline std::string ImportTCSignersListSet::w_signers = R"json({ +inline std::string ImportTCSignerListSet::w_signers = R"json({ "ledger": { "acroot": "BC35E65B52724CF258BDAC8B8E0D3B9CA0F012F5B243F6AAD1B671EDABD5188E", "close": 745594953, @@ -1075,7 +1075,7 @@ inline std::string ImportTCSignersListSet::w_signers = R"json({ } } })json"; -inline std::string ImportTCSignersListSet::w_seed_empty = R"json({ +inline std::string ImportTCSignerListSet::w_seed_empty = R"json({ "ledger": { "acroot": "ECCAFDE52A6D5F1E36EB82EAA5247FF1D8ADE51FCF1ED0842850193018A510F7", "close": 743056482, @@ -1116,7 +1116,7 @@ inline std::string ImportTCSignersListSet::w_seed_empty = R"json({ } } })json"; -inline std::string ImportTCSignersListSet::w_regular_key_empty = R"json({ +inline std::string ImportTCSignerListSet::w_regular_key_empty = R"json({ "ledger": { "acroot": "E222F46D5F35C79FDA3BB98973E2024EF9F6FA7B26471CC9CEF2CE033FA0E6E7", "close": 743169800, @@ -1157,7 +1157,7 @@ inline std::string ImportTCSignersListSet::w_regular_key_empty = R"json({ } } })json"; -inline std::string ImportTCSignersListSet::w_signers_empty = R"json({ +inline std::string ImportTCSignerListSet::w_signers_empty = R"json({ "ledger": { "acroot": "987438A87AD998B7D7ED04A280FB5414C76E8475D621A55FB8463F15CEEEAD49", "close": 743172592, @@ -1261,4 +1261,4 @@ inline std::string ImportTCHalving::base_genesis = R"json({ } // namespace test } // namespace ripple -#endif \ No newline at end of file +#endif diff --git a/src/test/app/Import_test.cpp b/src/test/app/Import_test.cpp index f9fb090bec..1a65de4865 100644 --- a/src/test/app/Import_test.cpp +++ b/src/test/app/Import_test.cpp @@ -1898,7 +1898,7 @@ class Import_test : public beast::unit_test::suite // different keys. { auto const xpopJson = - import::loadXpop(ImportTCSignersListSet::w_signers); + import::loadXpop(ImportTCSignerListSet::w_signers); env(import::import(alice, xpopJson), msig(bob, dave), fee((3 * feeDrops) * 10), @@ -1910,7 +1910,7 @@ class Import_test : public beast::unit_test::suite // different keys. - empty innerSigners { Json::Value xpopJson = - import::loadXpop(ImportTCSignersListSet::w_signers); + import::loadXpop(ImportTCSignerListSet::w_signers); xpopJson[jss::transaction][jss::blob] = "12000C22000000002400000014201B0000002B201D00005359202300000002" "6840000000001E84B073008114AE123A8556F3CF91154711376AFB0F894F83" @@ -1927,7 +1927,7 @@ class Import_test : public beast::unit_test::suite // different keys. { Json::Value xpopJson = - import::loadXpop(ImportTCSignersListSet::w_signers); + import::loadXpop(ImportTCSignerListSet::w_signers); xpopJson[jss::transaction][jss::blob] = "12000C22000000002400000014201B0000002B201D00005359202300000002" "6840000000001E84B073008114AE123A8556F3CF91154711376AFB0F894F83" @@ -1953,7 +1953,7 @@ class Import_test : public beast::unit_test::suite // temMALFORMED - Import: inner txn signature verify failed { Json::Value xpopJson = - import::loadXpop(ImportTCSignersListSet::w_signers); + import::loadXpop(ImportTCSignerListSet::w_signers); xpopJson[jss::transaction][jss::blob] = "12000C2200000008240000001A201B000003B9201D00005359202300000000" "6840000000001E84B073008114AE123A8556F3CF91154711376AFB0F894F83" @@ -2768,7 +2768,7 @@ class Import_test : public beast::unit_test::suite env.close(); } - // tefIMPORT_BLACKHOLED - SignersListSet (w/seed) + // tefIMPORT_BLACKHOLED - SignerListSet (w/seed) { test::jtx::Env env{ *this, network::makeNetworkVLConfig(21337, keys)}; @@ -2792,7 +2792,7 @@ class Import_test : public beast::unit_test::suite // Import with Master Key Json::Value tmpXpop = - import::loadXpop(ImportTCSignersListSet::w_seed); + import::loadXpop(ImportTCSignerListSet::w_seed); env(import::import(alice, tmpXpop), ter(tefIMPORT_BLACKHOLED), fee(feeDrops * 10), @@ -3244,7 +3244,7 @@ class Import_test : public beast::unit_test::suite env(noop(alice), sig(bob), fee(feeDrops), ter(tefBAD_AUTH)); } - // w/ signers list -> dne + // w/ signer list -> dne { test::jtx::Env env{ *this, network::makeNetworkVLConfig(21337, keys)}; @@ -3975,7 +3975,7 @@ class Import_test : public beast::unit_test::suite env(noop(alice), sig(carol), fee(feeDrops), ter(tesSUCCESS)); } - // w/ signers list -> funded (update regular key) + // w/ signer list -> funded (update regular key) { test::jtx::Env env{ *this, network::makeNetworkVLConfig(21337, keys)}; @@ -4049,7 +4049,7 @@ class Import_test : public beast::unit_test::suite BEAST_EXPECT(acctSle->getAccountID(sfRegularKey) == dave.id()); env(noop(alice), sig(dave), fee(feeDrops), ter(tesSUCCESS)); - // confirm signers list not set + // confirm signer list not set auto const k = keylet::signers(alice); BEAST_EXPECT(env.current()->read(k) == nullptr); } @@ -4351,9 +4351,9 @@ class Import_test : public beast::unit_test::suite } void - testSignersListSet(FeatureBitset features) + testSignerListSet(FeatureBitset features) { - testcase("signers list set tx"); + testcase("signer list set tx"); using namespace test::jtx; using namespace std::literals; @@ -4394,7 +4394,7 @@ class Import_test : public beast::unit_test::suite // import tx auto const xpopJson = - import::loadXpop(ImportTCSignersListSet::w_seed_bad_fee); + import::loadXpop(ImportTCSignerListSet::w_seed_bad_fee); Json::Value tx = import::import(alice, xpopJson); tx[jss::Sequence] = 0; // tx[jss::Fee] = 0; @@ -4438,7 +4438,7 @@ class Import_test : public beast::unit_test::suite // import tx auto const xpopJson = - import::loadXpop(ImportTCSignersListSet::w_seed); + import::loadXpop(ImportTCSignerListSet::w_seed); Json::Value tx = import::import(alice, xpopJson); tx[jss::Sequence] = 0; tx[jss::Fee] = 0; @@ -4523,7 +4523,7 @@ class Import_test : public beast::unit_test::suite // import tx auto const burnAmt = XRP(2); auto const xpopJson = - import::loadXpop(ImportTCSignersListSet::w_regular_key); + import::loadXpop(ImportTCSignerListSet::w_regular_key); Json::Value tx = import::import(alice, xpopJson); tx[jss::Sequence] = 0; tx[jss::Fee] = 0; @@ -4614,7 +4614,7 @@ class Import_test : public beast::unit_test::suite // import tx auto const xpopJson = - import::loadXpop(ImportTCSignersListSet::w_signers); + import::loadXpop(ImportTCSignerListSet::w_signers); Json::Value tx = import::import(alice, xpopJson); tx[jss::Sequence] = 0; tx[jss::Fee] = 0; @@ -4685,7 +4685,7 @@ class Import_test : public beast::unit_test::suite // import tx auto const xpopJson = - import::loadXpop(ImportTCSignersListSet::w_seed); + import::loadXpop(ImportTCSignerListSet::w_seed); env(import::import(alice, xpopJson), fee(feeDrops * 10), ter(tesSUCCESS)); @@ -4771,7 +4771,7 @@ class Import_test : public beast::unit_test::suite auto const envAlice = env.balance(alice); BEAST_EXPECT(envAlice == XRP(1000)); - // set the signers list + // set the signer list env(signers(alice, 2, {{bob, 1}, {carol, 1}})); env(noop(alice), msig(bob, carol), @@ -4787,7 +4787,7 @@ class Import_test : public beast::unit_test::suite // import tx auto const xpopJson = - import::loadXpop(ImportTCSignersListSet::w_seed_empty); + import::loadXpop(ImportTCSignerListSet::w_seed_empty); env(import::import(alice, xpopJson), fee(feeDrops * 10), ter(tesSUCCESS)); @@ -4852,7 +4852,7 @@ class Import_test : public beast::unit_test::suite env(noop(alice), sig(bob), fee(feeDrops), ter(tesSUCCESS)); env.close(); - // set the signers list + // set the signer list env(signers(alice, 2, {{bob, 1}, {carol, 1}})); env(noop(alice), msig(bob, carol), @@ -4868,7 +4868,7 @@ class Import_test : public beast::unit_test::suite // import tx auto const xpopJson = - import::loadXpop(ImportTCSignersListSet::w_regular_key_empty); + import::loadXpop(ImportTCSignerListSet::w_regular_key_empty); env(import::import(alice, xpopJson), fee(feeDrops * 10), sig(bob), @@ -4935,7 +4935,7 @@ class Import_test : public beast::unit_test::suite auto const envAlice = env.balance(alice); BEAST_EXPECT(envAlice == XRP(1000)); - // set the signers list + // set the signer list env(signers(alice, 2, {{bob, 1}, {carol, 1}})); env(noop(alice), msig(bob, carol), @@ -4951,7 +4951,7 @@ class Import_test : public beast::unit_test::suite // import tx auto const xpopJson = - import::loadXpop(ImportTCSignersListSet::w_signers_empty); + import::loadXpop(ImportTCSignerListSet::w_signers_empty); env(import::import(alice, xpopJson), msig(bob, carol), fee((3 * feeDrops) * 10), @@ -6228,7 +6228,7 @@ class Import_test : public beast::unit_test::suite testAccountSetFlags(features); testSetRegularKey(features); testSetRegularKeyFlags(features); - testSignersListSet(features); + testSignerListSet(features); testUsingTickets(features); testAccountIndex(features); testHookIssuer(features); diff --git a/src/test/app/SetHookTSH_test.cpp b/src/test/app/SetHookTSH_test.cpp index 425eedbafc..223a200b8d 100644 --- a/src/test/app/SetHookTSH_test.cpp +++ b/src/test/app/SetHookTSH_test.cpp @@ -4492,14 +4492,14 @@ struct SetHookTSH0_test : public beast::unit_test::suite } } - // SignersListSet + // SignerListSet // | otxn | tsh | sls | // | A | A | S | // | A | S | S | void - testSignersListSetTSH(FeatureBitset features) + testSignerListSetTSH(FeatureBitset features) { - testcase("signers list set tsh"); + testcase("signer list set tsh"); using namespace test::jtx; using namespace std::literals; @@ -4527,7 +4527,7 @@ struct SetHookTSH0_test : public beast::unit_test::suite // set tsh hook setTSHHook(env, account, testStrong); - // signers list set + // signer list set env(signers(account, 2, {{signer1, 1}, {signer2, 1}}), fee(XRP(1)), ter(tesSUCCESS)); @@ -4566,7 +4566,7 @@ struct SetHookTSH0_test : public beast::unit_test::suite // set tsh hook setTSHHook(env, signer2, testStrong); - // signers list set + // signer list set env(signers(account, 2, {{signer1, 1}, {signer2, 1}}), fee(XRP(1)), ter(tesSUCCESS)); @@ -6914,7 +6914,7 @@ struct SetHookTSH0_test : public beast::unit_test::suite testPaymentChannelFundTSH(features); testSetHookTSH(features); testSetRegularKeyTSH(features); - testSignersListSetTSH(features); + testSignerListSetTSH(features); testTicketCreateTSH(features); testTrustSetTSH(features); testURITokenMintTSH(features); diff --git a/src/test/app/SetHook_test.cpp b/src/test/app/SetHook_test.cpp index 4c2fc39d3d..b36d18989d 100644 --- a/src/test/app/SetHook_test.cpp +++ b/src/test/app/SetHook_test.cpp @@ -75,6 +75,16 @@ using JSSMap = [[maybe_unused]] std::string const x##_hash_str = to_string(x##_hash); \ [[maybe_unused]] Keylet const x##_keylet = keylet::hookDefinition(x##_hash); +#define EXPECT_HOOK_FEE(x, fee) \ + do \ + { \ + auto const hookSLE = env.le(x##_keylet); \ + BEAST_EXPECTS( \ + hookSLE->getFieldAmount(sfFee) == XRPAmount{fee}, \ + "Hook fee mismatch: expected " #fee " got " + \ + to_string(hookSLE->getFieldAmount(sfFee))); \ + } while (false) + class SetHook0_test : public beast::unit_test::suite { private: @@ -2710,6 +2720,7 @@ class SetHook0_test : public beast::unit_test::suite M("Install Accept Hook"), HSFEE); env.close(); + EXPECT_HOOK_FEE(accept, 9); env(pay(bob, alice, XRP(1)), M("Test Accept Hook"), fee(XRP(1))); env.close(); @@ -2731,6 +2742,7 @@ class SetHook0_test : public beast::unit_test::suite M("Install Rollback Hook"), HSFEE); env.close(); + EXPECT_HOOK_FEE(rollback, 9); env(pay(bob, alice, XRP(1)), M("Test Rollback Hook"), @@ -2793,7 +2805,7 @@ class SetHook0_test : public beast::unit_test::suite // same loop again but with a guard call { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( (module (type (;0;) (func (param i32 i32) (result i64))) (type (;1;) (func (param i32 i32) (result i32))) @@ -2828,15 +2840,17 @@ class SetHook0_test : public beast::unit_test::suite (export "hook" (func 2))) )[test.hook]"]; - env(ripple::test::jtx::hook(alice, {{hso(hook)}}, 0), + HASH_WASM(hook); + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm)}}, 0), M("Loop 1 with guards"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 14); } // simple looping, c { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -2852,11 +2866,14 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,2); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("Loop 2 in C"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 100); env(pay(bob, alice, XRP(1)), M("Test Loop 2"), fee(XRP(1))); env.close(); @@ -2864,7 +2881,7 @@ class SetHook0_test : public beast::unit_test::suite // complex looping, c { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -2896,11 +2913,14 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,2); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("Loop 3 in C"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 1944); env(pay(bob, alice, XRP(1)), M("Test Loop 3"), fee(XRP(1))); env.close(); @@ -2908,7 +2928,7 @@ class SetHook0_test : public beast::unit_test::suite // complex looping missing a guard { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -2942,7 +2962,8 @@ class SetHook0_test : public beast::unit_test::suite } )[test.hook]"]; - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("Loop 4 in C"), HSFEE, ter(temMALFORMED)); @@ -2963,7 +2984,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g(uint32_t, uint32_t); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -3284,10 +3305,12 @@ class SetHook0_test : public beast::unit_test::suite } )[test.hook]"]; - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + HASH_WASM(hook); + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set emit"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 342); Json::Value invoke; invoke[jss::TransactionType] = "Invoke"; @@ -3858,7 +3881,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -3931,12 +3954,14 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set etxn_details"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 2436); // invoke the hook env(pay(bob, alice, XRP(1)), M("test etxn_details"), fee(XRP(1))); @@ -3952,7 +3977,7 @@ class SetHook0_test : public beast::unit_test::suite auto const alice = Account{"alice"}; auto const bob = Account{"bob"}; - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -4013,8 +4038,9 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); + HASH_WASM(hook); // install the hook on alice - auto hsobj = hso(hook, overrideFlag); + auto hsobj = hso(hook_wasm, overrideFlag); hsobj[jss::HookOn] = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFF" "FE"; // payment high @@ -4022,6 +4048,7 @@ class SetHook0_test : public beast::unit_test::suite M("set etxn_fee_base"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 77); // invoke the hook env(pay(bob, alice, XRP(1)), M("test etxn_fee_base"), fee(XRP(1))); @@ -4057,7 +4084,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -4097,12 +4124,14 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set etxn_nonce"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 11657); // invoke the hook env(pay(bob, alice, XRP(1)), M("test etxn_nonce"), fee(XRP(1))); @@ -4122,7 +4151,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -4147,12 +4176,14 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set etxn_reserve"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 69); // invoke the hook env(pay(bob, alice, XRP(1)), M("test etxn_reserve"), fee(XRP(1))); @@ -4171,7 +4202,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -4188,12 +4219,14 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set fee_base"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 20); // invoke the hook env(pay(bob, alice, XRP(1)), M("test fee_base"), fee(XRP(1))); @@ -4212,7 +4245,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -4321,11 +4354,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_compare"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 583); env(pay(bob, alice, XRP(1)), M("test float_compare"), fee(XRP(1))); env.close(); @@ -4345,7 +4381,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -4520,11 +4556,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_divide"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 1799); env(pay(bob, alice, XRP(1)), M("test float_divide"), fee(XRP(1))); env.close(); @@ -4544,7 +4583,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -4649,11 +4688,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_int"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 1178); env(pay(bob, alice, XRP(1)), M("test float_int"), fee(XRP(1))); env.close(); @@ -4673,7 +4715,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -4737,11 +4779,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_invert"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 329); env(pay(bob, alice, XRP(1)), M("test float_invert"), fee(XRP(1))); env.close(); @@ -4761,7 +4806,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -4820,11 +4865,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_log"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 388); env(pay(bob, alice, XRP(1)), M("test float_log"), fee(XRP(1))); env.close(); @@ -4844,7 +4892,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -4949,11 +4997,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_mantissa"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 309); env(pay(bob, alice, XRP(1)), M("test float_mantissa"), fee(XRP(1))); env.close(); @@ -4973,7 +5024,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -5104,11 +5155,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_mulratio"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 1683); env(pay(bob, alice, XRP(1)), M("test float_mulratio"), fee(XRP(1))); env.close(); @@ -5128,7 +5182,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -5403,12 +5457,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_multiply"), HSFEE); env.close(); - + EXPECT_HOOK_FEE(hook, 3180); env(pay(bob, alice, XRP(1)), M("test float_multiply"), fee(XRP(1))); env.close(); } @@ -5427,7 +5483,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -5474,11 +5530,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_negate"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 105); env(pay(bob, alice, XRP(1)), M("test float_negate"), fee(XRP(1))); env.close(); @@ -5498,7 +5557,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -5515,11 +5574,14 @@ class SetHook0_test : public beast::unit_test::suite : rollback(0,0,1); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_one"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 20); env(pay(bob, alice, XRP(1)), M("test float_one"), fee(XRP(1))); env.close(); @@ -5539,7 +5601,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -5593,11 +5655,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_root"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 257); env(pay(bob, alice, XRP(1)), M("test float_root"), fee(XRP(1))); env.close(); @@ -5617,7 +5682,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -5666,11 +5731,14 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_set"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 343); env(pay(bob, alice, XRP(1)), M("test float_set"), fee(XRP(1))); env.close(); @@ -5690,7 +5758,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -5779,11 +5847,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_sign"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 296); env(pay(bob, alice, XRP(1)), M("test float_sign"), fee(XRP(1))); env.close(); @@ -5803,7 +5874,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -5984,11 +6055,14 @@ class SetHook0_test : public beast::unit_test::suite } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_sto"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 920); env(pay(bob, alice, XRP(1)), M("test float_sto"), fee(XRP(1))); env.close(); @@ -6008,7 +6082,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -6126,11 +6200,14 @@ class SetHook0_test : public beast::unit_test::suite } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_sto_set"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 187); env(pay(bob, alice, XRP(1)), M("test float_sto_set"), fee(XRP(1))); env.close(); @@ -6150,7 +6227,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -6306,11 +6383,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set float_sum"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 1735); env(pay(bob, alice, XRP(1)), M("test float_sum"), fee(XRP(1))); env.close(); @@ -6330,7 +6410,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -6357,12 +6437,15 @@ class SetHook0_test : public beast::unit_test::suite accept((uint32_t)acc, 20, 0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set hook_account"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 72); // invoke the hook env(pay(bob, alice, XRP(1)), M("test hook_account"), fee(XRP(1))); @@ -6390,7 +6473,8 @@ class SetHook0_test : public beast::unit_test::suite } // install the same hook bob - env(ripple::test::jtx::hook(bob, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + bob, {{hso(hook_wasm, overrideFlag)}}, 0), M("set hook_account 2"), HSFEE); env.close(); @@ -6452,7 +6536,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -6481,12 +6565,14 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set hook_again"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 54); env(pay(bob, alice, XRP(1)), M("test hook_again"), fee(XRP(1))); env.close(); @@ -6529,7 +6615,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -6555,12 +6641,15 @@ class SetHook0_test : public beast::unit_test::suite accept((uint32_t)hash, 32, 0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set hook_hash"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 62); // invoke the hook env(pay(bob, alice, XRP(1)), M("test hook_hash"), fee(XRP(1))); @@ -6588,7 +6677,7 @@ class SetHook0_test : public beast::unit_test::suite BEAST_EXPECT(memcmp(hash.data(), retStr.data(), 32) == 0); } - TestHook hook2 = wasm[R"[test.hook]( + TestHook hook2_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -6614,12 +6703,15 @@ class SetHook0_test : public beast::unit_test::suite accept((uint32_t)hash, 32, 0); } )[test.hook]"]; + HASH_WASM(hook2); // install a slightly different hook on bob - env(ripple::test::jtx::hook(bob, {{hso(hook2, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + bob, {{hso(hook2_wasm, overrideFlag)}}, 0), M("set hook_hash 2"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook2, 62); // invoke the hook env(pay(bob, alice, XRP(1)), M("test hook_hash 2"), fee(XRP(1))); @@ -6662,11 +6754,8 @@ class SetHook0_test : public beast::unit_test::suite BEAST_EXPECT(memcmp(hash1.data(), hash2.data(), 32) != 0); // compute the hashes - auto computedHash2 = ripple::sha512Half_s( - ripple::Slice(hook.data(), hook.size())); - - auto computedHash1 = ripple::sha512Half_s( - ripple::Slice(hook2.data(), hook2.size())); + auto computedHash2 = hook_hash; + auto computedHash1 = hook2_hash; // ensure the computed hashes match BEAST_EXPECT(computedHash1 == hash1); @@ -6689,7 +6778,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -6771,6 +6860,7 @@ class SetHook0_test : public beast::unit_test::suite } )[test.hook]"]; + HASH_WASM(hook); Json::Value jv; jv[jss::Account] = alice.human(); @@ -6779,7 +6869,7 @@ class SetHook0_test : public beast::unit_test::suite jv[jss::Hooks] = Json::Value{Json::arrayValue}; Json::Value iv; - iv[jss::CreateCode] = strHex(hook); + iv[jss::CreateCode] = strHex(hook_wasm); iv[jss::HookOn] = "0000000000000000000000000000000000000000000000000000000000000000"; iv[jss::HookApiVersion] = 0U; @@ -6798,6 +6888,7 @@ class SetHook0_test : public beast::unit_test::suite jv[jss::Hooks][0U][jss::Hook] = iv; env(jv, M("set hook_param"), HSFEE, ter(tesSUCCESS)); env.close(); + EXPECT_HOOK_FEE(hook, 2412); // invoke env(pay(bob, alice, XRP(1)), M("test hook_param"), fee(XRP(1))); @@ -6958,6 +7049,7 @@ class SetHook0_test : public beast::unit_test::suite )[test.hook]"]; HASH_WASM(checker); + HASH_WASM(setter); Json::Value jv; jv[jss::Account] = alice.human(); @@ -7007,6 +7099,8 @@ class SetHook0_test : public beast::unit_test::suite env(jv, M("set hook_param_set"), HSFEE, ter(tesSUCCESS)); env.close(); + EXPECT_HOOK_FEE(checker, 475); + EXPECT_HOOK_FEE(setter, 759); // invoke env(pay(bob, alice, XRP(1)), M("test hook_param_set"), fee(XRP(1))); @@ -7025,7 +7119,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -7037,14 +7131,21 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,hook_pos()); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice in all four spots env(ripple::test::jtx::hook( - alice, {{hso(hook), hso(hook), hso(hook), hso(hook)}}, 0), + alice, + {{hso(hook_wasm), + hso(hook_wasm), + hso(hook_wasm), + hso(hook_wasm)}}, + 0), M("set hook_pos"), HSFEE, ter(tesSUCCESS)); env.close(); + EXPECT_HOOK_FEE(hook, 11); // invoke the hooks env(pay(bob, alice, XRP(1)), M("test hook_pos"), fee(XRP(1))); @@ -7151,6 +7252,7 @@ class SetHook0_test : public beast::unit_test::suite } )[test.hook]"]; + HASH_WASM(skip); HASH_WASM(pos); // install the hook on alice in one places @@ -7165,6 +7267,8 @@ class SetHook0_test : public beast::unit_test::suite HSFEE, ter(tesSUCCESS)); env.close(); + EXPECT_HOOK_FEE(skip, 263); + EXPECT_HOOK_FEE(pos, 11); // invoke the hooks { @@ -7201,7 +7305,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -7279,12 +7383,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set ledger_keylet"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 415); env(pay(bob, alice, XRP(1)), M("test ledger_keylet"), fee(XRP(1))); env.close(); @@ -7303,7 +7409,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -7328,12 +7434,14 @@ class SetHook0_test : public beast::unit_test::suite accept((uint32_t)hash, 32, 0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set ledger_last_hash"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 59); for (uint32_t i = 0; i < 3; ++i) { @@ -7377,7 +7485,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -7389,12 +7497,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,ledger_last_time()); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set ledger_last_time"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 11); // invoke the hook a few times for (uint32_t i = 0; i < 3; ++i) @@ -7445,7 +7555,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -7472,12 +7582,14 @@ class SetHook0_test : public beast::unit_test::suite accept((uint32_t)nonce, 64, 0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set ledger_nonce"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 74); // invoke the hook auto const seq = @@ -7543,7 +7655,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -7555,12 +7667,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,ledger_seq()); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set ledger_seq"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 11); // invoke the hook a few times for (uint32_t i = 0; i < 3; ++i) @@ -7602,7 +7716,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -7652,12 +7766,14 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set meta_slot"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 139); env(pay(bob, alice, XRP(1)), M("test meta_slot"), fee(XRP(1))); env.close(); @@ -7699,7 +7815,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -7765,9 +7881,10 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,2); } )[test.hook]"]; + HASH_WASM(hook); // before featureHooksUpdate1 - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set xpop_slot (disabled)"), HSFEE, ter(temMALFORMED)); @@ -7777,10 +7894,11 @@ class SetHook0_test : public beast::unit_test::suite env.close(); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set xpop_slot"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 3245); auto checkResult = [this](auto const& meta, uint64_t expectedCode) -> void { @@ -7820,7 +7938,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -7867,12 +7985,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set otxn_field"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 732); // invoke the hook env(pay(alice, bob, XRP(1)), M("test otxn_field"), fee(XRP(1))); @@ -7890,7 +8010,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -7950,12 +8070,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set otxn_id"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 1077); // invoke the hook env(pay(bob, alice, XRP(1)), M("test otxn_id"), fee(XRP(1))); @@ -7973,7 +8095,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -8032,12 +8154,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set otxn_slot"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 6442); // invoke the hook env(pay(bob, alice, XRP(1)), M("test otxn_slot"), fee(XRP(1))); @@ -8055,7 +8179,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -8092,12 +8216,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set otxn_type"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 51); // invoke the hook env(pay(bob, alice, XRP(1)), M("test otxn_type"), fee(XRP(1))); @@ -8126,7 +8252,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -8208,12 +8334,14 @@ class SetHook0_test : public beast::unit_test::suite } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set otxn_param"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 2412); // invoke Json::Value invoke; @@ -8249,7 +8377,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -8339,12 +8467,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set slot"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 232); // invoke the hook env(pay(bob, alice, XRP(1)), M("test slot"), fee(XRP(1))); @@ -8362,7 +8492,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -8397,12 +8527,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set slot_clear"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 83); // invoke the hook env(pay(bob, alice, XRP(1)), M("test slot_clear"), fee(XRP(1))); @@ -8420,7 +8552,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -8462,12 +8594,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set slot_count"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 97); // invoke the hook env(pay(bob, alice, XRP(1)), M("test slot_count"), fee(XRP(1))); @@ -8485,7 +8619,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -8537,12 +8671,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set slot_float"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 112); // invoke the hook env(pay(bob, alice, XRP(1)), M("test slot_float"), fee(XRP(1))); @@ -8560,7 +8696,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -8647,12 +8783,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set slot_set"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 11653); // invoke the hook env(pay(bob, alice, XRP(1)), M("test slot_set"), fee(XRP(1))); @@ -8670,7 +8808,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -8726,12 +8864,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set slot_size"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 114); // invoke the hook env(pay(bob, alice, XRP(1)), M("test slot_size"), fee(XRP(1))); @@ -8750,7 +8890,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -8847,12 +8987,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set slot_subarray"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 6212); // generate an array of memos to attach Json::Value jv; @@ -8890,7 +9032,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -8967,12 +9109,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set slot_subfield"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 6109); // invoke the hook env(pay(bob, alice, XRP(1)), M("test slot_subfield"), fee(XRP(1))); @@ -8993,7 +9137,7 @@ class SetHook0_test : public beast::unit_test::suite // set up a trustline which we can retrieve later env(trust(alice, bob["USD"](600))); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -9106,12 +9250,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set slot_subfield"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 284); // invoke the hook env(pay(bob, alice, XRP(1)), M("test slot_type"), fee(XRP(1))); @@ -9131,7 +9277,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -9190,12 +9336,15 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set state"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 2254); // invoke the hook env(pay(bob, alice, XRP(1)), M("test state"), fee(XRP(1))); @@ -9205,7 +9354,7 @@ class SetHook0_test : public beast::unit_test::suite // override hook with a second version that just reads those state // objects { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -9246,12 +9395,15 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set state 2"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 2134); // invoke the hook env(pay(bob, alice, XRP(1)), M("test state 2"), fee(XRP(1))); @@ -9272,7 +9424,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), bob); { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -9312,12 +9464,15 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set state_foreign"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 72); // invoke the hook env(pay(bob, alice, XRP(1)), M("test state_foreign"), fee(XRP(1))); @@ -9325,7 +9480,7 @@ class SetHook0_test : public beast::unit_test::suite // set a second hook on bob that will read the state objects from alice { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -9402,12 +9557,15 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on bob - env(ripple::test::jtx::hook(bob, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + bob, {{hso(hook_wasm, overrideFlag)}}, 0), M("set state_foreign 2"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 2408); // invoke the hook @@ -9423,7 +9581,7 @@ class SetHook0_test : public beast::unit_test::suite testcase("Test state_foreign_set max"); using namespace jtx; - static const std::vector ns_maxHook = { + static const std::vector ns_maxHook_wasm = { 0x00U, 0x61U, 0x73U, 0x6dU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x36U, 0x07U, 0x60U, 0x02U, 0x7fU, 0x7fU, 0x01U, 0x7fU, 0x60U, 0x02U, 0x7fU, 0x7fU, 0x01U, 0x7eU, 0x60U, 0x03U, 0x7fU, 0x7fU, @@ -9483,6 +9641,7 @@ class SetHook0_test : public beast::unit_test::suite 0x52U, 0x65U, 0x61U, 0x63U, 0x68U, 0x65U, 0x64U, 0x00U, 0x6bU, 0x65U, 0x79U, 0x32U, 0x00U, 0x63U, 0x6fU, 0x6eU, 0x74U, 0x65U, 0x6eU, 0x74U, 0x32U}; + HASH_WASM(ns_maxHook); Env env{*this, features}; @@ -9493,10 +9652,11 @@ class SetHook0_test : public beast::unit_test::suite // install the hook on alice env(ripple::test::jtx::hook( - alice, {{hso(ns_maxHook, overrideFlag)}}, 0), + alice, {{hso(ns_maxHook_wasm, overrideFlag)}}, 0), M("set state_foreign_set_max"), HSFEE); env.close(); + EXPECT_HOOK_FEE(ns_maxHook, 103); // invoke the hook for (uint32_t i = 0; i < 255; ++i) @@ -9674,6 +9834,7 @@ class SetHook0_test : public beast::unit_test::suite env(json, M("set state_foreign_set"), HSFEE); env.close(); + EXPECT_HOOK_FEE(grantor, 103); } // install the grantee hook on bob @@ -9683,6 +9844,7 @@ class SetHook0_test : public beast::unit_test::suite bob, {{hso(grantee_wasm, overrideFlag)}}, 0); env(json, M("set state_foreign_set 2"), HSFEE); env.close(); + EXPECT_HOOK_FEE(grantee, 234); } auto const aliceid = Account("alice").id(); @@ -9944,6 +10106,7 @@ class SetHook0_test : public beast::unit_test::suite bob, {{hso(exhaustion_wasm, overrideFlag)}}, 0); env(json, M("set state_foreign_set 12"), HSFEE); env.close(); + EXPECT_HOOK_FEE(exhaustion, 10582); } // now invoke repeatedly until exhaustion is reached @@ -10044,7 +10207,7 @@ class SetHook0_test : public beast::unit_test::suite // bounds and buffer size checks { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -10087,12 +10250,15 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set state_set 1"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 143); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); @@ -10140,7 +10306,7 @@ class SetHook0_test : public beast::unit_test::suite // first hook will set two state objects with different keys and data on // alice { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -10211,12 +10377,15 @@ class SetHook0_test : public beast::unit_test::suite } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set state_set 1"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 85); BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 1); @@ -10302,7 +10471,7 @@ class SetHook0_test : public beast::unit_test::suite // make amother hook to override an existing state and delete an // existing state { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -10357,8 +10526,9 @@ class SetHook0_test : public beast::unit_test::suite } )[test.hook]"]; + HASH_WASM(hook); - TestHook hook2 = wasm[R"[test.hook]( + TestHook hook2_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -10402,14 +10572,21 @@ class SetHook0_test : public beast::unit_test::suite } )[test.hook]"]; + HASH_WASM(hook2); + // install the hook on alice env(ripple::test::jtx::hook( alice, - {{{hso(hook, overrideFlag)}, {}, {}, {hso(hook2, 0)}}}, + {{{hso(hook_wasm, overrideFlag)}, + {}, + {}, + {hso(hook2_wasm, 0)}}}, 0), M("set state_set 2"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 82); + EXPECT_HOOK_FEE(hook2, 525); // two hooks + two state objects = 4 BEAST_EXPECT((*env.le("alice"))[sfOwnerCount] == 4); @@ -10418,7 +10595,7 @@ class SetHook0_test : public beast::unit_test::suite // updated state is also available on his side. caution must be // taken because bob's hooks will execute first if bob's is the // otxn. therefore we will flip to a payment from alice to bob here - TestHook hook3 = wasm[R"[test.hook]( + TestHook hook3_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -10478,12 +10655,15 @@ class SetHook0_test : public beast::unit_test::suite } )[test.hook]"]; + HASH_WASM(hook3); // install the hook on bob - env(ripple::test::jtx::hook(bob, {{hso(hook3, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + bob, {{hso(hook3_wasm, overrideFlag)}}, 0), M("set state_set 3"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook3, 560); // invoke the hook with cho (rollback after alice's hooks have // executed) @@ -10536,7 +10716,7 @@ class SetHook0_test : public beast::unit_test::suite // create a hook state inside the weak side of an execution, while the // strong side is rolled back { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -10580,15 +10760,17 @@ class SetHook0_test : public beast::unit_test::suite } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice, deleting the other hook env(ripple::test::jtx::hook( alice, - {{{hso(hook, overrideFlag)}, {}, {}, {hso_delete()}}}, + {{{hso(hook_wasm, overrideFlag)}, {}, {}, {hso_delete()}}}, 0), M("set state_set 4"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 52); // invoke from alice to cho, this will cause a rollback, however the // hook state should still be updated because the hook specified @@ -10701,6 +10883,7 @@ class SetHook0_test : public beast::unit_test::suite env(json, M("set state_set 6"), HSFEE); env.close(); + EXPECT_HOOK_FEE(exhaustion, 54114); } // now invoke repeatedly until exhaustion is reached @@ -10853,6 +11036,7 @@ class SetHook0_test : public beast::unit_test::suite to_string(UINT256_BIT[ttACCOUNT_SET]); env(jv, M("Create scaled state hook"), HSFEE, ter(tesSUCCESS)); env.close(); + EXPECT_HOOK_FEE(scaled_state, 227); BEAST_EXPECT((*env.le(gary))[sfOwnerCount] == 1); BEAST_EXPECT(!env.le(gary)->isFieldPresent(sfHookStateCount)); @@ -10903,7 +11087,7 @@ class SetHook0_test : public beast::unit_test::suite // tests for set_state_cache if (extHookStateEnabled) { - TestHook extended_state_reserve_hook = wasm[R"[test.hook]( + TestHook extended_state_reserve_hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -10948,14 +11132,17 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(extended_state_reserve_hook); // install the hook on gary - Json::Value jv = hso(extended_state_reserve_hook, overrideFlag); + Json::Value jv = + hso(extended_state_reserve_hook_wasm, overrideFlag); jv[jss::HookOn] = "fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff" "bfffff"; // only invoke high env(ripple::test::jtx::hook(hank, {{jv}}, 0), HSFEE); env.close(); + EXPECT_HOOK_FEE(extended_state_reserve_hook, 95); Json::Value jv1 = noop(hank); jv1[sfHookStateScale.fieldName] = 8; @@ -10998,7 +11185,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -11168,19 +11355,22 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set sto_emplace"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 15024); // invoke the hook env(pay(bob, alice, XRP(1)), M("test sto_emplace"), fee(XRP(1))); } { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -11217,6 +11407,7 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,result); } )[test.hook]"]; + HASH_WASM(hook); for (auto f : {features, features - fixHookAPI20251128}) { @@ -11229,10 +11420,11 @@ class SetHook0_test : public beast::unit_test::suite // install the hook on alice env(ripple::test::jtx::hook( - alice, {{hso(hook, overrideFlag)}}, 0), + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set sto_emplace"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 36); // invoke the hook env(pay(bob, alice, XRP(1)), @@ -11270,7 +11462,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -11383,12 +11575,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set sto_erase"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 10021); // invoke the hook env(pay(bob, alice, XRP(1)), M("test sto_erase"), fee(XRP(1))); @@ -11408,7 +11602,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -11458,19 +11652,22 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook( + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set sto_subarray"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 95); // invoke the hook env(pay(bob, alice, XRP(1)), M("test sto_subarray"), fee(XRP(1))); } { - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -11501,6 +11698,7 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,result1+result2); } )[test.hook]"]; + HASH_WASM(hook); for (auto isfixHookAPI20251128 : {true, false}) { @@ -11513,10 +11711,11 @@ class SetHook0_test : public beast::unit_test::suite // install the hook on alice env(ripple::test::jtx::hook( - alice, {{hso(hook, overrideFlag)}}, 0), + alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set sto_subarray"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 19); // invoke the hook env(pay(bob, alice, XRP(1)), @@ -11570,7 +11769,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -11630,12 +11829,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set sto_subfield"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 123); // invoke the hook env(pay(bob, alice, XRP(1)), M("test sto_subfield"), fee(XRP(1))); @@ -11654,7 +11855,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -11713,12 +11914,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set sto_validate"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 130); // invoke the hook env(pay(bob, alice, XRP(1)), M("test sto_validate"), fee(XRP(1))); @@ -11917,7 +12120,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -11943,12 +12146,14 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set trace"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 103); // invoke the hook env(pay(bob, alice, XRP(1)), M("test trace"), fee(XRP(1))); @@ -11967,7 +12172,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -11987,12 +12192,14 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set trace_float"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 37); // invoke the hook env(pay(bob, alice, XRP(1)), M("test trace_float"), fee(XRP(1))); @@ -12011,7 +12218,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -12031,12 +12238,14 @@ class SetHook0_test : public beast::unit_test::suite return accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set trace_num"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 37); // invoke the hook env(pay(bob, alice, XRP(1)), M("test trace_num"), fee(XRP(1))); @@ -12053,7 +12262,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -12299,12 +12508,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set util_accid"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 3101); // invoke the hook env(pay(bob, alice, XRP(1)), M("test util_accid"), fee(XRP(1))); @@ -12323,7 +12534,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -12980,12 +13191,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set util_keylet"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 1786); // invoke the hook env(pay(bob, alice, XRP(1)), M("test util_keylet"), fee(XRP(1))); @@ -13003,7 +13216,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -13433,12 +13646,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set util_raddr"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 4279); // invoke the hook env(pay(bob, alice, XRP(1)), M("test util_raddr"), fee(XRP(1))); @@ -13456,7 +13671,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -13806,12 +14021,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set util_sha512h"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 2875); // invoke the hook env(pay(bob, alice, XRP(1)), M("test util_sha512h"), fee(XRP(1))); @@ -13829,7 +14046,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), alice); env.fund(XRP(10000), bob); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g (uint32_t id, uint32_t maxiter); #define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1) @@ -13920,12 +14137,14 @@ class SetHook0_test : public beast::unit_test::suite accept(0,0,0); } )[test.hook]"]; + HASH_WASM(hook); // install the hook on alice - env(ripple::test::jtx::hook(alice, {{hso(hook, overrideFlag)}}, 0), + env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0), M("set util_verify"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 230); // invoke the hook env(pay(bob, alice, XRP(1)), M("test util_verify"), fee(XRP(1))); @@ -13950,7 +14169,7 @@ class SetHook0_test : public beast::unit_test::suite env.fund(XRP(10000), hookacc); env.close(); - TestHook hook = wasm[R"[test.hook]( + TestHook hook_wasm = wasm[R"[test.hook]( #include extern int32_t _g(uint32_t, uint32_t); extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code); @@ -14279,6 +14498,7 @@ class SetHook0_test : public beast::unit_test::suite } } )[test.hook]"]; + HASH_WASM(hook); bool const hasFeature = env.current()->rules().enabled(featureHookCanEmit); @@ -14314,15 +14534,16 @@ class SetHook0_test : public beast::unit_test::suite if (i == 1) { - Json::Value h = hso(hook, overrideFlag); + Json::Value h = hso(hook_wasm, overrideFlag); env(ripple::test::jtx::hook(hookacc, {{h}}, 0), M("set hookcanemit"), HSFEE); env.close(); + EXPECT_HOOK_FEE(hook, 755); } else if (i == 2) { - Json::Value h = hso(hook, overrideFlag); + Json::Value h = hso(hook_wasm, overrideFlag); env(ripple::test::jtx::hook(acc, {{h}}, 0), M("set hookcanemit"), HSFEE); @@ -14330,7 +14551,7 @@ class SetHook0_test : public beast::unit_test::suite } { - Json::Value h = hso(hook, overrideFlag); + Json::Value h = hso(hook_wasm, overrideFlag); env(ripple::test::jtx::hook(acc, {{h}}, 0), M("set hookcanemit"), HSFEE); @@ -14362,7 +14583,7 @@ class SetHook0_test : public beast::unit_test::suite { // same result with no-HookCanEmit - Json::Value h = hso(hook, overrideFlag); + Json::Value h = hso(hook_wasm, overrideFlag); h[jss::HookCanEmit] = "0000000000000000000000000000000000000000000000000000000000" "400000"; @@ -14401,7 +14622,7 @@ class SetHook0_test : public beast::unit_test::suite { // install the hook on acc - Json::Value hookCanEmitHook = hso(hook, overrideFlag); + Json::Value hookCanEmitHook = hso(hook_wasm, overrideFlag); hookCanEmitHook[jss::HookCanEmit] = "00000000000000000000000000000000000000000000000000" "00000000000000"; @@ -14440,7 +14661,7 @@ class SetHook0_test : public beast::unit_test::suite { // install the hook on acc - Json::Value hookCanEmitHook = hso(hook, overrideFlag); + Json::Value hookCanEmitHook = hso(hook_wasm, overrideFlag); hookCanEmitHook[jss::HookCanEmit] = "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" "FFFFFFFFFFFFFF"; diff --git a/src/test/app/Touch_test.cpp b/src/test/app/Touch_test.cpp index 6943e5d370..f3a545f5df 100644 --- a/src/test/app/Touch_test.cpp +++ b/src/test/app/Touch_test.cpp @@ -880,9 +880,9 @@ struct Touch_test : public beast::unit_test::suite } void - testSignersListSet(FeatureBitset features) + testSignerListSet(FeatureBitset features) { - testcase("signers list set"); + testcase("signer list set"); using namespace test::jtx; using namespace std::literals; @@ -895,7 +895,7 @@ struct Touch_test : public beast::unit_test::suite env.fund(XRP(1000), alice, signer1, signer2); env.close(); - // signers list set + // signer list set env(signers(alice, 2, {{signer1, 1}, {signer2, 1}}), ter(tesSUCCESS)); env.close(); @@ -1384,7 +1384,7 @@ struct Touch_test : public beast::unit_test::suite testPaymentChannelFund(features); testSetHook(features); testSetRegularKey(features); - testSignersListSet(features); + testSignerListSet(features); testTicketCreate(features); testTrustSet(features); testURITokenMint(features); diff --git a/src/test/jtx/AMM.h b/src/test/jtx/AMM.h index 52039f74ae..9f3a07e8b6 100644 --- a/src/test/jtx/AMM.h +++ b/src/test/jtx/AMM.h @@ -125,7 +125,6 @@ class AMM STAmount const asset1_; STAmount const asset2_; uint256 const ammID_; - IOUAmount const initialLPTokens_; bool log_; bool doClose_; // Predict next purchase price @@ -138,6 +137,7 @@ class AMM std::uint32_t const fee_; AccountID const ammAccount_; Issue const lptIssue_; + IOUAmount const initialLPTokens_; public: AMM(Env& env, @@ -194,6 +194,12 @@ class AMM Issue const& issue2, std::optional const& account = std::nullopt) const; + std::tuple + balances(std::optional const& account = std::nullopt) const + { + return balances(asset1_.get(), asset2_.get(), account); + } + [[nodiscard]] bool expectLPTokens(AccountID const& account, IOUAmount const& tokens) const; @@ -428,6 +434,9 @@ class AMM [[nodiscard]] bool expectAuctionSlot(auto&& cb) const; + + IOUAmount + initialTokens(); }; namespace amm { diff --git a/src/test/jtx/AMMTest.h b/src/test/jtx/AMMTest.h index 0481dc98a4..4902e5f499 100644 --- a/src/test/jtx/AMMTest.h +++ b/src/test/jtx/AMMTest.h @@ -33,6 +33,15 @@ class AMM; enum class Fund { All, Acct, Gw, IOUOnly }; +struct TestAMMArg +{ + std::optional> pool = std::nullopt; + std::uint16_t tfee = 0; + std::optional ter = std::nullopt; + std::vector features = {supported_amendments()}; + bool noLog = false; +}; + void fund( jtx::Env& env, @@ -85,6 +94,11 @@ class AMMTestBase : public beast::unit_test::suite std::uint16_t tfee = 0, std::optional const& ter = std::nullopt, std::vector const& features = {supported_amendments()}); + + void + testAMM( + std::function&& cb, + TestAMMArg const& arg); }; class AMMTest : public jtx::AMMTestBase diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index dc5b4be48f..34056d3b08 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -646,6 +646,12 @@ class Env void disableFeature(uint256 const feature); + bool + enabled(uint256 feature) const + { + return current()->rules().enabled(feature); + } + private: void fund(bool setDefaultRipple, STAmount const& amount, Account const& account); diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 089d3508d7..66a866ea0a 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -18,8 +18,9 @@ //============================================================================== #include - #include + +#include #include #include #include @@ -38,12 +39,10 @@ number(STAmount const& a) return a; } -static IOUAmount -initialTokens(STAmount const& asset1, STAmount const& asset2) +IOUAmount +AMM::initialTokens() { - auto const product = number(asset1) * number(asset2); - return (IOUAmount)(product.mantissa() >= 0 ? root2(product) - : root2(-product)); + return getLPTokensBalance(); } AMM::AMM( @@ -64,7 +63,6 @@ AMM::AMM( , asset1_(asset1) , asset2_(asset2) , ammID_(keylet::amm(asset1_.issue(), asset2_.issue()).key) - , initialLPTokens_(initialTokens(asset1, asset2)) , log_(log) , doClose_(close) , lastPurchasePrice_(0) @@ -77,6 +75,7 @@ AMM::AMM( asset1_.issue().currency, asset2_.issue().currency, ammAccount_)) + , initialLPTokens_(initialTokens()) { } diff --git a/src/test/jtx/impl/AMMTest.cpp b/src/test/jtx/impl/AMMTest.cpp index 7ba567a37d..eb80d9ce73 100644 --- a/src/test/jtx/impl/AMMTest.cpp +++ b/src/test/jtx/impl/AMMTest.cpp @@ -17,9 +17,9 @@ */ //============================================================================== -#include - #include +#include +#include #include #include #include @@ -104,15 +104,37 @@ AMMTestBase::testAMM( std::uint16_t tfee, std::optional const& ter, std::vector const& vfeatures) +{ + testAMM( + std::move(cb), + TestAMMArg{ + .pool = pool, .tfee = tfee, .ter = ter, .features = vfeatures}); +} + +void +AMMTestBase::testAMM( + std::function&& cb, + TestAMMArg const& arg) { using namespace jtx; - for (auto const& features : vfeatures) + std::string logs; + + for (auto const& features : arg.features) { - Env env{*this, features}; + // Env env{ + // *this, + // features, + // arg.noLog ? std::make_unique(&logs) : nullptr}; + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); auto const [asset1, asset2] = - pool ? *pool : std::make_pair(XRP(10000), USD(10000)); + arg.pool ? *arg.pool : std::make_pair(XRP(10000), USD(10000)); auto tofund = [&](STAmount const& a) -> STAmount { if (a.native()) { @@ -142,7 +164,7 @@ AMMTestBase::testAMM( alice, asset1, asset2, - CreateArg{.log = false, .tfee = tfee, .err = ter}); + CreateArg{.log = false, .tfee = arg.tfee, .err = arg.ter}); if (BEAST_EXPECT( ammAlice.expectBalances(asset1, asset2, ammAlice.tokens()))) cb(ammAlice, env); diff --git a/src/test/jtx/impl/hook.cpp b/src/test/jtx/impl/hook.cpp index 8602e0053f..07b72ad003 100644 --- a/src/test/jtx/impl/hook.cpp +++ b/src/test/jtx/impl/hook.cpp @@ -65,29 +65,16 @@ hso_delete(void (*f)(Json::Value& jv)) Json::Value hso(std::vector const& wasmBytes, void (*f)(Json::Value& jv)) { - if (wasmBytes.size() == 0) - throw std::runtime_error("empty hook wasm passed to hso()"); - - Json::Value jv; - jv[jss::CreateCode] = strHex(wasmBytes); - { - jv[jss::HookOn] = - "0000000000000000000000000000000000000000000000000000000000000000"; - jv[jss::HookNamespace] = to_string(uint256{beast::zero}); - jv[jss::HookApiVersion] = Json::Value{0}; - } - - if (f) - f(jv); - - return jv; + return hso(strHex(wasmBytes), f); } Json::Value hso(std::string const& wasmHex, void (*f)(Json::Value& jv)) { if (wasmHex.size() == 0) - throw std::runtime_error("empty hook wasm passed to hso()"); + throw std::runtime_error( + "empty hook wasm passed to hso(): run " + "src/test/app/build_test_hooks.sh to generate the hook wasm"); Json::Value jv; jv[jss::CreateCode] = wasmHex; diff --git a/src/test/rpc/AMMInfo_test.cpp b/src/test/rpc/AMMInfo_test.cpp index c1e059a3ea..6503de16ad 100644 --- a/src/test/rpc/AMMInfo_test.cpp +++ b/src/test/rpc/AMMInfo_test.cpp @@ -203,98 +203,107 @@ class AMMInfo_test : public jtx::AMMTestBase } void - testVoteAndBid() + testVoteAndBid(FeatureBitset features) { testcase("Vote and Bid"); using namespace jtx; - testAMM([&](AMM& ammAlice, Env& env) { - BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRP(10000), USD(10000), IOUAmount{10000000, 0})); - std::unordered_map votes; - votes.insert({alice.human(), 0}); - for (int i = 0; i < 7; ++i) - { - Account a(std::to_string(i)); - votes.insert({a.human(), 50 * (i + 1)}); - fund(env, gw, {a}, {USD(10000)}, Fund::Acct); - ammAlice.deposit(a, 10000000); - ammAlice.vote(a, 50 * (i + 1)); - } - BEAST_EXPECT(ammAlice.expectTradingFee(175)); - Account ed("ed"); - Account bill("bill"); - env.fund(XRP(1000), bob, ed, bill); - env(ammAlice.bid( - {.bidMin = 100, .authAccounts = {carol, bob, ed, bill}})); - BEAST_EXPECT(ammAlice.expectAmmRpcInfo( - XRP(80000), - USD(80000), - IOUAmount{79994400}, - std::nullopt, - std::nullopt, - ammAlice.ammAccount())); - for (auto i = 0; i < 2; ++i) - { - std::unordered_set authAccounts = { - carol.human(), bob.human(), ed.human(), bill.human()}; - auto const ammInfo = i ? ammAlice.ammRpcInfo() - : ammAlice.ammRpcInfo( - std::nullopt, - std::nullopt, - std::nullopt, - std::nullopt, - ammAlice.ammAccount()); - auto const& amm = ammInfo[jss::amm]; - try + testAMM( + [&](AMM& ammAlice, Env& env) { + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRP(10000), USD(10000), IOUAmount{10000000, 0})); + std::unordered_map votes; + votes.insert({alice.human(), 0}); + for (int i = 0; i < 7; ++i) { - // votes - auto const voteSlots = amm[jss::vote_slots]; - auto votesCopy = votes; - for (std::uint8_t i = 0; i < 8; ++i) + Account a(std::to_string(i)); + votes.insert({a.human(), 50 * (i + 1)}); + fund(env, gw, {a}, {USD(10001)}, Fund::Acct); + ammAlice.deposit(a, 10000000); + ammAlice.vote(a, 50 * (i + 1)); + } + BEAST_EXPECT(ammAlice.expectTradingFee(175)); + Account ed("ed"); + Account bill("bill"); + env.fund(XRP(1000), bob, ed, bill); + env(ammAlice.bid( + {.bidMin = 100, .authAccounts = {carol, bob, ed, bill}})); + BEAST_EXPECT(ammAlice.expectAmmRpcInfo( + XRPAmount(80000000005), + STAmount{USD, UINT64_C(80'000'00000000005), -11}, + IOUAmount{79994400}, + std::nullopt, + std::nullopt, + ammAlice.ammAccount())); + for (auto i = 0; i < 2; ++i) + { + std::unordered_set authAccounts = { + carol.human(), bob.human(), ed.human(), bill.human()}; + auto const ammInfo = i ? ammAlice.ammRpcInfo() + : ammAlice.ammRpcInfo( + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + ammAlice.ammAccount()); + auto const& amm = ammInfo[jss::amm]; + try { - if (!BEAST_EXPECT( - votes[voteSlots[i][jss::account].asString()] == - voteSlots[i][jss::trading_fee].asUInt() && - voteSlots[i][jss::vote_weight].asUInt() == - 12500)) + // votes + auto const voteSlots = amm[jss::vote_slots]; + auto votesCopy = votes; + for (std::uint8_t i = 0; i < 8; ++i) + { + if (!BEAST_EXPECT( + votes[voteSlots[i][jss::account] + .asString()] == + voteSlots[i][jss::trading_fee] + .asUInt() && + voteSlots[i][jss::vote_weight].asUInt() == + 12500)) + return; + votes.erase(voteSlots[i][jss::account].asString()); + } + if (!BEAST_EXPECT(votes.empty())) return; - votes.erase(voteSlots[i][jss::account].asString()); - } - if (!BEAST_EXPECT(votes.empty())) - return; - votes = votesCopy; + votes = votesCopy; - // bid - auto const auctionSlot = amm[jss::auction_slot]; - for (std::uint8_t i = 0; i < 4; ++i) - { - if (!BEAST_EXPECT(authAccounts.contains( + // bid + auto const auctionSlot = amm[jss::auction_slot]; + for (std::uint8_t i = 0; i < 4; ++i) + { + if (!BEAST_EXPECT(authAccounts.contains( + auctionSlot[jss::auth_accounts][i] + [jss::account] + .asString()))) + return; + authAccounts.erase( auctionSlot[jss::auth_accounts][i][jss::account] - .asString()))) + .asString()); + } + if (!BEAST_EXPECT(authAccounts.empty())) return; - authAccounts.erase( - auctionSlot[jss::auth_accounts][i][jss::account] - .asString()); + BEAST_EXPECT( + auctionSlot[jss::account].asString() == + alice.human() && + auctionSlot[jss::discounted_fee].asUInt() == 17 && + auctionSlot[jss::price][jss::value].asString() == + "5600" && + auctionSlot[jss::price][jss::currency].asString() == + to_string(ammAlice.lptIssue().currency) && + auctionSlot[jss::price][jss::issuer].asString() == + to_string(ammAlice.lptIssue().account)); + } + catch (std::exception const& e) + { + fail(e.what(), __FILE__, __LINE__); } - if (!BEAST_EXPECT(authAccounts.empty())) - return; - BEAST_EXPECT( - auctionSlot[jss::account].asString() == alice.human() && - auctionSlot[jss::discounted_fee].asUInt() == 17 && - auctionSlot[jss::price][jss::value].asString() == - "5600" && - auctionSlot[jss::price][jss::currency].asString() == - to_string(ammAlice.lptIssue().currency) && - auctionSlot[jss::price][jss::issuer].asString() == - to_string(ammAlice.lptIssue().account)); - } - catch (std::exception const& e) - { - fail(e.what(), __FILE__, __LINE__); } - } - }); + }, + std::nullopt, + 0, + std::nullopt, + {features}); } void @@ -337,9 +346,11 @@ class AMMInfo_test : public jtx::AMMTestBase void run() override { + using namespace jtx; + auto const all = supported_amendments(); testErrors(); testSimpleRpc(); - testVoteAndBid(); + testVoteAndBid(all); testFreeze(); testInvalidAmmField(); } diff --git a/src/xrpld/app/hook/detail/applyHook.cpp b/src/xrpld/app/hook/detail/applyHook.cpp index 15ce193bfc..b1097bd77d 100644 --- a/src/xrpld/app/hook/detail/applyHook.cpp +++ b/src/xrpld/app/hook/detail/applyHook.cpp @@ -1691,7 +1691,7 @@ hook::finalizeHookResult( ptr->add(s); SerialIter sit(s.slice()); - sleEmitted->emplace_back(ripple::STObject(sit, sfEmittedTxn)); + sleEmitted->set(ripple::STObject(sit, sfEmittedTxn)); auto page = applyCtx.view().dirInsert( keylet::emittedDir(), emittedId, [&](SLE::ref sle) { (*sle)[sfFlags] = lsfEmittedDir; diff --git a/src/xrpld/app/misc/AMMHelpers.h b/src/xrpld/app/misc/AMMHelpers.h index be75abce07..b5c257e330 100644 --- a/src/xrpld/app/misc/AMMHelpers.h +++ b/src/xrpld/app/misc/AMMHelpers.h @@ -51,6 +51,8 @@ reduceOffer(auto const& amount) } // namespace detail +enum class IsDeposit : bool { No = false, Yes = true }; + /** Calculate LP Tokens given AMM pool reserves. * @param asset1 AMM one side of the pool reserve * @param asset2 AMM another side of the pool reserve @@ -70,7 +72,7 @@ ammLPTokens( * @return tokens */ STAmount -lpTokensIn( +lpTokensOut( STAmount const& asset1Balance, STAmount const& asset1Deposit, STAmount const& lptAMMBalance, @@ -99,7 +101,7 @@ ammAssetIn( * @return tokens out amount */ STAmount -lpTokensOut( +lpTokensIn( STAmount const& asset1Balance, STAmount const& asset1Withdraw, STAmount const& lptAMMBalance, @@ -113,7 +115,7 @@ lpTokensOut( * @return calculated asset amount */ STAmount -withdrawByTokens( +ammAssetOut( STAmount const& assetBalance, STAmount const& lptAMMBalance, STAmount const& lpTokens, @@ -517,13 +519,13 @@ square(Number const& n); * withdraw to cancel out the precision loss. * @param lptAMMBalance LPT AMM Balance * @param lpTokens LP tokens to deposit or withdraw - * @param isDeposit true if deposit, false if withdraw + * @param isDeposit Yes if deposit, No if withdraw */ STAmount adjustLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, - bool isDeposit); + IsDeposit isDeposit); /** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if * the adjusted LP tokens are less than the provided LP tokens. @@ -533,7 +535,7 @@ adjustLPTokens( * @param lptAMMBalance LPT AMM Balance * @param lpTokens LP tokens to deposit or withdraw * @param tfee trading fee in basis points - * @param isDeposit true if deposit, false if withdraw + * @param isDeposit Yes if deposit, No if withdraw * @return */ std::tuple, STAmount> @@ -544,7 +546,7 @@ adjustAmountsByLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, std::uint16_t tfee, - bool isDeposit); + IsDeposit isDeposit); /** Positive solution for quadratic equation: * x = (-b + sqrt(b**2 + 4*a*c))/(2*a) @@ -552,6 +554,134 @@ adjustAmountsByLPTokens( Number solveQuadraticEq(Number const& a, Number const& b, Number const& c); +STAmount +multiply(STAmount const& amount, Number const& frac, Number::rounding_mode rm); + +namespace detail { + +inline Number::rounding_mode +getLPTokenRounding(IsDeposit isDeposit) +{ + // Minimize on deposit, maximize on withdraw to ensure + // AMM invariant sqrt(poolAsset1 * poolAsset2) >= LPTokensBalance + return isDeposit == IsDeposit::Yes ? Number::downward : Number::upward; +} + +inline Number::rounding_mode +getAssetRounding(IsDeposit isDeposit) +{ + // Maximize on deposit, minimize on withdraw to ensure + // AMM invariant sqrt(poolAsset1 * poolAsset2) >= LPTokensBalance + return isDeposit == IsDeposit::Yes ? Number::upward : Number::downward; +} + +} // namespace detail + +/** Round AMM equal deposit/withdrawal amount. Deposit/withdrawal formulas + * calculate the amount as a fractional value of the pool balance. The rounding + * takes place on the last step of multiplying the balance by the fraction if + * AMMv1_3 is enabled. + */ +template +STAmount +getRoundedAsset( + Rules const& rules, + STAmount const& balance, + A const& frac, + IsDeposit isDeposit) +{ + auto const rm = detail::getAssetRounding(isDeposit); + return multiply(balance, frac, rm); +} + +/** Round AMM single deposit/withdrawal amount. + * The lambda's are used to delay evaluation until the function + * is executed so that the calculation is not done twice. noRoundCb() is + * called if AMMv1_3 is disabled. Otherwise, the rounding is set and + * the amount is: + * isDeposit is Yes - the balance multiplied by productCb() + * isDeposit is No - the result of productCb(). The rounding is + * the same for all calculations in productCb() + */ +STAmount +getRoundedAsset( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& balance, + std::function&& productCb, + IsDeposit isDeposit); + +/** Round AMM deposit/withdrawal LPToken amount. Deposit/withdrawal formulas + * calculate the lptokens as a fractional value of the AMM total lptokens. + * The rounding takes place on the last step of multiplying the balance by + * the fraction if AMMv1_3 is enabled. The tokens are then + * adjusted to factor in the loss in precision (we only keep 16 significant + * digits) when adding the lptokens to the balance. + */ +STAmount +getRoundedLPTokens( + Rules const& rules, + STAmount const& balance, + Number const& frac, + IsDeposit isDeposit); + +/** Round AMM single deposit/withdrawal LPToken amount. + * The lambda's are used to delay evaluation until the function is executed + * so that the calculations are not done twice. + * noRoundCb() is called if AMMv1_3 is disabled. Otherwise, the rounding is set + * and the lptokens are: + * if isDeposit is Yes - the result of productCb(). The rounding is + * the same for all calculations in productCb() + * if isDeposit is No - the balance multiplied by productCb() + * The lptokens are then adjusted to factor in the loss in precision + * (we only keep 16 significant digits) when adding the lptokens to the balance. + */ +STAmount +getRoundedLPTokens( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& lptAMMBalance, + std::function&& productCb, + IsDeposit isDeposit); + +/* Next two functions adjust asset in/out amount to factor in the adjusted + * lptokens. The lptokens are calculated from the asset in/out. The lptokens are + * then adjusted to factor in the loss in precision. The adjusted lptokens might + * be less than the initially calculated tokens. Therefore, the asset in/out + * must be adjusted. The rounding might result in the adjusted amount being + * greater than the original asset in/out amount. If this happens, + * then the original amount is reduced by the difference in the adjusted amount + * and the original amount. The actual tokens and the actual adjusted amount + * are then recalculated. The minimum of the original and the actual + * adjusted amount is returned. + */ +std::pair +adjustAssetInByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee); +std::pair +adjustAssetOutByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee); + +/** Find a fraction of tokens after the tokens are adjusted. The fraction + * is used to adjust equal deposit/withdraw amount. + */ +Number +adjustFracByTokens( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& tokens, + Number const& frac); + } // namespace ripple #endif // RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED diff --git a/src/xrpld/app/misc/AMMUtils.h b/src/xrpld/app/misc/AMMUtils.h index 52fe819a28..a3d6ba39e8 100644 --- a/src/xrpld/app/misc/AMMUtils.h +++ b/src/xrpld/app/misc/AMMUtils.h @@ -123,6 +123,17 @@ isOnlyLiquidityProvider( Issue const& ammIssue, AccountID const& lpAccount); +/** Due to rounding, the LPTokenBalance of the last LP might + * not match the LP's trustline balance. If it's within the tolerance, + * update LPTokenBalance to match the LP's trustline balance. + */ +Expected +verifyAndAdjustLPTokenBalance( + Sandbox& sb, + STAmount const& lpTokens, + std::shared_ptr& ammSle, + AccountID const& account); + } // namespace ripple #endif // RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED diff --git a/src/xrpld/app/misc/detail/AMMHelpers.cpp b/src/xrpld/app/misc/detail/AMMHelpers.cpp index 76b977e9a9..644482bcc9 100644 --- a/src/xrpld/app/misc/detail/AMMHelpers.cpp +++ b/src/xrpld/app/misc/detail/AMMHelpers.cpp @@ -27,6 +27,9 @@ ammLPTokens( STAmount const& asset2, Issue const& lptIssue) { + // AMM invariant: sqrt(asset1 * asset2) >= LPTokensBalance + auto const rounding = Number::downward; + NumberRoundModeGuard g(rounding); auto const tokens = root2(asset1 * asset2); return toSTAmount(lptIssue, tokens); } @@ -38,7 +41,7 @@ ammLPTokens( * where f1 = 1 - tfee, f2 = (1 - tfee/2)/f1 */ STAmount -lpTokensIn( +lpTokensOut( STAmount const& asset1Balance, STAmount const& asset1Deposit, STAmount const& lptAMMBalance, @@ -48,8 +51,10 @@ lpTokensIn( auto const f2 = feeMultHalf(tfee) / f1; Number const r = asset1Deposit / asset1Balance; auto const c = root2(f2 * f2 + r / f1) - f2; - auto const t = lptAMMBalance * (r - c) / (1 + c); - return toSTAmount(lptAMMBalance.issue(), t); + + // minimize tokens out + auto const frac = (r - c) / (1 + c); + return multiply(lptAMMBalance, frac, Number::downward); } /* Equation 4 solves equation 3 for b: @@ -78,8 +83,10 @@ ammAssetIn( auto const a = 1 / (t2 * t2); auto const b = 2 * d / t2 - 1 / f1; auto const c = d * d - f2 * f2; - return toSTAmount( - asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c)); + + // maximize deposit + auto const frac = solveQuadraticEq(a, b, c); + return multiply(asset1Balance, frac, Number::upward); } /* Equation 7: @@ -87,7 +94,7 @@ ammAssetIn( * where R = b/B, c = R*fee + 2 - fee */ STAmount -lpTokensOut( +lpTokensIn( STAmount const& asset1Balance, STAmount const& asset1Withdraw, STAmount const& lptAMMBalance, @@ -96,8 +103,10 @@ lpTokensOut( Number const fr = asset1Withdraw / asset1Balance; auto const f1 = getFee(tfee); auto const c = fr * f1 + 2 - f1; - auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2; - return toSTAmount(lptAMMBalance.issue(), t); + + // maximize tokens in + auto const frac = (c - root2(c * c - 4 * fr)) / 2; + return multiply(lptAMMBalance, frac, Number::upward); } /* Equation 8 solves equation 7 for b: @@ -111,7 +120,7 @@ lpTokensOut( * R = (t1**2 + t1*(f - 2)) / (t1*f - 1) */ STAmount -withdrawByTokens( +ammAssetOut( STAmount const& assetBalance, STAmount const& lptAMMBalance, STAmount const& lpTokens, @@ -119,8 +128,10 @@ withdrawByTokens( { auto const f = getFee(tfee); Number const t1 = lpTokens / lptAMMBalance; - auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); - return toSTAmount(assetBalance.issue(), b); + + // minimize withdraw + auto const frac = (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); + return multiply(assetBalance, frac, Number::downward); } Number @@ -133,12 +144,12 @@ STAmount adjustLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, - bool isDeposit) + IsDeposit isDeposit) { // Force rounding downward to ensure adjusted tokens are less or equal // to requested tokens. saveNumberRoundMode rm(Number::setround(Number::rounding_mode::downward)); - if (isDeposit) + if (isDeposit == IsDeposit::Yes) return (lptAMMBalance + lpTokens) - lptAMMBalance; return (lpTokens - lptAMMBalance) + lptAMMBalance; } @@ -151,47 +162,10 @@ adjustAmountsByLPTokens( STAmount const& lptAMMBalance, STAmount const& lpTokens, std::uint16_t tfee, - bool isDeposit) + IsDeposit isDeposit) { - auto const lpTokensActual = - adjustLPTokens(lptAMMBalance, lpTokens, isDeposit); - - if (lpTokensActual == beast::zero) - { - auto const amount2Opt = - amount2 ? std::make_optional(STAmount{}) : std::nullopt; - return std::make_tuple(STAmount{}, amount2Opt, lpTokensActual); - } - - if (lpTokensActual < lpTokens) - { - // Equal trade - if (amount2) - { - Number const fr = lpTokensActual / lpTokens; - auto const amountActual = toSTAmount(amount.issue(), fr * amount); - auto const amount2Actual = - toSTAmount(amount2->issue(), fr * *amount2); - return std::make_tuple(amountActual, amount2Actual, lpTokensActual); - } - - // Single trade - auto const amountActual = [&]() { - if (isDeposit) - return ammAssetIn( - amountBalance, lptAMMBalance, lpTokensActual, tfee); - return withdrawByTokens( - amountBalance, lptAMMBalance, lpTokensActual, tfee); - }(); - - return std::make_tuple(amountActual, std::nullopt, lpTokensActual); - } - - XRPL_ASSERT( - lpTokensActual == lpTokens, - "ripple::adjustAmountsByLPTokens : LP tokens match actual"); - - return {amount, amount2, lpTokensActual}; + // AMMv1_3 amendment adjusts tokens and amounts in deposit/withdraw + return std::make_tuple(amount, amount2, lpTokens); } Number @@ -215,4 +189,117 @@ solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c) return (2 * c) / (-b + root2(d)); } +STAmount +multiply(STAmount const& amount, Number const& frac, Number::rounding_mode rm) +{ + NumberRoundModeGuard g(rm); + auto const t = amount * frac; + return toSTAmount(amount.issue(), t, rm); +} + +STAmount +getRoundedAsset( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& balance, + std::function&& productCb, + IsDeposit isDeposit) +{ + auto const rm = detail::getAssetRounding(isDeposit); + if (isDeposit == IsDeposit::Yes) + return multiply(balance, productCb(), rm); + NumberRoundModeGuard g(rm); + return toSTAmount(balance.issue(), productCb(), rm); +} + +STAmount +getRoundedLPTokens( + Rules const& rules, + STAmount const& balance, + Number const& frac, + IsDeposit isDeposit) +{ + auto const rm = detail::getLPTokenRounding(isDeposit); + auto const tokens = multiply(balance, frac, rm); + return adjustLPTokens(balance, tokens, isDeposit); +} + +STAmount +getRoundedLPTokens( + Rules const& rules, + std::function&& noRoundCb, + STAmount const& lptAMMBalance, + std::function&& productCb, + IsDeposit isDeposit) +{ + auto const tokens = [&] { + auto const rm = detail::getLPTokenRounding(isDeposit); + if (isDeposit == IsDeposit::Yes) + { + NumberRoundModeGuard g(rm); + return toSTAmount(lptAMMBalance.issue(), productCb(), rm); + } + return multiply(lptAMMBalance, productCb(), rm); + }(); + return adjustLPTokens(lptAMMBalance, tokens, isDeposit); +} + +std::pair +adjustAssetInByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee) +{ + auto assetAdj = ammAssetIn(balance, lptAMMBalance, tokens, tfee); + auto tokensAdj = tokens; + // Rounding didn't work the right way. + // Try to adjust the original deposit amount by difference + // in adjust and original amount. Then adjust tokens and deposit amount. + if (assetAdj > amount) + { + auto const adjAmount = amount - (assetAdj - amount); + auto const t = lpTokensOut(balance, adjAmount, lptAMMBalance, tfee); + tokensAdj = adjustLPTokens(lptAMMBalance, t, IsDeposit::Yes); + assetAdj = ammAssetIn(balance, lptAMMBalance, tokensAdj, tfee); + } + return {tokensAdj, std::min(amount, assetAdj)}; +} + +std::pair +adjustAssetOutByTokens( + Rules const& rules, + STAmount const& balance, + STAmount const& amount, + STAmount const& lptAMMBalance, + STAmount const& tokens, + std::uint16_t tfee) +{ + auto assetAdj = ammAssetOut(balance, lptAMMBalance, tokens, tfee); + auto tokensAdj = tokens; + // Rounding didn't work the right way. + // Try to adjust the original deposit amount by difference + // in adjust and original amount. Then adjust tokens and deposit amount. + if (assetAdj > amount) + { + auto const adjAmount = amount - (assetAdj - amount); + auto const t = lpTokensIn(balance, adjAmount, lptAMMBalance, tfee); + tokensAdj = adjustLPTokens(lptAMMBalance, t, IsDeposit::No); + assetAdj = ammAssetOut(balance, lptAMMBalance, tokensAdj, tfee); + } + return {tokensAdj, std::min(amount, assetAdj)}; +} + +Number +adjustFracByTokens( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& tokens, + Number const& frac) +{ + return tokens / lptAMMBalance; +} + } // namespace ripple diff --git a/src/xrpld/app/misc/detail/AMMUtils.cpp b/src/xrpld/app/misc/detail/AMMUtils.cpp index 10a918098e..8a6917b8cc 100644 --- a/src/xrpld/app/misc/detail/AMMUtils.cpp +++ b/src/xrpld/app/misc/detail/AMMUtils.cpp @@ -16,6 +16,8 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ //============================================================================== + +#include #include #include #include @@ -462,4 +464,32 @@ isOnlyLiquidityProvider( return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE } +Expected +verifyAndAdjustLPTokenBalance( + Sandbox& sb, + STAmount const& lpTokens, + std::shared_ptr& ammSle, + AccountID const& account) +{ + if (auto const res = isOnlyLiquidityProvider(sb, lpTokens.issue(), account); + !res) + return Unexpected(res.error()); + else if (res.value()) + { + if (withinRelativeDistance( + lpTokens, + ammSle->getFieldAmount(sfLPTokenBalance), + Number{1, -3})) + { + ammSle->setFieldAmount(sfLPTokenBalance, lpTokens); + sb.update(ammSle); + } + else + { + return Unexpected(tecAMM_INVALID_TOKENS); + } + } + return true; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/AMMBid.cpp b/src/xrpld/app/tx/detail/AMMBid.cpp index e8a14c1492..ee89432766 100644 --- a/src/xrpld/app/tx/detail/AMMBid.cpp +++ b/src/xrpld/app/tx/detail/AMMBid.cpp @@ -79,6 +79,21 @@ AMMBid::preflight(PreflightContext const& ctx) JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts."; return temMALFORMED; } + else + { + AccountID account = ctx.tx[sfAccount]; + std::set unique; + for (auto const& obj : authAccounts) + { + auto authAccount = obj[sfAccount]; + if (authAccount == account || unique.contains(authAccount)) + { + JLOG(ctx.j.debug()) << "AMM Bid: Invalid auth.account."; + return temMALFORMED; + } + unique.insert(authAccount); + } + } } return preflight2(ctx); @@ -233,7 +248,9 @@ applyBid( auctionSlot.makeFieldAbsent(sfAuthAccounts); // Burn the remaining bid amount auto const saBurn = adjustLPTokens( - lptAMMBalance, toSTAmount(lptAMMBalance.issue(), burn), false); + lptAMMBalance, + toSTAmount(lptAMMBalance.issue(), burn), + IsDeposit::No); if (saBurn >= lptAMMBalance) { // This error case should never occur. diff --git a/src/xrpld/app/tx/detail/AMMClawback.cpp b/src/xrpld/app/tx/detail/AMMClawback.cpp index 162224ff91..4287067cf0 100644 --- a/src/xrpld/app/tx/detail/AMMClawback.cpp +++ b/src/xrpld/app/tx/detail/AMMClawback.cpp @@ -151,6 +151,17 @@ AMMClawback::applyGuts(Sandbox& sb) if (!accountSle) return tecINTERNAL; // LCOV_EXCL_LINE + // retrieve LP token balance inside the amendment gate to avoid + // inconsistent error behavior + auto const lpTokenBalance = ammLPHolds(sb, *ammSle, holder, j_); + if (lpTokenBalance == beast::zero) + return tecAMM_BALANCE; + + if (auto const res = + verifyAndAdjustLPTokenBalance(sb, lpTokenBalance, ammSle, holder); + !res) + return res.error(); // LCOV_EXCL_LINE + auto const expected = ammHolds( sb, *ammSle, @@ -248,10 +259,11 @@ AMMClawback::equalWithdrawMatchingOneAmount( STAmount const& amount) { auto frac = Number{amount} / amountBalance; - auto const amount2Withdraw = amount2Balance * frac; + auto amount2Withdraw = amount2Balance * frac; auto const lpTokensWithdraw = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); + if (lpTokensWithdraw > holdLPtokens) // if lptoken balance less than what the issuer intended to clawback, // clawback all the tokens. Because we are doing a two-asset withdrawal, @@ -272,18 +284,33 @@ AMMClawback::equalWithdrawMatchingOneAmount( mPriorBalance, ctx_.journal); - // Because we are doing a two-asset withdrawal, - // tfee is actually not used, so pass tfee as 0. + auto const& rules = sb.rules(); + + auto tokensAdj = + getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit::No); + + // LCOV_EXCL_START + if (tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, std::nullopt}; + // LCOV_EXCL_STOP + + frac = adjustFracByTokens(rules, lptAMMBalance, tokensAdj, frac); + auto amount2Rounded = + getRoundedAsset(rules, amount2Balance, frac, IsDeposit::No); + + auto amountRounded = + getRoundedAsset(rules, amountBalance, frac, IsDeposit::No); + return AMMWithdraw::withdraw( sb, ammSle, ammAccount, holder, amountBalance, - amount, - toSTAmount(amount2Balance.issue(), amount2Withdraw), + amountRounded, + amount2Rounded, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + tokensAdj, 0, FreezeHandling::fhIGNORE_FREEZE, WithdrawAll::No, diff --git a/src/xrpld/app/tx/detail/AMMDeposit.cpp b/src/xrpld/app/tx/detail/AMMDeposit.cpp index 675f560098..0dab0d68be 100644 --- a/src/xrpld/app/tx/detail/AMMDeposit.cpp +++ b/src/xrpld/app/tx/detail/AMMDeposit.cpp @@ -545,7 +545,7 @@ AMMDeposit::deposit( lptAMMBalance, lpTokensDeposit, tfee, - true); + IsDeposit::Yes); if (lpTokensDepositActual <= beast::zero) { @@ -628,6 +628,15 @@ AMMDeposit::deposit( return {tesSUCCESS, lptAMMBalance + lpTokensDepositActual}; } +static STAmount +adjustLPTokensOut( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& lpTokensDeposit) +{ + return adjustLPTokens(lptAMMBalance, lpTokensDeposit, IsDeposit::Yes); +} + /** Proportional deposit of pools assets in exchange for the specified * amount of LPTokens. */ @@ -645,16 +654,25 @@ AMMDeposit::equalDepositTokens( { try { + auto const tokensAdj = + adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit); + if (tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; auto const frac = - divide(lpTokensDeposit, lptAMMBalance, lptAMMBalance.issue()); + divide(tokensAdj, lptAMMBalance, lptAMMBalance.issue()); + // amounts factor in the adjusted tokens + auto const amountDeposit = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::Yes); + auto const amount2Deposit = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::Yes); return deposit( view, ammAccount, amountBalance, - multiply(amountBalance, frac, amountBalance.issue()), - multiply(amount2Balance, frac, amount2Balance.issue()), + amountDeposit, + amount2Deposit, lptAMMBalance, - lpTokensDeposit, + tokensAdj, depositMin, deposit2Min, std::nullopt, @@ -711,37 +729,49 @@ AMMDeposit::equalDepositLimit( std::uint16_t tfee) { auto frac = Number{amount} / amountBalance; - auto tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); - if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; - auto const amount2Deposit = amount2Balance * frac; + auto tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes); + if (tokensAdj == beast::zero) + { + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + auto const amount2Deposit = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::Yes); if (amount2Deposit <= amount2) return deposit( view, ammAccount, amountBalance, amount, - toSTAmount(amount2Balance.issue(), amount2Deposit), + amount2Deposit, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, lpTokensDepositMin, tfee); frac = Number{amount2} / amount2Balance; - tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac); - if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; - auto const amountDeposit = amountBalance * frac; + tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes); + if (tokensAdj == beast::zero) + { + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + } + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + auto const amountDeposit = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::Yes); if (amountDeposit <= amount) return deposit( view, ammAccount, amountBalance, - toSTAmount(amountBalance.issue(), amountDeposit), + amountDeposit, amount2, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, lpTokensDepositMin, @@ -767,17 +797,27 @@ AMMDeposit::singleDeposit( std::optional const& lpTokensDepositMin, std::uint16_t tfee) { - auto const tokens = lpTokensIn(amountBalance, amount, lptAMMBalance, tfee); + auto const tokens = adjustLPTokensOut( + view.rules(), + lptAMMBalance, + lpTokensOut(amountBalance, amount, lptAMMBalance, tfee)); if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; + { + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( + view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); + if (tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return deposit( view, ammAccount, amountBalance, - amount, + amountDepositAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, lpTokensDepositMin, @@ -801,8 +841,13 @@ AMMDeposit::singleDepositTokens( STAmount const& lpTokensDeposit, std::uint16_t tfee) { + auto const tokensAdj = + adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit); + if (tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + // the adjusted tokens are factored in auto const amountDeposit = - ammAssetIn(amountBalance, lptAMMBalance, lpTokensDeposit, tfee); + ammAssetIn(amountBalance, lptAMMBalance, tokensAdj, tfee); if (amountDeposit > amount) return {tecAMM_FAILED, STAmount{}}; return deposit( @@ -812,7 +857,7 @@ AMMDeposit::singleDepositTokens( amountDeposit, std::nullopt, lptAMMBalance, - lpTokensDeposit, + tokensAdj, std::nullopt, std::nullopt, std::nullopt, @@ -856,20 +901,29 @@ AMMDeposit::singleDepositEPrice( { if (amount != beast::zero) { - auto const tokens = - lpTokensIn(amountBalance, amount, lptAMMBalance, tfee); + auto const tokens = adjustLPTokensOut( + view.rules(), + lptAMMBalance, + lpTokensOut(amountBalance, amount, lptAMMBalance, tfee)); if (tokens <= beast::zero) - return {tecAMM_FAILED, STAmount{}}; - auto const ep = Number{amount} / tokens; + { + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( + view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); + if (tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + auto const ep = Number{amountDepositAdj} / tokensAdj; if (ep <= ePrice) return deposit( view, ammAccount, amountBalance, - amount, + amountDepositAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, std::nullopt, @@ -900,21 +954,37 @@ AMMDeposit::singleDepositEPrice( auto const a1 = c * c; auto const b1 = c * c * f2 * f2 + 2 * c - d * d; auto const c1 = 2 * c * f2 * f2 + 1 - 2 * d * f2; - auto const amountDeposit = toSTAmount( - amountBalance.issue(), - f1 * amountBalance * solveQuadraticEq(a1, b1, c1)); + auto amtNoRoundCb = [&] { + return f1 * amountBalance * solveQuadraticEq(a1, b1, c1); + }; + auto amtProdCb = [&] { return f1 * solveQuadraticEq(a1, b1, c1); }; + auto const amountDeposit = getRoundedAsset( + view.rules(), amtNoRoundCb, amountBalance, amtProdCb, IsDeposit::Yes); if (amountDeposit <= beast::zero) return {tecAMM_FAILED, STAmount{}}; - auto const tokens = - toSTAmount(lptAMMBalance.issue(), amountDeposit / ePrice); + auto tokNoRoundCb = [&] { return amountDeposit / ePrice; }; + auto tokProdCb = [&] { return amountDeposit / ePrice; }; + auto const tokens = getRoundedLPTokens( + view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::Yes); + // factor in the adjusted tokens + auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens( + view.rules(), + amountBalance, + amountDeposit, + lptAMMBalance, + tokens, + tfee); + if (tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + return deposit( view, ammAccount, amountBalance, - amountDeposit, + amountDepositAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, std::nullopt, std::nullopt, std::nullopt, diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.cpp b/src/xrpld/app/tx/detail/AMMWithdraw.cpp index 4fccec937b..c6a464f21b 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.cpp +++ b/src/xrpld/app/tx/detail/AMMWithdraw.cpp @@ -313,24 +313,9 @@ AMMWithdraw::applyGuts(Sandbox& sb) // might not match the LP's trustline balance if (auto const res = - isOnlyLiquidityProvider(sb, lpTokens.issue(), account_); + verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, account_); !res) return {res.error(), false}; - else if (res.value()) - { - if (withinRelativeDistance( - lpTokens, - ammSle->getFieldAmount(sfLPTokenBalance), - Number{1, -3})) - { - ammSle->setFieldAmount(sfLPTokenBalance, lpTokens); - sb.update(ammSle); - } - else - { - return {tecAMM_INVALID_TOKENS, false}; - } - } auto const tfee = getTradingFee(ctx_.view(), *ammSle, account_); @@ -523,7 +508,7 @@ AMMWithdraw::withdraw( lpTokensAMMBalance, lpTokensWithdraw, tfee, - false); + IsDeposit::No); return std::make_tuple( amountWithdraw, amount2Withdraw, lpTokensWithdraw); }(); @@ -682,6 +667,20 @@ AMMWithdraw::withdraw( amount2WithdrawActual); } +static STAmount +adjustLPTokensIn( + Rules const& rules, + STAmount const& lptAMMBalance, + STAmount const& lpTokensWithdraw, + WithdrawAll withdrawAll) +{ + if (withdrawAll == WithdrawAll::Yes) + return lpTokensWithdraw; + return adjustLPTokens(lptAMMBalance, lpTokensWithdraw, IsDeposit::No); +} + +/** Proportional withdrawal of pool assets for the amount of LPTokens. + */ std::pair AMMWithdraw::equalWithdrawTokens( Sandbox& view, @@ -785,16 +784,22 @@ AMMWithdraw::equalWithdrawTokens( journal); } - auto const frac = divide(lpTokensWithdraw, lptAMMBalance, noIssue()); - auto const withdrawAmount = - multiply(amountBalance, frac, amountBalance.issue()); - auto const withdraw2Amount = - multiply(amount2Balance, frac, amount2Balance.issue()); + auto const tokensAdj = adjustLPTokensIn( + view.rules(), lptAMMBalance, lpTokensWithdraw, withdrawAll); + if (tokensAdj == beast::zero) + return { + tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, std::nullopt}; + // the adjusted tokens are factored in + auto const frac = divide(tokensAdj, lptAMMBalance, noIssue()); + auto const amountWithdraw = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); + auto const amount2Withdraw = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); // LP is making equal withdrawal by tokens but the requested amount // of LP tokens is likely too small and results in one-sided pool // withdrawal due to round off. Fail so the user withdraws // more tokens. - if (withdrawAmount == beast::zero || withdraw2Amount == beast::zero) + if (amountWithdraw == beast::zero || amount2Withdraw == beast::zero) return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}}; return withdraw( @@ -803,10 +808,10 @@ AMMWithdraw::equalWithdrawTokens( ammAccount, account, amountBalance, - withdrawAmount, - withdraw2Amount, + amountWithdraw, + amount2Withdraw, lptAMMBalance, - lpTokensWithdraw, + tokensAdj, tfee, freezeHanding, withdrawAll, @@ -861,7 +866,16 @@ AMMWithdraw::equalWithdrawLimit( std::uint16_t tfee) { auto frac = Number{amount} / amountBalance; - auto const amount2Withdraw = amount2Balance * frac; + auto amount2Withdraw = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); + auto tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No); + if (tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + amount2Withdraw = + getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No); if (amount2Withdraw <= amount2) { return withdraw( @@ -870,26 +884,34 @@ AMMWithdraw::equalWithdrawLimit( ammAccount, amountBalance, amount, - toSTAmount(amount2.issue(), amount2Withdraw), + amount2Withdraw, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + tokensAdj, tfee); } frac = Number{amount2} / amount2Balance; - auto const amountWithdraw = amountBalance * frac; - XRPL_ASSERT( - amountWithdraw <= amount, - "ripple::AMMWithdraw::equalWithdrawLimit : maximum amountWithdraw"); + auto amountWithdraw = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); + tokensAdj = + getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No); + if (tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE + // factor in the adjusted tokens + frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac); + amountWithdraw = + getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No); + if (amountWithdraw > amount) + return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, ammSle, ammAccount, amountBalance, - toSTAmount(amount.issue(), amountWithdraw), + amountWithdraw, amount2, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac), + tokensAdj, tfee); } @@ -908,19 +930,29 @@ AMMWithdraw::singleWithdraw( STAmount const& amount, std::uint16_t tfee) { - auto const tokens = lpTokensOut(amountBalance, amount, lptAMMBalance, tfee); + auto const tokens = adjustLPTokensIn( + view.rules(), + lptAMMBalance, + lpTokensIn(amountBalance, amount, lptAMMBalance, tfee), + isWithdrawAll(ctx_.tx)); if (tokens == beast::zero) - return {tecAMM_FAILED, STAmount{}}; - + { + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + // factor in the adjusted tokens + auto const [tokensAdj, amountWithdrawAdj] = adjustAssetOutByTokens( + view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee); + if (tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE return withdraw( view, ammSle, ammAccount, amountBalance, - amount, + amountWithdrawAdj, std::nullopt, lptAMMBalance, - tokens, + tokensAdj, tfee); } @@ -945,8 +977,13 @@ AMMWithdraw::singleWithdrawTokens( STAmount const& lpTokensWithdraw, std::uint16_t tfee) { + auto const tokensAdj = adjustLPTokensIn( + view.rules(), lptAMMBalance, lpTokensWithdraw, isWithdrawAll(ctx_.tx)); + if (tokensAdj == beast::zero) + return {tecAMM_INVALID_TOKENS, STAmount{}}; + // the adjusted tokens are factored in auto const amountWithdraw = - withdrawByTokens(amountBalance, lptAMMBalance, lpTokensWithdraw, tfee); + ammAssetOut(amountBalance, lptAMMBalance, tokensAdj, tfee); if (amount == beast::zero || amountWithdraw >= amount) { return withdraw( @@ -957,7 +994,7 @@ AMMWithdraw::singleWithdrawTokens( amountWithdraw, std::nullopt, lptAMMBalance, - lpTokensWithdraw, + tokensAdj, tfee); } @@ -1006,11 +1043,24 @@ AMMWithdraw::singleWithdrawEPrice( // t = T*(T + A*E*(f - 2))/(T*f - A*E) Number const ae = amountBalance * ePrice; auto const f = getFee(tfee); - auto const tokens = lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / - (lptAMMBalance * f - ae); - if (tokens <= 0) - return {tecAMM_FAILED, STAmount{}}; - auto const amountWithdraw = toSTAmount(amount.issue(), tokens / ePrice); + auto tokNoRoundCb = [&] { + return lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / + (lptAMMBalance * f - ae); + }; + auto tokProdCb = [&] { + return (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae); + }; + auto const tokensAdj = getRoundedLPTokens( + view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::No); + if (tokensAdj <= beast::zero) + { + return {tecAMM_INVALID_TOKENS, STAmount{}}; + } + auto amtNoRoundCb = [&] { return tokensAdj / ePrice; }; + auto amtProdCb = [&] { return tokensAdj / ePrice; }; + // the adjusted tokens are factored in + auto const amountWithdraw = getRoundedAsset( + view.rules(), amtNoRoundCb, amount, amtProdCb, IsDeposit::No); if (amount == beast::zero || amountWithdraw >= amount) { return withdraw( @@ -1021,7 +1071,7 @@ AMMWithdraw::singleWithdrawEPrice( amountWithdraw, std::nullopt, lptAMMBalance, - toSTAmount(lptAMMBalance.issue(), tokens), + tokensAdj, tfee); } diff --git a/src/xrpld/app/tx/detail/AMMWithdraw.h b/src/xrpld/app/tx/detail/AMMWithdraw.h index ae9328cb05..1de91fd787 100644 --- a/src/xrpld/app/tx/detail/AMMWithdraw.h +++ b/src/xrpld/app/tx/detail/AMMWithdraw.h @@ -301,7 +301,7 @@ class AMMWithdraw : public Transactor std::uint16_t tfee); /** Check from the flags if it's withdraw all */ - WithdrawAll + static WithdrawAll isWithdrawAll(STTx const& tx); }; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index aca956dad6..85b899b900 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -17,6 +17,9 @@ */ //============================================================================== +#include +#include +#include #include #include @@ -1674,4 +1677,309 @@ ValidPermissionedDomain::finalize( (sleStatus_[1] ? check(*sleStatus_[1], j) : true); } +void +ValidAMM::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (isDelete) + return; + + if (after) + { + auto const type = after->getType(); + // AMM object changed + if (type == ltAMM) + { + ammAccount_ = after->getAccountID(sfAccount); + lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance); + } + // AMM pool changed + else if ( + (type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) || + (type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID))) + { + ammPoolChanged_ = true; + } + } + + if (before) + { + // AMM object changed + if (before->getType() == ltAMM) + { + lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance); + } + } +} + +static bool +validBalances( + STAmount const& amount, + STAmount const& amount2, + STAmount const& lptAMMBalance, + ValidAMM::ZeroAllowed zeroAllowed) +{ + bool const positive = amount > beast::zero && amount2 > beast::zero && + lptAMMBalance > beast::zero; + if (zeroAllowed == ValidAMM::ZeroAllowed::Yes) + return positive || + (amount == beast::zero && amount2 == beast::zero && + lptAMMBalance == beast::zero); + return positive; +} + +bool +ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const +{ + if (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_) + { + // LPTokens and the pool can not change on vote + // LCOV_EXCL_START + JLOG(j.error()) << "AMMVote invariant failed: " + << lptAMMBalanceBefore_.value_or(STAmount{}) << " " + << lptAMMBalanceAfter_.value_or(STAmount{}) << " " + << ammPoolChanged_; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const +{ + if (ammPoolChanged_) + { + // The pool can not change on bid + // LCOV_EXCL_START + JLOG(j.error()) << "AMMBid invariant failed: pool changed"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + // LPTokens are burnt, therefore there should be fewer LPTokens + else if ( + lptAMMBalanceBefore_ && lptAMMBalanceAfter_ && + (*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || + *lptAMMBalanceAfter_ <= beast::zero)) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_ + << " " << *lptAMMBalanceAfter_; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeCreate( + STTx const& tx, + ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "AMMCreate invariant failed: AMM object is not created"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + else + { + auto const [amount, amount2] = ammPoolHolds( + view, + *ammAccount_, + tx[sfAmount].get(), + tx[sfAmount2].get(), + fhIGNORE_FREEZE, + j); + // Create invariant: + // sqrt(amount * amount2) == LPTokens + // all balances are greater than zero + if (!validBalances( + amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) || + ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) != + *lptAMMBalanceAfter_) + { + JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " " + << amount2 << " " << *lptAMMBalanceAfter_; + if (enforce) + return false; + } + } + + return true; +} + +bool +ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const +{ + if (ammAccount_) + { + // LCOV_EXCL_START + std::string const msg = (res == tesSUCCESS) + ? "AMM object is not deleted on tesSUCCESS" + : "AMM object is changed on tecINCOMPLETE"; + JLOG(j.error()) << "AMMDelete invariant failed: " << msg; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const +{ + if (ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMM swap invariant failed: AMM object changed"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + + return true; +} + +bool +ValidAMM::generalInvariant( + ripple::STTx const& tx, + ripple::ReadView const& view, + ZeroAllowed zeroAllowed, + beast::Journal const& j) const +{ + auto const [amount, amount2] = ammPoolHolds( + view, + *ammAccount_, + tx[sfAsset].get(), + tx[sfAsset2].get(), + fhIGNORE_FREEZE, + j); + // Deposit and Withdrawal invariant: + // sqrt(amount * amount2) >= LPTokens + // all balances are greater than zero + // unless on last withdrawal + auto const poolProductMean = root2(amount * amount2); + bool const nonNegativeBalances = + validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed); + bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_; + // Allow for a small relative error if strongInvariantCheck fails + auto weakInvariantCheck = [&]() { + return *lptAMMBalanceAfter_ != beast::zero && + withinRelativeDistance( + poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11}); + }; + if (!nonNegativeBalances || + (!strongInvariantCheck && !weakInvariantCheck())) + { + JLOG(j.error()) << "AMM " << tx.getTxnType() << " invariant failed: " + << tx.getHash(HashPrefix::transactionID) << " " + << ammPoolChanged_ << " " << amount << " " << amount2 + << " " << poolProductMean << " " + << lptAMMBalanceAfter_->getText() << " " + << ((*lptAMMBalanceAfter_ == beast::zero) + ? Number{1} + : ((*lptAMMBalanceAfter_ - poolProductMean) / + poolProductMean)); + return false; + } + + return true; +} + +bool +ValidAMM::finalizeDeposit( + ripple::STTx const& tx, + ripple::ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // LCOV_EXCL_START + JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted"; + if (enforce) + return false; + // LCOV_EXCL_STOP + } + else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce) + return false; + + return true; +} + +bool +ValidAMM::finalizeWithdraw( + ripple::STTx const& tx, + ripple::ReadView const& view, + bool enforce, + beast::Journal const& j) const +{ + if (!ammAccount_) + { + // Last Withdraw or Clawback deleted AMM + } + else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j)) + { + if (enforce) + return false; + } + + return true; +} + +bool +ValidAMM::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + // Delete may return tecINCOMPLETE if there are too many + // trustlines to delete. + if (result != tesSUCCESS && result != tecINCOMPLETE) + return true; + + bool const enforce = true; // view.rules().enabled(fixAMMv1_3); + + switch (tx.getTxnType()) + { + case ttAMM_CREATE: + return finalizeCreate(tx, view, enforce, j); + case ttAMM_DEPOSIT: + return finalizeDeposit(tx, view, enforce, j); + case ttAMM_CLAWBACK: + case ttAMM_WITHDRAW: + return finalizeWithdraw(tx, view, enforce, j); + case ttAMM_BID: + return finalizeBid(enforce, j); + case ttAMM_VOTE: + return finalizeVote(enforce, j); + case ttAMM_DELETE: + return finalizeDelete(enforce, result, j); + case ttCHECK_CASH: + case ttOFFER_CREATE: + case ttPAYMENT: + return finalizeDEX(enforce, j); + default: + break; + } + + return true; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 364ece0364..e854fd649b 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -617,6 +617,69 @@ class ValidPermissionedDomain beast::Journal const&); }; +class ValidAMM +{ + std::optional ammAccount_; + std::optional lptAMMBalanceAfter_; + std::optional lptAMMBalanceBefore_; + bool ammPoolChanged_; + +public: + enum class ZeroAllowed : bool { No = false, Yes = true }; + + ValidAMM() : ammPoolChanged_{false} + { + } + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); + +private: + bool + finalizeBid(bool enforce, beast::Journal const&) const; + bool + finalizeVote(bool enforce, beast::Journal const&) const; + bool + finalizeCreate( + STTx const&, + ReadView const&, + bool enforce, + beast::Journal const&) const; + bool + finalizeDelete(bool enforce, TER res, beast::Journal const&) const; + bool + finalizeDeposit( + STTx const&, + ReadView const&, + bool enforce, + beast::Journal const&) const; + // Includes clawback + bool + finalizeWithdraw( + STTx const&, + ReadView const&, + bool enforce, + beast::Journal const&) const; + bool + finalizeDEX(bool enforce, beast::Journal const&) const; + bool + generalInvariant( + STTx const&, + ReadView const&, + ZeroAllowed zeroAllowed, + beast::Journal const&) const; +}; + // additional invariant checks can be declared above and then added to this // tuple using InvariantChecks = std::tuple< @@ -636,7 +699,8 @@ using InvariantChecks = std::tuple< NFTokenCountTracking, ValidClawback, ValidMPTIssuance, - ValidPermissionedDomain>; + ValidPermissionedDomain, + ValidAMM>; /** * @brief get a tuple of all invariant checks diff --git a/src/xrpld/app/tx/detail/Offer.h b/src/xrpld/app/tx/detail/Offer.h index 23129952c3..0ebe5c08cc 100644 --- a/src/xrpld/app/tx/detail/Offer.h +++ b/src/xrpld/app/tx/detail/Offer.h @@ -21,6 +21,8 @@ #define RIPPLE_APP_BOOK_OFFER_H_INCLUDED #include + +#include #include #include #include @@ -169,8 +171,21 @@ class TOffer : private TOfferBase * always returns true. */ bool - checkInvariant(TAmounts const&, beast::Journal j) const + checkInvariant(TAmounts const& consumed, beast::Journal j) const { + if (consumed.in > m_amounts.in || consumed.out > m_amounts.out) + { + // LCOV_EXCL_START + JLOG(j.error()) + << "AMMOffer::checkInvariant failed: consumed " + << to_string(consumed.in) << " " << to_string(consumed.out) + << " amounts " << to_string(m_amounts.in) << " " + << to_string(m_amounts.out); + + return false; + // LCOV_EXCL_STOP + } + return true; } };