Skip to content

Commit cc10aba

Browse files
gregtatcammanojsdoshi
authored andcommitted
fix(AMM): prevent orphaned objects, inconsistent ledger state: (XRPLF#4626)
When an AMM account is deleted, the owner directory entries must be deleted in order to ensure consistent ledger state. * When deleting AMM account: * Clean up AMM owner dir, linking AMM account and AMM object * Delete trust lines to AMM * Disallow `CheckCreate` to AMM accounts * AMM cannot cash a check * Constrain entries in AuthAccounts array to be accounts * AuthAccounts is an array of objects for the AMMBid transaction * SetTrust (TrustSet): Allow on AMM only for LP tokens * If the destination is an AMM account and the trust line doesn't exist, then: * If the asset is not the AMM LP token, then fail the tx with `tecNO_PERMISSION` * If the AMM is in empty state, then fail the tx with `tecAMM_EMPTY` * This disallows trustlines to AMM in empty state * Add AMMID to AMM root account * Remove lsfAMM flag and use sfAMMID instead * Remove owner dir entry for ltAMM * Add `AMMDelete` transaction type to handle amortized deletion * Limit number of trust lines to delete on final withdraw + AMMDelete * Put AMM in empty state when LPTokens is 0 upon final withdraw * Add `tfTwoAssetIfEmpty` deposit option in AMM empty state * Fail all AMM transactions in AMM empty state except special deposit * Add `tecINCOMPLETE` to indicate that not all AMM trust lines are deleted (i.e. partial deletion) * This is handled in Transactor similar to deleted offers * Fail AMMDelete with `tecINTERNAL` if AMM root account is nullptr * Don't validate for invalid asset pair in AMMDelete * AMMWithdraw deletes AMM trust lines and AMM account/object only if the number of trust lines is less than max * Current `maxDeletableAMMTrustLines` = 512 * Check no directory left after AMM trust lines are deleted * Enable partial trustline deletion in AMMWithdraw * Add `tecAMM_NOT_EMPTY` to fail any transaction that expects an AMM in empty state * Clawback considerations * Disallow clawback out of AMM account * Disallow AMM create if issuer can claw back This patch applies to the AMM implementation in XRPLF#4294. Acknowledgements: Richard Holland and Nik Bougalis for responsibly disclosing this issue. Bug Bounties and Responsible Disclosures: We welcome reviews of the project code and urge researchers to responsibly disclose any issues they may find. To report a bug, please send a detailed report to: [email protected]
1 parent f0e82df commit cc10aba

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1536
-247
lines changed

API-CHANGELOG.md

+22
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,28 @@ Additions are intended to be non-breaking (because they are purely additive).
4747
- Adds the [Clawback transaction type](https://github.com/XRPLF/XRPL-Standards/blob/master/XLS-39d-clawback/README.md#331-clawback-transaction), containing these fields:
4848
- `Account`: The issuer of the asset being clawed back. Must also be the sender of the transaction.
4949
- `Amount`: The amount being clawed back, with the `Amount.issuer` being the token holder's address.
50+
- Adds [AMM](https://github.com/XRPLF/XRPL-Standards/discussions/78) ([#4294](https://github.com/XRPLF/rippled/pull/4294), [#4626](https://github.com/XRPLF/rippled/pull/4626)) feature:
51+
- Adds `amm_info` API to retrieve AMM information for a given tokens pair.
52+
- Adds `AMMCreate` transaction type to create `AMM` instance.
53+
- Adds `AMMDeposit` transaction type to deposit funds into `AMM` instance.
54+
- Adds `AMMWithdraw` transaction type to withdraw funds from `AMM` instance.
55+
- Adds `AMMVote` transaction type to vote for the trading fee of `AMM` instance.
56+
- Adds `AMMBid` transaction type to bid for the Auction Slot of `AMM` instance.
57+
- Adds `AMMDelete` transaction type to delete `AMM` instance.
58+
- Adds `sfAMMID` to `AccountRoot` to indicate that the account is `AMM`'s account. `AMMID` is used to fetch `ltAMM`.
59+
- Adds `lsfAMMNode` `TrustLine` flag to indicate that one side of the `TrustLine` is `AMM` account.
60+
- Adds `tfLPToken`, `tfSingleAsset`, `tfTwoAsset`, `tfOneAssetLPToken`, `tfLimitLPToken`, `tfTwoAssetIfEmpty`,
61+
`tfWithdrawAll`, `tfOneAssetWithdrawAll` which allow a trader to specify different fields combination
62+
for `AMMDeposit` and `AMMWithdraw` transactions.
63+
- Adds new transaction result codes:
64+
- tecUNFUNDED_AMM: insufficient balance to fund AMM. The account does not have funds for liquidity provision.
65+
- tecAMM_BALANCE: AMM has invalid balance. Calculated balances greater than the current pool balances.
66+
- tecAMM_FAILED: AMM transaction failed. Fails due to a processing failure.
67+
- tecAMM_INVALID_TOKENS: AMM invalid LP tokens. Invalid input values, format, or calculated values.
68+
- tecAMM_EMPTY: AMM is in empty state. Transaction expects AMM in non-empty state (LP tokens > 0).
69+
- tecAMM_NOT_EMPTY: AMM is not in empty state. Transaction expects AMM in empty state (LP tokens == 0).
70+
- tecAMM_ACCOUNT: AMM account. Clawback of AMM account.
71+
- tecINCOMPLETE: Some work was completed, but more submissions required to finish. AMMDelete partially deletes the trustlines.
5072

5173
## XRP Ledger version 1.11.0
5274

Builds/CMake/RippledCore.cmake

+1
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ target_sources (rippled PRIVATE
476476
src/ripple/app/rdb/impl/Wallet.cpp
477477
src/ripple/app/tx/impl/AMMBid.cpp
478478
src/ripple/app/tx/impl/AMMCreate.cpp
479+
src/ripple/app/tx/impl/AMMDelete.cpp
479480
src/ripple/app/tx/impl/AMMDeposit.cpp
480481
src/ripple/app/tx/impl/AMMVote.cpp
481482
src/ripple/app/tx/impl/AMMWithdraw.cpp

src/ripple/app/misc/AMMUtils.h

+20
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,26 @@ ammAccountHolds(
9393
AccountID const& ammAccountID,
9494
Issue const& issue);
9595

96+
/** Delete trustlines to AMM. If all trustlines are deleted then
97+
* AMM object and account are deleted. Otherwise tecIMPCOMPLETE is returned.
98+
*/
99+
TER
100+
deleteAMMAccount(
101+
Sandbox& view,
102+
Issue const& asset,
103+
Issue const& asset2,
104+
beast::Journal j);
105+
106+
/** Initialize Auction and Voting slots and set the trading/discounted fee.
107+
*/
108+
void
109+
initializeFeeAuctionVote(
110+
ApplyView& view,
111+
std::shared_ptr<SLE>& ammSle,
112+
AccountID const& account,
113+
Issue const& lptIssue,
114+
std::uint16_t tfee);
115+
96116
} // namespace ripple
97117

98118
#endif // RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED

src/ripple/app/misc/impl/AMMUtils.cpp

+118
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,122 @@ ammAccountHolds(
188188
return STAmount{issue};
189189
}
190190

191+
static TER
192+
deleteAMMTrustLines(
193+
Sandbox& sb,
194+
AccountID const& ammAccountID,
195+
std::uint16_t maxTrustlinesToDelete,
196+
beast::Journal j)
197+
{
198+
return cleanupOnAccountDelete(
199+
sb,
200+
keylet::ownerDir(ammAccountID),
201+
[&](LedgerEntryType nodeType,
202+
uint256 const&,
203+
std::shared_ptr<SLE>& sleItem) -> TER {
204+
// Should only have the trustlines
205+
if (nodeType != LedgerEntryType::ltRIPPLE_STATE)
206+
{
207+
JLOG(j.error())
208+
<< "deleteAMMTrustLines: deleting non-trustline "
209+
<< nodeType;
210+
return tecINTERNAL;
211+
}
212+
213+
// Trustlines must have zero balance
214+
if (sleItem->getFieldAmount(sfBalance) != beast::zero)
215+
{
216+
JLOG(j.error())
217+
<< "deleteAMMTrustLines: deleting trustline with "
218+
"non-zero balance.";
219+
return tecINTERNAL;
220+
}
221+
222+
return deleteAMMTrustLine(sb, sleItem, ammAccountID, j);
223+
},
224+
j,
225+
maxTrustlinesToDelete);
226+
}
227+
228+
TER
229+
deleteAMMAccount(
230+
Sandbox& sb,
231+
Issue const& asset,
232+
Issue const& asset2,
233+
beast::Journal j)
234+
{
235+
auto ammSle = sb.peek(keylet::amm(asset, asset2));
236+
if (!ammSle)
237+
{
238+
JLOG(j.error()) << "deleteAMMAccount: AMM object does not exist "
239+
<< asset << " " << asset2;
240+
return tecINTERNAL;
241+
}
242+
243+
auto const ammAccountID = (*ammSle)[sfAccount];
244+
auto sleAMMRoot = sb.peek(keylet::account(ammAccountID));
245+
if (!sleAMMRoot)
246+
{
247+
JLOG(j.error()) << "deleteAMMAccount: AMM account does not exist "
248+
<< to_string(ammAccountID);
249+
return tecINTERNAL;
250+
}
251+
252+
if (auto const ter =
253+
deleteAMMTrustLines(sb, ammAccountID, maxDeletableAMMTrustLines, j);
254+
ter != tesSUCCESS)
255+
return ter;
256+
257+
auto const ownerDirKeylet = keylet::ownerDir(ammAccountID);
258+
if (sb.exists(ownerDirKeylet) && !sb.emptyDirDelete(ownerDirKeylet))
259+
{
260+
JLOG(j.error()) << "deleteAMMAccount: cannot delete root dir node of "
261+
<< toBase58(ammAccountID);
262+
return tecINTERNAL;
263+
}
264+
265+
sb.erase(ammSle);
266+
sb.erase(sleAMMRoot);
267+
268+
return tesSUCCESS;
269+
}
270+
271+
void
272+
initializeFeeAuctionVote(
273+
ApplyView& view,
274+
std::shared_ptr<SLE>& ammSle,
275+
AccountID const& account,
276+
Issue const& lptIssue,
277+
std::uint16_t tfee)
278+
{
279+
// AMM creator gets the voting slot.
280+
STArray voteSlots;
281+
STObject voteEntry{sfVoteEntry};
282+
if (tfee != 0)
283+
voteEntry.setFieldU16(sfTradingFee, tfee);
284+
voteEntry.setFieldU32(sfVoteWeight, VOTE_WEIGHT_SCALE_FACTOR);
285+
voteEntry.setAccountID(sfAccount, account);
286+
voteSlots.push_back(voteEntry);
287+
ammSle->setFieldArray(sfVoteSlots, voteSlots);
288+
// AMM creator gets the auction slot for free.
289+
auto& auctionSlot = ammSle->peekFieldObject(sfAuctionSlot);
290+
auctionSlot.setAccountID(sfAccount, account);
291+
// current + sec in 24h
292+
auto const expiration = std::chrono::duration_cast<std::chrono::seconds>(
293+
view.info().parentCloseTime.time_since_epoch())
294+
.count() +
295+
TOTAL_TIME_SLOT_SECS;
296+
auctionSlot.setFieldU32(sfExpiration, expiration);
297+
auctionSlot.setFieldAmount(sfPrice, STAmount{lptIssue, 0});
298+
// Set the fee
299+
if (tfee != 0)
300+
ammSle->setFieldU16(sfTradingFee, tfee);
301+
else if (ammSle->isFieldPresent(sfTradingFee))
302+
ammSle->makeFieldAbsent(sfTradingFee);
303+
if (auto const dfee = tfee / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION)
304+
auctionSlot.setFieldU16(sfDiscountedFee, dfee);
305+
else if (auctionSlot.isFieldPresent(sfDiscountedFee))
306+
auctionSlot.makeFieldAbsent(sfDiscountedFee);
307+
}
308+
191309
} // namespace ripple

src/ripple/app/paths/impl/BookStep.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ class BookStep : public StepImp<TIn, TOut, BookStep<TIn, TOut, TDerived>>
9999
, ownerPaysTransferFee_(ctx.ownerPaysTransferFee)
100100
, j_(ctx.j)
101101
{
102-
if (auto const ammSle = ctx.view.read(keylet::amm(in, out)))
102+
if (auto const ammSle = ctx.view.read(keylet::amm(in, out));
103+
ammSle && ammSle->getFieldAmount(sfLPTokenBalance) != beast::zero)
103104
ammLiquidity_.emplace(
104105
ctx.view,
105106
(*ammSle)[sfAccount],

src/ripple/app/tx/impl/AMMBid.cpp

+7-4
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ AMMBid::preclaim(PreclaimContext const& ctx)
9494
return terNO_AMM;
9595
}
9696

97+
auto const lpTokensBalance = (*ammSle)[sfLPTokenBalance];
98+
if (lpTokensBalance == beast::zero)
99+
return tecAMM_EMPTY;
100+
97101
if (ctx.tx.isFieldPresent(sfAuthAccounts))
98102
{
99103
for (auto const& account : ctx.tx.getFieldArray(sfAuthAccounts))
@@ -114,7 +118,6 @@ AMMBid::preclaim(PreclaimContext const& ctx)
114118
JLOG(ctx.j.debug()) << "AMM Bid: account is not LP.";
115119
return tecAMM_INVALID_TOKENS;
116120
}
117-
auto const lpTokensBalance = (*ammSle)[sfLPTokenBalance];
118121

119122
auto const bidMin = ctx.tx[~sfBidMin];
120123

@@ -204,10 +207,10 @@ applyBid(
204207
Number const& burn) -> TER {
205208
auctionSlot.setAccountID(sfAccount, account_);
206209
auctionSlot.setFieldU32(sfExpiration, current + TOTAL_TIME_SLOT_SECS);
207-
if (fee == 0)
208-
auctionSlot.makeFieldAbsent(sfDiscountedFee);
209-
else
210+
if (fee != 0)
210211
auctionSlot.setFieldU16(sfDiscountedFee, fee);
212+
else if (auctionSlot.isFieldPresent(sfDiscountedFee))
213+
auctionSlot.makeFieldAbsent(sfDiscountedFee);
211214
auctionSlot.setFieldAmount(
212215
sfPrice, toSTAmount(lpTokens.issue(), minPrice));
213216
if (ctx_.tx.isFieldPresent(sfAuthAccounts))

src/ripple/app/tx/impl/AMMCreate.cpp

+22-36
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
173173
auto isLPToken = [&](STAmount const& amount) -> bool {
174174
if (auto const sle =
175175
ctx.view.read(keylet::account(amount.issue().account)))
176-
return (sle->getFlags() & lsfAMM);
176+
return sle->isFieldPresent(sfAMMID);
177177
return false;
178178
};
179179

@@ -184,7 +184,21 @@ AMMCreate::preclaim(PreclaimContext const& ctx)
184184
return tecAMM_INVALID_TOKENS;
185185
}
186186

187-
return tesSUCCESS;
187+
// Disallow AMM if the issuer has clawback enabled
188+
auto clawbackDisabled = [&](Issue const& issue) -> TER {
189+
if (isXRP(issue))
190+
return tesSUCCESS;
191+
if (auto const sle = ctx.view.read(keylet::account(issue.account));
192+
!sle)
193+
return tecINTERNAL;
194+
else if (sle->getFlags() & lsfAllowTrustLineClawback)
195+
return tecNO_PERMISSION;
196+
return tesSUCCESS;
197+
};
198+
199+
if (auto const ter = clawbackDisabled(amount.issue()); ter != tesSUCCESS)
200+
return ter;
201+
return clawbackDisabled(amount2.issue());
188202
}
189203

190204
static std::pair<TER, bool>
@@ -247,54 +261,26 @@ applyCreate(
247261
// A user can only receive LPTokens through affirmative action -
248262
// either an AMMDeposit, TrustSet, crossing an offer, etc.
249263
sleAMMRoot->setFieldU32(
250-
sfFlags, lsfAMM | lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
264+
sfFlags, lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth);
265+
// Link the root account and AMM object
266+
sleAMMRoot->setFieldH256(sfAMMID, ammKeylet.key);
251267
sb.insert(sleAMMRoot);
252268

253269
// Calculate initial LPT balance.
254270
auto const lpTokens = ammLPTokens(amount, amount2, lptIss);
255271

256272
// Create ltAMM
257273
auto ammSle = std::make_shared<SLE>(ammKeylet);
258-
if (ctx_.tx[sfTradingFee] != 0)
259-
ammSle->setFieldU16(sfTradingFee, ctx_.tx[sfTradingFee]);
260274
ammSle->setAccountID(sfAccount, *ammAccount);
261275
ammSle->setFieldAmount(sfLPTokenBalance, lpTokens);
262276
auto const& [issue1, issue2] = std::minmax(amount.issue(), amount2.issue());
263277
ammSle->setFieldIssue(sfAsset, STIssue{sfAsset, issue1});
264278
ammSle->setFieldIssue(sfAsset2, STIssue{sfAsset2, issue2});
265-
// AMM creator gets the voting slot.
266-
STArray voteSlots;
267-
STObject voteEntry{sfVoteEntry};
268-
if (ctx_.tx[sfTradingFee] != 0)
269-
voteEntry.setFieldU16(sfTradingFee, ctx_.tx[sfTradingFee]);
270-
voteEntry.setFieldU32(sfVoteWeight, 100000);
271-
voteEntry.setAccountID(sfAccount, account_);
272-
voteSlots.push_back(voteEntry);
273-
ammSle->setFieldArray(sfVoteSlots, voteSlots);
274-
// AMM creator gets the auction slot for free.
275-
auto& auctionSlot = ammSle->peekFieldObject(sfAuctionSlot);
276-
auctionSlot.setAccountID(sfAccount, account_);
277-
// current + sec in 24h
278-
auto const expiration =
279-
std::chrono::duration_cast<std::chrono::seconds>(
280-
ctx_.view().info().parentCloseTime.time_since_epoch())
281-
.count() +
282-
TOTAL_TIME_SLOT_SECS;
283-
auctionSlot.setFieldU32(sfExpiration, expiration);
284-
auctionSlot.setFieldAmount(sfPrice, STAmount{lpTokens.issue(), 0});
279+
// AMM creator gets the auction slot and the voting slot.
280+
initializeFeeAuctionVote(
281+
ctx_.view(), ammSle, account_, lptIss, ctx_.tx[sfTradingFee]);
285282
sb.insert(ammSle);
286283

287-
// Add owner directory to link the root account and AMM object.
288-
if (auto const page = sb.dirInsert(
289-
keylet::ownerDir(*ammAccount),
290-
ammSle->key(),
291-
describeOwnerDir(*ammAccount));
292-
!page)
293-
{
294-
JLOG(j_.debug()) << "AMM Instance: failed to insert owner dir";
295-
return {tecDIR_FULL, false};
296-
}
297-
298284
// Send LPT to LP.
299285
auto res = accountSend(sb, *ammAccount, account_, lpTokens, ctx_.journal);
300286
if (res != tesSUCCESS)

0 commit comments

Comments
 (0)