As of 3 Nov 2026, XRP Ledger has 2 audit reports by 1 security firm on Trustblock; the latest is by CertiK, dated 15 Aug 2023. Those reports list no open critical findings.
Record last updated . It holds audits, findings and contracts up to Nov 2026, and incidents up to Oct 2026. Trustblock does not review code.
Newest first
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 0 |
| Medium | 0 | 0 | 8 |
| Low | 2 | 1 | 24 |
`AMMDeposit(tfTwoAsset)` allows specifying `amount` and `amount2`. They are used as maximum values consumed by the deposit operation. However, that doesn't allow the user to set the protection from front-running. Other implementations of AMM, like Uniswap, allow the user to set the minimum amounts accepted: ```solidity function addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline ``` If `amountAMin`/`amountBMin` conditions are not satisfied, the transaction is rejected. This effectively protects the user from front-running and sudden pool spot-price changes for other reasons. Other modes of `AMMDeposit` also don't provide protection from price fluctuations.
Open the report```cpp=242 if (computedPrice >= *bidMin && computedPrice <= *bidMax) return computedPrice; JLOG(ctx_.journal.debug()) << "AMM Bid: not in range " << computedPrice << *bidMin << " " << *bidMax; ``` `AMMBid` allows setting `sfBidMin` and `sfBidMax`. `computedPrice` is calculated based on the current slot state. The code doesn't allow raising the bid via setting `sfBinMin` if `sfBidMax` is also specified.
Open the report```cpp=81 cur.out = toAmount<TOut>( getIssue(balances.out), cur.out * fib[ammContext_.curIters() - 1], Number::rounding_mode::downward); cur.in = swapAssetOut(balances, cur.out, tradingFee_); ``` `generateFibSeqOffer()` is supposed to generate AMM offers with the offer size based on Fibonacci sequence. The starting `cur.in = InitialFibSeqPct * initialBalances_.in = 0.025% of initialBalances_.in`. `cur.out` is approximately the same. Then `cur.out` is multiplied by `fib` elements based on the iteration. Since `fib[17] = 4181`, `cur.out` will get bigger than `balances.out`. That is unexpected by `swapAssetOut()` - `cur.in` will become negative or overflow.
Open the reportIt is possible to perform a single asset withdrawal via `AMMWithdraw(tfOneAssetLPToken)`. However, the formula (8) is inaccurate. It is more advantageous for the user to perform `AMMWithdraw(tfLPToken)` and then swap the redundant token.
Open the reportIn `AMMWithdraw::preclaim()` if `sfAmount`/`sfAmount2` are set, it is checked that * account is authorized to hold the tokens and the trustline exists * AMM account or currency is not frozen * account is not frozen However, if `tfLPToken` is set, it is not checked that account is authorized to hold the tokens. If `tfWithdrawAll` is set, all three checks are skipped. As a result, the frozen account can get tokens from AMM or tokens can be extracted from frozen AMM.
Open the reportIt is possible to perform a single asset deposit via `AMMDeposit(tfSingleAsset)`. However, the formulas [(3) and (4)](https://github.com/XRPLF/XRPL-Standards/discussions/78) are inaccurate. Fees are always taken for half of the deposited asset and pool value change due to fees is not accounted for. The variable amount should be virtually swapped with the corresponding fee taken. So the users will prefer `AMMDeposit(tfSingleAsset)` then `AMMWithdraw(tfLPToken)` instead of normal swap to save some fees. Current logic behind single asset deposit formula **(3)**: $$\Delta_{LPTokens} = \Gamma_{LPTokens} * \left[ \left(1+ \frac{\Delta_{B}-TFee*(1-W_B)*\Delta_B}{\Gamma_B} \right) ^ {W_B} - 1 \right] $$ 1. The deposited amount $$ \Delta_B $$ is split into $$ W_B\Delta_B + (1-W_B)\Delta_B = W_B\Delta_B + W_A\Delta_B$$ 2. Then $$W_A\Delta_B$$ of AssetB is virtually swapped for AssetA 3. Pool spot-price $$ SP = \frac{\frac{\Gamma_A}{W_A}}{\frac{\Gamma_B}{W_B}} $$ (see **(II)**) is used to get $$ \Delta_A = W_B\Delta_B \frac{\Gamma_A}{\Gamma_B}$$ 4. Two assets $$\left( \Delta_A, W_B\Delta_B \right) = \left( W_B\Delta_B \frac{\Gamma_A}{\Gamma_B} , W_B\Delta_B \right)$$ are deposited to pool with $$\left( \Gamma_A, \Gamma_B \right)$$ reserves 5. The pool ratio $$ \frac{\Gamma_A}{\Gamma_B} $$ (and therefore pool spot-price) remains the same since $$ \frac{\Gamma'_A}{\Gamma'_B} = \frac{\Gamma_A + W_B\Delta_B \frac{\Gamma_A}{\Gamma_B}}{\Gamma_B + W_B\Delta_B} = \frac{\Gamma_A}{\Gamma_B} $$ 6. Since we virtually swapped $$W_A\Delta_B$$, only $$ TFee W_A $$ was taken, that's why $$ \Delta_B $$ in **(3)** is reduced by $$ \left( 1-TFee(1-W_B)\right) $$ However, the pool state changes during the swap operation, so we can't perform the swap using the spot-price and then perform a deposit using the same spot-price. In case $$ \Delta_B $$ is comparable to $$ \Gamma_B $$ and $$ Tfee $$ is significant, the resulting $$ \Delta_{LPTokens} $$ will be higher than obtainable via real swap and `AMMDeposit(tfTwoAsset)`. See the example in the Scenario section. The proposed logic of single asset deposit is: 1. Let $$ W_A = W_B = 0.5 $$ 2. We want to split $$ \Delta_B $$ into two parts $$ x + (\Delta_B - x) $$ and swap $$ x $$ for $$ \Delta_A = \Gamma_A \frac{x(1-TFee)}{\Gamma_B + x(1-TFee)} $$ (see **(9)**) 3. The $$x$$ should be so that the final pool reserves ratio $$\frac{\Gamma_A}{\Gamma_B+\Delta_B} $$ is equal to the ratio of assets deposited $$ \frac{\Delta_A}{\Delta_B - x} $$ and is equal to the pool reserves ratio after the swap $$ \frac{\Gamma_A - \Delta_A}{\Gamma_B + x} $$ 4. Using $$ \frac{\Gamma_A}{\Gamma_B + \Delta_B} = \frac{\Delta_A}{\Delta_B-x} $$ and substituting $$ \Delta_A $$ we get $$ (\Delta_B - x)(\Gamma_B + x(1-TFee)) = (\Gamma_B + \Delta_B)x(1-TFee) $$ 5. Solving it gives $$ x = \Gamma_B (\sqrt{ \left( \frac{1-TFee/2}{1-TFee} \right) ^2 + \frac{\Delta_B}{\Gamma_B(1-TFee)}} - \frac{1-TFee/2}{1-TFee} )$$ 6. After the swap of $$ x $$ of AssetB for $$ \Delta_A $$ of AssetA, we perform all assets deposit with $$ \Delta_{LPTokens} = \Gamma_{LPTokens} \frac{\Delta_A}{\Gamma_A - \Delta_A} = \Gamma_{LPTokens} \frac{\Delta_B-x}{\Gamma_B + x} $$ The final formula instead of **(3)** would be: $$ \Delta_{LPTokens} = \Gamma_{LPTokens} \frac{\Delta_B-x}{\Gamma_B + x} = \Gamma_{LPTokens} \frac{ \frac{\Delta_B}{\Gamma_B} - (\sqrt{ \left( \frac{1-TFee/2}{1-TFee} \right) ^2 + \frac{\Delta_B}{\Gamma_B(1-TFee)}} - \frac{1-TFee/2}{1-TFee} ) }{1 + {\sqrt{ \left( \frac{1-TFee/2}{1-TFee} \right) ^2 + \frac{\Delta_B}{\Gamma_B(1-TFee)}} - \frac{1-TFee/2}{1-TFee} } } $$
Open the reportIt is possible to perform a single asset withdrawal via `AMMWithdraw(tfSingleAsset)`. However, the formulas [(7) and (8)](https://github.com/XRPLF/XRPL-Standards/discussions/78) are inaccurate. It is more advantageous for the user to perform `AMMWithdraw(tfLPToken)` and then swap the redundant token. We propose to follow this logic: 1. Our goal is to get $$ \Delta_B = b_{withdrawn} + b_{swapped} $$ 2. Assuming we first withdraw $$ x $$ fraction of $$ \Gamma_{LPTokens} $$ 3. That gives $$ (x \Gamma_A, x\Gamma_B) $$ and pool in state $$ ((1-x)\Gamma_A, (1-x)\Gamma_B) $$ 4. Using **(9)** we can swap $$ x\Gamma_A $$ of AssetA for $$ b_{swapped} = \Gamma'_B \frac{x\Gamma_A(1-TFee)}{\Gamma'_A + x\Gamma_A(1-TFee)} $$ of AssetB 5. Using $$ \Delta_B = b_{withdrawn}+ b_{swapped} = x\Gamma_B + \Gamma'_B \frac{x\Gamma_A(1-TFee)}{\Gamma'_A + x\Gamma_A(1-TFee)} $$ we get $$ \Delta_B - x\Gamma_B = (1-x)\Gamma_B \frac{x\Gamma_A(1-TFee)}{(1-x)\Gamma_A + x\Gamma_A(1-TFee)} $$ 6. Simplifying that we get $$ (\frac{\Delta_B}{\Gamma_B} - x)(1-x + x(1-TFee)) = (1-x)x(1-TFee)$$ 7. Which gives $$ x^2 + x(TFee - 2 - \frac{\Delta_B}{\Gamma_B}Tfee) + \frac{\Delta_B}{\Gamma_B} = 0 $$ 8. Updated **(7')** is $$ \frac{\Delta_{LPTokens}}{\Gamma_{LPTokens}}= x = \frac{ \frac{\Delta_B}{\Gamma_B}Tfee + 2 - TFee - \sqrt{(\frac{\Delta_B}{\Gamma_B}Tfee + 2 - TFee)^2 - 4\frac{\Delta_B}{\Gamma_B}}}{2} $$ With current formulas, users should never `AMMWithdraw(tfSingleAsset)` to prevent extra spending.
Open the report`MinSlotPrice` is set to zero. So, if `bidMin` is not specified, `sfPrice` is also set to zero according to the formula. There is no motivation for arbitrageurs to raise the price. They can always buy a slot for zero, perform the arbitrage and give up the slot to the next participant for free.
Open the report```cpp=1567 if (maxNodesToDelete && ++deleted > *maxNodesToDelete) ``` `maxNodesToDelete` is `std::optional<uint16_t>`, `deleted` is `uint16_t`. If `maxNodesToDelete == 65535` and there are enough entries in the account directory, the `deleted` will overflow and the code will not work as expected. `maxDeletableAMMTrustLines` passed to `cleanupOnAccountDelete()` is `size_t` set `512`. The ledger allows more than 65535 entries in the account directory according to `View.cpp::confineOwnerCount()`.
Open the reportThere are typos in the code: - "truslines" is supposed to be "trustlines" - "view" is supposed to be "View"
Open the reportMagic numbers are used as-is in code.
Open the report```h=168 JSS(asset_frozen); // in: amm_info JSS(asset2_frozen); // in: amm_info ``` `asset_frozen` and `asset2_frozen` fields are described as `in`, however, they are in fact `out`. `EPrice` is used not only by `AMMDeposit`, but also by `AMMWithdraw`.
Open the report```cpp=151 if (bidMin && bidMax && bidMin >= bidMax) ``` It is disallowed to have `bidMin == bidMax` in `AMMBid::preclaim()`, however, that is a valid scenario: the user wants to pay exactly `bidMin`, not more.
Open the report`invalidAMMAmount()` has the argument `nonNegative` which is `false` by default. The comment states: ```cpp=60 * If zero is false and amount is beast::zero then invalid amount. ``` The code is: ```cpp=101 if (!nonNegative && amount <= beast::zero) return temBAD_AMOUNT; return tesSUCCESS; ``` As a result, the meaning of `!nonNegative` is `mustBePositive`. However, in math "nonNegative" means "zero or positive". Providing `true` as an argument to the function allows the `amount` to be zero. Using negations in bool variable names and negation of the variable itself decreases the code readability.
Open the report```cpp=652 auto const amount2Withdraw = amount2Balance * frac; ``` `multiply(STAmount, STAmount, Issue)` performs some checks and Number normalization, however, it is not used in `AMMWithdraw::equalWithdrawTokens()`.
Open the report`sfDiscountedFee` is stored as `UINT32`, but `sfTradingFee` as `UINT16`. Discounted fee is supposed to be less or equal to normal trading fee. It is unclear why bigger type used.
Open the reportThe TradingFee of the AMM instance is a votable parameter. Any account that holds the corresponding LPTokens can cast a vote using the new `AMMVote` transaction. During `AMMVote` processing the weights of all the votes are recomputed, and the TradingFee for the AMM instance is updated. Up to 8 voting slots supported. However, `sfVoteSlots` is only updated if a voter is part of it. Consider the scenario: - The Griefer obtains a big amount of LPTokens via `AMMDeposit`. - The Griefer separates the amount into 8 equal parts, distributes it among 8 accounts, and performs `AMMVote(1%)` from each. - All 8 `sfVoteSlots` are now occupied by an account with a significant weight. - The Griefer combines the LPTokens and performs `AMMWithdraw`. - As a result, **no one can refresh the votes array** until they get a significant LPTokens balance.
Open the report`std::chrono` is not used in `ammAuctionTimeSlot()`.
Open the reportIn `changeSpotPriceQuality()` and `withdrawByTokens()` the argument `tFee` is declared as `uint32_t`. In other code it is `uint16_t`.
Open the report`tfWithdrawAll` flag is checked in `AMMDeposit::preclaim()`, however, it can't be used with `AMMDeposit`.
Open the reportThe current slot price is calculated by the formula: $$ f(t) = B * 1.05*(1-t^{60}) + M $$ According to the description: "The slot price approaches `M` as the slot approaches expiration (i.e., as `t` approaches 1)." However, 1. During the first 18 slots the price drops gradually from $$ \approx B * 1.05 + M $$ (with `t = 0.05`) to $$ \approx B * 1.048 + M $$ (with `t = 0.90`) 2. Then drops significantly to $$ \approx B + M $$ (with `t = 0.95`) 3. And then drops to $$ M $$, which is currently set to zero So, the speed of descent is highly uneven.
Open the report```cpp=192 return (xrpLiquid(ctx.view, accountID, !sle, ctx.j) >= deposit) ? TER(tesSUCCESS) : (!sle ? tecINSUF_RESERVE_LINE : tecAMM_UNFUNDED); ``` Expressions with ternary conditional operators are harder to read. The expression can be simplified using regular `if`s and `return`.
Open the report```cpp=122 // Returns the amount an account can spend of the currency type saDefault, or // returns saDefault if this account is the issuer of the the currency in // question. Should be used in favor of accountHolds when questioning how much // an account can spend while also allowing currency issuers to spend // unlimited amounts of their own currency (since they can always issue more). [[nodiscard]] STAmount accountHolds( ReadView const& view, ``` The comment describes `accountFunds()` function, not `accountHolds()`. ```cpp=38 * specified by Asset1In. This is essentially an equal asset deposit * and a swap. ``` In fact, that is a swap and a deposit. ```cpp=48 * a. amount of asset1 if specified in Asset1In specifies the maximum * amount of asset1 that the trader is willing to deposit ``` `amount` is always specified in this mode, however, it can be zero. ```cpp=80 /** Attempt to create the AMM instance. */ ``` The same comment is used in `AMMWithdraw.h`, `AMMDeposit.h`, and other files. It should be removed or updated. ```cpp=535 * Use equation 1 to compute the amount of , given the amount in Asset1In. ``` `t` is missing. ```cpp=153 * @param ctx ``` There is no argument `ctx`.
Open the report```cpp=497 JLOG(j.fatal()) << "Invariant failed: account root created " "by a non-Payment or by an unsuccessful transaction"; ``` `ValidNewAccountRoot::finalize()` error message assumes the account can only be created by `ttPAYMENT` transaction. However, `ttAMM_CREATE` should also be mentioned. ```cpp=309 JLOG(j_.debug()) << "AMM Instance: success " << *ammAccount << " " << ammKeylet.key << " " << lptIss << " " << amount << " " << amount2; auto addOrderBook = ``` Using `lpTokens` instead of `lptIss` is preferable. ```cpp=245 << "AMM Bid: not in range " << computedPrice << *bidMin << " " ``` " " is absent between values.
Open the report```cpp=567 auto const [withdrawAmount, withdraw2Amount] = getAmounts(frac); ``` ```cpp=577 auto const [withdrawAmount, withdraw2Amount] = getAmounts(frac); ``` `withdrawAmount` and `withdraw2Amount` are shadowed by the local variable with the same names.
Open the report```cpp=153 auto const timeSlot = ammAuctionTimeSlot( ledger->info().parentCloseTime.time_since_epoch().count(), auctionSlot); auction[jss::time_interval] = timeSlot ? *timeSlot : 0; ``` `ammAuctionTimeSlot()` returns 0-19 in case the auction has not yet expired and `std::nullopt` otherwise. In case of expired `auction[jss::time_interval]` is set 0. The same value is set during the first 1.2 hours. That could be misleading.
Open the report```cpp=218 auto res = redeemIOU(sb, account_, saBurn, lpTokens.issue(), ctx_.journal); if (res != tesSUCCESS) { JLOG(ctx_.journal.debug()) << "AMM Bid: failed to redeem."; return res; } ammSle->setFieldAmount(sfLPTokenBalance, lptAMMBalance - saBurn); ``` `saBurn` LPTokens are redeemed from `account_` and then `sfLPTokenBalance` is reduced. However, these values can be different due to rounding error.
Open the report```cpp=73 constexpr std::uint32_t fib[AMMContext::MaxIterations] = { 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040}; ``` `fib[29]` is zero because the initializer list has only 29 elements. This can be unexpected.
Open the report```cpp=296 // Refund the previous owner. If the time slot is 0 then // the owner is refunded full amount. auto const refund = fractionRemaining * pricePurchased; if (refund > *payPrice) ``` Despite the comment states that the owner is refunded full amount during slot 0, in fact, `fractionRemaining` is maximum 0.95 and the owner always looses money even if outbid in the first second.
Open the reportDifferent magic numbers are used as-is in code.
Open the reportThe TradingFee of the AMM instance is a votable parameter. Any account that holds the corresponding LPTokens can cast a vote using the new `AMMVote` transaction. During `AMMVote` processing the weights of all the votes are recomputed, and the TradingFee for the AMM instance is updated. Up to 8 voting slots supported. The voter doesn't know what `sfTradingFee` to set. Consider the scenario: - Imagine that a Voter with weight 10 wants to set the fee to 0.5%. - At that moment all other voters with a sum weight of 10 voted for a 1% fee. - So, Voter has to submit `AMMVote(0%)` to get the desired 0.5%. - After some time other voters change the desired fee to 0%. - The Voter has to re-submit `AMMVote(1%)` to get the desired 0.5%. - As a result, the voter **can't set the desired fee** but rather sets the value that balances the other votes instead. - This can lead to more `AMMVote` submitted than expected.
Open the reportThe argument `deposit` of `AMMDeposit::deposit()` should be named `view`. `lptTokensAMMBalance` should be named `lptAMMBalance`.
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="xrp-ledger"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us