Skip to content

feat[venom]: support max_outsize=INF in raw_call - #5271

Merged
harkal merged 25 commits into
vyperlang:masterfrom
harkal:feat/venom/raw-call-bytes-inf-return
Sep 17, 2026
Merged

harkal merged 25 commits into
vyperlang:masterfrom
harkal:feat/venom/raw-call-bytes-inf-return

Conversation

@harkal

@harkal harkal commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

What I did

Closes: #5246

raw_call(..., max_outsize=INF) returns the whole return data as Bytes[INF], or (bool, Bytes[INF]) with revert_on_failure=False, sized by returndatasize at runtime.

max_outsize was a compile-time cap: proxies, forwarders and multicall wrappers had to guess a bound or truncate silently. Venom already has Bytes[INF] locals and unbounded extcall returns; raw_call was the last return-data ingress without an unbounded form. Venom only; the legacy pipeline rejects it.

#pragma experimental-codegen

@external
@payable
def forward(_target: address) -> Bytes[INF]:
    return raw_call(_target, msg.data, max_outsize=INF, value=msg.value)

How I did it

Semantics (vyper/builtins/functions.py, vyper/builtins/_signatures.py). RawCall._is_unbounded_outsize recognises the INF name on max_outsize and nothing else, so gas=INF and value=INF still fail the uint256 check.BuiltinFunctionT._validate_arg_types had its per-kwarg loop body extracted into _validate_kwarg so RawCall canoverride it for that one kwarg without duplicating the loop; no behaviour change for other builtins. RawCall.infer_kwarg_types annotates the kwarg with INF's own type because local analysis re-visits every kwarg value against those types. fetch_call_return returns BytesT(INF) / TupleT([BoolT(), BytesT(INF)]).

Which type drives codegen. The unbounded branch in lower_raw_call is keyed on the call's own return type, not on the kwarg and not on the node annotation. The Call node's _metadata["type"] is the consumer-widened type: x: Bytes[INF] = raw_call(t, d, max_outsize=32) annotates the call as Bytes[INF], and branching on it would drop the cap. Codegen therefore recomputes node.func._metadata["type"].fetch_call_return(node), which is pure. There is no stored accessor for a builtin call's own return type: call_return_type is only written for external ContractFunctionT calls, and the other widenable builtins (concat, slice, abi_encode, ...) legitimately produce the widened buffer type, so extending the metadata would add a second writer for a single consumer. Two runtime tests pin the bounded cap through a Bytes[INF] consumer; they fail if the annotation is used.

Lowering (vyper/codegen_venom/builtins/system.py, context.py). On the unbounded path the call is emitted with retsz=0 (the existing zero-output path). The ok block then calls materialize_returndata_bytes: returndatasize -> checked 32 + ceil32(len) -> scratch allocation -> length store -> padding zero -> returndatacopy. With revert_on_failure=False the (bool, Bytes[INF]) result is a dynamic tuple frame around that value; the bytes member isthe return data on both success and failure, as on the bounded path. The bounded branches are textually unchanged apart from an assertion that the semantic type matches the literal, so bounded output is byte-identical (verified below).

Cost. The return data is copied raw, no decode: the caller pays memory expansion for the actual returndatasizeonly, and the callee can only produce what the caller's gas allowed. x: Bytes[INF] = raw_call(...) copies the value twice (scratch, then the local), the same as unbounded extcall returns today.

Legacy. The existing unbounded-sequence gate (_validate_legacy_function_body_no_unbounded_sequences) already rejects every position, declaration and expression-only (len(...), keccak256(...), [...][0]), with unbounded sequence types require --experimental-codegen. Tests pin that message and that the same code compiles under Venom.

Docs. raw_call entry in docs/built-in-functions.rst, plus a types-unbounded label in docs/types.rst for the cross-reference.

