I'm now playing around with this new basic blog template from Next.js examples --> available on Next.js GitHub.

Luciano Lupo Notes.

Challenged myself to audit something I have no clue of...

Cover Image for Challenged myself to audit something I have no clue of...

1. Initial Setup

First went to https://github.com/XRPLF/rippled and followed the steps to set up the dev environment, after the install of all the things needed I ran the tests to make sure everything was working fine. In that process I had troubles with Conan, first installed v2, but stuff was not working so I went to v1 and it worked like a charm, also had to learn a bit about profile configs and profile file for MacOS(M1 Pro chip).

After setting up the dev environment, I pulled the XLS-0033d branch and started looking at the codebase.

2. Start the research

Went thru all the files that had changes, wihout looking at the description first ( that was on porpouse to have a clean read of the code without context, so I see how helpfull are the comments and readability to understand about something I've no idea of ).

In real life scenario all the 90 files in the PR should be in the scope, for this test case I took the ones with more changes, and the ones that seem more relevant, checked also the .h files for each but no gonna put it on the report:

# Scope
src/xrpld/app/tx/detail/MPTokenAuthorize.cpp
src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp
src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp
src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp
src/libxrpl/basics/MPTAmount.cpp
src/libxrpl/protocol/Asset.cpp
src/libxrpl/protocol/Issue.cpp
src/libxrpl/protocol/MPTIssue.cpp
src/libxrpl/protocol/STAmount.cpp
src/xrpld/app/tx/detail/Payment.cpp
src/xrpld/app/tx/detail/Clawback.cpp

src/libxrpl/protocol/STInteger.cpp
src/libxrpl/protocol/STObject.cpp
src/libxrpl/protocol/STParsedJSON.cpp
src/libxrpl/protocol/STTx.cpp
src/xrpld/app/ledger/detail/LedgerToJson.cpp
src/xrpld/app/misc/NetworkOPs.cpp
src/xrpld/app/tx/detail/InvariantCheck.cpp
src/xrpld/app/tx/detail/SetTrust.cpp
src/xrpld/app/tx/detail/applySteps.cpp
src/xrpld/ledger/detail/View.cpp
src/xrpld/rpc/detail/MPTokenIssuanceID.cpp

After going through all the files I went back to the PR description and started reading it and making a list of questions for myself:

  • what is XRP ledger?
  • how it works?
  • where is the state of the data saved? (all the new state that will be saved in the next block)
  • had hacks in the past?
  • how consensus works in Ripple ecosystem? can sandwitch attacks happen here?
  • what are trust lines, how they work ?
  • what are trust lines and rippling in XRP Ledger?
  • why use this instead of trust lines ?
  • the MPTs idea is to be like an ERC20 ?
  • how similar is to the ERC20 properties ( decimals, name, sybmol, all that...)?
  • how is related to NFTs in ripple ecosystem, has some similarities?
  • how math and precision loss gets involved in the feature?
  • what IOUs are?
  • why MPTs will represent a smaller overall balance amount as compared to trust lines? can that lead to errors between MPTs and CBDCs swaps?
  • what are tickets in XRP Ledger?
  • what are Transaction Common Fields?

To answer that I went to links inside the PR, official docs, links mentioned on email with the test, and other resources.

  • https://www.vance.tech/blog/ripple-payment-protocol
  • https://xrpl.org/docs/concepts
  • https://xrpl.org/docs/concepts/tokens/fungible-tokens#trust-lines
  • https://github.com/XRPLF/rippled/blob/develop/docs/consensus.md
  • https://github.com/XRPLF/rippled/blob/develop/docs/CodingStyle.md
  • https://ripple.com/insights/the-building-blocks-of-institutional-defi-on-xrp-ledger/
  • https://medium.com/ripplexdev/highlighting-the-ripplex-bug-bounty-program-545ea787f900
  • https://blog.multichainmedia.xyz/index.php/2024/07/18/multi-purpose-tokens-mpt-on-xrpl/
  • https://www.technologyreview.com/2017/06/16/151164/first-large-scale-analysis-of-the-ripple-cryptocurrency-network/
  • https://hacken.io/insights/ripple-hack-explained/
  • https://lunu.io/news/urgent-alert-uncover-the-ripple-xrp-hack-protect-your-crypto-now/
  • https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0033d-multi-purpose-tokens
  • https://xrpl.org/docs/references/protocol/transactions/common-fields
  • https://xrpintel.com/source
  • https://blog.multichainmedia.xyz/index.php/2024/07/18/multi-purpose-tokens-mpt-on-xrpl/
  • https://ripple.com/insights/the-building-blocks-of-institutional-defi-on-xrp-ledger/
  • https://xrpl.org/docs/concepts/consensus-protocol/consensus-protections
  • https://www.analyticsinsight.net/latest-news/xrp-security-challenges-and-best-practices
  • https://engineering.ripple.com/a-developers-guide-to-payment-engine-system-design/
  • https://dev.to/ripplexdev/xrp-ledger-amm-bug-fix-now-integrated-a-detailed-analysis-4915
  • https://www.crypto-news-flash.com/ethereums-evm-vs-xrpl-insider-sparks-debate-threatens-ripples-protocol/
  • https://medium.com/evmos/ripple-and-peersyst-utilize-evmos-to-enable-evm-and-cosmos-interoperability-3e1b17731d08
  • https://cointelegraph.com/news/ripple-xrp-ledger-smart-contracts-evm-sidechain
  • https://medium.com/@fuzz_ai/improving-ripple-unit-test-coverage-with-fuzzing-5292030a3ce0
  • https://www.researchgate.net/publication/371013371_A_Ripple_for_Change_Analysis_of_Frontrunning_in_the_XRP_Ledger

What is XRP ledger? how it works?

The idea is that while we might be unwilling to make a deal with somebody we do not know, we might instead be willing to make a deal a friend, who makes a deal with their friend, and so on until the funds reach the intended recipient. It turns out that just such a financial network already exists in the form of Ripple. This is a payments network in which users make connections to other users they trust and agree to allow the transfer of funds between them. from: https://www.technologyreview.com/2017/06/16/151164/first-large-scale-analysis-of-the-ripple-cryptocurrency-network/

had hacks in the past?

Not much....

What is a Trust Line ?

Each "trust line" is a bidirectional relationship consisting of: The identifiers for the two accounts that the trust line connects. A single, shared balance, which is positive from the perspective of one account and negative from the other perspective.

Why use this instead of trust lines ?

For clarity, trust lines were invented primarily to service the idea of "community credit" and also to enhance liquidity on the ledger by making the same types of currency fungible amongst differing issuers (see rippling for an example of each). MPTs, on the other hand, have three primary design motivations that are subtly different from trust lines: (1) to enable tokenization using as little space (in bytes) on ledger as possible; (2) to eliminate floating point numbers and floating point math from the tokenization primitive; and (3) to make payment implementation simpler by, for example, removing rippling and allowing MPT usage in places like escrows or payment channels in more natural ways.

The MPTs idea is to be like an ERC20 ?

Multi-Purpose Tokens (MPTs) bridge the gap between fungible and non-fungible tokens and share parallels with ERC-1155 tokens on Ethereum. They are akin to “semi-fungible” tokens whereby key associated metadata can be attached. This provides them with more flexibility than fungible tokens, while they’re not truly unique such as with NFTs.

https://blog.multichainmedia.xyz/index.php/2024/07/18/multi-purpose-tokens-mpt-on-xrpl/

You can think of MPT as a ‘best of both worlds’ token standard, somewhere between fungible and non-fungible tokens. Fungible tokens, as they exist on XRPL today.

https://ripple.com/insights/the-building-blocks-of-institutional-defi-on-xrp-ledger/

How math and precision loss gets involved in the feature?

Uses a fixed-point balance representation instead of a floating-point representation

3. Repeat this iterative process of Q&A with myself

Went back to the codebase and started with all the context and made a first batch of questions. questions will be marked with a //q prefix:

on file MPTokenAuthorize.cpp

        std::uint32_t const uOwnerCount = sleAcct->getFieldU32(sfOwnerCount);
        XRPAmount const reserveCreate(
            (uOwnerCount < 2) ? XRPAmount(beast::zero)
             //q what happens if uOwnerCount is very large? possible overflow due to the +1?
                              : view.fees().accountReserve(uOwnerCount + 1));

accountReserve takes a <size_t> as input and owner count is Uint32

https://en.cppreference.com/w/cpp/types/numeric_limits/max https://www.reddit.com/r/cpp_questions/comments/jetu17/what_is_difference_between_size_t_unsigned_int/

LLM deepseek-coder-v2 says this about uint32:

In C++, size_t and uint32_t are both unsigned integer types, but they have different ranges. size_t is typically defined as an unsigned integer type that is at least as large as the largest object on the target platform. The exact size can vary depending on the system architecture (e.g., 32-bit or 64-bit). On a 32-bit system, size_t is usually 32 bits, while on a 64-bit system, it is usually 64 bits.

uint32_t is an unsigned integer type that is guaranteed to be at least 32 bits wide.

so if uOwnerCount = std::numeric_limits<uint32_t>::max() and the system is 32-bit the +1 will overflow, but seems a little unlikely and not worth to persue further for now (not gonna add make a test nor PoC to try it), but can be a good informational, "Validate max of uOwnerCount".


           return tecDIR_FULL;

        auto mptoken = std::make_shared<SLE>(mptokenKey);
        //q what make_shared does?
        (*mptoken)[sfAccount] = args.account;
        (*mptoken)[sfMPTokenIssuanceID] = args.mptIssuanceID;
        (*mptoken)[sfFlags] = 0;
        (*mptoken)[sfOwnerNode] = *ownerNode;
        view.insert(mptoken);

https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared https://stackoverflow.com/questions/20895648/difference-in-make-shared-and-normal-shared-ptr-in-c


    if (flagsIn != flagsOut)
        sleMpt->setFieldU32(sfFlags, flagsOut);

    view.update(sleMpt);
    //q what happens if update fails?
    return tesSUCCESS;

nothing happens....


        std::shared_ptr<SLE const> sleMpt = ctx.view.read(
            keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], accountID));

        // There is an edge case where holder deletes MPT after issuance has
        // already been destroyed. So we must check for unauthorize before
        // fetching the MPTIssuance object(since it doesn't exist)
