Version information
- Vyper:
0.5.0b2+commit.5764199a, master at 5764199a9ab64e13bc64d8f41371378db4fa6ab0 (checked September 5, 2026).
- Also observed at the port's pinned compiler,
8af5e83c10af4f065cc660fe20293dac11fdff83. The intervening commit changes only .github/workflows/codeql.yml.
- Python 3.13.14; macOS 26.6.2, arm64.
- Venom (
--experimental-codegen), Cancun; bytecode metadata disabled.
Observed behavior
Changing a bound in an external function that is never called changes the cost of calling a separate length(Bytes[INF]) function with four bytes from roughly 2,100 gas to 34 million gas.
The complete Vyper contract is:
# pragma version 0.5.0b2
BOUND: constant(uint256) = 32
@external
@pure
def echo(values: DynArray[Bytes[BOUND], 64]) -> DynArray[Bytes[BOUND], 64]:
return values
@external
@pure
def length(data: Bytes[INF]) -> uint256:
return len(data)
Compile once with BOUND = 32 and once with BOUND = 32768. In both cases, deploy a fresh contract and call only length(hex"01020304"); do not call echo.
Fresh measurements against the master commit above:
Bound in unused echo function |
O2 call gas |
O3 call gas |
| 32 |
2,126 |
2,132 |
| 32,768 |
34,050,123 |
34,050,129 |
All four calls return 4. These numbers are the gasleft() difference around the call, including the identical Solidity caller overhead; deployment is outside the measurement. Foundry's whole-test gas number is different and should not be substituted for the logged call gas.
Reproduction
git clone https://github.com/aave/aave-v4.git aave-v4-vyper-feedback
cd aave-v4-vyper-feedback
git checkout 1f7129042fe57e24bfc4c934be5088514cb93503
python3.13 -m venv .venv
.venv/bin/python -m pip install 'vyper @ git+https://github.com/vyperlang/vyper.git@5764199a9ab64e13bc64d8f41371378db4fa6ab0'
With Foundry available (measured using Forge 1.8.1 and Solidity 0.8.28):
.venv/bin/python scripts/check_vyper_memory_allocation.py
The self-contained runner compiles both variants with O2/O3, generates a temporary Solidity measurement harness, and executes four local tests. It needs no RPC, deployed protocol, or Aave test fixtures. Small source, large source, and retained pinned-compiler results. The current-master rerun produced exactly the same call-gas values.
Expected behavior / investigation direction
Executing the small function should not incur memory-expansion cost proportional to bounds in an unrelated, unexecuted external function. Investigate whether runtime allocation starts above a contract-wide static-memory maximum rather than the memory needed by the selected entry point. This is a hypothesis about the mechanism; the cross-function cost coupling is directly measured.
This looks related to the allocation design discussed in #4426 and #4856. The report isolates cross-function coupling, rather than the small extra buffer allocations described in #4103 and #4117.
Version information
0.5.0b2+commit.5764199a, master at5764199a9ab64e13bc64d8f41371378db4fa6ab0(checked September 5, 2026).8af5e83c10af4f065cc660fe20293dac11fdff83. The intervening commit changes only.github/workflows/codeql.yml.--experimental-codegen), Cancun; bytecode metadata disabled.Observed behavior
Changing a bound in an external function that is never called changes the cost of calling a separate
length(Bytes[INF])function with four bytes from roughly 2,100 gas to 34 million gas.The complete Vyper contract is:
Compile once with
BOUND = 32and once withBOUND = 32768. In both cases, deploy a fresh contract and call onlylength(hex"01020304"); do not callecho.Fresh measurements against the master commit above:
echofunctionAll four calls return
4. These numbers are thegasleft()difference around the call, including the identical Solidity caller overhead; deployment is outside the measurement. Foundry's whole-test gas number is different and should not be substituted for the logged call gas.Reproduction
With Foundry available (measured using Forge 1.8.1 and Solidity 0.8.28):
The self-contained runner compiles both variants with O2/O3, generates a temporary Solidity measurement harness, and executes four local tests. It needs no RPC, deployed protocol, or Aave test fixtures. Small source, large source, and retained pinned-compiler results. The current-master rerun produced exactly the same call-gas values.
Expected behavior / investigation direction
Executing the small function should not incur memory-expansion cost proportional to bounds in an unrelated, unexecuted external function. Investigate whether runtime allocation starts above a contract-wide static-memory maximum rather than the memory needed by the selected entry point. This is a hypothesis about the mechanism; the cross-function cost coupling is directly measured.
This looks related to the allocation design discussed in #4426 and #4856. The report isolates cross-function coupling, rather than the small extra buffer allocations described in #4103 and #4117.