Managing Deterministic State with Fixed-Size Arrays in Vyper
Learn how to use fixed-size arrays in Vyper to prevent unbounded gas consumption and ensure deterministic memory usage in smart contracts.
03 Mar 2026, 12:05 UTC

The Problem: Unbounded Gas and State Bloat
In smart contract development, dynamic arrays—which can grow indefinitely—introduce a critical risk: the "Out of Gas" (OOG) error. If a contract iterates over a list that grows too large, the transaction cost may eventually exceed the block gas limit, effectively freezing the contract's functionality. This is often referred to as a Denial of Service (DoS) vulnerability.
Vyper solves this by enforcing fixed-size arrays. By requiring the array size to be defined as a constant at compile time, Vyper ensures that gas costs for iteration and memory allocation are predictable and deterministic from the moment of deployment.
Prerequisites
- Vyper compiler installed (version 0.3.x or later).
- A basic understanding of Ethereum storage slots (SSTORE/SLOAD).
- A local development environment (such as ApeWorX or Brownie) for deployment and testing.
Implementing Fixed-Size Arrays
To implement a bounded list in Vyper, you must declare the array with a constant integer. You cannot use a variable or a function return value to define the size of an array.
# Example: A bounded voting registry
# The size must be a literal constant
MAX_VOTERS: constant(uint256) = 100
voters: public(HashMap[uint256, address])
voter_list: public(address[MAX_VOTERS])
current_voter_count: public(uint256)
@external
def add_voter(voter_address: address):
# Manual bounds check to prevent overflow of the fixed array
assert self.current_voter_count < MAX_VOTERS, "Registry is full"
# Assign the address to the specific index
self.voter_list[self.current_voter_count] = voter_address
self.current_voter_count += 1Engineering Decisions: Storage vs. Memory
Where you declare your array significantly impacts the gas cost of your transactions. Vyper distinguishes between storage (permanent on-chain state) and memory (temporary execution state).
| Feature | Storage Array | Memory Array |
|---|---|---|
| Persistence | Persists between transactions | Wiped after execution |
| Gas Cost | High (SSTORE/SLOAD) | Low (MLOAD/MSTORE) |
| Use Case | User registries, configuration | Temporary calculations, sorting |
Handling Bounds and Runtime Safety
Vyper provides built-in runtime bounds checking. If your code attempts to access an index that is outside the declared size (e.g., accessing index 101 in an array of 100), the EVM will trigger a revert. However, relying solely on the runtime revert is often insufficient for a clean user experience; manual assert statements should be used to provide descriptive error messages.
Verification and Testing
To verify that your fixed-size array is behaving as expected, perform the following checks:
- Compilation Check: Attempt to declare an array using a non-constant variable (e.g.,
my_array: public(uint256[some_variable])). The Vyper compiler should reject this with a compilation error. - Boundary Test: Call the
add_voterfunction in the example above 100 times. On the 101st call, verify that the transaction reverts with the message "Registry is full". - Bytecode Analysis: Compare the bytecode of a contract with a small array versus a very large array. You will notice that larger arrays increase the deployment cost and the overall contract size.
Limitations and Risks
- Deployment Limits: Extremely large fixed-size arrays can cause the contract to exceed the maximum contract size limit (24KB), leading to deployment failure.
- Initialization Costs: Initializing a very large array in a single transaction can lead to an Out of Gas error during the constructor phase.
- No Dynamic Resizing: Once deployed, you cannot "push" or "pop" elements to change the array's length. You must manage a separate counter (like
current_voter_count) to track the active elements within the fixed bounds.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.