//q check if this edge case is cover in tests
        // if holder wants to delete/unauthorize a mpt
        if (ctx.tx.getFlags() & tfMPTUnauthorize)
        {
            if (!sleMpt)

here is the test:

            {
                // alice pays bob 100 tokens
                mptAlice.pay(alice, bob, 100);

                // bob tries to delete his MPToken, but fails since he still
                // holds tokens
                mptAlice.authorize(
                    {.account = &bob,
                     .flags = tfMPTUnauthorize,
                     .err = tecHAS_OBLIGATIONS});

                // bob pays back alice 100 tokens
                mptAli

on file MPTokenIssuanceCreate.cpp

//q what means TEC and NotTEC?
NotTEC
MPTokenIssuanceCreate::preflight(PreflightContext const& ctx)
{
    if (auto const maxAmt = ctx.tx[~sfMaximumAmount])
     //q is this part tested?
    {
        if (maxAmt == 0)
            return temMALFORMED;

        if (maxAmt > maxMPTokenAmount)
            return temMALFORMED;
    }
    //q what is preflight?
    return preflight2(ctx);

tec codes data: https://xrpl.org/docs/references/protocol/transactions/transaction-results/tec-codes#tec-codes


        view.insert(mptIssuance);
    }

    // Update owner count.
    //q what happens if adjustOwnerCount fails?, how can it fail?
    adjustOwnerCount(view, acct, 1, journal);

    return tesSUCCESS;

on file MPTokenIssuanceDestroy.cpp

    view().erase(mpt);
//q what happens if peek() fails?
    adjustOwnerCount(view(), view().peek(keylet::account(issuer)), -1, j_);

    return tesSUCCESS;
}

on file View.cpp

requireAuth(ReadView const& view, MPTIssue const& mpt, AccountID const& account)
{
    auto const mptID = keylet::mptIssuance(mpt.getMptID());
    if (auto const sle = view.read(mptID);
        sle && sle->getFieldU32(sfFlags) & lsfMPTRequireAuth)
    {
        auto const mptokenID = keylet::mptoken(mptID.key, account);
        if (auto const tokSle = view.read(mptokenID); tokSle &&
//q what about this commented code? @audit informational
            //(sle->getFlags() & lsfMPTRequireAuth) &&
            !(tokSle->getFlags() & lsfMPTAuthorized))
            return TER{tecNO_AUTH};
    }
    return tesSUCCESS;
}

//q check this math properly
            if (sle->getFieldU64(sfOutstandingAmount) + saAmount.mpt().value() >
                (*sle)[~sfMaximumAmount].value_or(maxMPTokenAmount))
                return tecMPT_MAX_AMOUNT_EXCEEDED;
        }
