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  JLOG(ctx_.journal.debug())
227  << "AMM Bid: invalid burn " << burn << " " << lptAMMBalance;
228  return tecAMM_BALANCE;
229  }
230  auto res =
231  redeemIOU(sb, account_, saBurn, lpTokens.issue(), ctx_.journal);
232  if (res != tesSUCCESS)
233  {
234  JLOG(ctx_.journal.debug()) << "AMM Bid: failed to redeem.";
235  return res;
236  }
237  ammSle->setFieldAmount(sfLPTokenBalance, lptAMMBalance - saBurn);
238  sb.update(ammSle);
239  return tesSUCCESS;
240  };
241 
242  TER res = tesSUCCESS;
243 
244  auto const bidMin = ctx_.tx[~sfBidMin];
245  auto const bidMax = ctx_.tx[~sfBidMax];
246 
247  auto getPayPrice =
248  [&](Number const& computedPrice) -> Expected<Number, TER> {
249  auto const payPrice = [&]() -> std::optional<Number> {
250  // Both min/max bid price are defined
251  if (bidMin && bidMax)
252  {
253  if (computedPrice <= *bidMax)
254  return std::max(computedPrice, Number(*bidMin));
255  JLOG(ctx_.journal.debug())
256  << "AMM Bid: not in range " << computedPrice << " "
257  << *bidMin << " " << *bidMax;
258  return std::nullopt;
259  }
260  // Bidder pays max(bidPrice, computedPrice)
261  if (bidMin)
262  {
263  return std::max(computedPrice, Number(*bidMin));
264  }
265  else if (bidMax)
266  {
267  if (computedPrice <= *bidMax)
268  return computedPrice;
269  JLOG(ctx_.journal.debug()) << "AMM Bid: not in range "
270  << computedPrice << " " << *bidMax;
271  return std::nullopt;
272  }
273  else
274  return computedPrice;
275  }();
276  if (!payPrice)
277  return Unexpected(tecAMM_FAILED);
278  else if (payPrice > lpTokens)
280  return *payPrice;
281  };
282 
283  // No one owns the slot or expired slot.
284  if (auto const acct = auctionSlot[~sfAccount]; !acct || !validOwner(*acct))
285  {
286  if (auto const payPrice = getPayPrice(minSlotPrice); !payPrice)
287  return {payPrice.error(), false};
288  else
289  res = updateSlot(discountedFee, *payPrice, *payPrice);
290  }
291  else
292  {
293  // Price the slot was purchased at.
294  STAmount const pricePurchased = auctionSlot[sfPrice];
295  assert(timeSlot);
296  auto const fractionUsed =
297  (Number(*timeSlot) + 1) / AUCTION_SLOT_TIME_INTERVALS;
298  auto const fractionRemaining = Number(1) - fractionUsed;
299  auto const computedPrice = [&]() -> Number {
300  auto const p1_05 = Number(105, -2);
301  // First interval slot price
302  if (*timeSlot == 0)
303  return pricePurchased * p1_05 + minSlotPrice;
304  // Other intervals slot price
305  return pricePurchased * p1_05 * (1 - power(fractionUsed, 60)) +
306  minSlotPrice;
307  }();
308 
309  auto const payPrice = getPayPrice(computedPrice);
310 
311  if (!payPrice)
312  return {payPrice.error(), false};
313 
314  // Refund the previous owner. If the time slot is 0 then
315  // the owner is refunded 95% of the amount.
316  auto const refund = fractionRemaining * pricePurchased;
317  if (refund > *payPrice)
318  {
319  JLOG(ctx_.journal.debug())
320  << "AMM Bid: invalid refund " << refund << " " << *payPrice;
321  return {tecINSUFFICIENT_PAYMENT, false};
322  }
323  res = accountSend(
324  sb,
325  account_,
326  auctionSlot[sfAccount],
327  toSTAmount(lpTokens.issue(), refund),
328  ctx_.journal);
329  if (res != tesSUCCESS)
330  {
331  JLOG(ctx_.journal.debug()) << "AMM Bid: failed to refund.";
332  return {res, false};
333  }
334 
335  auto const burn = *payPrice - refund;
336  res = updateSlot(discountedFee, *payPrice, burn);
337  }
338 
339  return {res, res == tesSUCCESS};
340 }
341 
342 TER
344 {
345  // This is the ledger view that we work against. Transactions are applied
346  // as we go on processing transactions.
347  Sandbox sb(&ctx_.view());
348 
349  auto const result = applyBid(ctx_, sb, account_, j_);
350  if (result.second)
351  sb.apply(ctx_.rawView());
352 
353  return result.first;
354 }
355 
356 } // namespace ripple
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:343
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::tecAMM_BALANCE
@ tecAMM_BALANCE
Definition: TER.h:300
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::tecINSUFFICIENT_PAYMENT
@ tecINSUFFICIENT_PAYMENT
Definition: TER.h:298
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