Vyper has incorrect storage layout for contracts containing large arrays
Description
Vyper is a Pythonic Smart Contract Language for the Ethereum Virtual Machine (EVM). Contracts containing large arrays might underallocate the number of slots they need by 1. Prior to v0.3.8, the calculation to determine how many slots a storage variable needed used math.ceil(type_.size_in_bytes / 32). The intermediate floating point step can produce a rounding error if there are enough bits set in the IEEE-754 mantissa. Roughly speaking, if type_.size_in_bytes is large (> 2**46), and slightly less than a power of 2, the calculation can overestimate how many slots are needed by 1. If type_.size_in_bytes is slightly more than a power of 2, the calculation can underestimate how many slots are needed by 1. This issue is patched in version 0.3.8.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Vyper smart contract storage slot calculation using floating-point math.ceil can round incorrectly for large arrays, causing underallocation and potential storage collision.
The vulnerability resides in how Vyper calculates the number of storage slots required for contract variables. The original code used math.ceil(type_.size_in_bytes / 32), which can produce rounding errors due to IEEE-754 floating point representation for large sizes (greater than 2^46 bytes) [1]. When the size is slightly less than a power of two, the calculation overestimates needed slots; when slightly more, it underestimates by one slot [2].
Exploitation requires a Vyper contract containing a storage variable with a very large type (e.g., large arrays or structs exceeding 2^46 bytes). No special authentication or network position is needed beyond deploying such a contract. The rounding error occurs at compile time when the storage layout is computed, leading to incorrect slot allocation [1].
An attacker leveraging this bug could cause two variables to be assigned overlapping storage slots, since the underallocation leaves one slot claimed but not reserved. This can lead to data corruption, unexpected behavior, or exploitation of the contract's state [2].
The fix was implemented in Vyper version 0.3.8 by using a new storage_size_in_words method that avoids floating-point arithmetic [1]. Developers should upgrade to v0.3.8 or later.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
vyperPyPI | < 0.3.8 | 0.3.8 |
Affected products
1Patches
10bb7203b584eMerge pull request from GHSA-mgv8-gggw-mrg6
5 files changed · +75 −17
docs/types.rst+7 −0 modified@@ -520,6 +520,9 @@ A two dimensional list can be declared with ``_name: _ValueType[inner_size][oute # Returning the value in row 0 column 4 (in this case 14) return exampleList2D[0][4] +.. note:: + Defining an array in storage whose size is significantly larger than ``2**64`` can result in security vulnerabilities due to risk of overflow. + .. index:: !dynarrays Dynamic Arrays @@ -561,6 +564,10 @@ Dynamic arrays represent bounded arrays whose length can be modified at runtime, In the ABI, they are represented as ``_Type[]``. For instance, ``DynArray[int128, 3]`` gets represented as ``int128[]``, and ``DynArray[DynArray[int128, 3], 3]`` gets represented as ``int128[][]``. +.. note:: + Defining a dynamic array in storage whose size is significantly larger than ``2**64`` can result in security vulnerabilities due to risk of overflow. + + .. _types-struct: Structs
tests/cli/outputs/test_storage_layout_overrides.py+15 −0 modified@@ -95,6 +95,21 @@ def test_simple_collision(): ) +def test_overflow(): + code = """ +x: uint256[2] + """ + + storage_layout_override = {"x": {"slot": 2**256 - 1, "type": "uint256[2]"}} + + with pytest.raises( + StorageLayoutException, match=f"Invalid storage slot for var x, out of bounds: {2**256}\n" + ): + compile_code( + code, output_formats=["layout"], storage_layout_override=storage_layout_override + ) + + def test_incomplete_overrides(): code = """ name: public(String[64])
tests/functional/test_storage_slots.py+16 −0 modified@@ -1,3 +1,7 @@ +import pytest + +from vyper.exceptions import StorageLayoutException + code = """ struct StructOne: @@ -97,3 +101,15 @@ def test_reentrancy_lock(get_contract): assert [c.foo(0, i) for i in range(3)] == [987, 654, 321] assert [c.foo(1, i) for i in range(3)] == [123, 456, 789] assert c.h(0) == 123456789 + + +def test_allocator_overflow(get_contract): + code = """ +x: uint256 +y: uint256[max_value(uint256)] + """ + with pytest.raises( + StorageLayoutException, + match=f"Invalid storage slot for var y, tried to allocate slots 1 through {2**256}\n", + ): + get_contract(code)
vyper/semantics/analysis/data_positions.py+34 −17 modified@@ -6,6 +6,7 @@ from vyper.exceptions import StorageLayoutException from vyper.semantics.analysis.base import CodeOffset, StorageSlot from vyper.typing import StorageLayout +from vyper.utils import ceil32 def set_data_positions( @@ -121,8 +122,7 @@ def set_storage_slots_with_overrides( # Expect to find this variable within the storage layout overrides if node.target.id in storage_layout_overrides: var_slot = storage_layout_overrides[node.target.id]["slot"] - # Calculate how many storage slots are required - storage_length = math.ceil(varinfo.typ.size_in_bytes / 32) + storage_length = varinfo.typ.storage_size_in_words # Ensure that all required storage slots are reserved, and prevents other variables # from using these slots reserved_slots.reserve_slot_range(var_slot, storage_length, node.target.id) @@ -139,14 +139,29 @@ def set_storage_slots_with_overrides( return ret +class SimpleStorageAllocator: + def __init__(self, starting_slot: int = 0): + self._slot = starting_slot + + def allocate_slot(self, n, var_name): + ret = self._slot + if self._slot + n >= 2**256: + raise StorageLayoutException( + f"Invalid storage slot for var {var_name}, tried to allocate" + f" slots {self._slot} through {self._slot + n}" + ) + self._slot += n + return ret + + def set_storage_slots(vyper_module: vy_ast.Module) -> StorageLayout: """ Parse module-level Vyper AST to calculate the layout of storage variables. Returns the layout as a dict of variable name -> variable info """ # Allocate storage slots from 0 # note storage is word-addressable, not byte-addressable - storage_slot = 0 + allocator = SimpleStorageAllocator() ret: Dict[str, Dict] = {} @@ -165,36 +180,38 @@ def set_storage_slots(vyper_module: vy_ast.Module) -> StorageLayout: type_.set_reentrancy_key_position(StorageSlot(_slot)) continue - type_.set_reentrancy_key_position(StorageSlot(storage_slot)) + # TODO use one byte - or bit - per reentrancy key + # requires either an extra SLOAD or caching the value of the + # location in memory at entrance + slot = allocator.allocate_slot(1, variable_name) + + type_.set_reentrancy_key_position(StorageSlot(slot)) # TODO this could have better typing but leave it untyped until # we nail down the format better - ret[variable_name] = {"type": "nonreentrant lock", "slot": storage_slot} + ret[variable_name] = {"type": "nonreentrant lock", "slot": slot} - # TODO use one byte - or bit - per reentrancy key - # requires either an extra SLOAD or caching the value of the - # location in memory at entrance - storage_slot += 1 for node in vyper_module.get_children(vy_ast.VariableDecl): # skip non-storage variables if node.is_constant or node.is_immutable: continue varinfo = node.target._metadata["varinfo"] - varinfo.set_position(StorageSlot(storage_slot)) - type_ = varinfo.typ - # this could have better typing but leave it untyped until - # we understand the use case better - ret[node.target.id] = {"type": str(type_), "slot": storage_slot} - # CMC 2021-07-23 note that HashMaps get assigned a slot here. # I'm not sure if it's safe to avoid allocating that slot # for HashMaps because downstream code might use the slot # ID as a salt. - storage_slot += math.ceil(type_.size_in_bytes / 32) + n_slots = type_.storage_size_in_words + slot = allocator.allocate_slot(n_slots, node.target.id) + + varinfo.set_position(StorageSlot(slot)) + + # this could have better typing but leave it untyped until + # we understand the use case better + ret[node.target.id] = {"type": str(type_), "slot": slot} return ret @@ -216,7 +233,7 @@ def set_code_offsets(vyper_module: vy_ast.Module) -> Dict: type_ = varinfo.typ varinfo.set_position(CodeOffset(offset)) - len_ = math.ceil(type_.size_in_bytes / 32) * 32 + len_ = ceil32(type_.size_in_bytes) # this could have better typing but leave it untyped until # we understand the use case better
vyper/semantics/types/subscriptable.py+3 −0 modified@@ -103,6 +103,9 @@ def __init__(self, value_type: VyperType, length: int): if not 0 < length < 2**256: raise InvalidType("Array length is invalid") + if length >= 2**64: + warnings.warn("Use of large arrays can be unsafe!") + super().__init__(UINT256_T, value_type) self.length = length
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-6m97-7527-mh74ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-46247ghsaADVISORY
- github.com/pypa/advisory-database/tree/main/vulns/vyper/PYSEC-2023-307.yamlghsaWEB
- github.com/vyperlang/vyper/blob/6020b8bbf66b062d299d87bc7e4eddc4c9d1c157/vyper/semantics/validation/data_positions.pyghsax_refsource_MISCWEB
- github.com/vyperlang/vyper/commit/0bb7203b584e771b23536ba065a6efda457161bbghsax_refsource_MISCWEB
- github.com/vyperlang/vyper/security/advisories/GHSA-6m97-7527-mh74ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.