isGlobalFrozen(ReadView const& view, MPTIssue const& mpt)
    //q shouldn't this have different names? name is duplicated

isIndividualFrozen(
    //q shouldn't this have different names? name is duplicated

isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mpt)
    //q shouldn't this have different names? name is duplicated

checking tests

went thru all the test to understand what the guy tested, ( if it was only happy paths or more deep stuff ) and code coverage and branching coverage to see the %

        // MPTokenIssuanceCreate
        testCreateValidation(all);
        testCreateEnabled(all);

        // MPTokenIssuanceDestroy
        testDestroyValidation(all);
        testDestroyEnabled(all);

        // MPTokenAuthorize
        testAuthorizeValidation(all);
        testAuthorizeEnabled(all);

        // MPTokenIssuanceSet
        testSetValidation(all);
        testSetEnabled(all);

        // MPT clawback
        testClawbackValidation(all);
        testClawback(all);

        // Test Direct Payment
        testPayment(all);

        // Test MPT Amount is invalid in Tx, which don't support MPT
        testMPTInvalidInTx(all);

        // Test parsed MPTokenIssuanceID in API response metadata
        testTxJsonMetaFields(all);

If we compare this with NFT feature, there is a clear lack of tests....

4. Now I start thinking about Attack vectors

4.1 lookout for previous audits

