# How to hack (almost) any Starknet Cairo smart contract

By [Gershon Ballas - Ginger Security](https://paragraph.com/@gershon-ballas-ginger-security) · 2022-11-11

---

**_Disclaimer:_**\* This is not a hack in the underlying Starknet or Cairo architectures. Instead, it is a very common developer-introduced vulnerability that we’ve been seeing (and disclosing) in the wild. If you’re a Cairo developer — please read and understand this issue to prevent potential hacks in your dapps.\*

All of us around crypto know and love Starknet by Starkware… Vitalik himself seems to be a fan:

[https://twitter.com/VitalikButerin/status/1578472631002505217?ref\_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1578472631002505217%7Ctwgr%5Ee171fc4a5c52cd3a7a745da117d309c70039f19b%7Ctwcon%5Es1\_&ref\_url=https%3A%2F%2Fcdn.embedly.com%2Fwidgets%2Fmedia.html%3Ftype%3Dtext2Fhtmlkey%3Da19fcc184b9711e1b4764040d3dc5c07schema%3Dtwitterurl%3Dhttps3A%2F%2Ftwitter.com%2Fvitalikbuterin%2Fstatus%2F1578472631002505217image%3Dhttps3A%2F%2Fi.embed.ly%2F1%2Fimage3Furl3Dhttps253A252F252Fabs.twimg.com252Ferrors252Flogo46x38.png26key3Da19fcc184b9711e1b4764040d3dc5c07](https://twitter.com/VitalikButerin/status/1578472631002505217?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1578472631002505217%7Ctwgr%5Ee171fc4a5c52cd3a7a745da117d309c70039f19b%7Ctwcon%5Es1_&ref_url=https%3A%2F%2Fcdn.embedly.com%2Fwidgets%2Fmedia.html%3Ftype%3Dtext2Fhtmlkey%3Da19fcc184b9711e1b4764040d3dc5c07schema%3Dtwitterurl%3Dhttps3A%2F%2Ftwitter.com%2Fvitalikbuterin%2Fstatus%2F1578472631002505217image%3Dhttps3A%2F%2Fi.embed.ly%2F1%2Fimage3Furl3Dhttps253A252F252Fabs.twimg.com252Ferrors252Flogo46x38.png26key3Da19fcc184b9711e1b4764040d3dc5c07)

And it’s not for nothing. The Starkware team is absolute god-tier when it comes to ZK-scalability moon math. It’s the main reason their company has [recently become an $8 bln behemoth](https://www.reuters.com/markets/wealth/blockchain-tech-firm-starkware-raises-100-mln-valued-8-bln-2022-05-25) and is the leading [ZK-L2 technology by TVL](https://l2beat.com/scaling/tvl/) (dYdX and Immutable X both run using Starkware tech).

However…………. when it comes to Cairo, the language used for writing Starknet contracts…. the Starkware team made a huge flop. And I mean HUGE.

Today kids, we’re gonna learn about how pretty much any Cairo contract is hackable (potentially).

[Subscribe](null)

The devil in the details - The Uint256 struct
---------------------------------------------

**Solidity** has a number of native types developers can use:

*   **bool**
    
*   **uint8** to **uint256**
    
*   **address**
    
*   **bytes1** to **bytes32**
    
*   **mapping**
    

And a couple of others…

**Cairo**, on the other hand, has exactly one type — **felt**.

The **felt** is a 252-bit type that can be used for anything — signed/unsigned ints, booleans, addresses, and even CairoVM bytecode.

Why 252 bits? The reason for that has to do with Starkware STARK moon math… you can read more about it [here](https://www.cairo-lang.org/docs/hello_cairo/intro.html#field-element).

So if you want to…

*   represent a **bool** — you use a **felt**
    
*   represent a **uint8** — you use a **felt**
    
*   represent an **address** — you use a **felt**
    
*   you get the point…
    

But what if you want to represent a **uint256**? A **felt** is only 252 bits…

For that you’d need the **Uint256** struct, defined as such ([source](https://github.com/starkware-libs/cairo-lang/blob/0ba3ff59c1c86f2a30adc8fd144eaacb22c48ce9/src/starkware/cairo/common/uint256.cairo)):

    // Represents an integer in the range [0, 2^256).
    struct Uint256 {
        // The low 128 bits of the value.
        low: felt,
        // The high 128 bits of the value.
        high: felt,
    }
    

A **Uint256** struct is composed of two felts — one to represent the **low 128** bits, and another to represent the **high 128** bits.

![Cairo Uint256 struct](https://storage.googleapis.com/papyrus_images/d03d82ca23ca7fd65b8c08a180dca7921460888208eb5eeab65f4cdfab220bdf.png)

Cairo Uint256 struct

So basically every Uint256 has 2\*124 = 248 bits that are completely ignored and never get used.

But are they completely ignored? …. 👀

”Comparison is the thief of joy”
--------------------------------

As you may have already guessed… those junk bits are not ignored.

Let’s look at how **Uint256** comparisons work in Cairo.

Instead of using <, >, and == like a normal language would for its most used int types, Cairo uses these functions instead:

*   **uint256\_lt()** for <
    
*   **uint256\_gt()** for >
    
*   **uint256\_eq()** for ==
    

Now let’s deep dive into the uint256\_lt() func in order to see whether or not it really ignores those junk bits ([source](https://github.com/starkware-libs/cairo-lang/blob/0ba3ff59c1c86f2a30adc8fd144eaacb22c48ce9/src/starkware/cairo/common/uint256.cairo)):

    // Returns 1 if the first unsigned integer is less than the second unsigned integer.
    func uint256_lt{range_check_ptr}(a: Uint256, b: Uint256) -> (res: felt) {
        if (a.high == b.high) {
            return (is_le(a.low + 1, b.low),);
        }
        return (is_le(a.high + 1, b.high),);
    }
    

We can see that it calls the **is\_le()** func (used for comparing felts, [source](https://github.com/starkware-libs/cairo-lang/blob/0ba3ff59c1c86f2a30adc8fd144eaacb22c48ce9/src/starkware/cairo/common/math_cmp.cairo)):

    // Returns 1 if a <= b (or more precisely 0 <= b - a < RANGE_CHECK_BOUND).
    // Returns 0 otherwise.
    @known_ap_change
    func is_le{range_check_ptr}(a, b) -> felt {
        return is_nn(b - a);
    }
    
    // ...
    
    // Returns 1 if a >= 0 (or more precisely 0 <= a < RANGE_CHECK_BOUND).
    // Returns 0 otherwise.
    @known_ap_change
    func is_nn{range_check_ptr}(a) -> felt {
        %{ memory[ap] = 0 if 0 <= (ids.a % PRIME) < range_check_builtin.bound else 1 %}
        jmp out_of_range if [ap] != 0, ap++;
        [range_check_ptr] = a;
        ap += 20;
        let range_check_ptr = range_check_ptr + 1;
        return 1;
     
        out_of_range:
        %{ memory[ap] = 0 if 0 <= ((-ids.a - 1) % PRIME) < range_check_builtin.bound else 1 %}
        jmp need_felt_comparison if [ap] != 0, ap++;
        assert [range_check_ptr] = (-a) - 1;
        ap += 17;
        let range_check_ptr = range_check_ptr + 1;
        return 0;
     
        need_felt_comparison:
        assert_le_felt(RC_BOUND, a);
        return 0;
    }
    

I’ll save you the trouble of understanding how **is\_nn()** works… what you need to know is that **is\_le()** works as described — “Returns 1 if a <= b. Returns 0 otherwise.”

Hint — **is\_le()** looks at the whole 252-bits of the felt, not just the first 128!!! 🤦‍♂️

Okay, okay… let’s backtrack a little
------------------------------------

Let’s say we have two **Uint256** structs, A and B.

A is:

*   A.low = 0
    
*   A.high = 1
    
*   A should represent **0** + **1**\*2¹²⁸ = 2¹²⁸ = 340282366920938463463374607431768211456
    

B is:

*   B.low = 1
    
*   B.high = 1
    
*   B should represent **1** + **1**\*2¹²⁸ = 1+2¹²⁸ = 340282366920938463463374607431768211457
    

(B is greater than A by 1).

Let’s run **uint256\_le(A, B)**:

1.  **a.high == b.high** is TRUE
    
2.  returning **is\_le(a.low + 1, b.low)** evaluates to returning **is\_le(0 + 1, 1)**
    
3.  **is\_le(0 + 1, 1)** returns 1 (meaning TRUE)
    

So **uint256\_le(A, B)** evaluates to TRUE, just as expected (since A < B).

So far so good 😌

But what if we gave the **uint256\_le()** a malformed A? Specifically, an A whose **low felt**’s 129th bit has been turned on.

In this scenario, A\_malformed is:

*   A\_malformed.low = 2¹²⁹
    
*   A\_malformed.high = 1
    
*   A\_malformed should represent **0** + **1\*2**¹²⁸ = 2¹²⁸ = 340282366920938463463374607431768211456
    

B is:

*   B.low = 1
    
*   B.high = 1
    
*   B should represent **1** + **1\*2**¹²⁸ = 1+2¹²⁸ = 340282366920938463463374607431768211457
    

Running **uint256\_le(A\_malformed, B)** should produce the same result as **uint256\_le(A, B)** (the junk bits in A\_malformed should be ignored).

However, in reality:

1.  **a.high == b.high** is TRUE
    
2.  returning **is\_le(a.low + 1, b.low)** evaluates to returning **is\_le(2¹²⁹ + 1, 1)**
    
3.  **is\_le(2¹²⁹ + 1, 1)** returns 0 (meaning FALSE)
    

It’s not the result we’d expect… 😨

Of course, the same trick (writing to the junk bits to f\*ck up Uint256 comparisons) can be achieved by writing to the junk bits of the **high felt**.

So the Starkware team really flooked that one… Let’s see how we can exploit it.

Exploitation
------------

The **uint256\_le()** func is not the only one that is vulnerable to malformed inputs. Here is a partial list of other vulnerable functions:

**uint256\_add()**

*   will not produce malformed output
    
*   output can be made to be much higher than it should be
    

**uint256\_mul()**

*   output can be made to be much higher than it should be
    
*   may produce malformed output as well
    

**uint256\_sub()**

*   like **uint256\_add()**
    

**uint256\_lt()**

*   result can be chosen either way via malformed input (as we’ve shown above)
    

**uint256\_eq()**

*   func can be made to return FALSE even though inputs are equal
    

The savvy among you can already imagine how this may be used to $$$exploit$$$. But if you wanna test your skills, check out the [**cairo-auction**](https://github.com/paradigmxyz/paradigm-ctf-2022/blob/main/cairo-auction/public/contracts/auction.cairo) challenge from the [2022 Paradigm CTF challenge](https://github.com/paradigmxyz/paradigm-ctf-2022). It may be solved using your newly-acquired knowledge.

Mitigation and vulnerabilities in the wild
------------------------------------------

Preventing malformed Uint256 vulnerabilities is fairly simple. Simply call the provided **uint256\_check()** func on all Uint256 inputs and you’ll be fine.

Do Cairo devs actually do that? Mostly no.

We have not seen the **uint256\_check()** func called on inputs in 100% of the contracts that we were tasked with auditing so far. In some of them, this has led to **medium-severity vulnerabilities** (we will not disclose project names since some of them are still being patched).

And I wouldn’t blame the devs for that. **This Uint256 malformation issue cannot be found anywhere in the Cairo docs.**

With our eyes on the future
---------------------------

Luckily for us and for the Starkware team, Cairo 1.0 fixes all of that. In Cairo 1.0, Uint256 becomes a native type:

[https://medium.com/starkware/cairo-1-0-aa96eefb19a0](https://medium.com/starkware/cairo-1-0-aa96eefb19a0)

(I’m assuming this means the Uint256 malformation check will be built in, but you never know… 😅)

[Subscribe](null)

---

*Originally published on [Gershon Ballas - Ginger Security](https://paragraph.com/@gershon-ballas-ginger-security/how-to-hack-almost-any-starknet-cairo-smart-contract)*
