Compare commits

...
Author SHA1 Message Date
tequ 04c2929883 Merge remote-tracking branch 'upstream/dev' into hook-helper-func 2026-04-27 14:17:42 +09:00
Alloy Networks cd00ed72d8 change build instructions url 2026-04-24 11:12:28 +10:00
tequ 05a3e04f2d Fix BEAST_ENHANCED_LOGGING not working and restore original behavior 2026-04-24 11:11:40 +10:00
tequ 66f7294120 Test: hint build_test_hooks.sh when hook wasm is empty in hso() 2026-04-24 11:10:46 +10:00
Nicholas Dudfield 7f6ac75617 Revert "chore: use improved levelization script with threading and argparse"
This reverts commit 5c1d7d9ae9.
2026-04-24 11:09:19 +10:00
Nicholas Dudfield 4150f0383c chore: use improved levelization script with threading and argparse 2026-04-24 11:09:19 +10:00
Nicholas Dudfield 25123b370a chore: replace levelization shell script with python
Backport of XRPLF/rippled#6325. The python version runs ~80x faster.
2026-04-24 11:09:19 +10:00
tequ f90ed41802 enable ccache direct_mode 2026-04-24 11:06:51 +10:00
tequ 8c4c158d3a output ccache configuration in release-builder 2026-04-24 11:06:51 +10:00
tequ 2d2951875d fix: typo SignersListSet 2026-04-24 11:05:20 +10:00
tequ 9bfca63574 Update util_keylet fee test 2026-04-24 11:00:31 +10:00
tequ 1ba444ae7f Updated tests to align with the changes merged into the dev branch. 2026-04-24 11:00:31 +10:00
tequ f96d9b6e51 Add tests for Hooks fee 2026-04-24 11:00:31 +10:00
tequ 8cfee6c8a3 Merge fixAMMClawbackRounding amendment into featureAMMClawback amendment 2026-02-25 19:07:45 +10:00
yinyiqian1 8673599d2b fixAMMClawbackRounding: adjust last holder's LPToken balance (#5513)
Due to rounding, the LPTokenBalance of the last LP might not match the LP's trustline balance. This was fixed for `AMMWithdraw` in `fixAMMv1_1` by adjusting the LPTokenBalance to be the same as the trustline balance. Since `AMMClawback` is also performing a withdrawal, we need to adjust LPTokenBalance as well in `AMMClawback.`

