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
35{
36 if (!ammEnabled(ctx.rules))
37 return temDISABLED;
38
39 if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
40 return ret;
41
42 if (ctx.tx.getFlags() & tfUniversalMask)
43 {
44 JLOG(ctx.j.debug()) << "AMM Bid: invalid flags.";
45 return temINVALID_FLAG;
46 }
47
48 if (auto const res = invalidAMMAssetPair(
49 ctx.tx[sfAsset].get<Issue>(), ctx.tx[sfAsset2].get<Issue>()))
50 {
51 JLOG(ctx.j.debug()) << "AMM Bid: Invalid asset pair.";
52 return res;
53 }
54
55 if (auto const bidMin = ctx.tx[~sfBidMin])
56 {
57 if (auto const res = invalidAMMAmount(*bidMin))
58 {
59 JLOG(ctx.j.debug()) << "AMM Bid: invalid min slot price.";
60 return res;
61 }
62 }
63
64 if (auto const bidMax = ctx.tx[~sfBidMax])
65 {
66 if (auto const res = invalidAMMAmount(*bidMax))
67 {
68 JLOG(ctx.j.debug()) << "AMM Bid: invalid max slot price.";
69 return res;
70 }
71 }
72
73 if (ctx.tx.isFieldPresent(sfAuthAccounts))
74 {
75 if (auto const authAccounts = ctx.tx.getFieldArray(sfAuthAccounts);
76 authAccounts.size() > AUCTION_SLOT_MAX_AUTH_ACCOUNTS)
77 {
78 JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts.";
79 return temMALFORMED;
80 }
81 else if (ctx.rules.enabled(fixAMMv1_3))
82 {
83 AccountID account = ctx.tx[sfAccount];
85 for (auto const& obj : authAccounts)
86 {
87 auto authAccount = obj[sfAccount];
88 if (authAccount == account || unique.contains(authAccount))
89 {
90 JLOG(ctx.j.debug()) << "AMM Bid: Invalid auth.account.";
91 return temMALFORMED;
92 }
93 unique.insert(authAccount);
94 }
95 }
96 }
97
98 return preflight2(ctx);
99}
100
101TER
103{
104 auto const ammSle =
105 ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]));
106 if (!ammSle)
107 {
108 JLOG(ctx.j.debug()) << "AMM Bid: Invalid asset pair.";
109 return terNO_AMM;
110 }
111
112 auto const lpTokensBalance = (*ammSle)[sfLPTokenBalance];
113 if (lpTokensBalance == beast::zero)
114 return tecAMM_EMPTY;
115
116 if (ctx.tx.isFieldPresent(sfAuthAccounts))
117 {
118 for (auto const& account : ctx.tx.getFieldArray(sfAuthAccounts))
119 {
120 if (!ctx.view.read(keylet::account(account[sfAccount])))
121 {
122 JLOG(ctx.j.debug()) << "AMM Bid: Invalid Account.";
123 return terNO_ACCOUNT;
124 }
125 }
126 }
127
128 auto const lpTokens =
129 ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j);
130 // Not LP
131 if (lpTokens == beast::zero)
132 {
133 JLOG(ctx.j.debug()) << "AMM Bid: account is not LP.";
135 }
136
137 auto const bidMin = ctx.tx[~sfBidMin];
138
139 if (bidMin)
140 {
141 if (bidMin->issue() != lpTokens.issue())
142 {
143 JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken.";
144 return temBAD_AMM_TOKENS;
145 }
146 if (*bidMin > lpTokens || *bidMin >= lpTokensBalance)
147 {
148 JLOG(ctx.j.debug()) << "AMM Bid: Invalid Tokens.";
150 }
151 }
152
153 auto const bidMax = ctx.tx[~sfBidMax];
154 if (bidMax)
155 {
156 if (bidMax->issue() != lpTokens.issue())
157 {
158 JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken.";
159 return temBAD_AMM_TOKENS;
160 }
161 if (*bidMax > lpTokens || *bidMax >= lpTokensBalance)
162 {
163 JLOG(ctx.j.debug()) << "AMM Bid: Invalid Tokens.";
165 }
166 }
167
168 if (bidMin && bidMax && bidMin > bidMax)
169 {
170 JLOG(ctx.j.debug()) << "AMM Bid: Invalid Max/MinSlotPrice.";
172 }
173
174 return tesSUCCESS;
175}
176
179 ApplyContext& ctx_,
180 Sandbox& sb,
181 AccountID const& account_,
183{
184 using namespace std::chrono;
185 auto const ammSle =
186 sb.peek(keylet::amm(ctx_.tx[sfAsset], ctx_.tx[sfAsset2]));
187 if (!ammSle)
188 return {tecINTERNAL, false};
189 STAmount const lptAMMBalance = (*ammSle)[sfLPTokenBalance];
190 auto const lpTokens = ammLPHolds(sb, *ammSle, account_, ctx_.journal);
191 auto const& rules = ctx_.view().rules();
192 if (!rules.enabled(fixInnerObjTemplate))
193 {
194 if (!ammSle->isFieldPresent(sfAuctionSlot))
195 ammSle->makeFieldPresent(sfAuctionSlot);
196 }
197 else
198 {
199 XRPL_ASSERT(
200 ammSle->isFieldPresent(sfAuctionSlot),
201 "ripple::applyBid : has auction slot");
202 if (!ammSle->isFieldPresent(sfAuctionSlot))
203 return {tecINTERNAL, false};
204 }
205 auto& auctionSlot = ammSle->peekFieldObject(sfAuctionSlot);
206 auto const current =
207 duration_cast<seconds>(
209 .count();
210 // Auction slot discounted fee
211 auto const discountedFee =
212 (*ammSle)[sfTradingFee] / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION;
213 auto const tradingFee = getFee((*ammSle)[sfTradingFee]);
214 // Min price
215 auto const minSlotPrice =
216 lptAMMBalance * tradingFee / AUCTION_SLOT_MIN_FEE_FRACTION;
217
218 std::uint32_t constexpr tailingSlot = AUCTION_SLOT_TIME_INTERVALS - 1;
219
220 // If seated then it is the current slot-holder time slot, otherwise
221 // the auction slot is not owned. Slot range is in {0-19}
222 auto const timeSlot = ammAuctionTimeSlot(current, auctionSlot);
223
224 // Account must exist and the slot not expired.
225 auto validOwner = [&](AccountID const& account) {
226 // Valid range is 0-19 but the tailing slot pays MinSlotPrice
227 // and doesn't refund so the check is < instead of <= to optimize.
228 return timeSlot && *timeSlot < tailingSlot &&
229 sb.read(keylet::account(account));
230 };
231
232 auto updateSlot = [&](std::uint32_t fee,
233 Number const& minPrice,
234 Number const& burn) -> TER {
235 auctionSlot.setAccountID(sfAccount, account_);
236 auctionSlot.setFieldU32(sfExpiration, current + TOTAL_TIME_SLOT_SECS);
237 if (fee != 0)
238 auctionSlot.setFieldU16(sfDiscountedFee, fee);
239 else if (auctionSlot.isFieldPresent(sfDiscountedFee))
240 auctionSlot.makeFieldAbsent(sfDiscountedFee);
241 auctionSlot.setFieldAmount(
242 sfPrice, toSTAmount(lpTokens.issue(), minPrice));
243 if (ctx_.tx.isFieldPresent(sfAuthAccounts))
244 auctionSlot.setFieldArray(
245 sfAuthAccounts, ctx_.tx.getFieldArray(sfAuthAccounts));
246 else
247 auctionSlot.makeFieldAbsent(sfAuthAccounts);
248 // Burn the remaining bid amount
249 auto const saBurn = adjustLPTokens(
250 lptAMMBalance,
251 toSTAmount(lptAMMBalance.issue(), burn),
253 if (saBurn >= lptAMMBalance)
254 {
255 // This error case should never occur.
256 JLOG(ctx_.journal.fatal())
257 << "AMM Bid: LP Token burn exceeds AMM balance " << burn << " "
258 << lptAMMBalance;
259 return tecINTERNAL;
260 }
261 auto res =
262 redeemIOU(sb, account_, saBurn, lpTokens.issue(), ctx_.journal);
263 if (res != tesSUCCESS)
264 {
265 JLOG(ctx_.journal.debug()) << "AMM Bid: failed to redeem.";
266 return res;
267 }
268 ammSle->setFieldAmount(sfLPTokenBalance, lptAMMBalance - saBurn);
269 sb.update(ammSle);
270 return tesSUCCESS;
271 };
272
273 TER res = tesSUCCESS;
274
275 auto const bidMin = ctx_.tx[~sfBidMin];
276 auto const bidMax = ctx_.tx[~sfBidMax];
277
278 auto getPayPrice =
279 [&](Number const& computedPrice) -> Expected<Number, TER> {
280 auto const payPrice = [&]() -> std::optional<Number> {
281 // Both min/max bid price are defined
282 if (bidMin && bidMax)
283 {
284 if (computedPrice <= *bidMax)
285 return std::max(computedPrice, Number(*bidMin));
286 JLOG(ctx_.journal.debug())
287 << "AMM Bid: not in range " << computedPrice << " "
288 << *bidMin << " " << *bidMax;
289 return std::nullopt;
290 }
291 // Bidder pays max(bidPrice, computedPrice)
292 if (bidMin)
293 {
294 return std::max(computedPrice, Number(*bidMin));
295 }
296 else if (bidMax)
297 {
298 if (computedPrice <= *bidMax)
299 return computedPrice;
300 JLOG(ctx_.journal.debug()) << "AMM Bid: not in range "
301 << computedPrice << " " << *bidMax;
302 return std::nullopt;
303 }
304 else
305 return computedPrice;
306 }();
307 if (!payPrice)
309 else if (payPrice > lpTokens)
311 return *payPrice;
312 };
313
314 // No one owns the slot or expired slot.
315 if (auto const acct = auctionSlot[~sfAccount]; !acct || !validOwner(*acct))
316 {
317 if (auto const payPrice = getPayPrice(minSlotPrice); !payPrice)
318 return {payPrice.error(), false};
319 else
320 res = updateSlot(discountedFee, *payPrice, *payPrice);
321 }
322 else
323 {
324 // Price the slot was purchased at.
325 STAmount const pricePurchased = auctionSlot[sfPrice];
326 XRPL_ASSERT(timeSlot, "ripple::applyBid : timeSlot is set");
327 auto const fractionUsed =
328 (Number(*timeSlot) + 1) / AUCTION_SLOT_TIME_INTERVALS;
329 auto const fractionRemaining = Number(1) - fractionUsed;
330 auto const computedPrice = [&]() -> Number {
331 auto const p1_05 = Number(105, -2);
332 // First interval slot price
333 if (*timeSlot == 0)
334 return pricePurchased * p1_05 + minSlotPrice;
335 // Other intervals slot price
336 return pricePurchased * p1_05 * (1 - power(fractionUsed, 60)) +
337 minSlotPrice;
338 }();
339
340 auto const payPrice = getPayPrice(computedPrice);
341
342 if (!payPrice)
343 return {payPrice.error(), false};
344
345 // Refund the previous owner. If the time slot is 0 then
346 // the owner is refunded 95% of the amount.
347 auto const refund = fractionRemaining * pricePurchased;
348 if (refund > *payPrice)
349 {
350 // This error case should never occur.
351 JLOG(ctx_.journal.fatal()) << "AMM Bid: refund exceeds payPrice "
352 << refund << " " << *payPrice;
353 return {tecINTERNAL, false};
354 }
355 res = accountSend(
356 sb,
357 account_,
358 auctionSlot[sfAccount],
359 toSTAmount(lpTokens.issue(), refund),
360 ctx_.journal);
361 if (res != tesSUCCESS)
362 {
363 JLOG(ctx_.journal.debug()) << "AMM Bid: failed to refund.";
364 return {res, false};
365 }
366
367 auto const burn = *payPrice - refund;
368 res = updateSlot(discountedFee, *payPrice, burn);
369 }
370
371 return {res, res == tesSUCCESS};
372}
373
374TER
376{
377 // This is the ledger view that we work against. Transactions are applied
378 // as we go on processing transactions.
379 Sandbox sb(&ctx_.view());
380
381 auto const result = applyBid(ctx_, sb, account_, j_);
382 if (result.second)
383 sb.apply(ctx_.rawView());
384
385 return result.first;
386}
387
388} // 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:375
static TER preclaim(PreclaimContext const &ctx)
Definition AMMBid.cpp:102
static NotTEC preflight(PreflightContext const &ctx)
Definition AMMBid.cpp:34
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:686
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:484
std::uint32_t getFlags() const
Definition STObject.cpp:537
Discardable, editable view to a ledger.
Definition Sandbox.h:35
void apply(RawView &to)
Definition Sandbox.h:55
AccountID const account_
Definition Transactor.h:145
beast::Journal const j_
Definition Transactor.h:143
ApplyContext & ctx_
Definition Transactor.h:141
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:2348
STAmount toSTAmount(IOUAmount const &iou, Issue const &iss)
bool ammEnabled(Rules const &)
Return true if required AMM amendments are enabled.
Definition AMMCore.cpp:129
NotTEC preflight1(PreflightContext const &ctx)
Performs early sanity checks on the account and fee fields.
@ 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:2174
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
NotTEC preflight2(PreflightContext const &ctx)
Checks whether the signature appears valid.
@ tecAMM_EMPTY
Definition TER.h:332
@ tecINTERNAL
Definition TER.h:310
@ tecAMM_FAILED
Definition TER.h:330
@ tecAMM_INVALID_TOKENS
Definition TER.h:331
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:244
NotTEC invalidAMMAssetPair(Issue const &issue1, Issue const &issue2, std::optional< std::pair< Issue, Issue > > const &pair=std::nullopt)
Definition AMMCore.cpp:80
bool isTesSuccess(TER x) noexcept
Definition TER.h:674
constexpr std::uint32_t tfUniversalMask
Definition TxFlags.h:63
static std::pair< TER, bool > applyBid(ApplyContext &ctx_, Sandbox &sb, AccountID const &account_, beast::Journal j_)
Definition AMMBid.cpp:178
@ terNO_ACCOUNT
Definition TER.h:217
@ terNO_AMM
Definition TER.h:227
TERSubset< CanCvtToTER > TER
Definition TER.h:645
std::uint16_t constexpr AUCTION_SLOT_MAX_AUTH_ACCOUNTS
Definition AMMCore.h:36
TERSubset< CanCvtToNotTEC > NotTEC
Definition TER.h:605
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
@ temINVALID_FLAG
Definition TER.h:111
@ temDISABLED
Definition TER.h:114
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)