Skip to content

Commit a7c4a47

Browse files
gregtatcamWietseWind
authored andcommitted
fix: improper handling of large synthetic AMM offers:
A large synthetic offer was not handled correctly in the payment engine. This patch fixes that issue and introduces a new invariant check while processing synthetic offers.
1 parent d7d15a9 commit a7c4a47

File tree

9 files changed

+128
-26
lines changed

9 files changed

+128
-26
lines changed

src/ripple/app/misc/AMMHelpers.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ withinRelativeDistance(
134134
template <typename Amt>
135135
requires(
136136
std::is_same_v<Amt, STAmount> || std::is_same_v<Amt, IOUAmount> ||
137-
std::is_same_v<Amt, XRPAmount>)
137+
std::is_same_v<Amt, XRPAmount> || std::is_same_v<Amt, Number>)
138138
bool
139139
withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist)
140140
{

src/ripple/app/paths/AMMLiquidity.h

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,17 @@ class AMMLiquidity
137137
TAmounts<TIn, TOut>
138138
generateFibSeqOffer(TAmounts<TIn, TOut> const& balances) const;
139139

140-
/** Generate max offer
140+
/** Generate max offer.
141+
* If `fixAMMOverflowOffer` is active, the offer is generated as:
142+
* takerGets = 99% * balances.out takerPays = swapOut(takerGets).
143+
* Return nullopt if takerGets is 0 or takerGets == balances.out.
144+
*
145+
* If `fixAMMOverflowOffer` is not active, the offer is generated as:
146+
* takerPays = max input amount;
147+
* takerGets = swapIn(takerPays).
141148
*/
142-
AMMOffer<TIn, TOut>
143-
maxOffer(TAmounts<TIn, TOut> const& balances) const;
149+
std::optional<AMMOffer<TIn, TOut>>
150+
maxOffer(TAmounts<TIn, TOut> const& balances, Rules const& rules) const;
144151
};
145152

146153
} // namespace ripple

src/ripple/app/paths/AMMOffer.h

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,8 @@ class AMMOffer
5050
// the swap out of the entire side of the pool, in which case
5151
// the swap in amount is infinite.
5252
TAmounts<TIn, TOut> const amounts_;
53-
// If seated then current pool balances. Used in one-path limiting steps
54-
// to swap in/out.
55-
std::optional<TAmounts<TIn, TOut>> const balances_;
53+
// Current pool balances.
54+
TAmounts<TIn, TOut> const balances_;
5655
// The Spot Price quality if balances != amounts
5756
// else the amounts quality
5857
Quality const quality_;
@@ -63,7 +62,7 @@ class AMMOffer
6362
AMMOffer(
6463
AMMLiquidity<TIn, TOut> const& ammLiquidity,
6564
TAmounts<TIn, TOut> const& amounts,
66-
std::optional<TAmounts<TIn, TOut>> const& balances,
65+
TAmounts<TIn, TOut> const& balances,
6766
Quality const& quality);
6867

6968
Quality
@@ -142,6 +141,12 @@ class AMMOffer
142141
// AMM doesn't pay transfer fee on Payment tx
143142
return {ofrInRate, QUALITY_ONE};
144143
}
144+
145+
/** Check the new pool product is greater or equal to the old pool
146+
* product or if decreases then within some threshold.
147+
*/
148+
bool
149+
checkInvariant(TAmounts<TIn, TOut> const& consumed, beast::Journal j) const;
145150
};
146151

147152
} // namespace ripple

src/ripple/app/paths/impl/AMMLiquidity.cpp

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ AMMLiquidity<TIn, TOut>::generateFibSeqOffer(
9393
return cur;
9494
}
9595

96+
namespace {
9697
template <typename T>
9798
constexpr T
9899
maxAmount()
@@ -105,16 +106,41 @@ maxAmount()
105106
return STAmount(STAmount::cMaxValue / 2, STAmount::cMaxOffset);
106107
}
107108

