xrpld
Loading...
Searching...
No Matches
AMMBid.cpp
1#include <xrpl/tx/transactors/dex/AMMBid.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/Number.h>
5#include <xrpl/beast/utility/Zero.h>
6#include <xrpl/beast/utility/instrumentation.h>
7#include <xrpl/ledger/Sandbox.h>
8#include <xrpl/ledger/helpers/AMMHelpers.h>
9#include <xrpl/ledger/helpers/RippleStateHelpers.h>
10#include <xrpl/ledger/helpers/TokenHelpers.h>
11#include <xrpl/protocol/AMMCore.h>
12#include <xrpl/protocol/AccountID.h>
13#include <xrpl/protocol/AmountConversions.h>
14#include <xrpl/protocol/Feature.h>
15#include <xrpl/protocol/Indexes.h>
16#include <xrpl/protocol/Issue.h>
17#include <xrpl/protocol/MPTIssue.h>
18#include <xrpl/protocol/SField.h>
19#include <xrpl/protocol/STLedgerEntry.h>
20#include <xrpl/protocol/STTx.h>
21#include <xrpl/protocol/TER.h>
22#include <xrpl/protocol/XRPAmount.h>
23#include <xrpl/tx/ApplyContext.h>
24#include <xrpl/tx/Transactor.h>
25
26#include <algorithm>
27#include <chrono>
28#include <cstdint>
29#include <expected>
30#include <optional>
31#include <set>
32#include <utility>
33
34namespace xrpl {
35
36bool
38{
39 if (!ammEnabled(ctx.rules))
40 return false;
41
42 if (!ctx.rules.enabled(featureMPTokensV2) &&
43 (ctx.tx[sfAsset].holds<MPTIssue>() || ctx.tx[sfAsset2].holds<MPTIssue>()))
44 return false;
45
46 return true;
47}
48
51{
52 if (auto const res = invalidAMMAssetPair(ctx.tx[sfAsset], ctx.tx[sfAsset2]))
53 {
54 JLOG(ctx.j.debug()) << "AMM Bid: Invalid asset pair.";
55 return res;
56 }
57
58 if (auto const bidMin = ctx.tx[~sfBidMin])
59 {
60 if (auto const res = invalidAMMAmount(*bidMin))
61 {
62 JLOG(ctx.j.debug()) << "AMM Bid: invalid min slot price.";
63 return res;
64 }
65 }
66
67 if (auto const bidMax = ctx.tx[~sfBidMax])
68 {
69 if (auto const res = invalidAMMAmount(*bidMax))
70 {
71 JLOG(ctx.j.debug()) << "AMM Bid: invalid max slot price.";
72 return res;
73 }
74 }
75
76 if (ctx.tx.isFieldPresent(sfAuthAccounts))
77 {
78 auto const authAccounts = ctx.tx.getFieldArray(sfAuthAccounts);
79 if (authAccounts.size() > kAuctionSlotMaxAuthAccounts)
80 {
81 JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts.";
82 return temMALFORMED;
83 }
84 if (ctx.rules.enabled(fixAMMv1_3))
85 {
86 AccountID const account = ctx.tx[sfAccount];
88 for (auto const& obj : authAccounts)
89 {
90 auto authAccount = obj[sfAccount];
91 if (authAccount == account || unique.contains(authAccount))
92 {
93 JLOG(ctx.j.debug()) << "AMM Bid: Invalid auth.account.";
94 return temMALFORMED;
95 }
96 unique.insert(authAccount);
97 }
98 }
99 }
100
101 return tesSUCCESS;
102}
103
104TER
106{
107 auto const ammSle = ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]));
108 if (!ammSle)
109 {
110 JLOG(ctx.j.debug()) << "AMM Bid: Invalid asset pair.";
111 return terNO_AMM;
112 }
113
114 auto const lpTokensBalance = (*ammSle)[sfLPTokenBalance];
115 if (lpTokensBalance == beast::kZero)
116 return tecAMM_EMPTY;
117
118 if (ctx.tx.isFieldPresent(sfAuthAccounts))
119 {
120 for (auto const& account : ctx.tx.getFieldArray(sfAuthAccounts))
121 {
122 if (!ctx.view.read(keylet::account(account[sfAccount])))
123 {
124 JLOG(ctx.j.debug()) << "AMM Bid: Invalid Account.";
125 return terNO_ACCOUNT;
126 }
127 }
128 }
129
130 auto const lpTokens = ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j);
131 // Not LP
132 if (lpTokens == beast::kZero)
133 {
134 JLOG(ctx.j.debug()) << "AMM Bid: account is not LP.";
136 }
137
138 auto const bidMin = ctx.tx[~sfBidMin];
139
140 if (bidMin)
141 {
142 if (bidMin->asset() != lpTokens.asset())
143 {
144 JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken.";
145 return temBAD_AMM_TOKENS;
146 }
147 if (*bidMin > lpTokens || *bidMin >= lpTokensBalance)
148 {
149 JLOG(ctx.j.debug()) << "AMM Bid: Invalid Tokens.";
151 }
152 }
153
154 auto const bidMax = ctx.tx[~sfBidMax];
155 if (bidMax)
156 {
157 if (bidMax->asset() != lpTokens.asset())
158 {
159 JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken.";
160 return temBAD_AMM_TOKENS;
161 }
162 if (*bidMax > lpTokens || *bidMax >= lpTokensBalance)
163 {
164 JLOG(ctx.j.debug()) << "AMM Bid: Invalid Tokens.";
166 }
167 }
168
169 if (bidMin && bidMax && bidMin > bidMax)
170 {
171 JLOG(ctx.j.debug()) << "AMM Bid: Invalid Max/MinSlotPrice.";
173 }
174
175 return tesSUCCESS;
176}
177
180{
181 using namespace std::chrono;
182 auto const ammSle = sb.peek(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]));
183 if (!ammSle)
184 return {tecINTERNAL, false};
185 STAmount const lptAMMBalance = (*ammSle)[sfLPTokenBalance];
186 auto const lpTokens = ammLPHolds(sb, *ammSle, account, ctx.journal);
187 auto const& rules = ctx.view().rules();
188 if (!rules.enabled(fixInnerObjTemplate))
189 {
190 if (!ammSle->isFieldPresent(sfAuctionSlot))
191 ammSle->makeFieldPresent(sfAuctionSlot);
192 }
193 else
194 {
195 XRPL_ASSERT(ammSle->isFieldPresent(sfAuctionSlot), "xrpl::applyBid : has auction slot");
196 if (!ammSle->isFieldPresent(sfAuctionSlot))
197 return {tecINTERNAL, false};
198 }
199 auto& auctionSlot = ammSle->peekFieldObject(sfAuctionSlot);
200 auto const current =
202 // Auction slot discounted fee
203 auto const discountedFee = (*ammSle)[sfTradingFee] / kAuctionSlotDiscountedFeeFraction;
204 auto const tradingFee = getFee((*ammSle)[sfTradingFee]);
205 // Min price
206 auto const minSlotPrice = lptAMMBalance * tradingFee / kAuctionSlotMinFeeFraction;
207
208 static constexpr std::uint32_t kTailingSlot = kAuctionSlotTimeIntervals - 1;
209
210 // If seated then it is the current slot-holder time slot, otherwise
211 // the auction slot is not owned. Slot range is in {0-19}
212 auto const timeSlot = ammAuctionTimeSlot(current, auctionSlot);
213
214 // Account must exist and the slot not expired.
215 auto validOwner = [&](AccountID const& account) {
216 // Valid range is 0-19 but the tailing slot pays MinSlotPrice
217 // and doesn't refund so the check is < instead of <= to optimize.
218 return timeSlot && *timeSlot < kTailingSlot && sb.read(keylet::account(account));
219 };
220
221 auto updateSlot = [&](std::uint32_t fee, Number const& minPrice, Number const& burn) -> TER {
222 auctionSlot.setAccountID(sfAccount, account);
223 auctionSlot.setFieldU32(sfExpiration, current + kTotalTimeSlotSecs);
224 if (fee != 0)
225 {
226 auctionSlot.setFieldU16(sfDiscountedFee, fee);
227 }
228 else if (auctionSlot.isFieldPresent(sfDiscountedFee))
229 {
230 auctionSlot.makeFieldAbsent(sfDiscountedFee);
231 }
232 auctionSlot.setFieldAmount(sfPrice, toSTAmount(lpTokens.asset(), minPrice));
233 if (ctx.tx.isFieldPresent(sfAuthAccounts))
234 {
235 auctionSlot.setFieldArray(sfAuthAccounts, ctx.tx.getFieldArray(sfAuthAccounts));
236 }
237 else
238 {
239 auctionSlot.makeFieldAbsent(sfAuthAccounts);
240 }
241 // Burn the remaining bid amount
242 auto const saBurn =
243 adjustLPTokens(lptAMMBalance, toSTAmount(lptAMMBalance.asset(), burn), IsDeposit::No);
244 if (saBurn >= lptAMMBalance)
245 {
246 // This error case should never occur.
247 // LCOV_EXCL_START
248 JLOG(ctx.journal.fatal())
249 << "AMM Bid: LP Token burn exceeds AMM balance " << burn << " " << lptAMMBalance;
250 return tecINTERNAL;
251 // LCOV_EXCL_STOP
252 }
253 auto res = redeemIOU(sb, account, saBurn, lpTokens.get<Issue>(), ctx.journal);
254 if (!isTesSuccess(res))
255 {
256 JLOG(ctx.journal.debug()) << "AMM Bid: failed to redeem.";
257 return res;
258 }
259 ammSle->setFieldAmount(sfLPTokenBalance, lptAMMBalance - saBurn);
260 sb.update(ammSle);
261 return tesSUCCESS;
262 };
263
264 TER res = tesSUCCESS;
265
266 auto const bidMin = ctx.tx[~sfBidMin];
267 auto const bidMax = ctx.tx[~sfBidMax];
268
269 auto getPayPrice = [&](Number const& computedPrice) -> std::expected<Number, TER> {
270 auto const payPrice = [&]() -> std::optional<Number> {
271 // Both min/max bid price are defined
272 if (bidMin && bidMax)
273 {
274 if (computedPrice <= *bidMax)
275 return std::max(computedPrice, Number(*bidMin));
276 JLOG(ctx.journal.debug()) << "AMM Bid: not in range " << computedPrice << " "
277 << *bidMin << " " << *bidMax;
278 return std::nullopt;
279 }
280 // Bidder pays max(bidPrice, computedPrice)
281 if (bidMin)
282 {
283 return std::max(computedPrice, Number(*bidMin));
284 }
285 if (bidMax)
286 {
287 if (computedPrice <= *bidMax)
288 return computedPrice;
289 JLOG(ctx.journal.debug())
290 << "AMM Bid: not in range " << computedPrice << " " << *bidMax;
291 return std::nullopt;
292 }
293
294 return computedPrice;
295 }();
296 if (!payPrice)
297 {
299 }
300 if (payPrice > lpTokens)
301 {
303 }
304 return *payPrice;
305 };
306
307 // No one owns the slot or expired slot.
308 if (auto const acct = auctionSlot[~sfAccount]; !acct || !validOwner(*acct))
309 {
310 auto const payPrice = getPayPrice(minSlotPrice);
311 if (!payPrice)
312 {
313 return {payPrice.error(), false};
314 }
315
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, "xrpl::applyBid : timeSlot is set");
323 // NOLINTBEGIN(bugprone-unchecked-optional-access)
324 auto const fractionUsed = (Number(*timeSlot) + 1) / kAuctionSlotTimeIntervals;
325 auto const fractionRemaining = Number(1) - fractionUsed;
326 auto const computedPrice = [&]() -> Number {
327 auto const p105 = Number(105, -2);
328 // First interval slot price
329 if (*timeSlot == 0)
330 return pricePurchased * p105 + minSlotPrice;
331 // Other intervals slot price
332 return pricePurchased * p105 * (1 - power(fractionUsed, 60)) + minSlotPrice;
333 }();
334 // NOLINTEND(bugprone-unchecked-optional-access)
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())
348 << "AMM Bid: refund exceeds payPrice " << refund << " " << *payPrice;
349 return {tecINTERNAL, false};
350 }
351 res = accountSend(
352 sb, account, auctionSlot[sfAccount], toSTAmount(lpTokens.asset(), refund), ctx.journal);
353 if (!isTesSuccess(res))
354 {
355 JLOG(ctx.journal.debug()) << "AMM Bid: failed to refund.";
356 return {res, false};
357 }
358
359 auto const burn = *payPrice - refund;
360 res = updateSlot(discountedFee, *payPrice, burn);
361 }
362
363 return {res, isTesSuccess(res)};
364}
365
366TER
368{
369 // This is the ledger view that we work against. Transactions are applied
370 // as we go on processing transactions.
371 Sandbox sb(&ctx_.view());
372
373 auto const result = applyBid(ctx_, sb, accountID_, j_);
374 if (result.second)
375 sb.apply(ctx_.rawView());
376
377 return result.first;
378}
379
380void
382{
383 // No transaction-specific invariants yet (future work).
384}
385
386bool
388{
389 // No transaction-specific invariants yet (future work).
390 return true;
391}
392
393} // namespace xrpl
A generic endpoint for log messages.
Definition Journal.h:38
Stream fatal() const
Definition Journal.h:321
Stream debug() const
Definition Journal.h:297
TER doApply() override
Definition AMMBid.cpp:367
bool finalizeInvariants(STTx const &tx, TER result, XRPAmount fee, ReadView const &view, beast::Journal const &j) override
Check transaction-specific post-conditions after all entries have been visited.
Definition AMMBid.cpp:387
static NotTEC preflight(PreflightContext const &ctx)
Definition AMMBid.cpp:50
static bool checkExtraFeatures(PreflightContext const &ctx)
Definition AMMBid.cpp:37
void visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override
Inspect a single ledger entry modified by this transaction.
Definition AMMBid.cpp:381
static TER preclaim(PreclaimContext const &ctx)
Definition AMMBid.cpp:105
State information when applying a tx.
STTx const & tx
beast::Journal const journal
ApplyView & view()
A currency issued by an account.
Definition Issue.h:13
Number is a floating point type that can represent a wide range of values.
Definition Number.h:306
A view into a ledger.
Definition ReadView.h:31
virtual Rules const & rules() const =0
Returns the tx processing rules.
virtual SLE::const_pointer read(Keylet const &k) const =0
Return the state item associated with a key.
virtual LedgerHeader const & header() const =0
Returns information about the ledger.
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:171
Asset const & asset() const
Definition STAmount.h:478
std::shared_ptr< STLedgerEntry const > const & const_ref
STArray const & getFieldArray(SField const &field) const
Definition STObject.cpp:678
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:454
Discardable, editable view to a ledger.
Definition Sandbox.h:15
void apply(RawView &to)
Definition Sandbox.h:35
beast::Journal const j_
Definition Transactor.h:118
AccountID const accountID_
Definition Transactor.h:120
ApplyContext & ctx_
Definition Transactor.h:116
SLE::pointer peek(Keylet const &k) override
Prepare to modify the SLE associated with key.
void update(SLE::ref sle) override
Indicate changes to a peeked SLE.
SLE::const_pointer read(Keylet const &k) const override
Return the state item associated with a key.
T duration_cast(T... args)
T max(T... args)
Keylet amm(Asset const &issue1, Asset const &issue2) noexcept
AMM entry.
Definition Indexes.cpp:425
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:186
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
NotTEC invalidAMMAmount(STAmount const &amount, std::optional< std::pair< Asset, Asset > > const &pair=std::nullopt, bool validZero=false)
Validate the amount.
Definition AMMCore.cpp:97
@ terNO_AMM
Definition TER.h:219
@ terNO_ACCOUNT
Definition TER.h:209
STAmount ammLPHolds(ReadView const &view, Asset const &asset1, Asset const &asset2, AccountID const &ammAccount, AccountID const &lpAccount, beast::Journal const j)
Get the balance of LP tokens.
constexpr std::uint32_t kTotalTimeSlotSecs
Definition AMMCore.h:14
constexpr std::uint32_t kAuctionSlotDiscountedFeeFraction
Definition AMMCore.h:18
bool ammEnabled(Rules const &)
Return true if required AMM amendment is enabled.
Definition AMMCore.cpp:128
NotTEC invalidAMMAssetPair(Asset const &asset1, Asset const &asset2, std::optional< std::pair< Asset, Asset > > const &pair=std::nullopt)
Definition AMMCore.cpp:82
STAmount adjustLPTokens(STAmount const &lptAMMBalance, STAmount const &lpTokens, IsDeposit isDeposit)
Adjust LP tokens to deposit/withdraw.
Number power(Number const &f, unsigned n)
Definition Number.cpp:1178
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:594
TER accountSend(ApplyView &view, AccountID const &from, AccountID const &to, STAmount const &saAmount, beast::Journal j, WaiveTransferFee waiveFee=WaiveTransferFee::No, AllowMPTOverflow allowOverflow=AllowMPTOverflow::No)
Calls static accountSendIOU if saAmount represents Issue.
constexpr std::uint32_t kAuctionSlotMinFeeFraction
Definition AMMCore.h:19
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:28
static std::pair< TER, bool > applyBid(ApplyContext &ctx, Sandbox &sb, AccountID const &account, beast::Journal j)
Definition AMMBid.cpp:179
TER redeemIOU(ApplyView &view, AccountID const &account, STAmount const &amount, Issue const &issue, beast::Journal j)
constexpr std::uint16_t kAuctionSlotTimeIntervals
Definition AMMCore.h:15
@ temBAD_AMM_TOKENS
Definition TER.h:115
@ temMALFORMED
Definition TER.h:73
bool isTesSuccess(TER x) noexcept
Definition TER.h:663
Number getFee(std::uint16_t tfee)
Convert to the fee from the basis points.
Definition AMMCore.h:78
TERSubset< CanCvtToTER > TER
Definition TER.h:634
std::optional< std::uint8_t > ammAuctionTimeSlot(std::uint64_t current, STObject const &auctionSlot)
Get time slot of the auction slot.
Definition AMMCore.cpp:110
@ tecAMM_EMPTY
Definition TER.h:330
@ tecAMM_INVALID_TOKENS
Definition TER.h:329
@ tecAMM_FAILED
Definition TER.h:328
@ tecINTERNAL
Definition TER.h:308
constexpr std::uint16_t kAuctionSlotMaxAuthAccounts
Definition AMMCore.h:16
@ tesSUCCESS
Definition TER.h:240
STAmount toSTAmount(IOUAmount const &iou, Asset const &asset)
NetClock::time_point parentCloseTime
State information when determining if a tx is likely to claim a fee.
Definition Transactor.h:61
ReadView const & view
Definition Transactor.h:64
beast::Journal const j
Definition Transactor.h:69
State information when preflighting a tx.
Definition Transactor.h:18
beast::Journal const j
Definition Transactor.h:25
T time_since_epoch(T... args)
T unexpected(T... args)