This change includes:
1. Refactored `verifyAndAdjustLPTokenBalance` function in `AMMUtils`, which both`AMMWithdraw` and `AMMClawback` call to adjust LPTokenBalance.
2. Added the unit test `testLastHolderLPTokenBalance` to test the scenario.
3. Modify the existing unit tests for `fixAMMClawbackRounding`.
2026-02-25 19:07:45 +10:00
tequ ec65e622aa Merge fixAMMv1_3 amendment into featureAMM amendment 2026-02-25 16:20:43 +10:00
Gregory Tsipenyuk 65837f49e1 fix: Add AMMv1_3 amendment (#5203)
* Add AMM bid/create/deposit/swap/withdraw/vote invariants:
  - Deposit, Withdrawal invariants: `sqrt(asset1Balance * asset2Balance) >= LPTokens`.
  - Bid: `sqrt(asset1Balance * asset2Balance) > LPTokens` and the pool balances don't change.
  - Create: `sqrt(asset1Balance * assetBalance2) == LPTokens`.
  - Swap: `asset1BalanceAfter * asset2BalanceAfter >= asset1BalanceBefore * asset2BalanceBefore`
     and `LPTokens` don't change.
  - Vote: `LPTokens` and pool balances don't change.
  - All AMM and swap transactions: amounts and tokens are greater than zero, except on withdrawal if all tokens
    are withdrawn.
* Add AMM deposit and withdraw rounding to ensure AMM invariant:
  - On deposit, tokens out are rounded downward and deposit amount is rounded upward.
  - On withdrawal, tokens in are rounded upward and withdrawal amount is rounded downward.
* Add Order Book Offer invariant to verify consumed amounts. Consumed amounts are less than the offer.
* Fix Bid validation. `AuthAccount` can't have duplicate accounts or the submitter account.
2026-02-25 16:20:43 +10:00
RichardAH e5b21f026e Merge pull request #692 from Xahau/sync-2.4.0-rebased
Sync: Ripple(d) 2.4.0
2026-02-24 16:07:09 +10:00
tequ 63096d5fbc add test 2026-01-21 12:45:05 +09:00
tequ 2e128acdcf add execution test 2026-01-21 10:52:31 +09:00
tequ 043c60b62e Update new tests 2026-01-20 20:07:26 +09:00
tequ 5dd1198e4f Merge commit '5d9071695a616e1af378142e09649abc7d0e8afa' into hook-helper-func 2026-01-20 15:46:00 +09:00
tequ 5d9071695a Add tests for Hooks fee 2026-01-20 12:12:45 +09:00
tequ ec6dc93834 Allow helper functions at Hooks 2026-01-19 16:44:23 +09:00
42 changed files with 5054 additions and 1178 deletions

No files matched your search

+2 -2
View File
@@ -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.
+6
View File
@@ -53,6 +53,9 @@ Builds/levelization/results/paths.txt
Builds/levelization/results/includes/
Builds/levelization/results/includedby/
# Python
__pycache__
# Ignore tmp directory.
tmp
@@ -126,3 +129,6 @@ generated
# Suggested in-tree build directory
/.build/
guard_checker
guard_checker.dSYM
+3 -3
View File
@@ -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`
+283
View File
@@ -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/base_uint.h> -> 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()
-130
View File
@@ -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<paths.txt # open paths.txt for input
while read -r -u 4 count level include
do
echo ${include} ${count} | tee -a includes/${level}
echo ${level} ${count} | tee -a includedby/${include}
done
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
+1 -1
View File
@@ -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
+1
View File
@@ -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 &&
+11
View File
@@ -68,6 +68,17 @@ target_link_libraries(xrpl.imports.main
$<$<BOOL:${voidstar}>: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
$<$<OR:$<CONFIG:Debug>,$<BOOL:${BEAST_ENHANCED_LOGGING}>>:BEAST_ENHANCED_LOGGING=1>
)
include(add_module)
include(target_link_modules)
+265 -31
View File
@@ -5,6 +5,7 @@
#include <memory>
#include <optional>
#include <ostream>
#include <set>
#include <stack>
#include <string>
#include <string_view>
@@ -282,7 +283,8 @@ check_guard(
* might have unforeseen consequences, without also rolling back further
* changes that are fine.
*/
uint64_t rulesVersion = 0
uint64_t rulesVersion = 0,
std::set<int>* out_callees = nullptr
)
{
@@ -492,17 +494,27 @@ check_guard(
{
REQUIRE(1);
uint64_t callee_idx = LEB();
// disallow calling of user defined functions inside a hook
// record user-defined function calls if tracking is enabled
if (callee_idx > last_import_idx)
{
GUARDLOG(hook::log::CALL_ILLEGAL)
<< "GuardCheck "
<< "Hook calls a function outside of the whitelisted "
"imports "
<< "codesec: " << codesec << " hook byte offset: " << i
<< "\n";
if (out_callees != nullptr)
{
// record the callee for call graph analysis
out_callees->insert(callee_idx);
}
else
{
// if not tracking, maintain original behavior: reject
GUARDLOG(hook::log::CALL_ILLEGAL)
<< "GuardCheck "
<< "Hook calls a function outside of the whitelisted "
"imports "
<< "codesec: " << codesec << " hook byte offset: " << i
<< "\n";
return {};
return {};
}
}
// enforce guard call limit
@@ -838,6 +850,42 @@ validateGuards(
*/
uint64_t rulesVersion = 0x00)
{
// Structure to track function call graph information
struct FunctionInfo
{
int func_idx;
std::set<int> callees; // functions this function calls
std::set<int> callers; // functions that call this function
bool has_loops; // whether this function contains loops
uint64_t local_wce; // local worst-case execution count
uint64_t total_wce; // total WCE including callees
bool wce_calculated; // whether total_wce has been computed
bool in_calculation; // for cycle detection in WCE calculation
FunctionInfo()
: func_idx(-1)
, has_loops(false)
, local_wce(0)
, total_wce(0)
, wce_calculated(false)
, in_calculation(false)
{
}
FunctionInfo(int idx, uint64_t local_wce_val, bool has_loops_val)
: func_idx(idx)
, has_loops(has_loops_val)
, local_wce(local_wce_val)
, total_wce(0)
, wce_calculated(false)
, in_calculation(false)
{
}
};
// Call graph: maps function index to its information
std::map<int, FunctionInfo> call_graph;
uint64_t byteCount = wasm.size();
// 63 bytes is the smallest possible valid hook wasm
@@ -1170,6 +1218,12 @@ validateGuards(
if (DEBUG_GUARD)
printf("Function map: func %d -> type %d\n", j, type_idx);
func_type_map[j] = type_idx;
// Step 4: Initialize FunctionInfo for each user-defined
// function func_idx starts from last_import_number + 1
int actual_func_idx = last_import_number + 1 + j;
call_graph[actual_func_idx] = FunctionInfo();
call_graph[actual_func_idx].func_idx = actual_func_idx;
}
}
@@ -1211,9 +1265,6 @@ validateGuards(
return {};
}
int64_t maxInstrCountHook = 0;
int64_t maxInstrCountCbak = 0;
// second pass... where we check all the guard function calls follow the
// guard rules minimal other validation in this pass because first pass
// caught most of it
@@ -1247,6 +1298,7 @@ validateGuards(
std::optional<
std::reference_wrapper<std::vector<uint8_t> const>>
first_signature;
bool helper_function = false;
if (auto const& usage = import_type_map.find(j);
usage != import_type_map.end())
{
@@ -1278,7 +1330,7 @@ validateGuards(
}
}
}
else if (j == hook_type_idx)
else if (j == hook_type_idx) // hook() or cbak() function type
{
// pass
}
@@ -1291,7 +1343,8 @@ validateGuards(
<< "Codesec: " << section_type << " "
<< "Local: " << j << " "
<< "Offset: " << i << "\n";
return {};
// return {};
helper_function = true;
}
int param_count = parseLeb128(wasm, i, &i);
@@ -1308,12 +1361,19 @@ validateGuards(
return {};
}
}
else if (helper_function)
{
// pass
}
else if (param_count != (*first_signature).get().size() - 1)
{
GUARDLOG(hook::log::FUNC_TYPE_INVALID)
<< "Malformed transaction. "
<< "Hook API: " << *first_name
<< " has the wrong number of parameters.\n";
<< " has the wrong number of parameters.\n"
<< "param_count: " << param_count << " "
<< "first_signature: "
<< (*first_signature).get().size() - 1 << "\n";
return {};
}
@@ -1360,6 +1420,10 @@ validateGuards(
return {};
}
}
else if (helper_function)
{
// pass
}
else if ((*first_signature).get()[k + 1] != param_type)
{
GUARDLOG(hook::log::FUNC_PARAM_INVALID)
@@ -1436,6 +1500,10 @@ validateGuards(
return {};
}
}
else if (helper_function)
{
// pass
}
else if ((*first_signature).get()[0] != result_type)
{
GUARDLOG(hook::log::FUNC_RETURN_INVALID)
@@ -1487,6 +1555,17 @@ validateGuards(
// execution to here means we are up to the actual expr for the
// codesec/function
// Step 5: Calculate actual function index and prepare callees
// tracking
int actual_func_idx = last_import_number + 1 + j;
std::set<int>* out_callees_ptr = nullptr;
// Only track callees if this function is in the call_graph
if (call_graph.find(actual_func_idx) != call_graph.end())
{
out_callees_ptr = &call_graph[actual_func_idx].callees;
}
auto valid = check_guard(
wasm,
j,
@@ -1496,33 +1575,188 @@ validateGuards(
last_import_number,
guardLog,
guardLogAccStr,
rulesVersion);
rulesVersion,
out_callees_ptr);
if (!valid)
return {};
if (hook_func_idx && *hook_func_idx == j)
maxInstrCountHook = *valid;
else if (cbak_func_idx && *cbak_func_idx == j)
maxInstrCountCbak = *valid;
else
// Step 5: Store local WCE and build bidirectional call
// relationships
if (call_graph.find(actual_func_idx) != call_graph.end())
{
if (DEBUG_GUARD)
printf(
"code section: %d not hook_func_idx: %d or "
"cbak_func_idx: %d\n",
j,
*hook_func_idx,
(cbak_func_idx ? *cbak_func_idx : -1));
// assert(false);
call_graph[actual_func_idx].local_wce = *valid;
// Build bidirectional relationships: for each callee, add
// this function as a caller
for (int callee_idx : call_graph[actual_func_idx].callees)
{
if (call_graph.find(callee_idx) != call_graph.end())
{
call_graph[callee_idx].callers.insert(
actual_func_idx);
}
}
}
// Note: We will calculate total WCE later after processing all
// functions
i = code_end;
}
}
i = next_section;
}
// execution to here means guards are installed correctly
// Step 6: Cycle detection using DFS
// Lambda function for DFS-based cycle detection
std::set<int> visited;
std::set<int> rec_stack;
std::function<bool(int)> detect_cycles_dfs = [&](int func_idx) -> bool {
if (rec_stack.find(func_idx) != rec_stack.end())
{
// Found a cycle: func_idx is already in the recursion stack
return true;
}
return std::pair<uint64_t, uint64_t>{maxInstrCountHook, maxInstrCountCbak};
if (visited.find(func_idx) != visited.end())
{
// Already visited and no cycle found from this node
return false;
}
visited.insert(func_idx);
rec_stack.insert(func_idx);
// Check all callees
if (call_graph.find(func_idx) != call_graph.end())
{
for (int callee_idx : call_graph[func_idx].callees)
{
if (detect_cycles_dfs(callee_idx))
{
return true;
}
}
}
rec_stack.erase(func_idx);
return false;
};
// Run cycle detection on all user-defined functions
for (const auto& [func_idx, func_info] : call_graph)
{
if (detect_cycles_dfs(func_idx))
{
GUARDLOG(hook::log::CALL_ILLEGAL)
<< "GuardCheck: Recursive function calls detected. "
<< "Hooks cannot contain recursive or mutually recursive "
"functions.\n";
return {};
}
}
// Step 7: Calculate total WCE for each function using bottom-up approach
// Lambda function for recursive WCE calculation with memoization
std::function<uint64_t(int)> calculate_function_wce =
[&](int func_idx) -> uint64_t {
// Check if function exists in call graph
if (call_graph.find(func_idx) == call_graph.end())
{
// This is an imported function, WCE = 0 (already accounted for)
return 0;
}
FunctionInfo& func_info = call_graph[func_idx];
// If already calculated, return cached result
if (func_info.wce_calculated)
{
return func_info.total_wce;
}
// Detect circular dependency in WCE calculation (should not happen
// after cycle detection)
if (func_info.in_calculation)
{
GUARDLOG(hook::log::CALL_ILLEGAL)
<< "GuardCheck: Internal error - circular dependency detected "
"during WCE calculation.\n";
return 0xFFFFFFFFU; // Return large value to trigger overflow error
}
func_info.in_calculation = true;
// Start with local WCE
uint64_t total = func_info.local_wce;
// Add WCE of all callees
for (int callee_idx : func_info.callees)
{
uint64_t callee_wce = calculate_function_wce(callee_idx);
// Check for overflow
if (total > 0xFFFFU || callee_wce > 0xFFFFU ||
(total + callee_wce) > 0xFFFFU)
{
func_info.in_calculation = false;
return 0xFFFFFFFFU; // Signal overflow
}
total += callee_wce;
}
func_info.total_wce = total;
func_info.wce_calculated = true;
func_info.in_calculation = false;
return total;
};
// Calculate WCE for hook and cbak functions
int64_t hook_wce_actual = 0;
int64_t cbak_wce_actual = 0;
if (hook_func_idx)
{
int actual_hook_idx = last_import_number + 1 + *hook_func_idx;
hook_wce_actual = calculate_function_wce(actual_hook_idx);
if (hook_wce_actual >= 0xFFFFU)
{
GUARDLOG(hook::log::INSTRUCTION_EXCESS)
<< "GuardCheck: hook() function exceeds maximum instruction "
"count (65535). "
<< "Total WCE including called functions: " << hook_wce_actual
<< "\n";
return {};
}
if (DEBUG_GUARD)
printf("hook() total WCE: %ld\n", hook_wce_actual);
}
if (cbak_func_idx)
{
int actual_cbak_idx = last_import_number + 1 + *cbak_func_idx;
cbak_wce_actual = calculate_function_wce(actual_cbak_idx);
if (cbak_wce_actual >= 0xFFFFU)
{
GUARDLOG(hook::log::INSTRUCTION_EXCESS)
<< "GuardCheck: cbak() function exceeds maximum instruction "
"count (65535). "
<< "Total WCE including called functions: " << cbak_wce_actual
<< "\n";
return {};
}
if (DEBUG_GUARD)
printf("cbak() total WCE: %ld\n", cbak_wce_actual);
}
// execution to here means guards are installed correctly and WCE is within
// limits
return std::pair<uint64_t, uint64_t>{hook_wce_actual, cbak_wce_actual};
}
+6
View File
@@ -97,6 +97,12 @@ public:
static IOUAmount
minPositiveAmount();
friend std::ostream&
operator<<(std::ostream& os, IOUAmount const& x)
{
return os << to_string(x);
}
};
inline IOUAmount::IOUAmount(beast::Zero)
+3
View File
@@ -28,6 +28,9 @@
namespace ripple {
bool
isFeatureEnabled(uint256 const& feature);
class DigestAwareReadView;
/** Rules controlling protocol behavior. */
+1
View File
@@ -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 && \
+8
View File
@@ -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
File diff suppressed because it is too large. Load diff
+841 -241
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -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);
+9 -9
View File
@@ -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 @@ public:
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
#endif
+23 -23
View File
@@ -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 @@ public:
testAccountSetFlags(features);
testSetRegularKey(features);
testSetRegularKeyFlags(features);
testSignersListSet(features);
testSignerListSet(features);
testUsingTickets(features);
testAccountIndex(features);
testHookIssuer(features);
+6 -6
View File
@@ -4492,14 +4492,14 @@ private:
}
}
// 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 @@ private:
// 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 @@ private:
// 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 @@ private:
testPaymentChannelFundTSH(features);
testSetHookTSH(features);
testSetRegularKeyTSH(features);
testSignersListSetTSH(features);
testSignerListSetTSH(features);
testTicketCreateTSH(features);
testTrustSetTSH(features);
testURITokenMintTSH(features);
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -880,9 +880,9 @@ private:
}
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 @@ private:
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 @@ private:
testPaymentChannelFund(features);
testSetHook(features);
testSetRegularKey(features);
testSignersListSet(features);
testSignerListSet(features);
testTicketCreate(features);
testTrustSet(features);
testURITokenMint(features);
+16 -3
View File
@@ -58,8 +58,21 @@ cat $INPUT_FILE | tr '\n' '\f' |
then
echo '#include "api.h"' > "$WASM_DIR/test-$COUNTER-gen.c"
tr '\f' '\n' <<< $line >> "$WASM_DIR/test-$COUNTER-gen.c"
DECLARED="`tr '\f' '\n' <<< $line | grep -E '(extern|define) ' | grep -Eo '[a-z\-\_]+ *\(' | grep -v 'sizeof' | sed -E 's/[^a-z\-\_]//g' | sort | uniq`"
USED="`tr '\f' '\n' <<< $line | grep -vE '(extern|define) ' | grep -Eo '[a-z\-\_]+\(' | grep -v 'sizeof' | sed -E 's/[^a-z\-\_]//g' | grep -vE '^(hook|cbak)' | sort | uniq`"
DECLARED="`tr '\f' '\n' <<< $line \
| grep -E '(extern|static|define) ' \
| grep -Eo '[a-z\-\_]+ *\(' \
| grep -v 'sizeof' \
| sed -E 's/[^a-z\-\_]//g' \
| grep -vE '^__attribute__$' \
| sort | uniq`"
USED="`tr '\f' '\n' <<< $line \
| grep -vE '(extern|static|define) ' \
| grep -Eo '[a-z\-\_]+\(' \
| grep -v 'sizeof' \
| sed -E 's/[^a-z\-\_]//g' \
| grep -vE '^(__attribute__|hook|cbak)$' \
| sort | uniq`"
ONCE="`echo $DECLARED $USED | tr ' ' '\n' | sort | uniq -c | grep '1 ' | sed -E 's/^ *1 //g'`"
FILTER="`echo $DECLARED | tr ' ' '|' | sed -E 's/\|$//g'`"
UNDECL="`echo $ONCE | grep -v -E $FILTER 2>/dev/null || echo ''`"
@@ -69,7 +82,7 @@ cat $INPUT_FILE | tr '\n' '\f' |
echo "$line"
exit 1
fi
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined <<< "`tr '\f' '\n' <<< $line`" |
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined,--export=hook,--export=cbak <<< "`tr '\f' '\n' <<< $line`" |
hook-cleaner - - 2>/dev/null |
xxd -p -u -c 10 |
sed -E 's/../0x&U,/g' | sed -E 's/^/ /g' >> $OUTPUT_FILE
+10 -1
View File
@@ -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 @@ public:
Issue const& issue2,
std::optional<AccountID> const& account = std::nullopt) const;
std::tuple<STAmount, STAmount, STAmount>
balances(std::optional<AccountID> const& account = std::nullopt) const
{
return balances(asset1_.get<Issue>(), asset2_.get<Issue>(), account);
}
[[nodiscard]] bool
expectLPTokens(AccountID const& account, IOUAmount const& tokens) const;
@@ -428,6 +434,9 @@ private:
[[nodiscard]] bool
expectAuctionSlot(auto&& cb) const;
IOUAmount
initialTokens();
};
namespace amm {
+14
View File
@@ -33,6 +33,15 @@ class AMM;
enum class Fund { All, Acct, Gw, IOUOnly };
struct TestAMMArg
{
std::optional<std::pair<STAmount, STAmount>> pool = std::nullopt;
std::uint16_t tfee = 0;
std::optional<jtx::ter> ter = std::nullopt;
std::vector<FeatureBitset> features = {supported_amendments()};
bool noLog = false;
};
void
fund(
jtx::Env& env,
@@ -85,6 +94,11 @@ protected:
std::uint16_t tfee = 0,
std::optional<jtx::ter> const& ter = std::nullopt,
std::vector<FeatureBitset> const& features = {supported_amendments()});
void
testAMM(
std::function<void(jtx::AMM&, jtx::Env&)>&& cb,
TestAMMArg const& arg);
};
class AMMTest : public jtx::AMMTestBase
+6
View File
@@ -646,6 +646,12 @@ public:
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);
+6 -7
View File
@@ -18,8 +18,9 @@
//==============================================================================
#include <test/jtx/AMM.h>
#include <test/jtx/Env.h>
#include <xrpld/app/misc/AMMHelpers.h>
#include <xrpld/app/misc/AMMUtils.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpl/protocol/AMMCore.h>
@@ -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())
{
}
+28 -6
View File
@@ -17,9 +17,9 @@
*/
//==============================================================================
#include <test/jtx/AMMTest.h>
#include <test/jtx/AMM.h>
#include <test/jtx/AMMTest.h>
#include <test/jtx/CaptureLogs.h>
#include <test/jtx/Env.h>
#include <test/jtx/pay.h>
#include <xrpld/rpc/RPCHandler.h>
@@ -104,15 +104,37 @@ AMMTestBase::testAMM(
std::uint16_t tfee,
std::optional<jtx::ter> const& ter,
std::vector<FeatureBitset> const& vfeatures)
{
testAMM(
std::move(cb),
TestAMMArg{
.pool = pool, .tfee = tfee, .ter = ter, .features = vfeatures});
}
void
AMMTestBase::testAMM(
std::function<void(jtx::AMM&, jtx::Env&)>&& 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<CaptureLogs>(&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);
+4 -17
View File
@@ -65,29 +65,16 @@ hso_delete(void (*f)(Json::Value& jv))
Json::Value
hso(std::vector<uint8_t> 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;
+92 -81
View File
@@ -203,98 +203,107 @@ public:
}
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<std::string, std::uint16_t> 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<std::string> 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<std::string, std::uint16_t> 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<std::string> 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 @@ public:
void
run() override
{
using namespace jtx;
auto const all = supported_amendments();
testErrors();
testSimpleRpc();
testVoteAndBid();
testVoteAndBid(all);
testFreeze();
testInvalidAmmField();
}
+137 -7
View File
@@ -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, std::optional<STAmount>, 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 <typename A>
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<Number()>&& noRoundCb,
STAmount const& balance,
std::function<Number()>&& 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<Number()>&& noRoundCb,
STAmount const& lptAMMBalance,
std::function<Number()>&& 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<STAmount, STAmount>
adjustAssetInByTokens(
Rules const& rules,
STAmount const& balance,
STAmount const& amount,
STAmount const& lptAMMBalance,
STAmount const& tokens,
std::uint16_t tfee);
std::pair<STAmount, STAmount>
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
+11
View File
@@ -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<bool, TER>
verifyAndAdjustLPTokenBalance(
Sandbox& sb,
STAmount const& lpTokens,
std::shared_ptr<SLE>& ammSle,
AccountID const& account);
} // namespace ripple
#endif // RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED
+140 -53
View File
@@ -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<Number()>&& noRoundCb,
STAmount const& balance,
std::function<Number()>&& 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<Number()>&& noRoundCb,
STAmount const& lptAMMBalance,
std::function<Number()>&& 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<STAmount, STAmount>
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<STAmount, STAmount>
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
+30
View File
@@ -16,6 +16,8 @@
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/misc/AMMHelpers.h>
#include <xrpld/app/misc/AMMUtils.h>
#include <xrpld/ledger/Sandbox.h>
#include <xrpl/basics/Log.h>
@@ -462,4 +464,32 @@ isOnlyLiquidityProvider(
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
}
Expected<bool, TER>
verifyAndAdjustLPTokenBalance(
Sandbox& sb,
STAmount const& lpTokens,
std::shared_ptr<SLE>& ammSle,
AccountID const& account)
{
if (auto const res = isOnlyLiquidityProvider(sb, lpTokens.issue(), account);
!res)
return Unexpected<TER>(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<TER>(tecAMM_INVALID_TOKENS);
}
}
return true;
}
} // namespace ripple
+18 -1
View File
@@ -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<AccountID> 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.
+33 -6
View File
@@ -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,
+106 -36
View File
@@ -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<STAmount> 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,
+98 -48
View File
@@ -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<TER, STAmount>
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);
}
+1 -1
View File
@@ -301,7 +301,7 @@ private:
std::uint16_t tfee);
/** Check from the flags if it's withdraw all */
WithdrawAll
static WithdrawAll
isWithdrawAll(STTx const& tx);
};
+308
View File
@@ -17,6 +17,9 @@
*/
//==============================================================================
#include <xrpld/app/misc/AMMHelpers.h>
#include <xrpld/app/misc/AMMUtils.h>
#include <xrpld/app/misc/CredentialHelpers.h>
#include <xrpld/app/tx/detail/InvariantCheck.h>
#include <xrpld/app/misc/CredentialHelpers.h>
@@ -1674,4 +1677,309 @@ ValidPermissionedDomain::finalize(
(sleStatus_[1] ? check(*sleStatus_[1], j) : true);
}
void
ValidAMM::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> 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<Issue>(),
tx[sfAmount2].get<Issue>(),
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<Issue>(),
tx[sfAsset2].get<Issue>(),
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
+65 -1
View File
@@ -617,6 +617,69 @@ public:
beast::Journal const&);
};
class ValidAMM
{
std::optional<AccountID> ammAccount_;
std::optional<STAmount> lptAMMBalanceAfter_;
std::optional<STAmount> lptAMMBalanceBefore_;
bool ammPoolChanged_;
public:
enum class ZeroAllowed : bool { No = false, Yes = true };
ValidAMM() : ammPoolChanged_{false}
{
}
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
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
+16 -1
View File
@@ -21,6 +21,8 @@
#define RIPPLE_APP_BOOK_OFFER_H_INCLUDED
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/contract.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rules.h>
@@ -169,8 +171,21 @@ public:
* always returns true.
*/
bool
checkInvariant(TAmounts<TIn, TOut> const&, beast::Journal j) const
checkInvariant(TAmounts<TIn, TOut> 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;
}
};