rippled
Loading...
Searching...
No Matches
AMMBid.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2022 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <xrpld/app/misc/AMMHelpers.h>
21#include <xrpld/app/misc/AMMUtils.h>
22#include <xrpld/app/tx/detail/AMMBid.h>
23
24#include <xrpl/ledger/Sandbox.h>
25#include <xrpl/ledger/View.h>
26#include <xrpl/protocol/AMMCore.h>
27#include <xrpl/protocol/Feature.h>
28#include <xrpl/protocol/TER.h>
29#include <xrpl/protocol/TxFlags.h>
30
31namespace ripple {
32
33bool
35{
36 return ammEnabled(ctx.rules);
37}
38
41{
42 if (auto const res = invalidAMMAssetPair(
43 ctx.tx[sfAsset].get<Issue>(), ctx.tx[sfAsset2].get<Issue>()))
44 {
45 JLOG(ctx.j.debug()) << "AMM Bid: Invalid asset pair.";
46 return res;
47 }
48
49 if (auto const bidMin = ctx.tx[~sfBidMin])
50 {
51 if (auto const res = invalidAMMAmount(*bidMin))
52 {
53 JLOG(ctx.j.debug()) << "AMM Bid: invalid min slot price.";
54 return res;
55 }
56 }
57
58 if (auto const bidMax = ctx.tx[~sfBidMax])
59 {
60 if (auto const res = invalidAMMAmount(*bidMax))
61 {
62 JLOG(ctx.j.debug()) << "AMM Bid: invalid max slot price.";
63 return res;
64 }
65 }
66
67 if (ctx.tx.isFieldPresent(sfAuthAccounts))
68 {
69 if (auto const authAccounts = ctx.tx.getFieldArray(sfAuthAccounts);
70 authAccounts.size() > AUCTION_SLOT_MAX_AUTH_ACCOUNTS)
71 {
72 JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts.";
73 return temMALFORMED;
74 }
75 else if (ctx.rules.enabled(fixAMMv1_3))
76 {
77 AccountID account = ctx.tx[sfAccount];
79 for (auto const& obj : authAccounts)
80 {
81 auto authAccount = obj[sfAccount];
82 if (authAccount == account || unique.contains(authAccount))
83 {
84 JLOG(ctx.j.debug()) << "AMM Bid: Invalid auth.account.";
85 return temMALFORMED;
86 }
87 unique.insert(authAccount);
88 }
89 }
90 }
91
92 return tesSUCCESS;
93}
94
95TER
97{
98 auto const ammSle =
99 ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]));
100 if (!ammSle)
101 {
102 JLOG(ctx.j.debug()) << "AMM Bid: Invalid asset pair.";
103 return terNO_AMM;
104 }
105
106 auto const lpTokensBalance = (*ammSle)[sfLPTokenBalance];
107 if (lpTokensBalance == beast::zero)
108 return tecAMM_EMPTY;
109
110 if (ctx.tx.isFieldPresent(sfAuthAccounts))
111 {
112 for (auto const& account : ctx.tx.getFieldArray(sfAuthAccounts))
113 {
114 if (!ctx.view.read(keylet::account(account[sfAccount])))
115 {
116 JLOG(ctx.j.debug()) << "AMM Bid: Invalid Account.";
117 return terNO_ACCOUNT;
118 }
119 }
120 }
121
122 auto const lpTokens =
123 ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j);
124 // Not LP
125 if (lpTokens == beast::zero)
126 {
127 JLOG(ctx.j.debug()) << "AMM Bid: account is not LP.";
129 }
130
131 auto const bidMin = ctx.tx[~sfBidMin];
132
133 if (bidMin)
134 {
135 if (bidMin->issue() != lpTokens.issue())
136 {
137 JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken.";
138 return temBAD_AMM_TOKENS;
139 }
140 if (*bidMin > lpTokens || *bidMin >= lpTokensBalance)
141 {
142 JLOG(ctx.j.debug()) << "AMM Bid: Invalid Tokens.";
144 }
145 }
146
147 auto const bidMax = ctx.tx[~sfBidMax];
148 if (bidMax)
149 {
150 if (bidMax->issue() != lpTokens.issue())
151 {
152 JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken.";
153 return temBAD_AMM_TOKENS;
154 }
155 if (*bidMax > lpTokens || *bidMax >= lpTokensBalance)
156 {
157 JLOG(ctx.j.debug()) << "AMM Bid: Invalid Tokens.";
159 }
160 }
161
162 if (bidMin && bidMax && bidMin > bidMax)
163 {
164 JLOG(ctx.j.debug()) << "AMM Bid: Invalid Max/MinSlotPrice.";
166 }
167
168 return tesSUCCESS;
169}
170
173 ApplyContext& ctx_,
174 Sandbox& sb,
175 AccountID const& account_,
177{
178 using namespace std::chrono;
179 auto const ammSle =
180 sb.peek(keylet::amm(ctx_.tx[sfAsset], ctx_.tx[sfAsset2]));
181 if (!ammSle)
182 return {tecINTERNAL, false};
183 STAmount const lptAMMBalance = (*ammSle)[sfLPTokenBalance];
184 auto const lpTokens = ammLPHolds(sb, *ammSle, account_, ctx_.journal);
185 auto const& rules = ctx_.view().rules();
186 if (!rules.enabled(fixInnerObjTemplate))
187 {
188 if (!ammSle->isFieldPresent(sfAuctionSlot))
189 ammSle->makeFieldPresent(sfAuctionSlot);
190 }
191 else
192 {
193 XRPL_ASSERT(
194 ammSle->isFieldPresent(sfAuctionSlot),
195 "ripple::applyBid : has auction slot");
196 if (!ammSle->isFieldPresent(sfAuctionSlot))
197 return {tecINTERNAL, false};
198 }
199 auto& auctionSlot = ammSle->peekFieldObject(sfAuctionSlot);
200 auto const current =
201 duration_cast<seconds>(
203 .count();
204 // Auction slot discounted fee
205 auto const discountedFee =
206 (*ammSle)[sfTradingFee] / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION;
207 auto const tradingFee = getFee((*ammSle)[sfTradingFee]);
208 // Min price
209 auto const minSlotPrice =
210 lptAMMBalance * tradingFee / AUCTION_SLOT_MIN_FEE_FRACTION;
211
212 std::uint32_t constexpr tailingSlot = AUCTION_SLOT_TIME_INTERVALS - 1;
213
214 // If seated then it is the current slot-holder time slot, otherwise
215 // the auction slot is not owned. Slot range is in {0-19}
216 auto const timeSlot = ammAuctionTimeSlot(current, auctionSlot);
217
218 // Account must exist and the slot not expired.
219 auto validOwner = [&](AccountID const& account) {
220 // Valid range is 0-19 but the tailing slot pays MinSlotPrice
221 // and doesn't refund so the check is < instead of <= to optimize.
222 return timeSlot && *timeSlot < tailingSlot &&
223 sb.read(keylet::account(account));
224 };
225
226 auto updateSlot = [&](std::uint32_t fee,
227 Number const& minPrice,
228 Number const& burn) -> TER {
229 auctionSlot.setAccountID(sfAccount, account_);
230 auctionSlot.setFieldU32(sfExpiration, current + TOTAL_TIME_SLOT_SECS);
231 if (fee != 0)
232 auctionSlot.setFieldU16(sfDiscountedFee, fee);
233 else if (auctionSlot.isFieldPresent(sfDiscountedFee))
234 auctionSlot.makeFieldAbsent(sfDiscountedFee);
235 auctionSlot.setFieldAmount(
236 sfPrice, toSTAmount(lpTokens.issue(), minPrice));
237 if (ctx_.tx.isFieldPresent(sfAuthAccounts))
238 auctionSlot.setFieldArray(
239 sfAuthAccounts, ctx_.tx.getFieldArray(sfAuthAccounts));
240 else
241 auctionSlot.makeFieldAbsent(sfAuthAccounts);
242 // Burn the remaining bid amount
243 auto const saBurn = adjustLPTokens(
244 lptAMMBalance,
245 toSTAmount(lptAMMBalance.issue(), burn),
247 if (saBurn >= lptAMMBalance)
248 {
249 // This error case should never occur.
250 // LCOV_EXCL_START
251 JLOG(ctx_.journal.fatal())
252 << "AMM Bid: LP Token burn exceeds AMM balance " << burn << " "
253 << lptAMMBalance;
254 return tecINTERNAL;
255 // LCOV_EXCL_STOP
256 }
257 auto res =
258 redeemIOU(sb, account_, saBurn, lpTokens.issue(), ctx_.journal);
259 if (res != tesSUCCESS)
260 {
261 JLOG(ctx_.journal.debug()) << "AMM Bid: failed to redeem.";
262 return res;
263 }
264 ammSle->setFieldAmount(sfLPTokenBalance, lptAMMBalance - saBurn);
265 sb.update(ammSle);
266 return tesSUCCESS;
267 };
268
269 TER res = tesSUCCESS;
270
271 auto const bidMin = ctx_.tx[~sfBidMin];
272 auto const bidMax = ctx_.tx[~sfBidMax];
273
274 auto getPayPrice =
275 [&](Number const& computedPrice) -> Expected<Number, TER> {
276 auto const payPrice = [&]() -> std::optional<Number> {
277 // Both min/max bid price are defined
278 if (bidMin && bidMax)
279 {
280 if (computedPrice <= *bidMax)
281 return std::max(computedPrice, Number(*bidMin));
282 JLOG(ctx_.journal.debug())
283 << "AMM Bid: not in range " << computedPrice << " "
284 << *bidMin << " " << *bidMax;
285 return std::nullopt;
286 }
287 // Bidder pays max(bidPrice, computedPrice)
288 if (bidMin)
289 {
290 return std::max(computedPrice, Number(*bidMin));
291 }
292 else if (bidMax)
293 {
294 if (computedPrice <= *bidMax)
295 return computedPrice;
296 JLOG(ctx_.journal.debug()) << "AMM Bid: not in range "
297 << computedPrice << " " << *bidMax;
298 return std::nullopt;
299 }
300 else
301 return computedPrice;
302 }();
303 if (!payPrice)
305 else if (payPrice > lpTokens)
307 return *payPrice;
308 };
309
310 // No one owns the slot or expired slot.
311 if (auto const acct = auctionSlot[~sfAccount]; !acct || !validOwner(*acct))
312 {
313 if (auto const payPrice = getPayPrice(minSlotPrice); !payPrice)
314 return {payPrice.error(), false};
315 else
316 res = updateSlot(discountedFee, *payPrice, *payPrice);
317 }
318 else
319 {
320 // Price the slot was purchased at.
321 STAmount const pricePurchased = auctionSlot[sfPrice];
322 XRPL_ASSERT(timeSlot, "ripple::applyBid : timeSlot is set");
323 auto const fractionUsed =
324 (Number(*timeSlot) + 1) / AUCTION_SLOT_TIME_INTERVALS;
325 auto const fractionRemaining = Number(1) - fractionUsed;
326 auto const computedPrice = [&]() -> Number {
327 auto const p1_05 = Number(105, -2);
328 // First interval slot price
329 if (*timeSlot == 0)
330 return pricePurchased * p1_05 + minSlotPrice;
331 // Other intervals slot price
332 return pricePurchased * p1_05 * (1 - power(fractionUsed, 60)) +
333 minSlotPrice;
334 }();
335
336 auto const payPrice = getPayPrice(computedPrice);
337
338 if (!payPrice)
339 return {payPrice.error(), false};
340
341 // Refund the previous owner. If the time slot is 0 then
342 // the owner is refunded 95% of the amount.
343 auto const refund = fractionRemaining * pricePurchased;
344 if (refund > *payPrice)
345 {
346 // This error case should never occur.
347 JLOG(ctx_.journal.fatal()) << "AMM Bid: refund exceeds payPrice "
348 << refund << " " << *payPrice;
349 return {tecINTERNAL, false};
350 }
351 res = accountSend(
352 sb,
353 account_,
354 auctionSlot[sfAccount],
355 toSTAmount(lpTokens.issue(), refund),
356 ctx_.journal);
357 if (res != tesSUCCESS)
358 {
359 JLOG(ctx_.journal.debug()) << "AMM Bid: failed to refund.";
360 return {res, false};
361 }
362
363 auto const burn = *payPrice - refund;
364 res = updateSlot(discountedFee, *payPrice, burn);
365 }
366
367 return {res, res == tesSUCCESS};
368}
369
370TER
372{
373 // This is the ledger view that we work against. Transactions are applied
374 // as we go on processing transactions.
375 Sandbox sb(&ctx_.view());
376
377 auto const result = applyBid(ctx_, sb, account_, j_);
378 if (result.second)
379 sb.apply(ctx_.rawView());
380
381 return result.first;
382}
383
384} // namespace ripple
A generic endpoint for log messages.
Definition Journal.h:60
Stream fatal() const
Definition Journal.h:352
Stream debug() const
Definition Journal.h:328
TER doApply() override
Definition AMMBid.cpp:371
static TER preclaim(PreclaimContext const &ctx)
Definition AMMBid.cpp:96
static bool checkExtraFeatures(PreflightContext const &ctx)
Definition AMMBid.cpp:34
static NotTEC preflight(PreflightContext const &ctx)
Definition AMMBid.cpp:40
State information when applying a tx.
ApplyView & view()
beast::Journal const journal
A currency issued by an account.
Definition Issue.h:33
virtual std::shared_ptr< SLE const > read(Keylet const &k) const =0
Return the state item associated with a key.
virtual LedgerInfo const & info() const =0
Returns information about the ledger.
virtual Rules const & rules() const =0
Returns the tx processing rules.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:130
Issue const & issue() const
Definition STAmount.h:496
STArray const & getFieldArray(SField const &field) const
Definition STObject.cpp:702
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:484
Discardable, editable view to a ledger.
Definition Sandbox.h:35
void apply(RawView &to)
Definition Sandbox.h:55
AccountID const account_
Definition Transactor.h:147
beast::Journal const j_
Definition Transactor.h:145
ApplyContext & ctx_
Definition Transactor.h:143
void update(std::shared_ptr< SLE > const &sle) override
Indicate changes to a peeked SLE.
std::shared_ptr< SLE const > read(Keylet const &k) const override
Return the state item associated with a key.
std::shared_ptr< SLE > peek(Keylet const &k) override
Prepare to modify the SLE associated with key.
T is_same_v
T max(T... args)
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition Indexes.cpp:446
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:184
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:25
NotTEC invalidAMMAmount(STAmount const &amount, std::optional< std::pair< Issue, Issue > > const &pair=std::nullopt, bool validZero=false)
Validate the amount.
Definition AMMCore.cpp:95
std::uint32_t constexpr TOTAL_TIME_SLOT_SECS
Definition AMMCore.h:34
std::uint16_t constexpr AUCTION_SLOT_TIME_INTERVALS
Definition AMMCore.h:35
std::optional< std::uint8_t > ammAuctionTimeSlot(std::uint64_t current, STObject const &auctionSlot)
Get time slot of the auction slot.
Definition AMMCore.cpp:108
TER redeemIOU(ApplyView &view, AccountID const &account, STAmount const &amount, Issue const &issue, beast::Journal j)
Definition View.cpp:2365
STAmount toSTAmount(IOUAmount const &iou, Issue const &iss)
bool ammEnabled(Rules const &)
Return true if required AMM amendments are enabled.
Definition AMMCore.cpp:129
@ current
This was a new validation and was added.
TER accountSend(ApplyView &view, AccountID const &from, AccountID const &to, STAmount const &saAmount, beast::Journal j, WaiveTransferFee waiveFee=WaiveTransferFee::No)
Calls static accountSendIOU if saAmount represents Issue.
Definition View.cpp:2191
std::uint32_t constexpr AUCTION_SLOT_MIN_FEE_FRACTION
Definition AMMCore.h:39
STAmount adjustLPTokens(STAmount const &lptAMMBalance, STAmount const &lpTokens, IsDeposit isDeposit)
Adjust LP tokens to deposit/withdraw.
STAmount ammLPHolds(ReadView const &view, Currency const &cur1, Currency const &cur2, AccountID const &ammAccount, AccountID const &lpAccount, beast::Journal const j)
Get the balance of LP tokens.
Definition AMMUtils.cpp:113
@ tecAMM_EMPTY
Definition TER.h:333
@ tecINTERNAL
Definition TER.h:311
@ tecAMM_FAILED
Definition TER.h:331
@ tecAMM_INVALID_TOKENS
Definition TER.h:332
Number getFee(std::uint16_t tfee)
Convert to the fee from the basis points.
Definition AMMCore.h:101
Number power(Number const &f, unsigned n)
Definition Number.cpp:613
@ tesSUCCESS
Definition TER.h:245
NotTEC invalidAMMAssetPair(Issue const &issue1, Issue const &issue2, std::optional< std::pair< Issue, Issue > > const &pair=std::nullopt)
Definition AMMCore.cpp:80
static std::pair< TER, bool > applyBid(ApplyContext &ctx_, Sandbox &sb, AccountID const &account_, beast::Journal j_)
Definition AMMBid.cpp:172
@ terNO_ACCOUNT
Definition TER.h:217
@ terNO_AMM
Definition TER.h:227
TERSubset< CanCvtToTER > TER
Definition TER.h:649
std::uint16_t constexpr AUCTION_SLOT_MAX_AUTH_ACCOUNTS
Definition AMMCore.h:36
std::uint32_t constexpr AUCTION_SLOT_DISCOUNTED_FEE_FRACTION
Definition AMMCore.h:38
@ temMALFORMED
Definition TER.h:87
@ temBAD_AMM_TOKENS
Definition TER.h:129
NetClock::time_point parentCloseTime
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:80
ReadView const & view
Definition Transactor.h:83
beast::Journal const j
Definition Transactor.h:88
State information when preflighting a tx.
Definition Transactor.h:35
beast::Journal const j
Definition Transactor.h:42
T time_since_epoch(T... args)