Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: AMM swap rounding #5002

Merged
merged 2 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 119 additions & 14 deletions src/ripple/app/misc/AMMHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
#include <ripple/basics/Number.h>
#include <ripple/protocol/AMMCore.h>
#include <ripple/protocol/AmountConversions.h>
#include <ripple/protocol/Feature.h>
#include <ripple/protocol/Issue.h>
#include <ripple/protocol/Quality.h>
#include <ripple/protocol/Rules.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/protocol/STAmount.h>

Expand Down Expand Up @@ -228,10 +230,62 @@ swapAssetIn(
TIn const& assetIn,
std::uint16_t tfee)
{
return toAmount<TOut>(
getIssue(pool.out),
pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
Number::rounding_mode::downward);
if (auto const& rules = getCurrentTransactionRules();
rules && rules->enabled(fixAMMRounding))
{
// set rounding to always favor the amm. Clip to zero.
// calculate:
// pool.out -
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// and explicitly set the rounding modes
// Favoring the amm means we should:
// minimize:
// pool.out -
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// maximize:
// (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
// (pool.in * pool.out)
// minimize:
// (pool.in + assetIn * feeMult(tfee)),
// minimize:
// assetIn * feeMult(tfee)
// feeMult is: (1-fee), fee is tfee/100000
// minimize:
// 1-fee
// maximize:
// fee
saveNumberRoundMode _{Number::getround()};

Number::setround(Number::upward);
auto const numerator = pool.in * pool.out;
auto const fee = getFee(tfee);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to always round upwards getFee() and downward feeMult(). The fee is added to the pool so we want always to maximize it's effect. Can have the default just in case it needs to be changed in some instances:

inline Number
getFee(std::uint16_t tfee, Number::rounding_mode rounding = Number::rounding_mode::upward)
{
    if (auto const& rules = getCurrentTransactionRules();
        rules && rules->enabled(fixAMMRounding))
    {
        saveNumberRoundMode rm(Number::setround(rounding));
        return Number{tfee} / AUCTION_SLOT_FEE_SCALE_FACTOR;
    }
    return Number{tfee} / AUCTION_SLOT_FEE_SCALE_FACTOR;
}

inline Number
feeMult(std::uint16_t tfee, Number::rounding_mode rounding = Number::rounding_mode::downward)
{
    if (auto const& rules = getCurrentTransactionRules();
        rules && rules->enabled(fixAMMRounding))
    {
        saveNumberRoundMode rm(Number::setround(rounding));
        return 1 - getFee(tfee);
    }
    return 1 - getFee(tfee);
}


Number::setround(Number::downward);
auto const denom = pool.in + assetIn * (1 - fee);

if (denom.signum() <= 0)
return toAmount<TOut>(getIssue(pool.out), 0);

Number::setround(Number::upward);
auto const ratio = numerator / denom;

Number::setround(Number::downward);
auto const swapOut = pool.out - ratio;

if (swapOut.signum() < 0)
return toAmount<TOut>(getIssue(pool.out), 0);

return toAmount<TOut>(
getIssue(pool.out), swapOut, Number::rounding_mode::downward);
}
else
{
return toAmount<TOut>(
getIssue(pool.out),
pool.out -
(pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)),
Number::rounding_mode::downward);
}
}

