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