mirror of
https://github.com/Xahau/xahaud.git
synced 2026-06-26 03:56:41 +00:00
Compare commits
19 Commits
SecureUI
...
external-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e2c69deb2 | ||
|
|
ff763a500c | ||
|
|
a605aec57a | ||
|
|
bfcbbc3c5e | ||
|
|
d782f8cab4 | ||
|
|
8a61dd44e0 | ||
|
|
a8ca62a148 | ||
|
|
b7aeff95a9 | ||
|
|
b880c80c2b | ||
|
|
8666cdfb71 | ||
|
|
6d2a0b4e8b | ||
|
|
739ebfaba4 | ||
|
|
65166a9329 | ||
|
|
ca469b5d22 | ||
|
|
8cfee6c8a3 | ||
|
|
8673599d2b | ||
|
|
ec65e622aa | ||
|
|
65837f49e1 | ||
|
|
e5b21f026e |
@@ -68,6 +68,17 @@ target_link_libraries(xrpl.imports.main
|
|||||||
$<$<BOOL:${voidstar}>:antithesis-sdk-cpp>
|
$<$<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(add_module)
|
||||||
include(target_link_modules)
|
include(target_link_modules)
|
||||||
|
|
||||||
@@ -167,7 +178,108 @@ if(xrpld)
|
|||||||
file(GLOB_RECURSE sources CONFIGURE_DEPENDS
|
file(GLOB_RECURSE sources CONFIGURE_DEPENDS
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test/*.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/src/test/*.cpp"
|
||||||
)
|
)
|
||||||
|
if(HOOKS_TEST_ONLY OR DEFINED ENV{HOOKS_TEST_ONLY})
|
||||||
|
# Keep test infra but drop the individual *_test.cpp files
|
||||||
|
list(FILTER sources EXCLUDE REGEX "_test\\.cpp$")
|
||||||
|
message(STATUS "HOOKS_TEST_ONLY: excluded *_test.cpp from src/test/")
|
||||||
|
endif()
|
||||||
target_sources(rippled PRIVATE ${sources})
|
target_sources(rippled PRIVATE ${sources})
|
||||||
|
|
||||||
|
# Optional: include external hook test sources from another directory.
|
||||||
|
# Set via -DHOOKS_TEST_DIR=/path/to/tests or env HOOKS_TEST_DIR.
|
||||||
|
# Optionally set HOOKS_C_DIR to pass --hooks-c-dir args to the compiler
|
||||||
|
# (e.g. "tipbot=/path/to/hooks" — multiple values separated by ";").
|
||||||
|
#
|
||||||
|
# hookz build-test-hooks must be on PATH. It auto-compiles hooks referenced
|
||||||
|
# in each *_test.cpp and generates *_test_hooks.h next to the test file.
|
||||||
|
if(NOT HOOKS_TEST_DIR AND DEFINED ENV{HOOKS_TEST_DIR})
|
||||||
|
set(HOOKS_TEST_DIR $ENV{HOOKS_TEST_DIR})
|
||||||
|
endif()
|
||||||
|
if(NOT HOOKS_C_DIR AND DEFINED ENV{HOOKS_C_DIR})
|
||||||
|
set(HOOKS_C_DIR $ENV{HOOKS_C_DIR})
|
||||||
|
endif()
|
||||||
|
if(HOOKS_TEST_DIR AND EXISTS "${HOOKS_TEST_DIR}")
|
||||||
|
file(GLOB EXTERNAL_HOOK_TESTS CONFIGURE_DEPENDS
|
||||||
|
"${HOOKS_TEST_DIR}/*_test.cpp"
|
||||||
|
)
|
||||||
|
if(EXTERNAL_HOOK_TESTS)
|
||||||
|
# Build extra args for hookz build-test-hooks
|
||||||
|
set(_hooks_extra_args "")
|
||||||
|
set(_hooks_source_deps "")
|
||||||
|
if(HOOKS_C_DIR)
|
||||||
|
foreach(_dir ${HOOKS_C_DIR})
|
||||||
|
list(APPEND _hooks_extra_args "--hooks-c-dir" "${_dir}")
|
||||||
|
|
||||||
|
string(REGEX REPLACE "^[^=]+=" "" _hook_dir "${_dir}")
|
||||||
|
if(EXISTS "${_hook_dir}")
|
||||||
|
file(GLOB_RECURSE _hook_dir_deps CONFIGURE_DEPENDS
|
||||||
|
"${_hook_dir}/*.c"
|
||||||
|
"${_hook_dir}/*.h"
|
||||||
|
)
|
||||||
|
if(HOOKS_TEST_DIR)
|
||||||
|
list(FILTER _hook_dir_deps EXCLUDE REGEX "^${HOOKS_TEST_DIR}/")
|
||||||
|
endif()
|
||||||
|
list(APPEND _hooks_source_deps ${_hook_dir_deps})
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
list(REMOVE_DUPLICATES _hooks_source_deps)
|
||||||
|
endif()
|
||||||
|
if(HOOKS_COVERAGE OR DEFINED ENV{HOOKS_COVERAGE})
|
||||||
|
list(APPEND _hooks_extra_args "--hook-coverage")
|
||||||
|
message(STATUS "Hook coverage enabled: compiling hooks with hookz")
|
||||||
|
endif()
|
||||||
|
if(HOOKS_FORCE_RECOMPILE OR DEFINED ENV{HOOKS_FORCE_RECOMPILE})
|
||||||
|
list(APPEND _hooks_extra_args "--force-write" "--no-cache")
|
||||||
|
message(STATUS "Hook force recompile enabled (cache bypassed)")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Run hookz build-test-hooks on each test file before compilation
|
||||||
|
foreach(_test_file ${EXTERNAL_HOOK_TESTS})
|
||||||
|
get_filename_component(_stem ${_test_file} NAME_WE)
|
||||||
|
set(_hooks_header "${HOOKS_TEST_DIR}/${_stem}_hooks.h")
|
||||||
|
if(HOOKS_FORCE_RECOMPILE OR DEFINED ENV{HOOKS_FORCE_RECOMPILE})
|
||||||
|
# Always run — no DEPENDS, no OUTPUT caching
|
||||||
|
add_custom_target(compile_hooks_${_stem} ALL
|
||||||
|
COMMAND hookz build-test-hooks "${_test_file}" ${_hooks_extra_args}
|
||||||
|
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||||
|
COMMENT "Compiling hooks for ${_stem} (forced)"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
list(APPEND EXTERNAL_HOOK_TARGETS compile_hooks_${_stem})
|
||||||
|
else()
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT "${_hooks_header}"
|
||||||
|
COMMAND hookz build-test-hooks "${_test_file}" ${_hooks_extra_args}
|
||||||
|
DEPENDS "${_test_file}" ${_hooks_source_deps}
|
||||||
|
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||||
|
COMMENT "Compiling hooks for ${_stem}"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
list(APPEND EXTERNAL_HOOK_HEADERS "${_hooks_header}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
# Ensure headers are generated before rippled compiles
|
||||||
|
if(HOOKS_FORCE_RECOMPILE OR DEFINED ENV{HOOKS_FORCE_RECOMPILE})
|
||||||
|
foreach(_tgt ${EXTERNAL_HOOK_TARGETS})
|
||||||
|
add_dependencies(rippled ${_tgt})
|
||||||
|
endforeach()
|
||||||
|
else()
|
||||||
|
add_custom_target(compile_external_hooks DEPENDS ${EXTERNAL_HOOK_HEADERS})
|
||||||
|
add_dependencies(rippled compile_external_hooks)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_sources(rippled PRIVATE ${EXTERNAL_HOOK_TESTS})
|
||||||
|
# Keep the generated hook-header include path scoped to the external
|
||||||
|
# test sources so changing HOOKS_TEST_DIR doesn't invalidate the
|
||||||
|
# compile command for the rest of rippled.
|
||||||
|
set_property(
|
||||||
|
SOURCE ${EXTERNAL_HOOK_TESTS}
|
||||||
|
APPEND PROPERTY INCLUDE_DIRECTORIES "${HOOKS_TEST_DIR}"
|
||||||
|
)
|
||||||
|
message(STATUS "Including external hook tests from: ${HOOKS_TEST_DIR}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_link_libraries(rippled
|
target_link_libraries(rippled
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <functional>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
@@ -165,6 +166,7 @@ private:
|
|||||||
beast::severities::Severity thresh_;
|
beast::severities::Severity thresh_;
|
||||||
File file_;
|
File file_;
|
||||||
bool silent_ = false;
|
bool silent_ = false;
|
||||||
|
std::function<std::string(std::string const&)> transform_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Logs(beast::severities::Severity level);
|
Logs(beast::severities::Severity level);
|
||||||
@@ -203,6 +205,33 @@ public:
|
|||||||
std::string const& text,
|
std::string const& text,
|
||||||
bool console);
|
bool console);
|
||||||
|
|
||||||
|
/** Set a transform applied to every log message before output.
|
||||||
|
* Useful in tests to replace raw account IDs with human-readable names.
|
||||||
|
* Pass nullptr to clear.
|
||||||
|
*
|
||||||
|
* TODO: This is test-only infrastructure (used by TestEnv). Consider
|
||||||
|
* moving to SuiteLogs or a test-specific subclass if the Logs interface
|
||||||
|
* needs to stay clean for production.
|
||||||
|
*/
|
||||||
|
void
|
||||||
|
setTransform(std::function<std::string(std::string const&)> fn)
|
||||||
|
{
|
||||||
|
std::lock_guard lock(mutex_);
|
||||||
|
transform_ = std::move(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply the current transform to text (or return as-is if none set). */
|
||||||
|
std::string const&
|
||||||
|
applyTransform(std::string const& text) const
|
||||||
|
{
|
||||||
|
if (!transform_)
|
||||||
|
return text;
|
||||||
|
// Store in thread_local to return a const ref
|
||||||
|
thread_local std::string buf;
|
||||||
|
buf = transform_(text);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
std::string
|
std::string
|
||||||
rotate();
|
rotate();
|
||||||
|
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ getImportWhitelist(Rules const& rules)
|
|||||||
#define int64_t 0x7EU
|
#define int64_t 0x7EU
|
||||||
#define int32_t 0x7FU
|
#define int32_t 0x7FU
|
||||||
#define uint32_t 0x7FU
|
#define uint32_t 0x7FU
|
||||||
|
#define void_t 0x00U
|
||||||
|
|
||||||
#define HOOK_WRAP_PARAMS(...) __VA_ARGS__
|
#define HOOK_WRAP_PARAMS(...) __VA_ARGS__
|
||||||
|
|
||||||
@@ -427,11 +428,15 @@ getImportWhitelist(Rules const& rules)
|
|||||||
|
|
||||||
#include "hook_api.macro"
|
#include "hook_api.macro"
|
||||||
|
|
||||||
|
// Coverage callback: void __on_source_line(uint32_t line, uint32_t col)
|
||||||
|
whitelist["__on_source_line"] = {void_t, uint32_t, uint32_t};
|
||||||
|
|
||||||
#undef HOOK_API_DEFINITION
|
#undef HOOK_API_DEFINITION
|
||||||
#undef HOOK_WRAP_PARAMS
|
#undef HOOK_WRAP_PARAMS
|
||||||
#undef int64_t
|
#undef int64_t
|
||||||
#undef int32_t
|
#undef int32_t
|
||||||
#undef uint32_t
|
#undef uint32_t
|
||||||
|
#undef void_t
|
||||||
#pragma pop_macro("HOOK_API_DEFINITION")
|
#pragma pop_macro("HOOK_API_DEFINITION")
|
||||||
|
|
||||||
return whitelist;
|
return whitelist;
|
||||||
|
|||||||
@@ -1374,21 +1374,52 @@ validateGuards(
|
|||||||
int result_count = parseLeb128(wasm, i, &i);
|
int result_count = parseLeb128(wasm, i, &i);
|
||||||
CHECK_SHORT_HOOK();
|
CHECK_SHORT_HOOK();
|
||||||
|
|
||||||
// this needs a reliable hook cleaner otherwise it will catch
|
if (j == hook_type_idx)
|
||||||
// most compilers out
|
|
||||||
if (result_count != 1)
|
|
||||||
{
|
{
|
||||||
GUARDLOG(hook::log::FUNC_RETURN_COUNT)
|
// hook/cbak must return exactly one value (i64)
|
||||||
<< "Malformed transaction. "
|
if (result_count != 1)
|
||||||
<< "Hook declares a function type that returns fewer "
|
{
|
||||||
"or more than one value. "
|
GUARDLOG(hook::log::FUNC_RETURN_COUNT)
|
||||||
<< "\n";
|
<< "Malformed transaction. "
|
||||||
return {};
|
<< "hook/cbak function type must return exactly "
|
||||||
|
"one value. "
|
||||||
|
<< "\n";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (first_signature)
|
||||||
|
{
|
||||||
|
// For whitelisted imports, check expected return count.
|
||||||
|
// void_t (0x00) means 0 return values.
|
||||||
|
uint8_t expected_return =
|
||||||
|
(*first_signature).get()[0];
|
||||||
|
int expected_result_count =
|
||||||
|
(expected_return == 0x00U) ? 0 : 1;
|
||||||
|
if (result_count != expected_result_count)
|
||||||
|
{
|
||||||
|
GUARDLOG(hook::log::FUNC_RETURN_COUNT)
|
||||||
|
<< "Malformed transaction. "
|
||||||
|
<< "Hook API: " << *first_name
|
||||||
|
<< " has wrong return count "
|
||||||
|
<< "(expected " << expected_result_count
|
||||||
|
<< ", got " << result_count << ")."
|
||||||
|
<< "\n";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (result_count != 1)
|
||||||
|
{
|
||||||
|
GUARDLOG(hook::log::FUNC_RETURN_COUNT)
|
||||||
|
<< "Malformed transaction. "
|
||||||
|
<< "Hook declares a function type that returns "
|
||||||
|
"fewer or more than one value. "
|
||||||
|
<< "\n";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this can only ever be 1 in production, but in testing it may
|
|
||||||
// also be 0 or >1 so for completeness this loop is here but can
|
|
||||||
// be taken out in prod
|
|
||||||
for (int k = 0; k < result_count; ++k)
|
for (int k = 0; k < result_count; ++k)
|
||||||
{
|
{
|
||||||
int result_type = parseLeb128(wasm, i, &i);
|
int result_type = parseLeb128(wasm, i, &i);
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
[[maybe_unused]] ApplyContext& applyCtx = hookCtx.applyCtx; \
|
[[maybe_unused]] ApplyContext& applyCtx = hookCtx.applyCtx; \
|
||||||
[[maybe_unused]] auto& view = applyCtx.view(); \
|
[[maybe_unused]] auto& view = applyCtx.view(); \
|
||||||
[[maybe_unused]] auto j = applyCtx.app.journal("View"); \
|
[[maybe_unused]] auto j = applyCtx.app.journal("View"); \
|
||||||
|
[[maybe_unused]] auto jh = applyCtx.app.journal("HooksTrace"); \
|
||||||
[[maybe_unused]] WasmEdge_MemoryInstanceContext* memoryCtx = \
|
[[maybe_unused]] WasmEdge_MemoryInstanceContext* memoryCtx = \
|
||||||
WasmEdge_CallingFrameGetMemoryInstance(&frameCtx, 0); \
|
WasmEdge_CallingFrameGetMemoryInstance(&frameCtx, 0); \
|
||||||
[[maybe_unused]] unsigned char* memory = \
|
[[maybe_unused]] unsigned char* memory = \
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ public:
|
|||||||
|
|
||||||
static IOUAmount
|
static IOUAmount
|
||||||
minPositiveAmount();
|
minPositiveAmount();
|
||||||
|
|
||||||
|
friend std::ostream&
|
||||||
|
operator<<(std::ostream& os, IOUAmount const& x)
|
||||||
|
{
|
||||||
|
return os << to_string(x);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
inline IOUAmount::IOUAmount(beast::Zero)
|
inline IOUAmount::IOUAmount(beast::Zero)
|
||||||
|
|||||||
@@ -28,6 +28,9 @@
|
|||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
|
||||||
|
bool
|
||||||
|
isFeatureEnabled(uint256 const& feature);
|
||||||
|
|
||||||
class DigestAwareReadView;
|
class DigestAwareReadView;
|
||||||
|
|
||||||
/** Rules controlling protocol behavior. */
|
/** Rules controlling protocol behavior. */
|
||||||
|
|||||||
@@ -196,9 +196,10 @@ Logs::write(
|
|||||||
std::string const& text,
|
std::string const& text,
|
||||||
bool console)
|
bool console)
|
||||||
{
|
{
|
||||||
std::string s;
|
|
||||||
format(s, text, level, partition);
|
|
||||||
std::lock_guard lock(mutex_);
|
std::lock_guard lock(mutex_);
|
||||||
|
std::string const& transformed = transform_ ? transform_(text) : text;
|
||||||
|
std::string s;
|
||||||
|
format(s, transformed, level, partition);
|
||||||
file_.writeln(s);
|
file_.writeln(s);
|
||||||
if (!silent_)
|
if (!silent_)
|
||||||
std::cerr << s << '\n';
|
std::cerr << s << '\n';
|
||||||
|
|||||||
@@ -153,4 +153,12 @@ Rules::operator!=(Rules const& other) const
|
|||||||
{
|
{
|
||||||
return !(*this == other);
|
return !(*this == other);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
isFeatureEnabled(uint256 const& feature)
|
||||||
|
{
|
||||||
|
auto const& rules = getCurrentTransactionRules();
|
||||||
|
return rules && rules->enabled(feature);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -125,7 +125,6 @@ class AMM
|
|||||||
STAmount const asset1_;
|
STAmount const asset1_;
|
||||||
STAmount const asset2_;
|
STAmount const asset2_;
|
||||||
uint256 const ammID_;
|
uint256 const ammID_;
|
||||||
IOUAmount const initialLPTokens_;
|
|
||||||
bool log_;
|
bool log_;
|
||||||
bool doClose_;
|
bool doClose_;
|
||||||
// Predict next purchase price
|
// Predict next purchase price
|
||||||
@@ -138,6 +137,7 @@ class AMM
|
|||||||
std::uint32_t const fee_;
|
std::uint32_t const fee_;
|
||||||
AccountID const ammAccount_;
|
AccountID const ammAccount_;
|
||||||
Issue const lptIssue_;
|
Issue const lptIssue_;
|
||||||
|
IOUAmount const initialLPTokens_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AMM(Env& env,
|
AMM(Env& env,
|
||||||
@@ -194,6 +194,12 @@ public:
|
|||||||
Issue const& issue2,
|
Issue const& issue2,
|
||||||
std::optional<AccountID> const& account = std::nullopt) const;
|
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
|
[[nodiscard]] bool
|
||||||
expectLPTokens(AccountID const& account, IOUAmount const& tokens) const;
|
expectLPTokens(AccountID const& account, IOUAmount const& tokens) const;
|
||||||
|
|
||||||
@@ -428,6 +434,9 @@ private:
|
|||||||
|
|
||||||
[[nodiscard]] bool
|
[[nodiscard]] bool
|
||||||
expectAuctionSlot(auto&& cb) const;
|
expectAuctionSlot(auto&& cb) const;
|
||||||
|
|
||||||
|
IOUAmount
|
||||||
|
initialTokens();
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace amm {
|
namespace amm {
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ class AMM;
|
|||||||
|
|
||||||
enum class Fund { All, Acct, Gw, IOUOnly };
|
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
|
void
|
||||||
fund(
|
fund(
|
||||||
jtx::Env& env,
|
jtx::Env& env,
|
||||||
@@ -85,6 +94,11 @@ protected:
|
|||||||
std::uint16_t tfee = 0,
|
std::uint16_t tfee = 0,
|
||||||
std::optional<jtx::ter> const& ter = std::nullopt,
|
std::optional<jtx::ter> const& ter = std::nullopt,
|
||||||
std::vector<FeatureBitset> const& features = {supported_amendments()});
|
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
|
class AMMTest : public jtx::AMMTestBase
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ public:
|
|||||||
std::string const& partition,
|
std::string const& partition,
|
||||||
beast::severities::Severity threshold) override
|
beast::severities::Severity threshold) override
|
||||||
{
|
{
|
||||||
return std::make_unique<SuiteJournalSink>(partition, threshold, suite_);
|
return std::make_unique<SuiteJournalSink>(
|
||||||
|
partition, threshold, suite_, this);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -646,6 +647,12 @@ public:
|
|||||||
void
|
void
|
||||||
disableFeature(uint256 const feature);
|
disableFeature(uint256 const feature);
|
||||||
|
|
||||||
|
bool
|
||||||
|
enabled(uint256 feature) const
|
||||||
|
{
|
||||||
|
return current()->rules().enabled(feature);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void
|
void
|
||||||
fund(bool setDefaultRipple, STAmount const& amount, Account const& account);
|
fund(bool setDefaultRipple, STAmount const& amount, Account const& account);
|
||||||
|
|||||||
148
src/test/jtx/TestEnv.h
Normal file
148
src/test/jtx/TestEnv.h
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#ifndef TEST_JTX_TESTENV_H_INCLUDED
|
||||||
|
#define TEST_JTX_TESTENV_H_INCLUDED
|
||||||
|
|
||||||
|
#include <test/jtx/Env.h>
|
||||||
|
#include <xrpl/basics/Log.h>
|
||||||
|
#include <xrpl/protocol/AccountID.h>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <map>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace test {
|
||||||
|
namespace jtx {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TestEnv wraps Env with:
|
||||||
|
* - Named account registry: env.account("alice")
|
||||||
|
* - Auto log transform: replaces r-addresses with Account(name) in log output
|
||||||
|
* - Env-var driven per-partition log levels via TESTENV_LOGGING
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* TestEnv env{suite, features};
|
||||||
|
* auto const& alice = env.account("alice");
|
||||||
|
* auto const& bob = env.account("bob");
|
||||||
|
* env.fund(XRP(10000), alice, bob);
|
||||||
|
* // Logs now show Account(alice), Account(bob) instead of r-addresses
|
||||||
|
*
|
||||||
|
* Log levels via env var:
|
||||||
|
* TESTENV_LOGGING="HooksTrace=trace,View=debug"
|
||||||
|
*
|
||||||
|
* Valid levels: trace, debug, info, warning, error, fatal
|
||||||
|
*/
|
||||||
|
class TestEnv : public Env
|
||||||
|
{
|
||||||
|
std::map<std::string, Account> accounts_;
|
||||||
|
std::string prefix_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
TestEnv(beast::unit_test::suite& suite, FeatureBitset features)
|
||||||
|
: Env(suite, features)
|
||||||
|
{
|
||||||
|
installTransform();
|
||||||
|
applyLoggingEnvVar();
|
||||||
|
}
|
||||||
|
|
||||||
|
TestEnv(
|
||||||
|
beast::unit_test::suite& suite,
|
||||||
|
std::unique_ptr<Config> config,
|
||||||
|
FeatureBitset features,
|
||||||
|
std::unique_ptr<Logs> logs = nullptr,
|
||||||
|
beast::severities::Severity thresh = beast::severities::kError)
|
||||||
|
: Env(suite, std::move(config), features, std::move(logs), thresh)
|
||||||
|
{
|
||||||
|
installTransform();
|
||||||
|
applyLoggingEnvVar();
|
||||||
|
}
|
||||||
|
|
||||||
|
~TestEnv()
|
||||||
|
{
|
||||||
|
app().logs().setTransform(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get or create a named account.
|
||||||
|
/// First call creates the Account; subsequent calls return the same one.
|
||||||
|
Account const&
|
||||||
|
account(std::string const& name)
|
||||||
|
{
|
||||||
|
auto [it, inserted] = accounts_.try_emplace(name, name);
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a prefix that appears at the start of every log line.
|
||||||
|
/// Useful for visually separating test phases in trace output.
|
||||||
|
/// Pass empty string to clear.
|
||||||
|
void
|
||||||
|
setPrefix(std::string const& prefix)
|
||||||
|
{
|
||||||
|
prefix_ = prefix.empty() ? "" : "[" + prefix + "] ";
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static beast::severities::Severity
|
||||||
|
parseSeverity(std::string const& s)
|
||||||
|
{
|
||||||
|
if (s == "trace")
|
||||||
|
return beast::severities::kTrace;
|
||||||
|
if (s == "debug")
|
||||||
|
return beast::severities::kDebug;
|
||||||
|
if (s == "info")
|
||||||
|
return beast::severities::kInfo;
|
||||||
|
if (s == "warning")
|
||||||
|
return beast::severities::kWarning;
|
||||||
|
if (s == "error")
|
||||||
|
return beast::severities::kError;
|
||||||
|
if (s == "fatal")
|
||||||
|
return beast::severities::kFatal;
|
||||||
|
return beast::severities::kError;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
applyLoggingEnvVar()
|
||||||
|
{
|
||||||
|
// Parse TESTENV_LOGGING="Partition1=level,Partition2=level"
|
||||||
|
auto const* envVal = std::getenv("TESTENV_LOGGING");
|
||||||
|
if (!envVal || !envVal[0])
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::istringstream ss(envVal);
|
||||||
|
std::string pair;
|
||||||
|
while (std::getline(ss, pair, ','))
|
||||||
|
{
|
||||||
|
auto eq = pair.find('=');
|
||||||
|
if (eq == std::string::npos)
|
||||||
|
continue;
|
||||||
|
auto partition = pair.substr(0, eq);
|
||||||
|
auto level = pair.substr(eq + 1);
|
||||||
|
app().logs().get(partition).threshold(parseSeverity(level));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
installTransform()
|
||||||
|
{
|
||||||
|
app().logs().setTransform([this](std::string const& text) {
|
||||||
|
std::string out = prefix_ + text;
|
||||||
|
for (auto const& [name, acc] : accounts_)
|
||||||
|
{
|
||||||
|
auto raddr = toBase58(acc.id());
|
||||||
|
std::string::size_type pos = 0;
|
||||||
|
std::string replacement = "Account(" + name + ")";
|
||||||
|
while ((pos = out.find(raddr, pos)) != std::string::npos)
|
||||||
|
{
|
||||||
|
out.replace(pos, raddr.size(), replacement);
|
||||||
|
pos += replacement.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace jtx
|
||||||
|
} // namespace test
|
||||||
|
} // namespace ripple
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -18,8 +18,9 @@
|
|||||||
//==============================================================================
|
//==============================================================================
|
||||||
|
|
||||||
#include <test/jtx/AMM.h>
|
#include <test/jtx/AMM.h>
|
||||||
|
|
||||||
#include <test/jtx/Env.h>
|
#include <test/jtx/Env.h>
|
||||||
|
|
||||||
|
#include <xrpld/app/misc/AMMHelpers.h>
|
||||||
#include <xrpld/app/misc/AMMUtils.h>
|
#include <xrpld/app/misc/AMMUtils.h>
|
||||||
#include <xrpld/rpc/detail/RPCHelpers.h>
|
#include <xrpld/rpc/detail/RPCHelpers.h>
|
||||||
#include <xrpl/protocol/AMMCore.h>
|
#include <xrpl/protocol/AMMCore.h>
|
||||||
@@ -38,12 +39,10 @@ number(STAmount const& a)
|
|||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
static IOUAmount
|
IOUAmount
|
||||||
initialTokens(STAmount const& asset1, STAmount const& asset2)
|
AMM::initialTokens()
|
||||||
{
|
{
|
||||||
auto const product = number(asset1) * number(asset2);
|
return getLPTokensBalance();
|
||||||
return (IOUAmount)(product.mantissa() >= 0 ? root2(product)
|
|
||||||
: root2(-product));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AMM::AMM(
|
AMM::AMM(
|
||||||
@@ -64,7 +63,6 @@ AMM::AMM(
|
|||||||
, asset1_(asset1)
|
, asset1_(asset1)
|
||||||
, asset2_(asset2)
|
, asset2_(asset2)
|
||||||
, ammID_(keylet::amm(asset1_.issue(), asset2_.issue()).key)
|
, ammID_(keylet::amm(asset1_.issue(), asset2_.issue()).key)
|
||||||
, initialLPTokens_(initialTokens(asset1, asset2))
|
|
||||||
, log_(log)
|
, log_(log)
|
||||||
, doClose_(close)
|
, doClose_(close)
|
||||||
, lastPurchasePrice_(0)
|
, lastPurchasePrice_(0)
|
||||||
@@ -77,6 +75,7 @@ AMM::AMM(
|
|||||||
asset1_.issue().currency,
|
asset1_.issue().currency,
|
||||||
asset2_.issue().currency,
|
asset2_.issue().currency,
|
||||||
ammAccount_))
|
ammAccount_))
|
||||||
|
, initialLPTokens_(initialTokens())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
*/
|
*/
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
|
|
||||||
#include <test/jtx/AMMTest.h>
|
|
||||||
|
|
||||||
#include <test/jtx/AMM.h>
|
#include <test/jtx/AMM.h>
|
||||||
|
#include <test/jtx/AMMTest.h>
|
||||||
|
#include <test/jtx/CaptureLogs.h>
|
||||||
#include <test/jtx/Env.h>
|
#include <test/jtx/Env.h>
|
||||||
#include <test/jtx/pay.h>
|
#include <test/jtx/pay.h>
|
||||||
#include <xrpld/rpc/RPCHandler.h>
|
#include <xrpld/rpc/RPCHandler.h>
|
||||||
@@ -104,15 +104,37 @@ AMMTestBase::testAMM(
|
|||||||
std::uint16_t tfee,
|
std::uint16_t tfee,
|
||||||
std::optional<jtx::ter> const& ter,
|
std::optional<jtx::ter> const& ter,
|
||||||
std::vector<FeatureBitset> const& vfeatures)
|
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;
|
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] =
|
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 {
|
auto tofund = [&](STAmount const& a) -> STAmount {
|
||||||
if (a.native())
|
if (a.native())
|
||||||
{
|
{
|
||||||
@@ -142,7 +164,7 @@ AMMTestBase::testAMM(
|
|||||||
alice,
|
alice,
|
||||||
asset1,
|
asset1,
|
||||||
asset2,
|
asset2,
|
||||||
CreateArg{.log = false, .tfee = tfee, .err = ter});
|
CreateArg{.log = false, .tfee = arg.tfee, .err = arg.ter});
|
||||||
if (BEAST_EXPECT(
|
if (BEAST_EXPECT(
|
||||||
ammAlice.expectBalances(asset1, asset2, ammAlice.tokens())))
|
ammAlice.expectBalances(asset1, asset2, ammAlice.tokens())))
|
||||||
cb(ammAlice, env);
|
cb(ammAlice, env);
|
||||||
|
|||||||
@@ -203,98 +203,107 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
testVoteAndBid()
|
testVoteAndBid(FeatureBitset features)
|
||||||
{
|
{
|
||||||
testcase("Vote and Bid");
|
testcase("Vote and Bid");
|
||||||
|
|
||||||
using namespace jtx;
|
using namespace jtx;
|
||||||
testAMM([&](AMM& ammAlice, Env& env) {
|
testAMM(
|
||||||
BEAST_EXPECT(ammAlice.expectAmmRpcInfo(
|
[&](AMM& ammAlice, Env& env) {
|
||||||
XRP(10000), USD(10000), IOUAmount{10000000, 0}));
|
BEAST_EXPECT(ammAlice.expectAmmRpcInfo(
|
||||||
std::unordered_map<std::string, std::uint16_t> votes;
|
XRP(10000), USD(10000), IOUAmount{10000000, 0}));
|
||||||
votes.insert({alice.human(), 0});
|
std::unordered_map<std::string, std::uint16_t> votes;
|
||||||
for (int i = 0; i < 7; ++i)
|
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
|
|
||||||
{
|
{
|
||||||
// votes
|
Account a(std::to_string(i));
|
||||||
auto const voteSlots = amm[jss::vote_slots];
|
votes.insert({a.human(), 50 * (i + 1)});
|
||||||
auto votesCopy = votes;
|
fund(env, gw, {a}, {USD(10001)}, Fund::Acct);
|
||||||
for (std::uint8_t i = 0; i < 8; ++i)
|
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
|
||||||
votes[voteSlots[i][jss::account].asString()] ==
|
auto const voteSlots = amm[jss::vote_slots];
|
||||||
voteSlots[i][jss::trading_fee].asUInt() &&
|
auto votesCopy = votes;
|
||||||
voteSlots[i][jss::vote_weight].asUInt() ==
|
for (std::uint8_t i = 0; i < 8; ++i)
|
||||||
12500))
|
{
|
||||||
|
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;
|
return;
|
||||||
votes.erase(voteSlots[i][jss::account].asString());
|
votes = votesCopy;
|
||||||
}
|
|
||||||
if (!BEAST_EXPECT(votes.empty()))
|
|
||||||
return;
|
|
||||||
votes = votesCopy;
|
|
||||||
|
|
||||||
// bid
|
// bid
|
||||||
auto const auctionSlot = amm[jss::auction_slot];
|
auto const auctionSlot = amm[jss::auction_slot];
|
||||||
for (std::uint8_t i = 0; i < 4; ++i)
|
for (std::uint8_t i = 0; i < 4; ++i)
|
||||||
{
|
{
|
||||||
if (!BEAST_EXPECT(authAccounts.contains(
|
if (!BEAST_EXPECT(authAccounts.contains(
|
||||||
|
auctionSlot[jss::auth_accounts][i]
|
||||||
|
[jss::account]
|
||||||
|
.asString())))
|
||||||
|
return;
|
||||||
|
authAccounts.erase(
|
||||||
auctionSlot[jss::auth_accounts][i][jss::account]
|
auctionSlot[jss::auth_accounts][i][jss::account]
|
||||||
.asString())))
|
.asString());
|
||||||
|
}
|
||||||
|
if (!BEAST_EXPECT(authAccounts.empty()))
|
||||||
return;
|
return;
|
||||||
authAccounts.erase(
|
BEAST_EXPECT(
|
||||||
auctionSlot[jss::auth_accounts][i][jss::account]
|
auctionSlot[jss::account].asString() ==
|
||||||
.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)
|
},
|
||||||
{
|
std::nullopt,
|
||||||
fail(e.what(), __FILE__, __LINE__);
|
0,
|
||||||
}
|
std::nullopt,
|
||||||
}
|
{features});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -337,9 +346,11 @@ public:
|
|||||||
void
|
void
|
||||||
run() override
|
run() override
|
||||||
{
|
{
|
||||||
|
using namespace jtx;
|
||||||
|
auto const all = supported_amendments();
|
||||||
testErrors();
|
testErrors();
|
||||||
testSimpleRpc();
|
testSimpleRpc();
|
||||||
testVoteAndBid();
|
testVoteAndBid(all);
|
||||||
testFreeze();
|
testFreeze();
|
||||||
testInvalidAmmField();
|
testInvalidAmmField();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#ifndef TEST_UNIT_TEST_SUITE_JOURNAL_H
|
#ifndef TEST_UNIT_TEST_SUITE_JOURNAL_H
|
||||||
#define TEST_UNIT_TEST_SUITE_JOURNAL_H
|
#define TEST_UNIT_TEST_SUITE_JOURNAL_H
|
||||||
|
#include <xrpl/basics/Log.h>
|
||||||
#include <xrpl/beast/unit_test.h>
|
#include <xrpl/beast/unit_test.h>
|
||||||
#include <xrpl/beast/utility/Journal.h>
|
#include <xrpl/beast/utility/Journal.h>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -31,13 +32,18 @@ class SuiteJournalSink : public beast::Journal::Sink
|
|||||||
{
|
{
|
||||||
std::string partition_;
|
std::string partition_;
|
||||||
beast::unit_test::suite& suite_;
|
beast::unit_test::suite& suite_;
|
||||||
|
Logs* logs_ = nullptr;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SuiteJournalSink(
|
SuiteJournalSink(
|
||||||
std::string const& partition,
|
std::string const& partition,
|
||||||
beast::severities::Severity threshold,
|
beast::severities::Severity threshold,
|
||||||
beast::unit_test::suite& suite)
|
beast::unit_test::suite& suite,
|
||||||
: Sink(threshold, false), partition_(partition + " "), suite_(suite)
|
Logs* logs = nullptr)
|
||||||
|
: Sink(threshold, false)
|
||||||
|
, partition_(partition + " ")
|
||||||
|
, suite_(suite)
|
||||||
|
, logs_(logs)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,11 +103,12 @@ SuiteJournalSink::writeAlways(
|
|||||||
// Only write the string if the level at least equals the threshold.
|
// Only write the string if the level at least equals the threshold.
|
||||||
if (level >= threshold())
|
if (level >= threshold())
|
||||||
{
|
{
|
||||||
|
std::string const& output = logs_ ? logs_->applyTransform(text) : text;
|
||||||
// std::endl flushes → sync() → str()/str("") race in shared buffer →
|
// std::endl flushes → sync() → str()/str("") race in shared buffer →
|
||||||
// crashes
|
// crashes
|
||||||
static std::mutex log_mutex;
|
static std::mutex log_mutex;
|
||||||
std::lock_guard lock(log_mutex);
|
std::lock_guard lock(log_mutex);
|
||||||
suite_.log << s << partition_ << text << std::endl;
|
suite_.log << s << partition_ << output << std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,11 @@
|
|||||||
#include <xrpl/protocol/TER.h>
|
#include <xrpl/protocol/TER.h>
|
||||||
#include <xrpl/protocol/digest.h>
|
#include <xrpl/protocol/digest.h>
|
||||||
#include <any>
|
#include <any>
|
||||||
|
#include <fstream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
#include <set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <wasmedge/wasmedge.h>
|
#include <wasmedge/wasmedge.h>
|
||||||
|
|
||||||
@@ -302,6 +304,130 @@ static WasmEdge_String hookFunctionName =
|
|||||||
// see: lib/system/allocator.cpp
|
// see: lib/system/allocator.cpp
|
||||||
#define WasmEdge_kPageSize 65536ULL
|
#define WasmEdge_kPageSize 65536ULL
|
||||||
|
|
||||||
|
// --- Coverage infrastructure ---
|
||||||
|
//
|
||||||
|
// Global coverage accumulator keyed by hook hash. Persists across all hook
|
||||||
|
// executions in the process. Each __on_source_line call records a (line, col)
|
||||||
|
// pair under the executing hook's hash.
|
||||||
|
//
|
||||||
|
// Test API:
|
||||||
|
// hook::coverageReset() — clear all accumulated data
|
||||||
|
// hook::coverageHits(hookHash) — get hits for a specific hook
|
||||||
|
// hook::coverageLabel(hash, label) — register a human-readable label
|
||||||
|
// hook::coverageDump(path) — write all data to a file
|
||||||
|
//
|
||||||
|
// The dump file format is:
|
||||||
|
// [label or hash]
|
||||||
|
// hits=<line:col>,<line:col>,...
|
||||||
|
|
||||||
|
struct CoverageData
|
||||||
|
{
|
||||||
|
std::set<uint32_t> hits{};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Global accumulator — survives across HookContext lifetimes
|
||||||
|
inline std::map<ripple::uint256, CoverageData>&
|
||||||
|
coverageMap()
|
||||||
|
{
|
||||||
|
static std::map<ripple::uint256, CoverageData> map;
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash → label mapping (e.g. hash → "file:tipbot/tip.c")
|
||||||
|
inline std::map<ripple::uint256, std::string>&
|
||||||
|
coverageLabels()
|
||||||
|
{
|
||||||
|
static std::map<ripple::uint256, std::string> labels;
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void
|
||||||
|
coverageReset()
|
||||||
|
{
|
||||||
|
coverageMap().clear();
|
||||||
|
coverageLabels().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void
|
||||||
|
coverageLabel(ripple::uint256 const& hookHash, std::string const& label)
|
||||||
|
{
|
||||||
|
coverageLabels()[hookHash] = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::set<uint32_t> const*
|
||||||
|
coverageHits(ripple::uint256 const& hookHash)
|
||||||
|
{
|
||||||
|
auto& map = coverageMap();
|
||||||
|
auto it = map.find(hookHash);
|
||||||
|
if (it == map.end())
|
||||||
|
return nullptr;
|
||||||
|
return &it->second.hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool
|
||||||
|
coverageDump(std::string const& path)
|
||||||
|
{
|
||||||
|
auto& map = coverageMap();
|
||||||
|
if (map.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto& labels = coverageLabels();
|
||||||
|
|
||||||
|
std::ofstream out(path);
|
||||||
|
if (!out)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (auto const& [hash, data] : map)
|
||||||
|
{
|
||||||
|
auto it = labels.find(hash);
|
||||||
|
if (it != labels.end())
|
||||||
|
out << "[" << it->second << "]\n";
|
||||||
|
else
|
||||||
|
out << "[" << to_string(hash) << "]\n";
|
||||||
|
|
||||||
|
out << "hits=";
|
||||||
|
bool first = true;
|
||||||
|
for (auto key : data.hits)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
out << ",";
|
||||||
|
out << (key >> 16) << ":" << (key & 0xFFFF);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
out << "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Coverage host callback ---
|
||||||
|
|
||||||
|
inline WasmEdge_Result
|
||||||
|
onSourceLine(
|
||||||
|
void* data_ptr,
|
||||||
|
const WasmEdge_CallingFrameContext* frameCtx,
|
||||||
|
const WasmEdge_Value* in,
|
||||||
|
WasmEdge_Value* out)
|
||||||
|
{
|
||||||
|
// Called by hookz-instrumented WASM at each DWARF source location.
|
||||||
|
// in[0] = line number, in[1] = column number.
|
||||||
|
(void)out;
|
||||||
|
(void)frameCtx;
|
||||||
|
auto* hookCtx = reinterpret_cast<HookContext*>(data_ptr);
|
||||||
|
if (!hookCtx)
|
||||||
|
return WasmEdge_Result_Success;
|
||||||
|
|
||||||
|
uint32_t line = WasmEdge_ValueGetI32(in[0]);
|
||||||
|
uint32_t col = WasmEdge_ValueGetI32(in[1]);
|
||||||
|
|
||||||
|
// Pack (line, col) into a single uint32_t key.
|
||||||
|
// Limits: line < 65536, col < 65536 — more than sufficient for hooks.
|
||||||
|
uint32_t key = (line << 16) | (col & 0xFFFF);
|
||||||
|
coverageMap()[hookCtx->result.hookHash].hits.insert(key);
|
||||||
|
|
||||||
|
return WasmEdge_Result_Success;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HookExecutor is effectively a two-part function:
|
* HookExecutor is effectively a two-part function:
|
||||||
* The first part sets up the Hook Api inside the wasm import, ready for use
|
* The first part sets up the Hook Api inside the wasm import, ready for use
|
||||||
@@ -480,6 +606,22 @@ public:
|
|||||||
#undef HOOK_WRAP_PARAMS
|
#undef HOOK_WRAP_PARAMS
|
||||||
#pragma pop_macro("HOOK_API_DEFINITION")
|
#pragma pop_macro("HOOK_API_DEFINITION")
|
||||||
|
|
||||||
|
// Coverage callback: void __on_source_line(i32 line, i32 col)
|
||||||
|
// Registered unconditionally — production hooks don't import it,
|
||||||
|
// so it's harmless. Instrumented hooks call it at each DWARF
|
||||||
|
// source location to record line:col coverage hits.
|
||||||
|
{
|
||||||
|
static WasmEdge_ValType paramsOSL[] = {
|
||||||
|
WasmEdge_ValType_I32, WasmEdge_ValType_I32};
|
||||||
|
static auto* ftOSL =
|
||||||
|
WasmEdge_FunctionTypeCreate(paramsOSL, 2, nullptr, 0);
|
||||||
|
auto* hfOSL = WasmEdge_FunctionInstanceCreate(
|
||||||
|
ftOSL, hook::onSourceLine, (void*)(&ctx), 0);
|
||||||
|
static auto nameOSL =
|
||||||
|
WasmEdge_StringCreateByCString("__on_source_line");
|
||||||
|
WasmEdge_ModuleInstanceAddFunction(importObj, nameOSL, hfOSL);
|
||||||
|
}
|
||||||
|
|
||||||
WasmEdge_TableInstanceContext* hostTable =
|
WasmEdge_TableInstanceContext* hostTable =
|
||||||
WasmEdge_TableInstanceCreate(tableType);
|
WasmEdge_TableInstanceCreate(tableType);
|
||||||
WasmEdge_ModuleInstanceAddTable(importObj, tableName, hostTable);
|
WasmEdge_ModuleInstanceAddTable(importObj, tableName, hostTable);
|
||||||
|
|||||||
@@ -1267,7 +1267,7 @@ DEFINE_HOOK_FUNCTION(
|
|||||||
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
|
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
|
||||||
return OUT_OF_BOUNDS;
|
return OUT_OF_BOUNDS;
|
||||||
|
|
||||||
if (!j.trace())
|
if (!jh.trace())
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
if (read_len > 128)
|
if (read_len > 128)
|
||||||
@@ -1281,16 +1281,16 @@ DEFINE_HOOK_FUNCTION(
|
|||||||
|
|
||||||
if (read_len > 0)
|
if (read_len > 0)
|
||||||
{
|
{
|
||||||
j.trace() << "HookTrace[" << HC_ACC() << "]: "
|
JLOG(jh.trace()) << "HookTrace[" << HC_ACC() << "]: "
|
||||||
<< std::string_view(
|
<< std::string_view(
|
||||||
(const char*)memory + read_ptr, read_len)
|
(const char*)memory + read_ptr, read_len)
|
||||||
<< ": " << number;
|
<< ": " << number;
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
j.trace() << "HookTrace[" << HC_ACC() << "]: " << number;
|
JLOG(jh.trace()) << "HookTrace[" << HC_ACC() << "]: " << number;
|
||||||
return 0;
|
return 0;
|
||||||
HOOK_TEARDOWN();
|
HOOK_TEARDOWN();
|
||||||
}
|
}
|
||||||
@@ -1310,7 +1310,7 @@ DEFINE_HOOK_FUNCTION(
|
|||||||
NOT_IN_BOUNDS(dread_ptr, dread_len, memory_length))
|
NOT_IN_BOUNDS(dread_ptr, dread_len, memory_length))
|
||||||
return OUT_OF_BOUNDS;
|
return OUT_OF_BOUNDS;
|
||||||
|
|
||||||
if (!j.trace())
|
if (!jh.trace())
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
if (mread_len > 128)
|
if (mread_len > 128)
|
||||||
@@ -1370,8 +1370,8 @@ DEFINE_HOOK_FUNCTION(
|
|||||||
|
|
||||||
if (out_len > 0)
|
if (out_len > 0)
|
||||||
{
|
{
|
||||||
j.trace() << "HookTrace[" << HC_ACC() << "]: "
|
JLOG(jh.trace()) << "HookTrace[" << HC_ACC() << "]: "
|
||||||
<< std::string_view((const char*)output_storage, out_len);
|
<< std::string_view((const char*)output_storage, out_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -3547,7 +3547,7 @@ DEFINE_HOOK_FUNCTION(
|
|||||||
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
|
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
|
||||||
return OUT_OF_BOUNDS;
|
return OUT_OF_BOUNDS;
|
||||||
|
|
||||||
if (!j.trace())
|
if (!jh.trace())
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
if (read_len > 128)
|
if (read_len > 128)
|
||||||
@@ -3560,12 +3560,12 @@ DEFINE_HOOK_FUNCTION(
|
|||||||
|
|
||||||
if (float1 == 0)
|
if (float1 == 0)
|
||||||
{
|
{
|
||||||
j.trace() << "HookTrace[" << HC_ACC() << "]: "
|
JLOG(jh.trace()) << "HookTrace[" << HC_ACC() << "]: "
|
||||||
<< (read_len == 0
|
<< (read_len == 0
|
||||||
? ""
|
? ""
|
||||||
: std::string_view(
|
: std::string_view(
|
||||||
(const char*)memory + read_ptr, read_len))
|
(const char*)memory + read_ptr, read_len))
|
||||||
<< ": Float 0*10^(0) <ZERO>";
|
<< ": Float 0*10^(0) <ZERO>";
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3575,20 +3575,22 @@ DEFINE_HOOK_FUNCTION(
|
|||||||
if (man < minMantissa || man > maxMantissa || exp < minExponent ||
|
if (man < minMantissa || man > maxMantissa || exp < minExponent ||
|
||||||
exp > maxExponent)
|
exp > maxExponent)
|
||||||
{
|
{
|
||||||
j.trace() << "HookTrace[" << HC_ACC() << "]:"
|
JLOG(jh.trace()) << "HookTrace[" << HC_ACC() << "]:"
|
||||||
<< (read_len == 0
|
<< (read_len == 0
|
||||||
? ""
|
? ""
|
||||||
: std::string_view(
|
: std::string_view(
|
||||||
(const char*)memory + read_ptr, read_len))
|
(const char*)memory + read_ptr, read_len))
|
||||||
<< ": Float <INVALID>";
|
<< ": Float <INVALID>";
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
j.trace() << "HookTrace[" << HC_ACC() << "]:"
|
JLOG(jh.trace()) << "HookTrace[" << HC_ACC() << "]:"
|
||||||
<< (read_len == 0 ? ""
|
<< (read_len == 0
|
||||||
: std::string_view(
|
? ""
|
||||||
(const char*)memory + read_ptr, read_len))
|
: std::string_view(
|
||||||
<< ": Float " << (neg ? "-" : "") << man << "*10^(" << exp << ")";
|
(const char*)memory + read_ptr, read_len))
|
||||||
|
<< ": Float " << (neg ? "-" : "") << man << "*10^(" << exp
|
||||||
|
<< ")";
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
HOOK_TEARDOWN();
|
HOOK_TEARDOWN();
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ reduceOffer(auto const& amount)
|
|||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
|
enum class IsDeposit : bool { No = false, Yes = true };
|
||||||
|
|
||||||
/** Calculate LP Tokens given AMM pool reserves.
|
/** Calculate LP Tokens given AMM pool reserves.
|
||||||
* @param asset1 AMM one side of the pool reserve
|
* @param asset1 AMM one side of the pool reserve
|
||||||
* @param asset2 AMM another side of the pool reserve
|
* @param asset2 AMM another side of the pool reserve
|
||||||
@@ -70,7 +72,7 @@ ammLPTokens(
|
|||||||
* @return tokens
|
* @return tokens
|
||||||
*/
|
*/
|
||||||
STAmount
|
STAmount
|
||||||
lpTokensIn(
|
lpTokensOut(
|
||||||
STAmount const& asset1Balance,
|
STAmount const& asset1Balance,
|
||||||
STAmount const& asset1Deposit,
|
STAmount const& asset1Deposit,
|
||||||
STAmount const& lptAMMBalance,
|
STAmount const& lptAMMBalance,
|
||||||
@@ -99,7 +101,7 @@ ammAssetIn(
|
|||||||
* @return tokens out amount
|
* @return tokens out amount
|
||||||
*/
|
*/
|
||||||
STAmount
|
STAmount
|
||||||
lpTokensOut(
|
lpTokensIn(
|
||||||
STAmount const& asset1Balance,
|
STAmount const& asset1Balance,
|
||||||
STAmount const& asset1Withdraw,
|
STAmount const& asset1Withdraw,
|
||||||
STAmount const& lptAMMBalance,
|
STAmount const& lptAMMBalance,
|
||||||
@@ -113,7 +115,7 @@ lpTokensOut(
|
|||||||
* @return calculated asset amount
|
* @return calculated asset amount
|
||||||
*/
|
*/
|
||||||
STAmount
|
STAmount
|
||||||
withdrawByTokens(
|
ammAssetOut(
|
||||||
STAmount const& assetBalance,
|
STAmount const& assetBalance,
|
||||||
STAmount const& lptAMMBalance,
|
STAmount const& lptAMMBalance,
|
||||||
STAmount const& lpTokens,
|
STAmount const& lpTokens,
|
||||||
@@ -517,13 +519,13 @@ square(Number const& n);
|
|||||||
* withdraw to cancel out the precision loss.
|
* withdraw to cancel out the precision loss.
|
||||||
* @param lptAMMBalance LPT AMM Balance
|
* @param lptAMMBalance LPT AMM Balance
|
||||||
* @param lpTokens LP tokens to deposit or withdraw
|
* @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
|
STAmount
|
||||||
adjustLPTokens(
|
adjustLPTokens(
|
||||||
STAmount const& lptAMMBalance,
|
STAmount const& lptAMMBalance,
|
||||||
STAmount const& lpTokens,
|
STAmount const& lpTokens,
|
||||||
bool isDeposit);
|
IsDeposit isDeposit);
|
||||||
|
|
||||||
/** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if
|
/** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if
|
||||||
* the adjusted LP tokens are less than the provided LP tokens.
|
* the adjusted LP tokens are less than the provided LP tokens.
|
||||||
@@ -533,7 +535,7 @@ adjustLPTokens(
|
|||||||
* @param lptAMMBalance LPT AMM Balance
|
* @param lptAMMBalance LPT AMM Balance
|
||||||
* @param lpTokens LP tokens to deposit or withdraw
|
* @param lpTokens LP tokens to deposit or withdraw
|
||||||
* @param tfee trading fee in basis points
|
* @param tfee trading fee in basis points
|
||||||
* @param isDeposit true if deposit, false if withdraw
|
* @param isDeposit Yes if deposit, No if withdraw
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
std::tuple<STAmount, std::optional<STAmount>, STAmount>
|
std::tuple<STAmount, std::optional<STAmount>, STAmount>
|
||||||
@@ -544,7 +546,7 @@ adjustAmountsByLPTokens(
|
|||||||
STAmount const& lptAMMBalance,
|
STAmount const& lptAMMBalance,
|
||||||
STAmount const& lpTokens,
|
STAmount const& lpTokens,
|
||||||
std::uint16_t tfee,
|
std::uint16_t tfee,
|
||||||
bool isDeposit);
|
IsDeposit isDeposit);
|
||||||
|
|
||||||
/** Positive solution for quadratic equation:
|
/** Positive solution for quadratic equation:
|
||||||
* x = (-b + sqrt(b**2 + 4*a*c))/(2*a)
|
* x = (-b + sqrt(b**2 + 4*a*c))/(2*a)
|
||||||
@@ -552,6 +554,134 @@ adjustAmountsByLPTokens(
|
|||||||
Number
|
Number
|
||||||
solveQuadraticEq(Number const& a, Number const& b, Number const& c);
|
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
|
} // namespace ripple
|
||||||
|
|
||||||
#endif // RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED
|
#endif // RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED
|
||||||
|
|||||||
@@ -123,6 +123,17 @@ isOnlyLiquidityProvider(
|
|||||||
Issue const& ammIssue,
|
Issue const& ammIssue,
|
||||||
AccountID const& lpAccount);
|
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
|
} // namespace ripple
|
||||||
|
|
||||||
#endif // RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED
|
#endif // RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ ammLPTokens(
|
|||||||
STAmount const& asset2,
|
STAmount const& asset2,
|
||||||
Issue const& lptIssue)
|
Issue const& lptIssue)
|
||||||
{
|
{
|
||||||
|
// AMM invariant: sqrt(asset1 * asset2) >= LPTokensBalance
|
||||||
|
auto const rounding = Number::downward;
|
||||||
|
NumberRoundModeGuard g(rounding);
|
||||||
auto const tokens = root2(asset1 * asset2);
|
auto const tokens = root2(asset1 * asset2);
|
||||||
return toSTAmount(lptIssue, tokens);
|
return toSTAmount(lptIssue, tokens);
|
||||||
}
|
}
|
||||||
@@ -38,7 +41,7 @@ ammLPTokens(
|
|||||||
* where f1 = 1 - tfee, f2 = (1 - tfee/2)/f1
|
* where f1 = 1 - tfee, f2 = (1 - tfee/2)/f1
|
||||||
*/
|
*/
|
||||||
STAmount
|
STAmount
|
||||||
lpTokensIn(
|
lpTokensOut(
|
||||||
STAmount const& asset1Balance,
|
STAmount const& asset1Balance,
|
||||||
STAmount const& asset1Deposit,
|
STAmount const& asset1Deposit,
|
||||||
STAmount const& lptAMMBalance,
|
STAmount const& lptAMMBalance,
|
||||||
@@ -48,8 +51,10 @@ lpTokensIn(
|
|||||||
auto const f2 = feeMultHalf(tfee) / f1;
|
auto const f2 = feeMultHalf(tfee) / f1;
|
||||||
Number const r = asset1Deposit / asset1Balance;
|
Number const r = asset1Deposit / asset1Balance;
|
||||||
auto const c = root2(f2 * f2 + r / f1) - f2;
|
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:
|
/* Equation 4 solves equation 3 for b:
|
||||||
@@ -78,8 +83,10 @@ ammAssetIn(
|
|||||||
auto const a = 1 / (t2 * t2);
|
auto const a = 1 / (t2 * t2);
|
||||||
auto const b = 2 * d / t2 - 1 / f1;
|
auto const b = 2 * d / t2 - 1 / f1;
|
||||||
auto const c = d * d - f2 * f2;
|
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:
|
/* Equation 7:
|
||||||
@@ -87,7 +94,7 @@ ammAssetIn(
|
|||||||
* where R = b/B, c = R*fee + 2 - fee
|
* where R = b/B, c = R*fee + 2 - fee
|
||||||
*/
|
*/
|
||||||
STAmount
|
STAmount
|
||||||
lpTokensOut(
|
lpTokensIn(
|
||||||
STAmount const& asset1Balance,
|
STAmount const& asset1Balance,
|
||||||
STAmount const& asset1Withdraw,
|
STAmount const& asset1Withdraw,
|
||||||
STAmount const& lptAMMBalance,
|
STAmount const& lptAMMBalance,
|
||||||
@@ -96,8 +103,10 @@ lpTokensOut(
|
|||||||
Number const fr = asset1Withdraw / asset1Balance;
|
Number const fr = asset1Withdraw / asset1Balance;
|
||||||
auto const f1 = getFee(tfee);
|
auto const f1 = getFee(tfee);
|
||||||
auto const c = fr * f1 + 2 - f1;
|
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:
|
/* Equation 8 solves equation 7 for b:
|
||||||
@@ -111,7 +120,7 @@ lpTokensOut(
|
|||||||
* R = (t1**2 + t1*(f - 2)) / (t1*f - 1)
|
* R = (t1**2 + t1*(f - 2)) / (t1*f - 1)
|
||||||
*/
|
*/
|
||||||
STAmount
|
STAmount
|
||||||
withdrawByTokens(
|
ammAssetOut(
|
||||||
STAmount const& assetBalance,
|
STAmount const& assetBalance,
|
||||||
STAmount const& lptAMMBalance,
|
STAmount const& lptAMMBalance,
|
||||||
STAmount const& lpTokens,
|
STAmount const& lpTokens,
|
||||||
@@ -119,8 +128,10 @@ withdrawByTokens(
|
|||||||
{
|
{
|
||||||
auto const f = getFee(tfee);
|
auto const f = getFee(tfee);
|
||||||
Number const t1 = lpTokens / lptAMMBalance;
|
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
|
Number
|
||||||
@@ -133,12 +144,12 @@ STAmount
|
|||||||
adjustLPTokens(
|
adjustLPTokens(
|
||||||
STAmount const& lptAMMBalance,
|
STAmount const& lptAMMBalance,
|
||||||
STAmount const& lpTokens,
|
STAmount const& lpTokens,
|
||||||
bool isDeposit)
|
IsDeposit isDeposit)
|
||||||
{
|
{
|
||||||
// Force rounding downward to ensure adjusted tokens are less or equal
|
// Force rounding downward to ensure adjusted tokens are less or equal
|
||||||
// to requested tokens.
|
// to requested tokens.
|
||||||
saveNumberRoundMode rm(Number::setround(Number::rounding_mode::downward));
|
saveNumberRoundMode rm(Number::setround(Number::rounding_mode::downward));
|
||||||
if (isDeposit)
|
if (isDeposit == IsDeposit::Yes)
|
||||||
return (lptAMMBalance + lpTokens) - lptAMMBalance;
|
return (lptAMMBalance + lpTokens) - lptAMMBalance;
|
||||||
return (lpTokens - lptAMMBalance) + lptAMMBalance;
|
return (lpTokens - lptAMMBalance) + lptAMMBalance;
|
||||||
}
|
}
|
||||||
@@ -151,47 +162,10 @@ adjustAmountsByLPTokens(
|
|||||||
STAmount const& lptAMMBalance,
|
STAmount const& lptAMMBalance,
|
||||||
STAmount const& lpTokens,
|
STAmount const& lpTokens,
|
||||||
std::uint16_t tfee,
|
std::uint16_t tfee,
|
||||||
bool isDeposit)
|
IsDeposit isDeposit)
|
||||||
{
|
{
|
||||||
auto const lpTokensActual =
|
// AMMv1_3 amendment adjusts tokens and amounts in deposit/withdraw
|
||||||
adjustLPTokens(lptAMMBalance, lpTokens, isDeposit);
|
return std::make_tuple(amount, amount2, lpTokens);
|
||||||
|
|
||||||
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};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Number
|
Number
|
||||||
@@ -215,4 +189,117 @@ solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c)
|
|||||||
return (2 * c) / (-b + root2(d));
|
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
|
} // namespace ripple
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
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/app/misc/AMMUtils.h>
|
||||||
#include <xrpld/ledger/Sandbox.h>
|
#include <xrpld/ledger/Sandbox.h>
|
||||||
#include <xrpl/basics/Log.h>
|
#include <xrpl/basics/Log.h>
|
||||||
@@ -462,4 +464,32 @@ isOnlyLiquidityProvider(
|
|||||||
return Unexpected<TER>(tecINTERNAL); // LCOV_EXCL_LINE
|
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
|
} // namespace ripple
|
||||||
|
|||||||
@@ -79,6 +79,21 @@ AMMBid::preflight(PreflightContext const& ctx)
|
|||||||
JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts.";
|
JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts.";
|
||||||
return temMALFORMED;
|
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);
|
return preflight2(ctx);
|
||||||
@@ -233,7 +248,9 @@ applyBid(
|
|||||||
auctionSlot.makeFieldAbsent(sfAuthAccounts);
|
auctionSlot.makeFieldAbsent(sfAuthAccounts);
|
||||||
// Burn the remaining bid amount
|
// Burn the remaining bid amount
|
||||||
auto const saBurn = adjustLPTokens(
|
auto const saBurn = adjustLPTokens(
|
||||||
lptAMMBalance, toSTAmount(lptAMMBalance.issue(), burn), false);
|
lptAMMBalance,
|
||||||
|
toSTAmount(lptAMMBalance.issue(), burn),
|
||||||
|
IsDeposit::No);
|
||||||
if (saBurn >= lptAMMBalance)
|
if (saBurn >= lptAMMBalance)
|
||||||
{
|
{
|
||||||
// This error case should never occur.
|
// This error case should never occur.
|
||||||
|
|||||||
@@ -151,6 +151,17 @@ AMMClawback::applyGuts(Sandbox& sb)
|
|||||||
if (!accountSle)
|
if (!accountSle)
|
||||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
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(
|
auto const expected = ammHolds(
|
||||||
sb,
|
sb,
|
||||||
*ammSle,
|
*ammSle,
|
||||||
@@ -248,10 +259,11 @@ AMMClawback::equalWithdrawMatchingOneAmount(
|
|||||||
STAmount const& amount)
|
STAmount const& amount)
|
||||||
{
|
{
|
||||||
auto frac = Number{amount} / amountBalance;
|
auto frac = Number{amount} / amountBalance;
|
||||||
auto const amount2Withdraw = amount2Balance * frac;
|
auto amount2Withdraw = amount2Balance * frac;
|
||||||
|
|
||||||
auto const lpTokensWithdraw =
|
auto const lpTokensWithdraw =
|
||||||
toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac);
|
toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac);
|
||||||
|
|
||||||
if (lpTokensWithdraw > holdLPtokens)
|
if (lpTokensWithdraw > holdLPtokens)
|
||||||
// if lptoken balance less than what the issuer intended to clawback,
|
// if lptoken balance less than what the issuer intended to clawback,
|
||||||
// clawback all the tokens. Because we are doing a two-asset withdrawal,
|
// clawback all the tokens. Because we are doing a two-asset withdrawal,
|
||||||
@@ -272,18 +284,33 @@ AMMClawback::equalWithdrawMatchingOneAmount(
|
|||||||
mPriorBalance,
|
mPriorBalance,
|
||||||
ctx_.journal);
|
ctx_.journal);
|
||||||
|
|
||||||
// Because we are doing a two-asset withdrawal,
|
auto const& rules = sb.rules();
|
||||||
// tfee is actually not used, so pass tfee as 0.
|
|
||||||
|
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(
|
return AMMWithdraw::withdraw(
|
||||||
sb,
|
sb,
|
||||||
ammSle,
|
ammSle,
|
||||||
ammAccount,
|
ammAccount,
|
||||||
holder,
|
holder,
|
||||||
amountBalance,
|
amountBalance,
|
||||||
amount,
|
amountRounded,
|
||||||
toSTAmount(amount2Balance.issue(), amount2Withdraw),
|
amount2Rounded,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac),
|
tokensAdj,
|
||||||
0,
|
0,
|
||||||
FreezeHandling::fhIGNORE_FREEZE,
|
FreezeHandling::fhIGNORE_FREEZE,
|
||||||
WithdrawAll::No,
|
WithdrawAll::No,
|
||||||
|
|||||||
@@ -545,7 +545,7 @@ AMMDeposit::deposit(
|
|||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
lpTokensDeposit,
|
lpTokensDeposit,
|
||||||
tfee,
|
tfee,
|
||||||
true);
|
IsDeposit::Yes);
|
||||||
|
|
||||||
if (lpTokensDepositActual <= beast::zero)
|
if (lpTokensDepositActual <= beast::zero)
|
||||||
{
|
{
|
||||||
@@ -628,6 +628,15 @@ AMMDeposit::deposit(
|
|||||||
return {tesSUCCESS, lptAMMBalance + lpTokensDepositActual};
|
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
|
/** Proportional deposit of pools assets in exchange for the specified
|
||||||
* amount of LPTokens.
|
* amount of LPTokens.
|
||||||
*/
|
*/
|
||||||
@@ -645,16 +654,25 @@ AMMDeposit::equalDepositTokens(
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
auto const tokensAdj =
|
||||||
|
adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit);
|
||||||
|
if (tokensAdj == beast::zero)
|
||||||
|
return {tecAMM_INVALID_TOKENS, STAmount{}};
|
||||||
auto const frac =
|
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(
|
return deposit(
|
||||||
view,
|
view,
|
||||||
ammAccount,
|
ammAccount,
|
||||||
amountBalance,
|
amountBalance,
|
||||||
multiply(amountBalance, frac, amountBalance.issue()),
|
amountDeposit,
|
||||||
multiply(amount2Balance, frac, amount2Balance.issue()),
|
amount2Deposit,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
lpTokensDeposit,
|
tokensAdj,
|
||||||
depositMin,
|
depositMin,
|
||||||
deposit2Min,
|
deposit2Min,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
@@ -711,37 +729,49 @@ AMMDeposit::equalDepositLimit(
|
|||||||
std::uint16_t tfee)
|
std::uint16_t tfee)
|
||||||
{
|
{
|
||||||
auto frac = Number{amount} / amountBalance;
|
auto frac = Number{amount} / amountBalance;
|
||||||
auto tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac);
|
auto tokensAdj =
|
||||||
if (tokens == beast::zero)
|
getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes);
|
||||||
return {tecAMM_FAILED, STAmount{}};
|
if (tokensAdj == beast::zero)
|
||||||
auto const amount2Deposit = amount2Balance * frac;
|
{
|
||||||
|
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)
|
if (amount2Deposit <= amount2)
|
||||||
return deposit(
|
return deposit(
|
||||||
view,
|
view,
|
||||||
ammAccount,
|
ammAccount,
|
||||||
amountBalance,
|
amountBalance,
|
||||||
amount,
|
amount,
|
||||||
toSTAmount(amount2Balance.issue(), amount2Deposit),
|
amount2Deposit,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
tokens,
|
tokensAdj,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
lpTokensDepositMin,
|
lpTokensDepositMin,
|
||||||
tfee);
|
tfee);
|
||||||
frac = Number{amount2} / amount2Balance;
|
frac = Number{amount2} / amount2Balance;
|
||||||
tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac);
|
tokensAdj =
|
||||||
if (tokens == beast::zero)
|
getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes);
|
||||||
return {tecAMM_FAILED, STAmount{}};
|
if (tokensAdj == beast::zero)
|
||||||
auto const amountDeposit = amountBalance * frac;
|
{
|
||||||
|
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)
|
if (amountDeposit <= amount)
|
||||||
return deposit(
|
return deposit(
|
||||||
view,
|
view,
|
||||||
ammAccount,
|
ammAccount,
|
||||||
amountBalance,
|
amountBalance,
|
||||||
toSTAmount(amountBalance.issue(), amountDeposit),
|
amountDeposit,
|
||||||
amount2,
|
amount2,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
tokens,
|
tokensAdj,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
lpTokensDepositMin,
|
lpTokensDepositMin,
|
||||||
@@ -767,17 +797,27 @@ AMMDeposit::singleDeposit(
|
|||||||
std::optional<STAmount> const& lpTokensDepositMin,
|
std::optional<STAmount> const& lpTokensDepositMin,
|
||||||
std::uint16_t tfee)
|
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)
|
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(
|
return deposit(
|
||||||
view,
|
view,
|
||||||
ammAccount,
|
ammAccount,
|
||||||
amountBalance,
|
amountBalance,
|
||||||
amount,
|
amountDepositAdj,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
tokens,
|
tokensAdj,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
lpTokensDepositMin,
|
lpTokensDepositMin,
|
||||||
@@ -801,8 +841,13 @@ AMMDeposit::singleDepositTokens(
|
|||||||
STAmount const& lpTokensDeposit,
|
STAmount const& lpTokensDeposit,
|
||||||
std::uint16_t tfee)
|
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 =
|
auto const amountDeposit =
|
||||||
ammAssetIn(amountBalance, lptAMMBalance, lpTokensDeposit, tfee);
|
ammAssetIn(amountBalance, lptAMMBalance, tokensAdj, tfee);
|
||||||
if (amountDeposit > amount)
|
if (amountDeposit > amount)
|
||||||
return {tecAMM_FAILED, STAmount{}};
|
return {tecAMM_FAILED, STAmount{}};
|
||||||
return deposit(
|
return deposit(
|
||||||
@@ -812,7 +857,7 @@ AMMDeposit::singleDepositTokens(
|
|||||||
amountDeposit,
|
amountDeposit,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
lpTokensDeposit,
|
tokensAdj,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
@@ -856,20 +901,29 @@ AMMDeposit::singleDepositEPrice(
|
|||||||
{
|
{
|
||||||
if (amount != beast::zero)
|
if (amount != beast::zero)
|
||||||
{
|
{
|
||||||
auto const tokens =
|
auto const tokens = adjustLPTokensOut(
|
||||||
lpTokensIn(amountBalance, amount, lptAMMBalance, tfee);
|
view.rules(),
|
||||||
|
lptAMMBalance,
|
||||||
|
lpTokensOut(amountBalance, amount, lptAMMBalance, tfee));
|
||||||
if (tokens <= beast::zero)
|
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)
|
if (ep <= ePrice)
|
||||||
return deposit(
|
return deposit(
|
||||||
view,
|
view,
|
||||||
ammAccount,
|
ammAccount,
|
||||||
amountBalance,
|
amountBalance,
|
||||||
amount,
|
amountDepositAdj,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
tokens,
|
tokensAdj,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
@@ -900,21 +954,37 @@ AMMDeposit::singleDepositEPrice(
|
|||||||
auto const a1 = c * c;
|
auto const a1 = c * c;
|
||||||
auto const b1 = c * c * f2 * f2 + 2 * c - d * d;
|
auto const b1 = c * c * f2 * f2 + 2 * c - d * d;
|
||||||
auto const c1 = 2 * c * f2 * f2 + 1 - 2 * d * f2;
|
auto const c1 = 2 * c * f2 * f2 + 1 - 2 * d * f2;
|
||||||
auto const amountDeposit = toSTAmount(
|
auto amtNoRoundCb = [&] {
|
||||||
amountBalance.issue(),
|
return f1 * amountBalance * solveQuadraticEq(a1, b1, c1);
|
||||||
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)
|
if (amountDeposit <= beast::zero)
|
||||||
return {tecAMM_FAILED, STAmount{}};
|
return {tecAMM_FAILED, STAmount{}};
|
||||||
auto const tokens =
|
auto tokNoRoundCb = [&] { return amountDeposit / ePrice; };
|
||||||
toSTAmount(lptAMMBalance.issue(), 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(
|
return deposit(
|
||||||
view,
|
view,
|
||||||
ammAccount,
|
ammAccount,
|
||||||
amountBalance,
|
amountBalance,
|
||||||
amountDeposit,
|
amountDepositAdj,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
tokens,
|
tokensAdj,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
|
|||||||
@@ -313,24 +313,9 @@ AMMWithdraw::applyGuts(Sandbox& sb)
|
|||||||
// might not match the LP's trustline balance
|
// might not match the LP's trustline balance
|
||||||
|
|
||||||
if (auto const res =
|
if (auto const res =
|
||||||
isOnlyLiquidityProvider(sb, lpTokens.issue(), account_);
|
verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, account_);
|
||||||
!res)
|
!res)
|
||||||
return {res.error(), false};
|
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_);
|
auto const tfee = getTradingFee(ctx_.view(), *ammSle, account_);
|
||||||
|
|
||||||
@@ -523,7 +508,7 @@ AMMWithdraw::withdraw(
|
|||||||
lpTokensAMMBalance,
|
lpTokensAMMBalance,
|
||||||
lpTokensWithdraw,
|
lpTokensWithdraw,
|
||||||
tfee,
|
tfee,
|
||||||
false);
|
IsDeposit::No);
|
||||||
return std::make_tuple(
|
return std::make_tuple(
|
||||||
amountWithdraw, amount2Withdraw, lpTokensWithdraw);
|
amountWithdraw, amount2Withdraw, lpTokensWithdraw);
|
||||||
}();
|
}();
|
||||||
@@ -682,6 +667,20 @@ AMMWithdraw::withdraw(
|
|||||||
amount2WithdrawActual);
|
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>
|
std::pair<TER, STAmount>
|
||||||
AMMWithdraw::equalWithdrawTokens(
|
AMMWithdraw::equalWithdrawTokens(
|
||||||
Sandbox& view,
|
Sandbox& view,
|
||||||
@@ -785,16 +784,22 @@ AMMWithdraw::equalWithdrawTokens(
|
|||||||
journal);
|
journal);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto const frac = divide(lpTokensWithdraw, lptAMMBalance, noIssue());
|
auto const tokensAdj = adjustLPTokensIn(
|
||||||
auto const withdrawAmount =
|
view.rules(), lptAMMBalance, lpTokensWithdraw, withdrawAll);
|
||||||
multiply(amountBalance, frac, amountBalance.issue());
|
if (tokensAdj == beast::zero)
|
||||||
auto const withdraw2Amount =
|
return {
|
||||||
multiply(amount2Balance, frac, amount2Balance.issue());
|
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
|
// LP is making equal withdrawal by tokens but the requested amount
|
||||||
// of LP tokens is likely too small and results in one-sided pool
|
// of LP tokens is likely too small and results in one-sided pool
|
||||||
// withdrawal due to round off. Fail so the user withdraws
|
// withdrawal due to round off. Fail so the user withdraws
|
||||||
// more tokens.
|
// more tokens.
|
||||||
if (withdrawAmount == beast::zero || withdraw2Amount == beast::zero)
|
if (amountWithdraw == beast::zero || amount2Withdraw == beast::zero)
|
||||||
return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}};
|
return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}};
|
||||||
|
|
||||||
return withdraw(
|
return withdraw(
|
||||||
@@ -803,10 +808,10 @@ AMMWithdraw::equalWithdrawTokens(
|
|||||||
ammAccount,
|
ammAccount,
|
||||||
account,
|
account,
|
||||||
amountBalance,
|
amountBalance,
|
||||||
withdrawAmount,
|
amountWithdraw,
|
||||||
withdraw2Amount,
|
amount2Withdraw,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
lpTokensWithdraw,
|
tokensAdj,
|
||||||
tfee,
|
tfee,
|
||||||
freezeHanding,
|
freezeHanding,
|
||||||
withdrawAll,
|
withdrawAll,
|
||||||
@@ -861,7 +866,16 @@ AMMWithdraw::equalWithdrawLimit(
|
|||||||
std::uint16_t tfee)
|
std::uint16_t tfee)
|
||||||
{
|
{
|
||||||
auto frac = Number{amount} / amountBalance;
|
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)
|
if (amount2Withdraw <= amount2)
|
||||||
{
|
{
|
||||||
return withdraw(
|
return withdraw(
|
||||||
@@ -870,26 +884,34 @@ AMMWithdraw::equalWithdrawLimit(
|
|||||||
ammAccount,
|
ammAccount,
|
||||||
amountBalance,
|
amountBalance,
|
||||||
amount,
|
amount,
|
||||||
toSTAmount(amount2.issue(), amount2Withdraw),
|
amount2Withdraw,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac),
|
tokensAdj,
|
||||||
tfee);
|
tfee);
|
||||||
}
|
}
|
||||||
|
|
||||||
frac = Number{amount2} / amount2Balance;
|
frac = Number{amount2} / amount2Balance;
|
||||||
auto const amountWithdraw = amountBalance * frac;
|
auto amountWithdraw =
|
||||||
XRPL_ASSERT(
|
getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No);
|
||||||
amountWithdraw <= amount,
|
tokensAdj =
|
||||||
"ripple::AMMWithdraw::equalWithdrawLimit : maximum amountWithdraw");
|
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(
|
return withdraw(
|
||||||
view,
|
view,
|
||||||
ammSle,
|
ammSle,
|
||||||
ammAccount,
|
ammAccount,
|
||||||
amountBalance,
|
amountBalance,
|
||||||
toSTAmount(amount.issue(), amountWithdraw),
|
amountWithdraw,
|
||||||
amount2,
|
amount2,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac),
|
tokensAdj,
|
||||||
tfee);
|
tfee);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -908,19 +930,29 @@ AMMWithdraw::singleWithdraw(
|
|||||||
STAmount const& amount,
|
STAmount const& amount,
|
||||||
std::uint16_t tfee)
|
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)
|
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(
|
return withdraw(
|
||||||
view,
|
view,
|
||||||
ammSle,
|
ammSle,
|
||||||
ammAccount,
|
ammAccount,
|
||||||
amountBalance,
|
amountBalance,
|
||||||
amount,
|
amountWithdrawAdj,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
tokens,
|
tokensAdj,
|
||||||
tfee);
|
tfee);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,8 +977,13 @@ AMMWithdraw::singleWithdrawTokens(
|
|||||||
STAmount const& lpTokensWithdraw,
|
STAmount const& lpTokensWithdraw,
|
||||||
std::uint16_t tfee)
|
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 =
|
auto const amountWithdraw =
|
||||||
withdrawByTokens(amountBalance, lptAMMBalance, lpTokensWithdraw, tfee);
|
ammAssetOut(amountBalance, lptAMMBalance, tokensAdj, tfee);
|
||||||
if (amount == beast::zero || amountWithdraw >= amount)
|
if (amount == beast::zero || amountWithdraw >= amount)
|
||||||
{
|
{
|
||||||
return withdraw(
|
return withdraw(
|
||||||
@@ -957,7 +994,7 @@ AMMWithdraw::singleWithdrawTokens(
|
|||||||
amountWithdraw,
|
amountWithdraw,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
lpTokensWithdraw,
|
tokensAdj,
|
||||||
tfee);
|
tfee);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1006,11 +1043,24 @@ AMMWithdraw::singleWithdrawEPrice(
|
|||||||
// t = T*(T + A*E*(f - 2))/(T*f - A*E)
|
// t = T*(T + A*E*(f - 2))/(T*f - A*E)
|
||||||
Number const ae = amountBalance * ePrice;
|
Number const ae = amountBalance * ePrice;
|
||||||
auto const f = getFee(tfee);
|
auto const f = getFee(tfee);
|
||||||
auto const tokens = lptAMMBalance * (lptAMMBalance + ae * (f - 2)) /
|
auto tokNoRoundCb = [&] {
|
||||||
(lptAMMBalance * f - ae);
|
return lptAMMBalance * (lptAMMBalance + ae * (f - 2)) /
|
||||||
if (tokens <= 0)
|
(lptAMMBalance * f - ae);
|
||||||
return {tecAMM_FAILED, STAmount{}};
|
};
|
||||||
auto const amountWithdraw = toSTAmount(amount.issue(), tokens / ePrice);
|
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)
|
if (amount == beast::zero || amountWithdraw >= amount)
|
||||||
{
|
{
|
||||||
return withdraw(
|
return withdraw(
|
||||||
@@ -1021,7 +1071,7 @@ AMMWithdraw::singleWithdrawEPrice(
|
|||||||
amountWithdraw,
|
amountWithdraw,
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
lptAMMBalance,
|
lptAMMBalance,
|
||||||
toSTAmount(lptAMMBalance.issue(), tokens),
|
tokensAdj,
|
||||||
tfee);
|
tfee);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ private:
|
|||||||
std::uint16_t tfee);
|
std::uint16_t tfee);
|
||||||
|
|
||||||
/** Check from the flags if it's withdraw all */
|
/** Check from the flags if it's withdraw all */
|
||||||
WithdrawAll
|
static WithdrawAll
|
||||||
isWithdrawAll(STTx const& tx);
|
isWithdrawAll(STTx const& tx);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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/tx/detail/InvariantCheck.h>
|
||||||
|
|
||||||
#include <xrpld/app/misc/CredentialHelpers.h>
|
#include <xrpld/app/misc/CredentialHelpers.h>
|
||||||
@@ -1674,4 +1677,309 @@ ValidPermissionedDomain::finalize(
|
|||||||
(sleStatus_[1] ? check(*sleStatus_[1], j) : true);
|
(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
|
} // namespace ripple
|
||||||
|
|||||||
@@ -617,6 +617,69 @@ public:
|
|||||||
beast::Journal const&);
|
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
|
// additional invariant checks can be declared above and then added to this
|
||||||
// tuple
|
// tuple
|
||||||
using InvariantChecks = std::tuple<
|
using InvariantChecks = std::tuple<
|
||||||
@@ -636,7 +699,8 @@ using InvariantChecks = std::tuple<
|
|||||||
NFTokenCountTracking,
|
NFTokenCountTracking,
|
||||||
ValidClawback,
|
ValidClawback,
|
||||||
ValidMPTIssuance,
|
ValidMPTIssuance,
|
||||||
ValidPermissionedDomain>;
|
ValidPermissionedDomain,
|
||||||
|
ValidAMM>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief get a tuple of all invariant checks
|
* @brief get a tuple of all invariant checks
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
#define RIPPLE_APP_BOOK_OFFER_H_INCLUDED
|
#define RIPPLE_APP_BOOK_OFFER_H_INCLUDED
|
||||||
|
|
||||||
#include <xrpld/ledger/View.h>
|
#include <xrpld/ledger/View.h>
|
||||||
|
|
||||||
|
#include <xrpl/basics/Log.h>
|
||||||
#include <xrpl/basics/contract.h>
|
#include <xrpl/basics/contract.h>
|
||||||
#include <xrpl/protocol/Quality.h>
|
#include <xrpl/protocol/Quality.h>
|
||||||
#include <xrpl/protocol/Rules.h>
|
#include <xrpl/protocol/Rules.h>
|
||||||
@@ -169,8 +171,21 @@ public:
|
|||||||
* always returns true.
|
* always returns true.
|
||||||
*/
|
*/
|
||||||
bool
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto result = validateGuards(
|
auto result = validateGuards(
|
||||||
hook, // wasm to verify
|
hook,
|
||||||
logger,
|
logger,
|
||||||
hsacc,
|
hsacc,
|
||||||
hook_api::getImportWhitelist(ctx.rules),
|
hook_api::getImportWhitelist(ctx.rules),
|
||||||
|
|||||||
Reference in New Issue
Block a user