How to verify it

  • tests/functional/codegen/features/test_inf_raw_call.py (Venom only, 17 tests): empty and 10 KB return data; assignment to a Bytes[INF] local; external return; forwarding through an internal function (dret, inlining disabled) and into an extcall argument; (bool, Bytes[INF]) unpack, tuple return and direct subscript on success and on failure (revert data returned, not raised); is_static_call and is_delegate_call; msg.data forwarding proxy, including return data shorter than the forwarded calldata and not word-aligned; bounded max_outsize returned through a Bytes[INF] consumer keeps its cap.
  • tests/unit/semantics/types/test_inf.py: resolved return type for revert_on_failure x plain/static/delegate; ABI bytes / (bool, bytes).
  • tests/functional/syntax/test_raw_call.py: gas=INF / value=INF rejected; narrowing into Bytes[100] / (bool, Bytes[100]) rejected with the exact message; legacy rejection in four positions; bounded forms unchanged.

Commit message

`max_outsize` is a compile-time cap, so proxies, forwarders and
multicall wrappers have to guess a bound on the returndata or truncate
it silently. venom already has `Bytes[INF]` locals and unbounded
`extcall` returns; `raw_call` was the last returndata ingress without an
unbounded form.

accept `max_outsize=INF`: the call then returns `Bytes[INF]`, or `(bool,
Bytes[INF])` with `revert_on_failure=False`, sized by the actual
returndatasize. only `max_outsize` accepts the name, via a per-kwarg
validation hook factored out of `BuiltinFunctionT._validate_arg_types`,
so `gas=INF` and `value=INF` still fail the uint256 check.

codegen keys the unbounded branch on the call's own return type rather
than on the node annotation. the annotation is the consumer-widened
type, so `x: Bytes[INF] = raw_call(t, d, max_outsize=32)` annotates the
call `Bytes[INF]` and branching on it would drop the cap. the unbounded
call is emitted with no output buffer, and the whole returndata is
copied into a runtime-sized scratch buffer once it returns;
`revert_on_failure=False` wraps that in a dynamic tuple frame. the
bounded path keeps its static buffer and compiles to the same bytecode
as before. the legacy pipeline rejects every use through the existing
unbounded sequence gate.

Description for the changelog

raw_call accepts max_outsize=INF under the experimental code generator and returns the whole return data as Bytes[INF] (or (bool, Bytes[INF]) with revert_on_failure=False).

Cute Animal Picture

Put a link to a cute animal picture inside the parenthesis-->

@charles-cooper charles-cooper left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

surprisingly simple! lgtm pending clanker + @Sporarum @HodanPlodky review

@github-actions

Copy link
Copy Markdown

Gas Changes

No changes detected.

Summary

  • Total tests measured: 560
  • Changed: 0
  • Regressions (gas up): 0
  • Improvements (gas down): 0
  • New tests: 0
  • Deleted tests: 0
  • Newly failing: 0
  • Newly passing: 0

@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown

📊 Bytecode Size Changes (venom)

No changes detected.

Full bytecode sizes