109+
template <typename T>
110+
T
111+
maxOut(T const& out, Issue const& iss)
112+
{
113+
Number const res = out * Number{99, -2};
114+
return toAmount<T>(iss, res, Number::rounding_mode::downward);
115+
}
116+
} // namespace
117+
108118
template <typename TIn, typename TOut>
109-
AMMOffer<TIn, TOut>
110-
AMMLiquidity<TIn, TOut>::maxOffer(TAmounts<TIn, TOut> const& balances) const
119+
std::optional<AMMOffer<TIn, TOut>>
120+
AMMLiquidity<TIn, TOut>::maxOffer(
121+
TAmounts<TIn, TOut> const& balances,
122+
Rules const& rules) const
111123
{
112-
return AMMOffer<TIn, TOut>(
113-
*this,
114-
{maxAmount<TIn>(),
115-
swapAssetIn(balances, maxAmount<TIn>(), tradingFee_)},
116-
balances,
117-
Quality{balances});
124+
if (!rules.enabled(fixAMMOverflowOffer))
125+
{
126+
return AMMOffer<TIn, TOut>(
127+
*this,
128+
{maxAmount<TIn>(),
129+
swapAssetIn(balances, maxAmount<TIn>(), tradingFee_)},
130+
balances,
131+
Quality{balances});
132+
}
133+
else
134+
{
135+
auto const out = maxOut<TOut>(balances.out, issueOut());
136+
if (out <= TOut{0} || out >= balances.out)
137+
return std::nullopt;
138+
return AMMOffer<TIn, TOut>(
139+
*this,
140+
{swapAssetOut(balances, out, tradingFee_), out},
141+
balances,
142+
Quality{balances});
143+
}
118144
}
119145

