Skip to content

Advanced: Bytecode Placeholders

Overview

A bytecode placeholder reserves a fixed-width slot inside a data portion so that the value can be supplied after the bytecode has been assembled. This breaks the circular dependency that arises whenever a value embedded in the code depends on a property of the code that contains it:

  • Gas reserves in loops — the loop condition compares GAS against the cost of one more iteration, but that cost cannot be measured until the loop (including the condition itself) has been built.
  • Self-referential offsetsCODECOPY and CREATE2 initcode offsets depend on the length of the execution code that precedes the embedded data.
  • Jump targets — a JUMP destination is only known once the code up to that destination exists.

Placeholders solve this in two passes: build the code with a named slot, measure it, then substitute the measured value back in.

The Problem Placeholders Replace

Without placeholders, tests reserve space with a stand-in literal chosen to have the same encoded width as the final value, then rebuild the bytecode from scratch:

# Relies on "PUSH costs 3 gas regardless of the pushed value"
placeholder = Op.GT(Op.GAS, Op.PUSH1(0))
per_iter_gas = While(body=body, condition=placeholder).gas_cost(fork)
# Relies on "0xFF happens to be the same byte size as the final value"
placeholder_offset = 0xFF
factory_execution_template = Op.CODECOPY(0, placeholder_offset, init_code_size) + ...

Both patterns work, but the width match is an unchecked invariant maintained by a comment. If the final value needs a wider PUSH than the stand-in, the measurement silently describes bytecode that is no longer the bytecode being deployed. Placeholders make the width explicit and enforce it.

Creating a Placeholder

Pass data_placeholder with a name to any opcode that takes a data portion. The data portion is zeroed and the slot is registered under that name:

code = Op.POP(Op.PUSH2(data_placeholder="loop_cost"))

The opcode you choose fixes the slot width — PUSH2 reserves two bytes. Pick an opcode wide enough for the largest value you intend to substitute; substituting a value that does not fit is an error rather than a silent truncation.

Any opcode with a data portion is accepted: PUSH1PUSH32, which is the common case, as well as DUPN, SWAPN, and EXCHANGE. Until it is substituted, the slot holds zero, which for DUPN and SWAPN is not a valid index — see Restrictions.

The result is an ordinary Bytecode object: it concatenates, reports its length and gas cost, and can be nested inside further opcode calls.

An open slot cannot be converted to bytes

While any placeholder is still unsubstituted, bytes(code), code.hex(), and equality comparisons raise:

Exception: bytecode with active placeholders cannot be converted to bytes

This is deliberate — the reserved slot holds zero, so silently emitting it would deploy code that pushes the wrong value. Substitute every slot before handing the bytecode to pre.deploy_contract(), an Account, or a transaction. len() and gas_cost() deliberately keep working, since measuring the template is the whole point.

Substituting Values

Bytecode.substitute() takes placeholder names as keyword arguments and fills those slots in place. It returns None:

code = Op.POP(Op.PUSH2(data_placeholder="loop_cost"))

code.substitute(loop_cost=1000)

# bytes: 61 03e8 50
assert bytes(code) == bytes(Op.POP(Op.PUSH2(1000)))

Substituted names are no longer tracked afterwards, so the same slot cannot be filled twice.

Substitution mutates the bytecode

substitute() does not return a copy — there is no new_code = code.substitute(...), and assigning its result stores None. Because the object itself is modified, a template cannot be reused for two different values; build a fresh bytecode for each one.

Concatenation does copy, so a fragment used to build a larger program keeps its own open slot:

fragment = Op.POP(Op.PUSH2(data_placeholder="value"))
combined = Op.PUSH1(0xFF) + Op.POP + fragment

combined.substitute(value=0xBEEF)   # `fragment` is untouched

Why the Measurement Is Exact