Contract legacy-O2 legacy-Os -O2 -O3 -Os
curvefi/legacy/CurveStableSwapMetaNG.vy 24941 23567 19769 19059 18523
curvefi/amm/stableswap/meta_implementation/meta_implementation_v_700.vy 23599 22794 19565 18627 18314
curvefi/amm/stableswap/implementation/implementation_v_700.vy 24951 23758 19188 18363 18019
curvefi/legacy/CurveStableSwapNG.vy 24462 23287 18749 17981 17636
curvefi/amm/tricryptoswap/implementation/implementation_v_200.vy 20724 19959 17250 16689 16325
curvefi/amm/twocryptoswap/implementation/implementation_v_210.vy 17634 16894 14958 14376 14027
yearnfi/VaultV3.vy 19972 19063 14739 13818 13269
curvefi/legacy/CurveCryptoSwap2.vy 18947 18382 14619 14129 13954
yearnfi/VaultV2.vy 16676 15763 13258 12466 12049
curvefi/amm/stableswap/factory/factory_v_100.vy 14558 13978 11852 10780 10880
curvefi/gauge/child_gauge/implementation/implementation_v_110.vy 12338 11561 9781 9184 8795
curvefi/amm/stableswap/views/views_v_120.vy 12784 12368 9705 9059 9294
curvefi/gauge/child_gauge/implementation/implementation_v_100.vy 12017 11249 9514 8924 8538
curvefi/amm/tricryptoswap/math/math_v_200.vy 11189 11126 9029 8144 8170
curvefi/legacy/CurveCryptoMathOptimized3.vy 11188 11125 9028 8144 8170
curvefi/gauge/child_gauge/implementation/implementation_v_020.vy 10665 9947 8626 8100 7714
curvefi/registries/metaregistry/metaregistry_v_110.vy 7590 6732 6491 5710 5603
curvefi/helpers/router/router_v_110.vy 6717 6717 6251 5733 6035
curvefi/amm/tricryptoswap/views/views_v_200.vy 7821 7776 6111 5896 6045
curvefi/helpers/stable_swap_meta_zap/stable_swap_meta_zap_v_100.vy 7302 7067 5877 5350 5610
curvefi/amm/twocryptoswap/views/views_v_200.vy 6991 6946 5680 5479 5614
curvefi/registries/metaregistry/registry_handlers/stableswap/handler_v_110.vy 6633 6259 5533 4695 5238
curvefi/amm/twocryptoswap/math/math_v_210.vy 6800 6800 5506 5012 5039
curvefi/amm/twocryptoswap/factory/factory_v_200.vy 5540 5252 4617 3917 4047
curvefi/amm/tricryptoswap/factory/factory_v_200.vy 5246 5021 4483 3890 4020
curvefi/gauge/child_gauge/factory/factory_v_201.vy 4844 4547 3901 3675 3511
curvefi/registries/metaregistry/registry_handlers/tricryptoswap/handler_v_110.vy 4241 3939 3718 3334 3429
curvefi/registries/metaregistry/registry_handlers/twocryptoswap/handler_v_110.vy 4186 3884 3671 3251 3329
curvefi/gauge/child_gauge/factory/factory_v_100.vy 4183 3914 3408 3144 2971
yearnfi/VaultFactory.vy 3765 3617 2936 2158 2461
curvefi/registries/address_provider/address_provider_v_201.vy 2973 2782 2613 2440 2353
curvefi/helpers/rate_provider/rate_provider_v_101.vy 3260 3260 2535 2263 2296
curvefi/amm/stableswap/math/math_v_100.vy 3067 3046 2458 2253 2310
curvefi/helpers/rate_provider/rate_provider_v_100.vy 2847 2841 2273 1954 1974
curvefi/helpers/deposit_and_stake_zap/deposit_and_stake_zap_v_100.vy 2322 2316 1782 1611 1670
curvefi/governance/relayer/taiko/relayer_v_001.vy 2068 2064 1731 1510 1558
curvefi/governance/relayer/polygon_cdk/relayer_v_101.vy 1556 1523 1530 1324 1347
curvefi/governance/relayer/arb_orbit/relayer_v_101.vy 1266 1262 1242 1066 1115
curvefi/governance/relayer/op_stack/relayer_v_101.vy 1186 1182 1183 1014 1056
curvefi/governance/relayer/not_rollup/relayer_v_100.vy 1168 1153 1174 1011 1037
curvefi/governance/vault/vault_v_100.vy 964 941 862 823 839
curvefi/governance/relayer/relayer_v_100.vy 496 496 593 490 503
curvefi/governance/agent/agent_v_100.vy 541 541 430 402 406
curvefi/governance/agent/agent_v_101.vy 541 541 430 402 406

@harkal harkal changed the title feat[venom]: raw call bytes inf return feat[venom]: support max_outsize=INF in raw_call Sep 16, 2026
@harkal
harkal marked this pull request as ready for review September 16, 2026 18:02
Comment thread vyper/builtins/functions.py
Comment thread vyper/builtins/functions.py Fixed

@Sporarum Sporarum left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve pending nits

Comment thread docs/types.rst Outdated
Comment thread tests/functional/syntax/test_raw_call.py Outdated
@Sporarum

Sporarum commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Oh, and should close #5246

(add Closes: #5246 somewhere in the PR description, I usually put it in "What I did")

@harkal
harkal merged commit 1180f3e into vyperlang:master Sep 17, 2026
172 checks passed
@harkal
harkal deleted the feat/venom/raw-call-bytes-inf-return branch September 17, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support runtime-sized successful returndata in raw_call (follow-up to #4856)

5 participants