/** Swap assetOut out of the pool and swap in a proportional amount
Expand All @@ -250,11 +304,62 @@ swapAssetOut(
TOut const& assetOut,
std::uint16_t tfee)
{
return toAmount<TIn>(
getIssue(pool.in),
((pool.in * pool.out) / (pool.out - assetOut) - pool.in) /
feeMult(tfee),
Number::rounding_mode::upward);
if (auto const& rules = getCurrentTransactionRules();
rules && rules->enabled(fixAMMRounding))
{
// set rounding to always favor the amm. Clip to zero.
// calculate:
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) /
// (1-tfee/100000)
// maximize:
// ((pool.in * pool.out) / (pool.out - assetOut) - pool.in)
// maximize:
// (pool.in * pool.out) / (pool.out - assetOut)
// maximize:
// (pool.in * pool.out)
// minimize
// (pool.out - assetOut)
// minimize:
// (1-tfee/100000)
// maximize:
// tfee/100000

saveNumberRoundMode _{Number::getround()};

Number::setround(Number::upward);
auto const numerator = pool.in * pool.out;

Number::setround(Number::downward);
auto const denom = pool.out - assetOut;
if (denom.signum() <= 0)
{
return toMaxAmount<TIn>(getIssue(pool.in));
}

Number::setround(Number::upward);
auto const ratio = numerator / denom;
auto const numerator2 = ratio - pool.in;
auto const fee = getFee(tfee);

Number::setround(Number::downward);
auto const feeMult = 1 - fee;

Number::setround(Number::upward);
auto const swapIn = numerator2 / feeMult;
if (swapIn.signum() < 0)
return toAmount<TIn>(getIssue(pool.in), 0);

return toAmount<TIn>(
getIssue(pool.in), swapIn, Number::rounding_mode::upward);
}
else
{
return toAmount<TIn>(
getIssue(pool.in),
((pool.in * pool.out) / (pool.out - assetOut) - pool.in) /
feeMult(tfee),
Number::rounding_mode::upward);
}
}

/** Return square of n.
Expand All @@ -263,12 +368,12 @@ Number
square(Number const& n);

/** Adjust LP tokens to deposit/withdraw.
* Amount type keeps 16 digits. Maintaining the LP balance by adding deposited
* tokens or subtracting withdrawn LP tokens from LP balance results in
* losing precision in LP balance. I.e. the resulting LP balance
* Amount type keeps 16 digits. Maintaining the LP balance by adding
* deposited tokens or subtracting withdrawn LP tokens from LP balance
* results in losing precision in LP balance. I.e. the resulting LP balance
* is less than the actual sum of LP tokens. To adjust for this, subtract
* old tokens balance from the new one for deposit or vice versa for withdraw
* to cancel out the precision loss.
* old tokens balance from the new one for deposit or vice versa for
* withdraw to cancel out the precision loss.
* @param lptAMMBalance LPT AMM Balance
* @param lpTokens LP tokens to deposit or withdraw
* @param isDeposit true if deposit, false if withdraw
Expand Down
3 changes: 3 additions & 0 deletions src/ripple/app/tx/impl/Transactor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -831,8 +831,11 @@ Transactor::operator()()
{
JLOG(j_.trace()) << "apply: " << ctx_.tx.getTransactionID();

// raii classes for the current ledger rules. fixSTAmountCanonicalize and
// fixSTAmountCanonicalize predate the rulesGuard and should be replaced.
STAmountSO stAmountSO{view().rules().enabled(fixSTAmountCanonicalize)};
NumberSO stNumberSO{view().rules().enabled(fixUniversalNumber)};
CurrentTransactionRulesGuard currentTransctionRulesGuard(view().rules());

#ifdef DEBUG
{
Expand Down
7 changes: 7 additions & 0 deletions src/ripple/basics/Number.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ class Number
return x.mantissa_ < y.mantissa_;
}

/** Return the sign of the amount */
constexpr int
signum() const noexcept
{
return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0);
}