Because the placeholder's width is fixed when the code is built, the bytecode's length and gas cost are identical before and after substitution:

  • Every PUSH1PUSH32 costs the same 3 gas (G_VERY_LOW) regardless of width or pushed value, and none of them incur state gas.
  • The encoded length does not change, so every offset, jump target, and code-size calculation measured on the template remains valid.

len(), gas_cost(), state_cost(), and refund() all work on a template with open slots; only conversion to bytes is withheld.

This is what makes the two-pass approach sound: measure on the template, then substitute.

body = Op.MSTORE(0, Op.SHA3(0, 32))
loop = (
    Op.JUMPDEST
    + body
    + Op.JUMPI(Op.GT(Op.GAS, Op.PUSH2(data_placeholder="reserve")), 0)
)

# Measure the loop, including its own condition
per_iteration = loop.gas_cost(fork)

# Feed the measurement back into the code that produced it
loop.substitute(reserve=per_iteration)

assert loop.gas_cost(fork) == per_iteration

Multiple Placeholders

A single bytecode may carry any number of placeholders, as long as their names are distinct. They can be substituted together or one at a time:

code = Op.ADD(
    Op.PUSH2(data_placeholder="first"),
    Op.PUSH1(data_placeholder="second"),
)

# Together
code.substitute(first=0x1234, second=0xAB)
code = Op.ADD(
    Op.PUSH2(data_placeholder="first"),
    Op.PUSH1(data_placeholder="second"),
)

# Or progressively, leaving the remaining slots open
code.substitute(first=0x1234)
code.substitute(second=0xAB)

Substituting a subset is useful when the values become known at different points, for example a gas reserve known after measuring the loop and a jump target known after the surrounding program is assembled.

Concatenation

Placeholder offsets are tracked through concatenation, so a template can be built up from fragments and substituted at the end:

prefix = Op.PUSH1(0xFF) + Op.POP          # 3 bytes
suffix = Op.POP(Op.PUSH2(data_placeholder="value"))

combined = prefix + suffix                # offset shifts from 1 to 4
combined.substitute(value=0xBEEF)

Because names identify slots globally within a bytecode, concatenating two fragments that use the same name raises an exception rather than silently dropping one:

code = Op.POP(Op.PUSH2(data_placeholder="value"))
code + code   # Exception: Conflicting data placeholders between bytecode objects

Give each slot a distinct name, or substitute one fragment before combining it.

Restrictions

  • The opcode must have a data portion. Op.ADD(1, 2, data_placeholder="x") raises ValueError; there is nowhere to put the slot.
  • The name must be a string. Op.PUSH2(data_placeholder=1) raises ValueError.
  • Substitution checks the slot width, not the opcode's own constraints. A placeholder writes raw bytes into the data portion, so encoder validation that normally runs when the data portion is given directly is skipped. Op.DUPN(5) raises, because a DUPN index must be in [17, 235], but building Op.DUPN(data_placeholder="depth") and substituting depth=5 produces e605 without complaint. For PUSH1PUSH32 this is irrelevant, since every byte value is a valid operand; for DUPN, SWAPN, and EXCHANGE the caller is responsible for the range.
  • Bytecode containing placeholders cannot be repeated with *. Duplicating the bytes would duplicate the slot, leaving one name pointing at several offsets, so code * 3 raises ValueError. Multiplying by 0 or 1 is still allowed. Substitute first, then repeat.

Error Reference

Condition Exception Message
Opcode has no data portion ValueError `data_placeholder` requires an opcode with data portion
Name is not a string ValueError `data_placeholder` must be a str
Unknown name passed to substitute() KeyError Placeholder <name> not found in bytecode
Value too large for the slot ValueError Value <n> doesn't fit in <k> bytes (max <max>)
Negative value ValueError Value -1 doesn't fit in <k> bytes (max <max>)
Same name on both sides of + Exception Conflicting data placeholders between bytecode objects
* on bytecode with placeholders ValueError Cannot multiply bytecode containing placeholders
bytes(), hex(), or == while a slot is open Exception bytecode with active placeholders cannot be converted to bytes