120146
template <typename TIn, typename TOut>
@@ -167,15 +193,16 @@ AMMLiquidity<TIn, TOut>::getOffer(
167193
if (clobQuality && Quality{amounts} < clobQuality)
168194
return std::nullopt;
169195
return AMMOffer<TIn, TOut>(
170-
*this, amounts, std::nullopt, Quality{amounts});
196+
*this, amounts, balances, Quality{amounts});
171197
}
172198
else if (!clobQuality)
173199
{
174200
// If there is no CLOB to compare against, return the largest
175201
// amount, which doesn't overflow. The size is going to be
176202
// changed in BookStep per either deliver amount limit, or
177-
// sendmax, or available output or input funds.
178-
return maxOffer(balances);
203+
// sendmax, or available output or input funds. Might return
204+
// nullopt if the pool is small.
205+
return maxOffer(balances, view.rules());
179206
}
180207
else if (
181208
auto const amounts =
@@ -188,7 +215,10 @@ AMMLiquidity<TIn, TOut>::getOffer(
188215
catch (std::overflow_error const& e)
189216
{
190217
JLOG(j_.error()) << "AMMLiquidity::getOffer overflow " << e.what();
191-
return maxOffer(balances);
218+
if (!view.rules().enabled(fixAMMOverflowOffer))
219+
return maxOffer(balances, view.rules());
220+
else
221+
return std::nullopt;
192222
}
193223
catch (std::exception const& e)
194224
{

src/ripple/app/paths/impl/AMMOffer.cpp

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ template <typename TIn, typename TOut>
2727
AMMOffer<TIn, TOut>::AMMOffer(
2828
AMMLiquidity<TIn, TOut> const& ammLiquidity,
2929
TAmounts<TIn, TOut> const& amounts,
30-
std::optional<TAmounts<TIn, TOut>> const& balances,
30+
TAmounts<TIn, TOut> const& balances,
3131
Quality const& quality)
3232
: ammLiquidity_(ammLiquidity)
3333
, amounts_(amounts)
@@ -110,7 +110,7 @@ AMMOffer<TIn, TOut>::limitOut(
110110
// Change the offer size according to the conservation function. The offer
111111
// quality is increased in this case, but it doesn't matter since there is
112112
// only one path.
113-
return {swapAssetOut(*balances_, limit, ammLiquidity_.tradingFee()), limit};
113+
return {swapAssetOut(balances_, limit, ammLiquidity_.tradingFee()), limit};
114114
}
115115

116116
template <typename TIn, typename TOut>
@@ -122,7 +122,7 @@ AMMOffer<TIn, TOut>::limitIn(
122122
// See the comments above in limitOut().
123123
if (ammLiquidity_.multiPath())
124124
return quality().ceil_in(offrAmt, limit);
125-
return {limit, swapAssetIn(*balances_, limit, ammLiquidity_.tradingFee())};
125+
return {limit, swapAssetIn(balances_, limit, ammLiquidity_.tradingFee())};
126126
}
127127

128128
template <typename TIn, typename TOut>
@@ -132,7 +132,45 @@ AMMOffer<TIn, TOut>::getQualityFunc() const
132132
if (ammLiquidity_.multiPath())
133133
return QualityFunction{quality(), QualityFunction::CLOBLikeTag{}};
134134
return QualityFunction{
135-
*balances_, ammLiquidity_.tradingFee(), QualityFunction::AMMTag{}};
135+
balances_, ammLiquidity_.tradingFee(), QualityFunction::AMMTag{}};
136+
}
137+
138+
template <typename TIn, typename TOut>
139+
bool
140+
AMMOffer<TIn, TOut>::checkInvariant(
141+
TAmounts<TIn, TOut> const& consumed,
142+
beast::Journal j) const
143+
{
144+
if (consumed.in > amounts_.in || consumed.out > amounts_.out)
145+
{
146+
JLOG(j.error()) << "AMMOffer::checkInvariant failed: consumed "
147+
<< to_string(consumed.in) << " "
148+
<< to_string(consumed.out) << " amounts "
149+
<< to_string(amounts_.in) << " "
150+
<< to_string(amounts_.out);
151+
152+
return false;
153+
}
154+
155+
Number const product = balances_.in * balances_.out;
156+
auto const newBalances = TAmounts<TIn, TOut>{
157+
balances_.in + consumed.in, balances_.out - consumed.out};
158+
Number const newProduct = newBalances.in * newBalances.out;
159+
160+
if (newProduct >= product ||
161+
withinRelativeDistance(product, newProduct, Number{1, -7}))
162+
return true;
163+
164+
JLOG(j.error()) << "AMMOffer::checkInvariant failed: balances "
165+
<< to_string(balances_.in) << " "
166+
<< to_string(balances_.out) << " new balances "
167+
<< to_string(newBalances.in) << " "
168+
<< to_string(newBalances.out) << " product/newProduct "
169+
<< product << " " << newProduct << " diff "
170+
<< (product != Number{0}
171+
? to_string((product - newProduct) / product)
172+
: "undefined");
173+
return false;
136174
}
137175

138176
template class AMMOffer<STAmount, STAmount>;

src/ripple/app/paths/impl/BookStep.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,17 @@ BookStep<TIn, TOut, TDerived>::consumeOffer(
793793
TAmounts<TIn, TOut> const& stepAmt,
794794
TOut const& ownerGives) const
795795
{
796+
if (!offer.checkInvariant(ofrAmt, j_))
797+
{
798+
// purposely written as separate if statements so we get logging even
799+
// when the amendment isn't active.
800+
if (sb.rules().enabled(fixAMMOverflowOffer))
801+
{
802+
Throw<FlowException>(
803+
tecINVARIANT_FAILED, "AMM pool product invariant failed.");
804+
}
805+
}
806+
796807
// The offer owner gets the ofrAmt. The difference between ofrAmt and
797808
// stepAmt is a transfer fee that goes to book_.in.account
798809
{

src/ripple/app/tx/impl/Offer.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,15 @@ class TOffer : private TOfferBase<TIn, TOut>
163163
// CLOB offer pays the transfer fee
164164
return {ofrInRate, ofrOutRate};
165165
}
166+
167+
/** Check any required invariant. Limit order book offer
168+
* always returns true.
169+
*/
170+
bool
171+
checkInvariant(TAmounts<TIn, TOut> const&, beast::Journal j) const
172+
{
173+
return true;
174+
}
166175
};
167176

168177
using Offer = TOffer<>;

src/ripple/protocol/Feature.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ namespace detail {
7474
// Feature.cpp. Because it's only used to reserve storage, and determine how
7575
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
7676
// the actual number of amendments. A LogicError on startup will verify this.
77-
static constexpr std::size_t numFeatures = 67;
77+
static constexpr std::size_t numFeatures = 68;
7878

7979
/** Amendments that this server supports and the default voting behavior.
8080
Whether they are enabled depends on the Rules defined in the validated
@@ -354,6 +354,7 @@ extern uint256 const featureDID;
354354
extern uint256 const fixFillOrKill;
355355
extern uint256 const fixNFTokenReserve;
356356
extern uint256 const fixInnerObjTemplate;
357+
extern uint256 const fixAMMOverflowOffer;
357358

358359
} // namespace ripple
359360

src/ripple/protocol/impl/Feature.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,8 @@ REGISTER_FIX (fixDisallowIncomingV1, Supported::yes, VoteBehavior::De
460460
REGISTER_FEATURE(DID, Supported::yes, VoteBehavior::DefaultNo);
461461
REGISTER_FIX(fixFillOrKill, Supported::yes, VoteBehavior::DefaultNo);
462462
REGISTER_FIX (fixNFTokenReserve, Supported::yes, VoteBehavior::DefaultNo);
463-
REGISTER_FIX(fixInnerObjTemplate, Supported::yes, VoteBehavior::DefaultNo);
463+
REGISTER_FIX (fixInnerObjTemplate, Supported::yes, VoteBehavior::DefaultNo);
464+
REGISTER_FIX (fixAMMOverflowOffer, Supported::yes, VoteBehavior::DefaultYes);
464465

465466
// The following amendments are obsolete, but must remain supported
466467
// because they could potentially get enabled.

0 commit comments

Comments
 (0)