Looked up for previous audits, only found the Certik one about the AMM so checked if some of that apply to MPTs waste some time looking of projects/startups on the XPR ecosystem, looking for audits... found nothing.

4.2 checking math first look

Another pass on the code but this time looking for all the math and logic that could be exploited just because is math...

  • Divided by Zero:

    • NOT FOUND
  • substractions that can underflow

    • NOT FOUND
  • overflow in math operations (add, sub, mul)

    • NOT FOUND
  • DOS on FOR loops ?

    • NOT FOUND
  • Downcasting that will make overflows

    • NOT FOUND

4.2 checking math deeper/detailed look

Not gonna go with this step for this challenge/test but in a real life scenario definelty should be done.

4.3 checking possible attack vector already known for ERC20s and ERC1155s

with the Q&A in mind I started think about possible attack vectors and edge cases, also tried to understand if there was any parallelism with EVM common attacks because I'm familiar with those more than ripple ( I knew nothing about ripple before this report tbh ), and the idea of MPTs have some similarities with ERC20s and ERC1155s.

FrontRunning

according to this: researchGate FrontRunning XRP

"Frontrunning is a well-studied and understood issue on Ethereum. However, little effort has been made to analyze whether other blockchains are potentially vulnerable to theseattacks. In this paper, we conduct the first frontrunning analysison the XRP Ledger. We show that the XRP Ledger is vulnera-ble to frontrunning despite efforts to make frontrunning moredifficult by introducing a pseudo-random transaction order."

so all the EVM attacks that involve front runs should be thought about as a potential attack vector in this context.

Mitigations:

  • Implementing a delay mechanism that prevents transactions from being mined too quickly after another transaction by the same sender. This can be done using a minimum time between blocks or a similar approach BUT also this will open all the timleock related attack vectors....

Reentrancy Do not see how reentrancy can be applied here.

4.4 feed LLMs with the files

I have a server at home running local and open source LLMs so the code wont be sending to anyone ( anyway PR is open but still... ) so is worth to try to get some insights from the LLMs but to be honest I didn't found much on this step for other projects in the past...

4.5 Fuzzing

I know how to do fuzzing with Foundry and echidna but have no idea how rippled repository conduct fuzzing test so first of all I research on how fuzzing can be done in rippled ( probably copy fuzz test from NFTs or other feature if it has )

Finally I didn't conducted any Fuzz tests but all my knowledge about Fuzzing is from this courses ( so it is not much anyway...):

Cyfrin Updraft (Patrick Collins courses):

https://updraft.cyfrin.io/courses/security

https://updraft.cyfrin.io/courses/foundry

https://www.youtube.com/watch?v=IZTvXfC14Ig

https://www.youtube.com/watch?v=83q14K-WNKM

https://blog.sigmaprime.io/forge-testing-leveling.html

https://blog.softbinator.com/set-up-echidna-smart-contract-fuzzing-tool/

4.6 Look for informationals and code optimizations

skip this step to because I need a deep understand of C++ and it features to really make this step effective.

5. Get away form the code for a day o two, and then give it a last pass

this is important because you might find bugs or inefficiencies in your code when you come back after a break.

6. Write the Report

look out for latest audit reports conducted by softstack to copy the structure of their report write the report with:

  • feature summary
  • security issues found
  • informationals and optimizations
  • mitigations
  • recomendations

after that will pass the report thru an LLM to check spelling and grammar, and also make it more presentable/readable.