rippled
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 <ripple/app/tx/impl/AMMBid.h>
21 
22 #include <ripple/app/misc/AMMHelpers.h>
23 #include <ripple/app/misc/AMMUtils.h>
24 #include <ripple/ledger/Sandbox.h>
25 #include <ripple/ledger/View.h>
26 #include <ripple/protocol/AMMCore.h>
27 #include <ripple/protocol/Feature.h>
28 #include <ripple/protocol/STAccount.h>
29 #include <ripple/protocol/TER.h>
30 #include <ripple/protocol/TxFlags.h>
31 
32 namespace ripple {
33 
34 NotTEC
36 {
37  if (!ammEnabled(ctx.rules))
38  return temDISABLED;
39 
40  if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
41  return ret;
42 
43  if (ctx.tx.getFlags() & tfUniversalMask)
44  {
45  JLOG(ctx.j.debug()) << "AMM Bid: invalid flags.";
46  return temINVALID_FLAG;
47  }
48 
49  if (auto const res = invalidAMMAssetPair(ctx.tx[sfAsset], ctx.tx[sfAsset2]))
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 
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  }
82 
83  return preflight2(ctx);
84 }
85 
86 TER
88 {
89  auto const ammSle =
90  ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]));
91  if (!ammSle)
92  {
93  JLOG(ctx.j.debug()) << "AMM Bid: Invalid asset pair.";
94  return terNO_AMM;
95  }
96 
97  auto const lpTokensBalance = (*ammSle)[sfLPTokenBalance];
98  if (lpTokensBalance == beast::zero)
99  return tecAMM_EMPTY;
100 
102  {
103  for (auto const& account : ctx.tx.getFieldArray(sfAuthAccounts))
104  {
105  if (!ctx.view.read(keylet::account(account[sfAccount])))
106  {
107  JLOG(ctx.j.debug()) << "AMM Bid: Invalid Account.";
108  return terNO_ACCOUNT;
109  }
110  }
111  }
112 
113  auto const lpTokens =
114  ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j);
115  // Not LP
116  if (lpTokens == beast::zero)
117  {
118  JLOG(ctx.j.debug()) << "AMM Bid: account is not LP.";
119  return tecAMM_INVALID_TOKENS;
120  }
121 
122  auto const bidMin = ctx.tx[~sfBidMin];
123 
124  if (bidMin)
125  {
126  if (bidMin->issue() != lpTokens.issue())
127  {
128  JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken.";
129  return temBAD_AMM_TOKENS;
130  }
131  if (*bidMin > lpTokens || *bidMin >= lpTokensBalance)
132  {
133  JLOG(ctx.j.debug()) << "AMM Bid: Invalid Tokens.";
134  return tecAMM_INVALID_TOKENS;
135  }
136  }
137 
138  auto const bidMax = ctx.tx[~sfBidMax];
139  if (bidMax)
140  {
141  if (bidMax->issue() != lpTokens.issue())
142  {
143  JLOG(ctx.j.debug()) << "AMM Bid: Invalid LPToken.";
144  return temBAD_AMM_TOKENS;
145  }
146  if (*bidMax > lpTokens || *bidMax >= lpTokensBalance)
147  {
148  JLOG(ctx.j.debug()) << "AMM Bid: Invalid Tokens.";
149  return tecAMM_INVALID_TOKENS;
150  }
151  }
152 
153  if (bidMin && bidMax && bidMin > bidMax)
154  {
155  JLOG(ctx.j.debug()) << "AMM Bid: Invalid Max/MinSlotPrice.";
156  return tecAMM_INVALID_TOKENS;
157  }
158 
159  return tesSUCCESS;
160 }
161 
164  ApplyContext& ctx_,
165  Sandbox& sb,
166  AccountID const& account_,
167  beast::Journal j_)
168 {
169  using namespace std::chrono;
170  auto const ammSle =
171  sb.peek(keylet::amm(ctx_.tx[sfAsset], ctx_.tx[sfAsset2]));
172  if (!ammSle)
173  return {tecINTERNAL, false};
174  STAmount const lptAMMBalance = (*ammSle)[sfLPTokenBalance];
175  auto const lpTokens = ammLPHolds(sb, *ammSle, account_, ctx_.journal);
176  if (!ammSle->isFieldPresent(sfAuctionSlot))
177  ammSle->makeFieldPresent(sfAuctionSlot);
178  auto& auctionSlot = ammSle->peekFieldObject(sfAuctionSlot);
179  auto const current =
180  duration_cast<seconds>(
182  .count();
183  // Auction slot discounted fee
184  auto const discountedFee =
186  auto const tradingFee = getFee((*ammSle)[sfTradingFee]);
187  // Min price
188  auto const minSlotPrice =
189  lptAMMBalance * tradingFee / AUCTION_SLOT_MIN_FEE_FRACTION;
190 
191  std::uint32_t constexpr tailingSlot = AUCTION_SLOT_TIME_INTERVALS - 1;
192 
193  // If seated then it is the current slot-holder time slot, otherwise
194  // the auction slot is not owned. Slot range is in {0-19}
195  auto const timeSlot = ammAuctionTimeSlot(current, auctionSlot);
196 
197  // Account must exist and the slot not expired.
198  auto validOwner = [&](AccountID const& account) {
199  // Valid range is 0-19 but the tailing slot pays MinSlotPrice
200  // and doesn't refund so the check is < instead of <= to optimize.
201  return timeSlot && *timeSlot < tailingSlot &&
202  sb.read(keylet::account(account));
203  };
204 
205  auto updateSlot = [&](std::uint32_t fee,
206  Number const& minPrice,
207  Number const& burn) -> TER {
208  auctionSlot.setAccountID(sfAccount, account_);
209  auctionSlot.setFieldU32(sfExpiration, current + TOTAL_TIME_SLOT_SECS);
210  if (fee != 0)
211  auctionSlot.setFieldU16(sfDiscountedFee, fee);
212  else if (auctionSlot.isFieldPresent(sfDiscountedFee))
213  auctionSlot.makeFieldAbsent(sfDiscountedFee);
214  auctionSlot.setFieldAmount(
215  sfPrice, toSTAmount(lpTokens.issue(), minPrice));
216  if (ctx_.tx.isFieldPresent(sfAuthAccounts))
217  auctionSlot.setFieldArray(
219  else
220  auctionSlot.makeFieldAbsent(sfAuthAccounts);
221  // Burn the remaining bid amount
222  auto const saBurn = adjustLPTokens(
223  lptAMMBalance, toSTAmount(lptAMMBalance.issue(), burn), false);
224  if (saBurn >= lptAMMBalance)
225  {
226  // This error case should never occur.
227  JLOG(ctx_.journal.fatal())
228  << "AMM Bid: LP Token burn exceeds AMM balance " << burn << " "
229  << lptAMMBalance;
230  return tecINTERNAL;
231  }
232  auto res =
233  redeemIOU(sb, account_, saBurn, lpTokens.issue(), ctx_.journal);
234  if (res != tesSUCCESS)
235  {
236  JLOG(ctx_.journal.debug()) << "AMM Bid: failed to redeem.";
237  return res;
238  }
239  ammSle->setFieldAmount(sfLPTokenBalance, lptAMMBalance - saBurn);
240  sb.update(ammSle);
241  return tesSUCCESS;
242  };
243 
244  TER res = tesSUCCESS;
245 
246  auto const bidMin = ctx_.tx[~sfBidMin];
247  auto const bidMax = ctx_.tx[~sfBidMax];
248 
249  auto getPayPrice =
250  [&](Number const& computedPrice) -> Expected<Number, TER> {
251  auto const payPrice = [&]() -> std::optional<Number> {
252  // Both min/max bid price are defined
253  if (bidMin && bidMax)
254  {
255  if (computedPrice <= *bidMax)
256  return std::max(computedPrice, Number(*bidMin));
257  JLOG(ctx_.journal.debug())
258  << "AMM Bid: not in range " << computedPrice << " "
259  << *bidMin << " " << *bidMax;
260  return std::nullopt;
261  }
262  // Bidder pays max(bidPrice, computedPrice)
263  if (bidMin)
264  {
265  return std::max(computedPrice, Number(*bidMin));
266  }
267  else if (bidMax)
268  {
269  if (computedPrice <= *bidMax)
270  return computedPrice;
271  JLOG(ctx_.journal.debug()) << "AMM Bid: not in range "
272  << computedPrice << " " << *bidMax;
273  return std::nullopt;
274  }
275  else
276  return computedPrice;
277  }();
278  if (!payPrice)
279  return Unexpected(tecAMM_FAILED);
280  else if (payPrice > lpTokens)
282  return *payPrice;
283  };
284 
285  // No one owns the slot or expired slot.
286  if (auto const acct = auctionSlot[~sfAccount]; !acct || !validOwner(*acct))
287  {
288  if (auto const payPrice = getPayPrice(minSlotPrice); !payPrice)
289  return {payPrice.error(), false};
290  else
291  res = updateSlot(discountedFee, *payPrice, *payPrice);
292  }
293  else
294  {
295  // Price the slot was purchased at.
296  STAmount const pricePurchased = auctionSlot[sfPrice];
297  assert(timeSlot);
298  auto const fractionUsed =
299  (Number(*timeSlot) + 1) / AUCTION_SLOT_TIME_INTERVALS;
300  auto const fractionRemaining = Number(1) - fractionUsed;
301  auto const computedPrice = [&]() -> Number {
302  auto const p1_05 = Number(105, -2);
303  // First interval slot price
304  if (*timeSlot == 0)
305  return pricePurchased * p1_05 + minSlotPrice;
306  // Other intervals slot price
307  return pricePurchased * p1_05 * (1 - power(fractionUsed, 60)) +
308  minSlotPrice;
309  }();
310 
311  auto const payPrice = getPayPrice(computedPrice);
312 
313  if (!payPrice)
314  return {payPrice.error(), false};
315 
316  // Refund the previous owner. If the time slot is 0 then
317  // the owner is refunded 95% of the amount.
318  auto const refund = fractionRemaining * pricePurchased;
319  if (refund > *payPrice)
320  {
321  // This error case should never occur.
322  JLOG(ctx_.journal.fatal()) << "AMM Bid: refund exceeds payPrice "
323  << refund << " " << *payPrice;
324  return {tecINTERNAL, false};
325  }
326  res = accountSend(
327  sb,
328  account_,
329  auctionSlot[sfAccount],
330  toSTAmount(lpTokens.issue(), refund),
331  ctx_.journal);
332  if (res != tesSUCCESS)
333  {
334  JLOG(ctx_.journal.debug()) << "AMM Bid: failed to refund.";
335  return {res, false};
336  }
337 
338  auto const burn = *payPrice - refund;
339  res = updateSlot(discountedFee, *payPrice, burn);
340  }
341 
342  return {res, res == tesSUCCESS};
343 }
344 
345 TER
347 {
348  // This is the ledger view that we work against. Transactions are applied
349  // as we go on processing transactions.
350  Sandbox sb(&ctx_.view());
351 
352  auto const result = applyBid(ctx_, sb, account_, j_);
353  if (result.second)
354  sb.apply(ctx_.rawView());
355 
356  return result.first;
357 }
358 
359 } // namespace ripple
beast::Journal::fatal
Stream fatal() const
Definition: Journal.h:339
ripple::ReadView::info
virtual LedgerInfo const & info() const =0
Returns information about the ledger.
ripple::STArray::size
size_type size() const
Definition: STArray.h:248
ripple::STObject::getFieldArray
const STArray & getFieldArray(SField const &field) const
Definition: STObject.cpp:624
ripple::preflight2
NotTEC preflight2(PreflightContext const &ctx)
Checks whether the signature appears valid.
Definition: Transactor.cpp:130
ripple::sfDiscountedFee
const SF_UINT16 sfDiscountedFee
ripple::sfAsset
const SF_ISSUE sfAsset
ripple::PreclaimContext::view
ReadView const & view
Definition: Transactor.h:56
ripple::applyBid
static std::pair< TER, bool > applyBid(ApplyContext &ctx_, Sandbox &sb, AccountID const &account_, beast::Journal j_)
Definition: AMMBid.cpp:163
ripple::accountSend
TER accountSend(ApplyView &view, AccountID const &uSenderID, AccountID const &uReceiverID, STAmount const &saAmount, beast::Journal j, WaiveTransferFee waiveFee)
Definition: View.cpp:1142
ripple::PreclaimContext::j
const beast::Journal j
Definition: Transactor.h:60
ripple::STAmount::issue
Issue const & issue() const
Definition: STAmount.h:347
ripple::AUCTION_SLOT_TIME_INTERVALS
constexpr std::uint16_t AUCTION_SLOT_TIME_INTERVALS
Definition: AMMCore.h:35
ripple::Transactor::j_
const beast::Journal j_
Definition: Transactor.h:89
ripple::isTesSuccess
bool isTesSuccess(TER x)
Definition: TER.h:609
ripple::Sandbox::apply
void apply(RawView &to)
Definition: Sandbox.h:55
std::pair
ripple::sfLPTokenBalance
const SF_AMOUNT sfLPTokenBalance
ripple::AUCTION_SLOT_DISCOUNTED_FEE_FRACTION
constexpr std::uint32_t AUCTION_SLOT_DISCOUNTED_FEE_FRACTION
Definition: AMMCore.h:38
ripple::ammEnabled
bool ammEnabled(Rules const &)
Return true if required AMM amendments are enabled.
Definition: AMMCore.cpp:126
ripple::keylet::amm
Keylet amm(Issue const &issue1, Issue const &issue2) noexcept
AMM entry.
Definition: Indexes.cpp:375
ripple::AMMBid::preflight
static NotTEC preflight(PreflightContext const &ctx)
Definition: AMMBid.cpp:35
ripple::Unexpected
Unexpected(E(&)[N]) -> Unexpected< E const * >
ripple::detail::ApplyViewBase::update
void update(std::shared_ptr< SLE > const &sle) override
Indicate changes to a peeked SLE.
Definition: ApplyViewBase.cpp:146
ripple::getFee
Number getFee(std::uint16_t tfee)
Convert to the fee from the basis points.
Definition: AMMCore.h:109
ripple::tecAMM_EMPTY
@ tecAMM_EMPTY
Definition: TER.h:303
ripple::ApplyContext::journal
const beast::Journal journal
Definition: ApplyContext.h:51
ripple::AMMBid::doApply
TER doApply() override
Definition: AMMBid.cpp:346
ripple::ApplyContext::rawView
RawView & rawView()
Definition: ApplyContext.h:67
ripple::sfPrice
const SF_AMOUNT sfPrice
ripple::PreflightContext::j
const beast::Journal j
Definition: Transactor.h:38
ripple::preflight1
NotTEC preflight1(PreflightContext const &ctx)
Performs early sanity checks on the account and fee fields.
Definition: Transactor.cpp:78
ripple::sfTradingFee
const SF_UINT16 sfTradingFee
ripple::tecAMM_FAILED
@ tecAMM_FAILED
Definition: TER.h:301
ripple::invalidAMMAmount
NotTEC invalidAMMAmount(STAmount const &amount, std::optional< std::pair< Issue, Issue >> const &pair=std::nullopt, bool validZero=false)
Validate the amount.
Definition: AMMCore.cpp:94
ripple::sfExpiration
const SF_UINT32 sfExpiration
ripple::AUCTION_SLOT_MIN_FEE_FRACTION
constexpr std::uint32_t AUCTION_SLOT_MIN_FEE_FRACTION
Definition: AMMCore.h:39
ripple::base_uint
Integers of any length that is a multiple of 32-bits.
Definition: base_uint.h:82
ripple::temINVALID_FLAG
@ temINVALID_FLAG
Definition: TER.h:109
std::chrono::time_point::time_since_epoch
T time_since_epoch(T... args)
ripple::LedgerHeader::parentCloseTime
NetClock::time_point parentCloseTime
Definition: LedgerHeader.h:42
ripple::temBAD_AMM_TOKENS
@ temBAD_AMM_TOKENS
Definition: TER.h:127
ripple::sfAsset2
const SF_ISSUE sfAsset2
ripple::Expected
Definition: Expected.h:132
ripple::sfBidMax
const SF_AMOUNT sfBidMax
ripple::Number
Definition: Number.h:36
ripple::keylet::account
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition: Indexes.cpp:134
ripple::toSTAmount
STAmount toSTAmount(IOUAmount const &iou, Issue const &iss)
Definition: AmountConversions.h:30
ripple::tecAMM_INVALID_TOKENS
@ tecAMM_INVALID_TOKENS
Definition: TER.h:302
ripple::TERSubset< CanCvtToTER >
ripple::sfAuthAccounts
const SField sfAuthAccounts
ripple::Sandbox
Discardable, editable view to a ledger.
Definition: Sandbox.h:34
ripple::TER
TERSubset< CanCvtToTER > TER
Definition: TER.h:580
ripple::STAmount
Definition: STAmount.h:45
ripple::tecINTERNAL
@ tecINTERNAL
Definition: TER.h:281
ripple::STObject::getFlags
std::uint32_t getFlags() const
Definition: STObject.cpp:481
ripple::ValStatus::current
@ current
This was a new validation and was added.
ripple::ApplyContext
State information when applying a tx.
Definition: ApplyContext.h:35
beast::Journal
A generic endpoint for log messages.
Definition: Journal.h:58
std::uint32_t
ripple::invalidAMMAssetPair
NotTEC invalidAMMAssetPair(Issue const &issue1, Issue const &issue2, std::optional< std::pair< Issue, Issue >> const &pair=std::nullopt)
Definition: AMMCore.cpp:79
ripple::sfBidMin
const SF_AMOUNT sfBidMin
ripple::ReadView::read
virtual std::shared_ptr< SLE const > read(Keylet const &k) const =0
Return the state item associated with a key.
ripple::ApplyContext::view
ApplyView & view()
Definition: ApplyContext.h:54
ripple::PreclaimContext::tx
STTx const & tx
Definition: Transactor.h:58
ripple::power
Number power(Number const &f, unsigned n)
Definition: Number.cpp:601
ripple::terNO_ACCOUNT
@ terNO_ACCOUNT
Definition: TER.h:200
ripple::PreclaimContext
State information when determining if a tx is likely to claim a fee.
Definition: Transactor.h:52
ripple
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: RCLCensorshipDetector.h:29
ripple::TOTAL_TIME_SLOT_SECS
constexpr std::uint32_t TOTAL_TIME_SLOT_SECS
Definition: AMMCore.h:34
ripple::temDISABLED
@ temDISABLED
Definition: TER.h:112
ripple::redeemIOU
TER redeemIOU(ApplyView &view, AccountID const &account, STAmount const &amount, Issue const &issue, beast::Journal j)
Definition: View.cpp:1399
ripple::terNO_AMM
@ terNO_AMM
Definition: TER.h:210
ripple::STObject::isFieldPresent
bool isFieldPresent(SField const &field) const
Definition: STObject.cpp:428
ripple::ammLPHolds
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:104
ripple::Transactor::ctx_
ApplyContext & ctx_
Definition: Transactor.h:88
std::optional
beast::Journal::debug
Stream debug() const
Definition: Journal.h:315
ripple::AUCTION_SLOT_MAX_AUTH_ACCOUNTS
constexpr std::uint16_t AUCTION_SLOT_MAX_AUTH_ACCOUNTS
Definition: AMMCore.h:36
ripple::detail::ApplyViewBase::read
std::shared_ptr< SLE const > read(Keylet const &k) const override
Return the state item associated with a key.
Definition: ApplyViewBase.cpp:71
ripple::sfAccount
const SF_ACCOUNT sfAccount
ripple::detail::ApplyViewBase::peek
std::shared_ptr< SLE > peek(Keylet const &k) override
Prepare to modify the SLE associated with key.
Definition: ApplyViewBase.cpp:128
ripple::AMMBid::preclaim
static TER preclaim(PreclaimContext const &ctx)
Definition: AMMBid.cpp:87
ripple::temMALFORMED
@ temMALFORMED
Definition: TER.h:85
ripple::PreflightContext::tx
STTx const & tx
Definition: Transactor.h:35
std::max
T max(T... args)
ripple::PreflightContext
State information when preflighting a tx.
Definition: Transactor.h:31
ripple::PreflightContext::rules
const Rules rules
Definition: Transactor.h:36
ripple::adjustLPTokens
STAmount adjustLPTokens(STAmount const &lptAMMBalance, STAmount const &lpTokens, bool isDeposit)
Definition: AMMHelpers.cpp:133
ripple::tfUniversalMask
constexpr std::uint32_t tfUniversalMask
Definition: TxFlags.h:60
ripple::sfAuctionSlot
const SField sfAuctionSlot
ripple::tesSUCCESS
@ tesSUCCESS
Definition: TER.h:226
ripple::Transactor::account_
const AccountID account_
Definition: Transactor.h:91
ripple::ApplyContext::tx
STTx const & tx
Definition: ApplyContext.h:48
ripple::ammAuctionTimeSlot
std::optional< std::uint8_t > ammAuctionTimeSlot(std::uint64_t current, STObject const &auctionSlot)
Get time slot of the auction slot.
Definition: AMMCore.cpp:107
ripple::NotTEC
TERSubset< CanCvtToNotTEC > NotTEC
Definition: TER.h:540
std::chrono