friend constexpr bool
operator>(Number const& x, Number const& y) noexcept
{
Expand Down
12 changes: 11 additions & 1 deletion src/ripple/ledger/impl/View.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1147,7 +1147,17 @@ accountSend(
beast::Journal j,
WaiveTransferFee waiveFee)
{
assert(saAmount >= beast::zero);
if (view.rules().enabled(fixAMMRounding))
{
if (saAmount < beast::zero)
{
return tecINTERNAL;
}
}
else
{
assert(saAmount >= beast::zero);
}

/* If we aren't sending anything or if the sender is the same as the
* receiver then we don't need to do anything.
Expand Down
52 changes: 46 additions & 6 deletions src/ripple/protocol/AmountConversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
#include <ripple/basics/XRPAmount.h>
#include <ripple/protocol/STAmount.h>

#include <type_traits>

namespace ripple {

inline STAmount
Expand Down Expand Up @@ -130,16 +132,44 @@ toAmount(
saveNumberRoundMode rm(Number::getround());
if (isXRP(issue))
Number::setround(mode);

if constexpr (std::is_same_v<IOUAmount, T>)
return IOUAmount(n);
if constexpr (std::is_same_v<XRPAmount, T>)
else if constexpr (std::is_same_v<XRPAmount, T>)
return XRPAmount(static_cast<std::int64_t>(n));
if constexpr (std::is_same_v<STAmount, T>)
else if constexpr (std::is_same_v<STAmount, T>)
{
if (isXRP(issue))
return STAmount(issue, static_cast<std::int64_t>(n));
return STAmount(issue, n.mantissa(), n.exponent());
}
else
{
constexpr bool alwaysFalse = !std::is_same_v<T, T>;
static_assert(alwaysFalse, "Unsupported type for toAmount");
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using concept instead of the static_assert here and other places?

template <typename T>
concept ValidAmount =
    std::is_same_v<T, IOUAmount> ||
    std::is_same_v<T, XRPAmount> ||
    std::is_same_v<T, STAmount>;

template <ValidAmount T>
T
toMaxAmount(Issue const& issue)
{
 ...
}

I don't think max Number is applicable, at least not in the context that toMaxAmount() is used.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I got a compile error without the supporting Number.

I don't object to the concept, but I weakly prefer the code without it. I don't love that the is_same is repeated in both the body of the function and the concept, and the concept isn't important for distinguishing between overloads. I'll change it if you or others feel strongly, but I think I prefer the non-concept version.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It builds for me without Number on OSX, Linux, and Windows.

I prefer concept since it's clearly defines the type constraint. I don't feel strongly about it though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hear you. I don't think this is worth spending too much time on. I'm going to keep it as is.

template <typename T>
T
toMaxAmount(Issue const& issue)
{
if constexpr (std::is_same_v<IOUAmount, T>)
return IOUAmount(STAmount::cMaxValue, STAmount::cMaxOffset);
else if constexpr (std::is_same_v<XRPAmount, T>)
return XRPAmount(static_cast<std::int64_t>(STAmount::cMaxNativeN));
else if constexpr (std::is_same_v<STAmount, T>)
{
if (isXRP(issue))
return STAmount(
issue, static_cast<std::int64_t>(STAmount::cMaxNativeN));
return STAmount(issue, STAmount::cMaxValue, STAmount::cMaxOffset);
}
else
{
constexpr bool alwaysFalse = !std::is_same_v<T, T>;
static_assert(alwaysFalse, "Unsupported type for toMaxAmount");
}
}

inline STAmount
Expand All @@ -157,10 +187,15 @@ getIssue(T const& amt)
{
if constexpr (std::is_same_v<IOUAmount, T>)
return noIssue();
if constexpr (std::is_same_v<XRPAmount, T>)
else if constexpr (std::is_same_v<XRPAmount, T>)
return xrpIssue();
if constexpr (std::is_same_v<STAmount, T>)
else if constexpr (std::is_same_v<STAmount, T>)
return amt.issue();
else
{
constexpr bool alwaysFalse = !std::is_same_v<T, T>;
static_assert(alwaysFalse, "Unsupported type for getIssue");
}
}

template <typename T>
Expand All @@ -169,10 +204,15 @@ get(STAmount const& a)
{
if constexpr (std::is_same_v<IOUAmount, T>)
return a.iou();
if constexpr (std::is_same_v<XRPAmount, T>)
else if constexpr (std::is_same_v<XRPAmount, T>)
return a.xrp();
if constexpr (std::is_same_v<STAmount, T>)
else if constexpr (std::is_same_v<STAmount, T>)
return a;
else
{
constexpr bool alwaysFalse = !std::is_same_v<T, T>;
static_assert(alwaysFalse, "Unsupported type for get");
}
}

} // namespace ripple
Expand Down
3 changes: 2 additions & 1 deletion src/ripple/protocol/Feature.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 72;
static constexpr std::size_t numFeatures = 73;

/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
Expand Down Expand Up @@ -359,6 +359,7 @@ extern uint256 const featurePriceOracle;
extern uint256 const fixEmptyDID;
extern uint256 const fixXChainRewardRounding;
extern uint256 const fixPreviousTxnID;
extern uint256 const fixAMMRounding;

} // namespace ripple

Expand Down
37 changes: 37 additions & 0 deletions src/ripple/protocol/Rules.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include <ripple/basics/base_uint.h>
#include <ripple/beast/hash/uhash.h>
#include <ripple/protocol/STVector256.h>

#include <unordered_set>

namespace ripple {
Expand All @@ -42,9 +43,14 @@ class Rules
public:
Rules(Rules const&) = default;

Rules(Rules&&) = default;

Rules&
operator=(Rules const&) = default;

Rules&
operator=(Rules&&) = default;

Rules() = delete;

/** Construct an empty rule set.
Expand Down Expand Up @@ -90,5 +96,36 @@ class Rules
operator!=(Rules const& other) const;
};

std::optional<Rules> const&
getCurrentTransactionRules();

void
setCurrentTransactionRules(std::optional<Rules> r);

/** RAII class to set and restore the current transaction rules
*/
class CurrentTransactionRulesGuard
{
public:
explicit CurrentTransactionRulesGuard(Rules r)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class should not be CopyConstructible nor CopyAssignable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, agreed, thanks! Fixed in 94bbc9c8a9 [fold] CurrentTransactionRulesGuard should not be copyable or assignable

: saved_(getCurrentTransactionRules())
{
setCurrentTransactionRules(std::move(r));
}

~CurrentTransactionRulesGuard()
{
setCurrentTransactionRules(saved_);
}

CurrentTransactionRulesGuard() = delete;
CurrentTransactionRulesGuard(CurrentTransactionRulesGuard const&) = delete;
CurrentTransactionRulesGuard&
operator=(CurrentTransactionRulesGuard const&) = delete;

private:
std::optional<Rules> saved_;
};

} // namespace ripple
#endif
1 change: 1 addition & 0 deletions src/ripple/protocol/impl/Feature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ REGISTER_FEATURE(PriceOracle, Supported::yes, VoteBehavior::De
REGISTER_FIX (fixEmptyDID, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixXChainRewardRounding, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixPreviousTxnID, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixAMMRounding, Supported::yes, VoteBehavior::DefaultNo);

// The following amendments are obsolete, but must remain supported
// because they could potentially get enabled.
Expand Down
Loading
Loading