DEV Community

Cover image for CertiK’s Audit of the XRP Ledger Automated Market Maker - Findings and Review
CertiK
CertiK

Posted on • Updated on

CertiK’s Audit of the XRP Ledger Automated Market Maker - Findings and Review

Author: Alexey Malanov, Principal Blockchain Security Expert at CertiK

Introduction

CertiK recently conducted 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 ΓAΓB=k\Gamma_A * \Gamma_B = k , 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:

(ΓA+ΔA)(ΓBΔB)=k(1)(\Gamma_A + \Delta_A)(\Gamma_B - \Delta_B) = k \tag1

Opening brackets we get:
ΓAΔB+ΔAΓBΔAΔB=0(2)-\Gamma_A\Delta_B + \Delta_A\Gamma_B - \Delta_A\Delta_B = 0 \tag2

where,

ΓA\Gamma_A : Reserve of asset A
ΓB\Gamma_B : Reserve of asset B
ΔA\Delta_A : The amount of asset A that comes into the pool
ΔB\Delta_B : The amount of asset B that comes out in return

Taking into account the TFeeTFee taken from the incoming amount, from (2) we get the main formula of the swap:

ΔB=ΓBΔA(1TFee)ΓA+ΔA(1TFee)(3)\Delta_B = \Gamma_B \frac{\Delta_A(1-TFee)}{\Gamma_A + \Delta_A(1-TFee)} \tag3

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

If:

ΔAΔB=ΓAΓB=ΓA+ΔAΓB+ΔB=SpotPrice(4)\frac{\Delta_A}{\Delta_B} = \frac{\Gamma_A}{\Gamma_B} = \frac{\Gamma_A + \Delta_A}{\Gamma_B + \Delta_B} = SpotPrice \tag4

Then the user gets:

ΔLPTokens=ΓLPTokensΔAΓA=ΓLPTokensΔBΓB(5)\Delta_{LPTokens} = \Gamma_{LPTokens} \frac{\Delta_A}{\Gamma_A} = \Gamma_{LPTokens} \frac{\Delta_B}{\Gamma_B} \tag5

where,

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

For example, if the user doubles both asset reserves, they will get 100% of original ΓLPTokens\Gamma_{LPTokens} , 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.

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

APP-01 and APP-02: Suboptimal Single Asset Deposit Calculations

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 ΔB\Delta_B into two parts: x+(ΔBx)x + (\Delta_B - x) and swap xx for ΔA=ΓAx(1TFee)ΓB+x(1TFee)\Delta_A = \Gamma_A \frac{x(1-TFee)}{\Gamma_B + x(1-TFee)} (equation (3)).

2. We need to determine xx such that the final pool reserves ratio ΓAΓB+ΔB\frac{\Gamma_A}{\Gamma_B+\Delta_B} is equal to both the ratio of assets deposited ΔAΔBx\frac{\Delta_A}{\Delta_B - x} and the pool reserves ratio after the swap ΓAΔAΓB+x\frac{\Gamma_A - \Delta_A}{\Gamma_B + x} (equation (4)).

3. Using the equation ΓAΓB+ΔB=ΔAΔBx\frac{\Gamma_A}{\Gamma_B + \Delta_B} = \frac{\Delta_A}{\Delta_B-x} and substituting ΔA\Delta_A , we get the equation (ΔBx)(ΓB+x(1TFee))=(ΓB+ΔB)x(1TFee)(\Delta_B - x)(\Gamma_B + x(1-TFee)) = (\Gamma_B + \Delta_B)x(1-TFee) .

4. Solving this equation gives us x=ΓB((1TFee/21TFee)2+ΔBΓB(1TFee)1TFee/21TFee)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) .

5. After swapping xx of AssetB for ΔA\Delta_A of AssetA, we perform the complete assets deposit with ΔLPTokens=ΓLPTokensΔAΓAΔA =ΓLPTokensΔBxΓB+x\Delta_{LPTokens} = \Gamma_{LPTokens} \frac{\Delta_A}{\Gamma_A - \Delta_A}  = \Gamma_{LPTokens} \frac{\Delta_B-x}{\Gamma_B + x} (equation (5)).

The final formula for the perfect single asset deposit is:

ΔLPTokens=ΓLPTokensΔBxΓB+x=ΓLPTokens ΔBΓB( (1TFee/21TFee)2+ΔBΓB(1TFee)1TFee/21TFee)1+ (1TFee/21TFee)2+ΔBΓB(1TFee)1TFee/21TFee \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} } } 

Example

Let's consider an example with a significant TFeeTFee .
Let ΔB=ΓB\Delta_B = \Gamma_B , TFee=50%TFee = 50\% .

The formula used by the XRP Ledger AMM gave ΔLPTokensΓLPTokens= 1+ΔB(1TFee/2)ΓB 1=1+10.251  32.3%\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\% of LPTokens.

The proposed approach recommends:

  1. swapping xΓB=10.2510.52+110.510.2510.5=1.52+21.5   56.2%\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\% of AssetB

  2. for  ΔAΓA=ΔB0.5ΓB+ΔB0.5  56.2%/2100%+56.2%/2  21.9%\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\% of AssetA

  3. then depositing (21.9%,43.8%)(21.9\%, 43.8\%) into the pool with (78.1%,156.2%)(78.1\%, 156.2\%) gives ΔLPTokensΓLPTokens =ΔBxΓB+x  43.8%156.2%  28%\frac{\Delta_{LPTokens}}{\Gamma_{LPTokens}} = \frac{\Delta_B-x}{\Gamma_B+x}   \approx \frac{43.8\%}{156.2\%}  \approx  28\% 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 TFeeTFee .
Let ΔB=ΓB\Delta_B = \Gamma_B , TFee=0%TFee = 0\% .

The formula used by the XRP Ledger AMM gave ΔLPTokensΓLPTokens= 1+ΔBΓB 1=21  41.4%\frac{\Delta_{LPTokens}}{\Gamma_{LPTokens}} = \sqrt{  1 + \frac{\Delta_B}{\Gamma_B}  } - 1 = \sqrt{2} - 1   \approx 41.4\%

The proposed approach recommends:

  1. swapping xΓB=1+ΔBΓB1=2141.4%\frac{x}{\Gamma_B} = \sqrt{ 1 + \frac{\Delta_B}{\Gamma_B} } - 1 = \sqrt{2} - 1 \approx 41.4\% of AssetB for 29.3%29.3\% of AssetA first

  2. then depositing (29.3%,58.6%)(29.3\%, 58.6\%) into pool with (70.7%,141.4%)(70.7\%, 141.4\%)

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

And with a reasonable TFeeTFee .
Let ΔB=ΓB\Delta_B = \Gamma_B , TFee=1%TFee = 1\% .

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

The proposed approach recommends:

  1. swapping xΓB=(0.9950.99)2+ΔB0.99ΓB0.9950.99   2.021.005   41.63%\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\% of AssetB for 29.2%29.2\% of AssetA first

  2. then depositing (29.2%,58.4%)(29.2\%, 58.4\%) into the pool with (70.8%,141.6%)(70.8\%, 141.6\%)

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

In summary, with a reasonable TFeeTFee , 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 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().

        // 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);
Enter fullscreen mode Exit fullscreen mode

SafeMoon's implementation of swapAndLiquify()

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

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:

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
Enter fullscreen mode Exit fullscreen mode

Uniswap’s declaration of addLiquidity().

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

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" 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.

Top comments (0)