CertiK’s Audit of the XRP Ledger Automated Market Maker - Findings and Review
Tags: blockchain, cybersecurity, web3, xrp
Published: 2023-07-11 12:00:00

Author: *Alexey Malanov, Principal Blockchain Security Expert at CertiK*

## Introduction

CertiK recently [conducted](https://skynet.certik.com/projects/xrp-ledger) an extensive third-party audit of an Automated Market Maker (AMM) built on the XRP Ledger. In this technical blogpost, we will present some of the key findings from our audit and discuss how Ripple and CertiK collaborated to address these findings. Our objective is to provide valuable insights to current and prospective builders of other AMMs, as well as the developers, users, and stakeholders of the XRP Ledger AMM.

Total Findings: 31

0 Critical, 0 Major, 8 Medium, 12 Minor, 11 Informational

29 Findings Resolved, 2 Minor Issues Acknowledged

## About the XRP Ledger AMM

Ripple recognized the need for a non-custodial automated market maker (AMM) to add flexibility to the XRP Ledger. Ripple has chosen the most popular approach {%katex inline%}\Gamma_A * \Gamma_B = k{%endkatex %}, that means that the product of asset reserves (pool balances) is preserved by swap operations. 

For example, doubling one reserve in a pool via swap, leads to halving of another one.

In general:
{%katex%} (\Gamma_A + \Delta_A)(\Gamma_B - \Delta_B) = k    \tag1{%endkatex%}
Opening brackets we get:
{%katex%} -\Gamma_A\Delta_B + \Delta_A\Gamma_B - \Delta_A\Delta_B = 0    \tag2{%endkatex%}
where,

{%katex inline%}\Gamma_A{%endkatex%}: Reserve of asset A
{%katex inline%}\Gamma_B{%endkatex%}: Reserve of asset B
{%katex inline%}\Delta_A{%endkatex%}: The amount of asset A that comes into the pool
{%katex inline%}\Delta_B{%endkatex%}: The amount of asset B that comes out in return

Taking into account the {%katex inline%}TFee{%endkatex %} taken from the incoming amount, from (2) we get the main formula of the swap:
 {%katex%} \Delta_B = \Gamma_B \frac{\Delta_A(1-TFee)}{\Gamma_A + \Delta_A(1-TFee)} \tag3 {%endkatex%}

It is also possible to deposit and withdraw liquidity proportionally to current reserves:

If:
{%katex%} \frac{\Delta_A}{\Delta_B} = \frac{\Gamma_A}{\Gamma_B} = \frac{\Gamma_A + \Delta_A}{\Gamma_B + \Delta_B} = SpotPrice    \tag4{%endkatex%}

Then the user gets:
{%katex%} \Delta_{LPTokens} = \Gamma_{LPTokens} \frac{\Delta_A}{\Gamma_A} = \Gamma_{LPTokens} \frac{\Delta_B}{\Gamma_B}    \tag5{%endkatex%}

where,

{%katex inline%}\Gamma_{LPTokens}{%endkatex%}: Supply of LPTokens before the swap. Initially set as {%katex inline%}\Gamma_{LPTokensInitial} = \sqrt{\Gamma_A \Gamma_B}{%endkatex %} with the first deposit (pool creation).
{%katex inline%}\Delta_{LPTokens}{%endkatex %}: LPTokens the user gets for the deposit of {%katex inline%}(\Delta_A,\Delta_B){%endkatex %}.

For example, if the user doubles both asset reserves, they will get 100% of original {%katex inline%}\Gamma_{LPTokens}{%endkatex %}, that is 50% of the final total.


## Detailed Description and Implementation

Ripple has provided a detailed description of the features in the AMM, which can be found in their repository [here](https://github.com/XRPLF/XRPL-Standards/discussions/78).

## APP-01 and APP-02: Suboptimal Single Asset Deposit Calculations – Severity: Medium – Status: Resolved

![APP-01 and APP-02: Suboptimal Single Asset Deposit Calculations](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v411g1c39uomiyuj4o2s.png)

The XRP Ledger AMM allows users to perform a single asset deposit with a `AMMDeposit(tfSingleAsset)` transaction. This means that some part of the deposit is virtually swapped into another asset and then the usual two-asset deposit is performed.  However, our audit revealed that the formulas used by Ripple for single asset deposits had some miscalibrations. Fees were always taken for the half of the deposited asset, and pool value change due to fees was not accounted for. The variable amount should be virtually swapped instead with the corresponding fee taken. As a result, users would benefit from using `AMMDeposit(tfSingleAsset)` followed by `AMMWithdraw(tfLPToken)` instead of a normal swap, as it would save on fees.

### Perfect Single Asset Deposits

While the single asset deposit feature is not commonly described or implemented in traditional AMMs, it holds significant utility. Let's discuss the proposed logic for a perfect single asset deposit:

1\. We want to split the amount {%katex inline%}\Delta_B{%endkatex %} into two parts: {%katex inline%}x + (\Delta_B - x){%endkatex %} and swap {%katex inline%}x{%endkatex %} for {%katex inline%}\Delta_A = \Gamma_A \frac{x(1-TFee)}{\Gamma_B + x(1-TFee)}{%endkatex %} (equation (3)).

2\. We need to determine {%katex inline%}x{%endkatex %} such that the final pool reserves ratio {%katex inline%}\frac{\Gamma_A}{\Gamma_B+\Delta_B}{%endkatex %} is equal to both the ratio of assets deposited {%katex inline%}\frac{\Delta_A}{\Delta_B - x}{%endkatex %} and the pool reserves ratio after the swap {%katex inline%}\frac{\Gamma_A - \Delta_A}{\Gamma_B + x}{%endkatex %} (equation (4)).

3\. Using the equation {%katex inline%}\frac{\Gamma_A}{\Gamma_B + \Delta_B} = \frac{\Delta_A}{\Delta_B-x}{%endkatex %} and substituting {%katex inline%}\Delta_A{%endkatex %}, we get the equation {%katex inline%}(\Delta_B - x)(\Gamma_B + x(1-TFee)) = (\Gamma_B + \Delta_B)x(1-TFee){%endkatex %}.

4\. Solving this equation gives us {%katex inline%}x = \Gamma_B \left(\sqrt{\left(\frac{1-TFee/2}{1-TFee}\right)^2 + \frac{\Delta_B}{\Gamma_B(1-TFee)}} - \frac{1-TFee/2}{1-TFee}\right){%endkatex %}.

5\. After swapping {%katex inline%}x{%endkatex %} of AssetB for {%katex inline%}\Delta_A{%endkatex %} of AssetA, we perform the complete assets deposit with {%katex inline%}\Delta_{LPTokens} = \Gamma_{LPTokens} \frac{\Delta_A}{\Gamma_A - \Delta_A}  = \Gamma_{LPTokens} \frac{\Delta_B-x}{\Gamma_B + x}{%endkatex %} (equation (5)).

The final formula for the perfect single asset deposit is:

{%katex%} \Delta_{LPTokens} = \Gamma_{LPTokens} \frac{\Delta_B-x}{\Gamma_B + x} = \Gamma_{LPTokens} \frac{  \frac{\Delta_B}{\Gamma_B} - \left(\sqrt{  \left( \frac{1-TFee/2}{1-TFee} \right) ^2 + \frac{\Delta_B}{\Gamma_B(1-TFee)}} - \frac{1-TFee/2}{1-TFee} \right) }{1 + {\sqrt{  \left( \frac{1-TFee/2}{1-TFee} \right) ^2 + \frac{\Delta_B}{\Gamma_B(1-TFee)}} - \frac{1-TFee/2}{1-TFee} } }  {%endkatex%}

### Example

Let's consider an example with a significant {%katex inline%}TFee{%endkatex %}.
Let {%katex inline%}\Delta_B = \Gamma_B{%endkatex %}, {%katex inline%}TFee = 50\%{%endkatex %}.

The formula used by the XRP Ledger AMM gave {%katex inline%}\frac{\Delta_{LPTokens}}{\Gamma_{LPTokens}} = \sqrt{  1+\frac{\Delta_B(1- TFee/2) }{\Gamma_B}  } - 1 = \sqrt{1+1-0.25} - 1   \approx 32.3\%{%endkatex %} of LPTokens.

The proposed approach recommends:

1. swapping {%katex inline%}\frac{x}{\Gamma_B} = \sqrt{ \frac{1-0.25}{1-0.5}^2 +
\frac{1}{1-0.5} } - \frac{1-0.25}{1-0.5} = \sqrt{1.5^2 + 2} - 1.5   \approx  56.2\%{%endkatex %} of AssetB

2. for {%katex inline%}\frac{\Delta_A}{\Gamma_A} = \frac{\Delta_B 0.5}{\Gamma_B + \Delta_B 0.5}  \approx  \frac{56.2\%/2}{100\% + 56.2\%/2}  \approx  21.9\%{%endkatex %} of AssetA

3. then depositing {%katex inline%}(21.9\%, 43.8\%){%endkatex %} into the pool with {%katex inline%}(78.1\%, 156.2\%){%endkatex %} gives {%katex inline%}\frac{\Delta_{LPTokens}}{\Gamma_{LPTokens}} = \frac{\Delta_B-x}{\Gamma_B+x}   \approx \frac{43.8\%}{156.2\%}  \approx  28\%{%endkatex %} of LPTokens.

In other words, a single asset deposit (followed by immediate two-asset withdrawal) is more advantageous than a normal swap and would be preferred by users.


There is no difference with zero {%katex inline%}TFee{%endkatex %}.
Let {%katex inline%}\Delta_B = \Gamma_B{%endkatex %}, {%katex inline%}TFee = 0\%{%endkatex %}.

The formula used by the XRP Ledger AMM gave {%katex inline%}\frac{\Delta_{LPTokens}}{\Gamma_{LPTokens}} = \sqrt{  1 + \frac{\Delta_B}{\Gamma_B}  } - 1 = \sqrt{2} - 1   \approx 41.4\%{%endkatex %}

The proposed approach recommends:

1. swapping {%katex inline%}\frac{x}{\Gamma_B} = \sqrt{ 1 + \frac{\Delta_B}{\Gamma_B} } - 1 = \sqrt{2} - 1   \approx  41.4\%{%endkatex %} of AssetB for {%katex inline%}29.3\%{%endkatex %} of AssetA first

2. then depositing {%katex inline%}(29.3\%, 58.6\%){%endkatex %} into pool with {%katex inline%}(70.7\%, 141.4\%){%endkatex %}

3. giving the pool in state {%katex inline%}(100\%, 200\%){%endkatex %} and {%katex inline%}\frac{\Delta_{LPTokens}}{\Gamma_{LPTokens}}  \approx  \frac{58.6\%}{141.4\%}  \approx 41.4\%{%endkatex %} of LPTokens - same amount

And with a reasonable {%katex inline%}TFee{%endkatex %}.
Let {%katex inline%}\Delta_B = \Gamma_B{%endkatex %}, {%katex inline%}TFee = 1\%{%endkatex %}.

The formula used by the XRP Ledger AMM gave {%katex inline%}\frac{\Delta_{LPTokens}}{\Gamma_{LPTokens}} = \sqrt{  1 + \frac{0.995\Delta_B}{\Gamma_B}  } - 1 = \sqrt{1.995} - 1   \approx 41.244\%{%endkatex %}

The proposed approach recommends:

1. swapping {%katex inline%}\frac{x}{\Gamma_B} = \sqrt{ \left(\frac{0.995}{0.99}\right)^2 + \frac{\Delta_B}{0.99\Gamma_B} } - \frac{0.995}{0.99}  \approx   \sqrt{2.02} - 1.005   \approx  41.63\%{%endkatex %} of AssetB for {%katex inline%}29.2\%{%endkatex %} of AssetA first

2. then depositing {%katex inline%}(29.2\%, 58.4\%){%endkatex %} into the pool with {%katex inline%}(70.8\%, 141.6\%){%endkatex %}

3. resulting in the pool state of {%katex inline%}(100\%, 200\%){%endkatex %} and {%katex inline%}\frac{\Delta_{LPTokens}}{\Gamma_{LPTokens}}  \approx  \frac{58.4\%}{141.6\%}  \approx 41.21\%{%endkatex %} of LPTokens.

In summary, with a reasonable {%katex inline%}TFee{%endkatex %}, the difference in LPTokens between the two approaches is negligible (41.244% vs. 41.21%). Therefore, addressing this issue aims to provide consistency rather than substantial impact.

### Additional Observations
We also observed that [balancer.fi](https://balancer.fi/whitepaper.pdf) initially employed the same approach as Ripple in the XRP Ledger AMM.

Also CertiK regularly conducts audits on Solidity projects that perform single asset deposits via Uniswap-like AMMs. They do it the same wrong way we have just described: half of the asset is first swapped via the pool for ETH, then another half and swapped ETH are deposited via `addLiquidityETH()`.

```solidity
        // swap tokens for ETH
        swapTokensForEth(half); 

        // how much ETH did we just swap into?
        uint256 newBalance = address(this).balance.sub(initialBalance);

        // add liquidity to uniswap
        addLiquidity(otherHalf, newBalance);
```
<div style="text-align:center">

SafeMoon's [implementation](https://github.com/safemoonprotocol/Safemoon.sol/blob/main/Safemoon.sol#L1068-L1075) of `swapAndLiquify()` 
</div>

Since during the first swap the pool reserve ratio has changed, "halves" no longer can be deposited completely, small share of ETH will be returned back by AMM. If the contract doesn't provide a way to withdraw those ETHs, they will be locked in the contract forever.

## AMD-01: Protection from Front-Running – Severity: Medium – Status: Resolved

![AMD-01: Protection from Front-Running](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b20kjdgc6wnzgu0h1734.png)

Our audit also highlighted the importance of protecting against front-running attacks targeting users of the XRP Ledger AMM. Consider a scenario where an attacker can steal the funds of a liquidity provider.

1. Let the pool have (1000 TokenA, 1000 TokenB) with a fair price 1:1. Let the trading fee be 0 for simplicity.

2. The Victim wants to `AMMDeposit(tfTwoAsset, 100 TokenA, 100 TokenB)`. They expect to get 10% of existing LPTokens.

3. However, the Attaker swaps 9000 TokenB for 900 TokenA leaving the pool in state (100 TokenA, 10000 TokenB).

4. Since two-assets deposit must preserve the spot price (pool reserves ratio), `AMMDeposit` only takes (1 TokenA, 100 TokenB), moves the pool in the state (101 TokenA, 10100 TokenB), and gives 1% of LPTokens in return.

5. The Attacker swaps back 909 TokenA for 9090 TokenB, leaving the pool in the state (1010 TokenA, 1010 TokenB) with the original price.

6. The Attacker gets `9090 - 9 - 9000 = 81` TokenB profit. That was effectively stolen from the Victim.

7. The Victim gets 99 TokenA and 1% LPTokens instead of the expected 10% LPTokens.

This is a well-known potential issue for all AMMs. 

Other AMM implementations, such as 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
```
<div style="text-align:center">

Uniswap’s [declaration](https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol#L61-L69) of `addLiquidity()`.
</div>

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.

The initial design of the AMM had no similar protection. `AMMDeposit(tfTwoAsset)` allowed specifying `amount` and `amount2`, but they were used as maximum values consumed by the deposit operation.

Ripple heeded our advice and mitigated the issue as recommended.


## AMV-01 and AMV-02: Voting on the Trading Fee – Severity: Minor – Status: Acknowledged

![AMV-01 and AMV-02: Voting on the Trading Fee](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/unq0ew9t98gucfmq0rr0.png)

In the XRP Ledger AMM, the TradingFee parameter is subject to voting. Any account holding the corresponding LPTokens has the ability to cast a vote using the `AMMVote` transaction. The voting power is determined by the balance of LPTokens held. During the processing of `AMMVote`, the weights of all the votes are recalculated, and the TradingFee for the AMM instance is updated. The system remembers the votes of the top eight largest holders.

However, the voting mechanism can be susceptible to manipulation in a number of ways. Let's consider a few scenarios:


1. Exploiting Fees: A well-capitalized actor can exploit the system by conducting trades without incurring fees. Here's an example:
   - The "[Griefer](https://en.wiktionary.org/wiki/griefer)" holds significant balances of TokenA and TokenB but wishes to rebalance their portfolio.
   - The Griefer calls `AMMDeposit(tfTwoAsset)` with substantial amounts.
   - Leveraging their significant LPTokens balance, the Griefer submits `AMMVote(0%)` to minimize the impact of fees.
   - The Griefer executes the desired swap operation with minimal fees.
   - Finally, the Griefer performs `AMMWithdraw(tfTwoAsset)` to retrieve their tokens, effectively utilizing quick liquidity deposits to avoid or minimize fees.

2. Vote Manipulation: A well-capitalized actor can exert influence over the voting process to the detriment of other voters. Let's examine a scenario:
  - Suppose the Griefer has a weight of 81, while other traders have weights of 10 each, totaling 70.
  - According to the distribution, the Griefer should have only 54% voting power.
  - However, by dividing their LPTokens equally among 8 accounts and casting votes from each account, the Griefer can push out other voters, obtaining 100% voting power.
  - Once the Griefer has achieved this, they can consolidate the LPTokens back into a single account.

3. Voting Slot Occupation: Voting slots are only updated if a voter becomes an active participant. Here's a scenario illustrating this:
  - The Griefer obtains a significant amount of LPTokens through `AMMDeposit`.
  - They divide the LPTokens equally into 8 parts, distributing them among 8 accounts, and cast `AMMVote(maximum fee)` from each account.
  - As a result, all 8 voting slots are now occupied by accounts with substantial weights.
  - Afterward, the Griefer combines the LPTokens and performs `AMMWithdraw`, retrieving their funds.
  - Consequently, until someone obtains a significant LPTokens balance, no one can refresh the votes array.

4. Determining the final fee is achieved by calculating a weighted average of all the votes. However, this can lead to a situation where voters struggle to set their desired fee value due to the influence of other voters. For instance:
  - Suppose a voter with a weight of 10 wishes to set the fee to 0.5%.
  - At that moment, other voters with a cumulative weight of 10 have voted for a 1% fee.
  - In order to attain the desired 0.5% fee, the voter is compelled to submit `AMMVote(0%)`.
  - If, over time, other voters change their desired fee to 0%, the voter must resubmit `AMMVote(1%)` to maintain the desired 0.5% fee.
  - As a result, the voter cannot directly set the desired fee but must instead strike a balance that accommodates the preferences of other voters. This dynamic can be likened to a tug of war game.

Despite these strategies enabling users to utilize the voting feature in ways that were not originally intended, we have consulted with Ripple and decided against introducing additional constraints or altering the design. Among the scenarios mentioned, only the third one could be addressed without significant modifications, and appropriate measures were taken.

Nevertheless, analyzing potential strategies and scenarios is crucial for conducting a comprehensive risk assessment of the system.

## Conclusion

The audit of the XRP Ledger AMM revealed findings specific to the project, with varying levels of severity. Of 32 identified issues, 30 were resolved as recommended, and two minor issues were acknowledged. It is common for large projects to have some implementation quirks that can be refined. Our collaboration with Ripple ensured that these findings were thoroughly evaluated and appropriate actions were taken to improve the overall security, consistency, and user experience of the XRP Ledger AMM.

By presenting these findings and the collaborative efforts between Ripple and CertiK, we aim to provide valuable insights to developers and stakeholders, enhancing the understanding of the XRP Ledger AMM's inner workings and promoting its continuous improvement.

_Disclaimer: The findings and recommendations in this blogpost are based on the audit conducted by CertiK at the time of the evaluation. As technology evolves, it is essential to keep pace with updates and advancements in the field. We encourage ongoing security assessments and collaborations between project teams and security experts to ensure the highest level of security for blockchain-based systems._