Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x58 |
| Mnemonic | PC |
| Gas | 2 |
| Stack Input | (none) |
| Stack Output | $pc (program counter value before this instruction executes, as a 256-bit unsigned integer) |
| Behavior | Pushes the byte offset of the PC instruction itself within the executing bytecode onto the stack. The value is the position of the 0x58 byte, not the position after it. PC is a legacy opcode: it is deprecated in Solidity inline assembly (since Solidity 0.7.x) and is invalid inside EOF (EVM Object Format) containers under EIP-3670. In non-EOF (“legacy”) bytecode it remains functional and costs a fixed 2 gas. |
Threat Surface
PC exposes the absolute byte position of the currently executing instruction within the contract’s bytecode. Unlike CALLER, COINBASE, or TIMESTAMP, PC does not reveal external environment data — it reveals internal code structure. This makes it an introspection opcode: it lets a program reason about where inside itself it is executing.
The threat surface centers on four properties:
-
PC values are fragile and compiler-dependent. The value returned by PC is a raw byte offset that changes whenever the bytecode changes — adding a single byte of code before a PC instruction shifts its return value. This means any contract logic that depends on a specific PC value is silently broken by compiler upgrades, optimizer changes, code refactoring, or the addition/removal of any preceding instruction. Solidity, Vyper, and Huff all produce different bytecode layouts, and even minor Solidity version bumps can rearrange internal jump tables. Code that was tested against one compiler version may behave entirely differently when recompiled.
-
PC enables code obfuscation and anti-analysis. MEV bots and proprietary trading contracts use PC to make bytecode harder to reverse-engineer. By computing jump targets dynamically from PC values (e.g.,
PC + constant → JUMP), the code avoids placing explicit PUSH values for jump destinations, defeating pattern-matching decompilers that rely onPUSH <addr> JUMPsequences. This creates an asymmetry: the obfuscated contract works correctly at its deployed offsets, but decompilation tools produce incomplete or incorrect control flow graphs. Security auditors and automated analysis tools (Mythril, Slither, Dedaub) can fail to resolve PC-derived jump targets, leaving vulnerabilities hidden. -
PC is deprecated under EOF (EIP-3670 / EIP-7692). The EVM Object Format explicitly bans PC from EOF-validated containers. EOF replaces dynamic jumps (
JUMP,JUMPItargeting stack-computed destinations) with static relative jumps (RJUMP,RJUMPI,RJUMPV), eliminating the need for runtime position awareness. Any contract relying on PC that migrates to EOF will fail validation at deploy time. This creates a hard migration cliff for legacy patterns. -
PC is occasionally misused for pseudo-randomness. Because PC returns a value that “looks” unpredictable to a naive developer (it’s a number that depends on code layout), it has been used as entropy in randomness schemes. The value is fully deterministic — anyone who can read the deployed bytecode (i.e., everyone) can compute the exact PC value at any point, making it zero-entropy input to any hash.
Smart Contract Threats
T1: Position-Dependent Logic — Fragile Code That Breaks on Recompilation (High)
Contract logic that branches on or computes from specific PC values is inherently fragile. The PC return value is a byte offset into the contract’s deployed bytecode, and this offset is determined entirely by the compiler at compile time. Any change to the bytecode shifts the offset:
-
Compiler version upgrades. Solidity’s optimizer, code generator, and ABI encoder have changed significantly between versions (e.g., 0.6.x → 0.8.x). The same Solidity source compiled with different versions produces different bytecode, shifting every PC value. A contract that stores or checks PC values computed under Solidity 0.6.12 will produce different values under 0.8.24.
-
Optimizer settings. Enabling or changing the optimizer run count changes instruction ordering and eliminates dead code, shifting all byte offsets. A contract compiled with
--optimize --optimize-runs 200has different PC values than one compiled with--optimize-runs 10000or without optimization entirely. -
Code additions or removals. Adding a new function, modifier, or even an import to the contract shifts the bytecode layout. If any logic depends on a PC value being a specific number, the contract silently malfunctions after the change — no compilation error, no runtime revert, just incorrect behavior.
-
Proxy and library patterns. Contracts deployed behind proxies may be redeployed with updated implementations. If the implementation uses PC-dependent logic, the new implementation’s different bytecode layout invalidates all previous PC assumptions.
Why it matters: Position-dependent code creates silent, hard-to-detect bugs that survive compilation and testing (the tests use the same compiler version) but break in production after upgrades. There is no compiler warning for logic errors caused by shifted PC values.
T2: Bytecode Obfuscation and Anti-Analysis (Medium)
PC is the primary tool for building opaque control flow in EVM bytecode, widely used by MEV bots and proprietary contracts to resist reverse engineering:
-
Dynamic jump target computation. Instead of
PUSH2 0x1234 JUMP, obfuscated code usesPC ADD JUMPorPC PUSH1 0x0A ADD JUMPto compute jump destinations relative to the current position. Decompilers that pattern-matchPUSH <value> JUMPto build control flow graphs cannot resolve these targets without symbolic execution or concrete trace analysis. -
Self-referencing dispatch tables. Contracts can use PC within their function selector dispatcher to compute function entry points as offsets from the dispatch table’s position. This hides the mapping between function selectors and code locations, forcing analysts to trace execution from the entry point.
-
Layered obfuscation with CODECOPY. Combining PC with CODECOPY allows a contract to read its own bytecode at a position-relative offset, interpret it as data, and use it for jump targets or constants. This conflates the code/data boundary and defeats static analysis tools that assume code and data are separable.
-
MEV bot strategy protection. Competitive MEV searchers use PC-based obfuscation to prevent competitors from cloning their strategies. If a profitable arbitrage bot’s logic is readable via Etherscan’s decompiler, competitors can replicate it within hours. PC-derived control flow makes this significantly harder, creating a competitive moat but also hiding potential vulnerabilities from security researchers.
Why it matters: Obfuscation is a double-edged sword. While it protects intellectual property, it also hides vulnerabilities from auditors and automated security tools. A PC-obfuscated contract that contains a reentrancy bug or access control flaw is harder to detect and harder to verify as safe. Protocols that integrate with unverified, obfuscated contracts (common in DeFi aggregation) inherit this opacity risk.
T3: PC as Pseudo-Randomness Source (Medium)
Using PC as entropy in randomness schemes is fundamentally broken, though less commonly seen than block.timestamp or block.prevrandao misuse:
-
Fully deterministic. The PC value at any point in a contract’s execution is fixed by the deployed bytecode. Anyone can disassemble the contract, identify the PC instruction’s byte offset, and know exactly what value it will push. There is no variability across calls, blocks, or transactions.
-
Visible to all participants. Unlike block-level variables where at least the block proposer has privileged early access, PC values are computable by any observer who reads the bytecode from the blockchain. An attacker doesn’t need validator access to predict the value.
-
False sense of complexity. Developers sometimes combine PC with other values in a hash, assuming the combination is unpredictable:
keccak256(abi.encodePacked(pc_value, msg.sender, block.timestamp)). But sincepc_valueis a constant for any given call path, it adds zero entropy. The attacker precomputes it and factors it out of the hash. -
Interaction with inline assembly. In Solidity inline assembly,
pc()was historically accessible before deprecation. Developers writing low-level code might use it as a “unique” value per code location, not realizing it’s deterministic.
Why it matters: Any lottery, NFT mint order, or game outcome that incorporates PC values in its randomness is exploitable by anyone who can read the contract’s bytecode — which is everyone on a public blockchain.
T4: Reliance on PC Values That Change With Compiler Updates (High)
This is a specific manifestation of T1 that deserves separate treatment because of how it interacts with the Solidity upgrade lifecycle:
-
Hardcoded PC offsets in deployment scripts. Some deployment or initialization patterns store PC offsets computed during testing and use them in constructor arguments or storage slots. When the contract is recompiled for production (potentially with a different Solidity version or optimizer setting), these stored offsets no longer correspond to the actual bytecode.
-
Off-chain systems that depend on PC values. Indexers, monitoring tools, and off-chain MEV systems may rely on known PC offsets within a contract to identify which code path was executed (e.g., “if the execution reached PC 0x1A3, the swap function was called”). Compiler upgrades invalidate these mappings silently.
-
Cross-contract PC assumptions. If contract A passes a PC-derived value to contract B for any purpose (e.g., as a callback identifier), and contract A is redeployed with updated bytecode, the value changes but contract B has no way to detect this.
Why it matters: The coupling between PC values and exact bytecode layout creates hidden dependencies that survive code review but break on recompilation. The failure mode is silent incorrect behavior, not a revert.
T5: Anti-Analysis Hindering Security Audits (Medium)
PC-based obfuscation directly undermines the security audit process:
-
Incomplete decompilation. Tools like Dedaub, Panoramix, and Heimdall produce incomplete control flow graphs for contracts with PC-derived jumps. Auditors must fall back to manual opcode-by-opcode analysis, which is time-consuming and error-prone for large contracts.
-
False negatives in automated scanners. Mythril, Slither, and Securify rely on symbolic execution or static analysis to detect vulnerabilities. When jump targets are computed from PC values, the analysis engine may fail to explore reachable code paths, producing false negatives (missed vulnerabilities).
-
DeFi integration risk. Protocols that integrate with third-party contracts (e.g., DEX aggregators routing through unverified MEV bot contracts) cannot audit the obfuscated contracts. If the obfuscated contract has a vulnerability that allows token theft or unexpected reverts, the integrating protocol inherits the risk without visibility.
Why it matters: Obfuscation and security are in direct tension. The Ethereum ecosystem’s security model relies heavily on public code verification and audit. PC-based obfuscation bypasses this model.
Protocol-Level Threats
P1: EOF Deprecation — PC Becomes Invalid in EOF Containers (Medium)
EIP-3670 (EOF - Code Validation) specifies that the PC opcode (0x58) is not a valid instruction inside EOF-formatted bytecode containers. EIP-7692 (EOFv1 Meta) consolidates this as part of the broader EOF rollout:
-
Deploy-time rejection. Any EOF container that includes a 0x58 byte as an instruction (not as a PUSH operand) will fail validation and be rejected at deploy time. This is a hard break, not a soft deprecation.
-
Migration cliff for legacy contracts. Contracts that use PC in their bytecode cannot be directly ported to EOF. The PC instruction must be replaced with alternative logic (e.g., using
RJUMP/RJUMPIfor control flow, or storing constants explicitly instead of computing them from position). This affects hand-written bytecode (Huff, raw assembly) more than Solidity contracts, since the Solidity compiler has already deprecatedpc()in inline assembly. -
Dual-format transition period. During the transition period where both legacy and EOF bytecode coexist on-chain, legacy contracts using PC continue to function. But any new contract targeting EOF cannot use PC, creating a divergence between legacy and modern contract capabilities.
-
Static relative jumps replace PC’s role. EOF introduces
RJUMP(EIP-4200),RJUMPI, andRJUMPVas static relative jump instructions. These encode jump offsets as signed immediates in the bytecode, eliminating the need for runtime position computation via PC. The jump targets are validated at deploy time, making the control flow fully static and analyzable.
Security implications: The removal of PC from EOF is a net positive for security — it eliminates an entire class of obfuscation techniques and forces control flow to be statically analyzable. However, the transition creates risk for projects that depend on PC-based patterns and must refactor before migrating to EOF.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
| PC at byte offset 0 | Returns 0 if 0x58 is the first byte of the contract | Extremely unlikely in practice (contracts start with dispatcher logic), but a value of 0 could be misinterpreted as a null/error indicator in downstream logic. |
| PC after multi-byte PUSH instructions | PC returns the offset of the 0x58 byte; PUSH1-PUSH32 instructions occupy 2-33 bytes. A PC after a PUSH32 might return a value 34 higher than a PC one instruction earlier. | The non-linear relationship between instruction count and PC value confuses developers who expect “instruction index” rather than “byte offset.” |
| PC inside a DELEGATECALL | Returns the byte offset within the delegatecalled contract’s code, not the caller’s code | A library using PC sees its own code offsets, not the proxy’s. If the library is redeployed with different bytecode, PC values change even though the proxy address stays the same. |
| PC in constructor (initcode) | Returns the byte offset within the initcode, not the deployed runtime code | PC values during construction differ from PC values during runtime execution. Code that stores a PC value during construction and uses it at runtime is comparing offsets in two different bytecode segments. |
| PC at maximum contract size (24,576 bytes per EIP-170) | Returns at most 24,575 (0x5FFF) | Always fits in 2 bytes; never a 256-bit overflow concern. |
| PC as JUMP destination | PC JUMP jumps to the position of the PC instruction itself, creating an infinite loop (0x58 is not 0x5B/JUMPDEST) | Results in an invalid jump and immediate revert, consuming all gas up to that point. A common mistake in hand-written bytecode. |
| PC value used as PUSH operand boundary | When scanning bytecode, a 0x58 byte might be a PUSH operand rather than a PC instruction | Static analysis tools must track PUSH boundaries to distinguish PC instructions from data bytes. Misclassification leads to incorrect disassembly. |
| PC in STATICCALL context | Behaves identically to a normal call; returns byte offset within the called contract’s code | No special behavior. Static context only prevents state modification. |
Real-World Exploits
Exploit 1: MEV Bot Obfuscation Exploited — Unauditable Contracts Drained ($1M+, 2022-2023, Recurring)
Root cause: MEV bots deployed with heavily obfuscated bytecode (using PC-derived jump targets and opaque control flow) contained exploitable vulnerabilities that were invisible to both the bot operators’ limited auditing and external security tools.
Details: Throughout 2022-2023, multiple MEV sandwich and arbitrage bots were drained by attackers who reverse-engineered the obfuscated contracts to find flaws. The bots used PC-based control flow obfuscation to hide their trading strategies from competitors. This same obfuscation made it impractical for the bot operators to perform thorough security audits — the bytecode was written in raw assembly or Huff, verified by testing rather than formal analysis, and the PC-dependent control flow prevented automated tools from providing coverage guarantees.
Attackers who invested the effort to manually trace the obfuscated bytecode discovered vulnerabilities including:
- Missing access controls on fund withdrawal functions
- Insufficient validation of callback data in flash loan callbacks
- Reentrancy vulnerabilities in profit-distribution logic
The most notable incident was the September 2022 drain of a prominent MEV bot (0xbaDc0dE) for approximately $1.45M in ETH. The bot’s bytecode was heavily obfuscated with position-dependent jump logic, making automated auditing infeasible. The attacker exploited a flaw in the bot’s callback handling to redirect funds.
PC’s role: PC-based obfuscation created a false sense of security for bot operators — they believed that if competitors couldn’t reverse-engineer the code, attackers couldn’t either. In practice, obfuscation raised the cost of analysis but didn’t eliminate it, and it simultaneously prevented the operators from benefiting from standardized security tooling.
Impact: $1M+ across multiple incidents. Established that obfuscation is not a substitute for security auditing.
References:
- Flashbots: Anatomy of an MEV Bot Exploit (0xbaDc0dE)
- DeGatchi: Smart Contract Obfuscation Techniques
Exploit 2: Tornado Cash Governance Attack — Metamorphic Code Deployment ($1M+ TORN, May 2023)
Root cause: Governance approved a proposal contract that appeared benign but contained a hidden selfdestruct capability. The attacker destroyed the contract and redeployed malicious code at the same address via CREATE2, exploiting the DAO’s approval of a fixed address.
Details: On May 20, 2023, an attacker submitted a Tornado Cash governance proposal that ostensibly penalized “cheating” relayers. The proposal’s bytecode was crafted to appear similar to a previously approved legitimate proposal. Governance token holders voted to approve it.
After approval, the attacker called selfdestruct on the proposal contract (which had been given execution authority by the governance system), clearing the code at that address. They then used a deployer contract with CREATE2 to deploy completely different, malicious bytecode to the same address. This new code granted the attacker 10,000 TORN votes (enough for total governance control), which they used to:
- Grant themselves additional TORN tokens
- Modify the Tornado Cash router
- Drain locked TORN from governance vaults
PC’s role: While PC was not the direct exploit vector, the attack leveraged the same class of code introspection and position-dependent reasoning that PC enables. The attacker’s metamorphic deployment relied on bytecode-level manipulation where code identity was tied to address (a position), not to content. The attack demonstrated that code at a given “position” (address) is not guaranteed to be the same code that was audited — a principle directly analogous to PC’s fragility, where code at a given byte offset is not guaranteed to be the same after redeployment. The broader lesson is that any trust model based on code position (whether byte offset or contract address) is fundamentally brittle.
Impact: The attacker gained full governance control of Tornado Cash. ~$1M+ in TORN tokens extracted before the attacker voluntarily submitted a reversal proposal. Demonstrated that governance systems must verify code content, not just code address.
References:
- Rekt News: Tornado Cash Governance
- pcaversaccio: Tornado Cash Exploit PoC
- Composable Security: Understanding the Tornado Cash Governance Attack
Exploit 3: On-Chain Games Using Bytecode Properties for Randomness (2017-2019, Recurring)
Root cause: Multiple on-chain games and lotteries used combinations of block-level variables and bytecode introspection values (including values derived from code position) as sources of randomness. All such values are fully deterministic and predictable.
Details: During the 2017-2019 gambling DApp era, developers experimented with various “creative” randomness sources beyond the standard block.timestamp and blockhash. Some contracts used inline assembly to read PC values, code offsets, or combined CODESIZE with other stack values to produce “unique” numbers per function. These were hashed alongside block variables:
// Pattern observed in multiple gambling contracts (simplified)
function roll() external payable {
uint256 seed;
assembly {
seed := pc()
}
uint256 result = uint256(keccak256(abi.encodePacked(
seed, block.timestamp, block.difficulty, msg.sender
))) % 100;
if (result < 50) {
payable(msg.sender).transfer(msg.value * 2);
}
}The pc() value is constant for every call to roll() — it always returns the same byte offset. An attacker simply deploys a contract that computes the same hash with the known PC constant and the predictable block variables, calling roll() only when the result is favorable. The PC adds zero entropy.
PC’s role: Developers used pc() under the mistaken belief that it provided per-call or per-block variability. In reality, PC returns a compile-time constant for any given code path, making it equivalent to a hardcoded number for randomness purposes.
Impact: Cumulative losses across gambling DApps in the hundreds of ETH range. Contributed to industry adoption of Chainlink VRF and commit-reveal schemes.
References:
- OWASP: SC08 Insecure Randomness
- Quantstamp: Proper Treatment of Randomness on EVM-Compatible Networks
Attack Scenarios
Scenario A: Position-Dependent Logic Breaks on Recompilation
// Vulnerable: Uses PC value as a "code fingerprint" to verify bytecode integrity
contract VulnerableIntegrityCheck {
uint256 public expectedPC;
// Deployed and tested with Solidity 0.8.19, optimizer runs = 200
// The developer records the PC value during deployment
function recordFingerprint() external {
uint256 pcVal;
assembly {
pcVal := pc()
}
expectedPC = pcVal;
}
function sensitiveOperation() external {
uint256 currentPC;
assembly {
currentPC := pc()
}
// Intended: verify the code hasn't been tampered with
// Reality: breaks if recompiled with ANY different settings
require(currentPC == expectedPC, "integrity check failed");
// ... privileged logic ...
}
// Attack: Recompile with Solidity 0.8.24 or different optimizer runs.
// recordFingerprint() now stores a different PC value.
// sensitiveOperation() always reverts because its PC value also changed,
// but to a DIFFERENT new value. The contract is bricked.
//
// Alternatively, if both happen to shift by the same amount (unlikely but
// possible), the check passes despite the bytecode being different.
}Scenario B: PC-Based Obfuscation Hiding a Backdoor
// Raw bytecode (Huff-style) -- an obfuscated contract with a hidden drain function
//
// The contract uses PC-derived jumps to hide control flow from decompilers.
// Function dispatcher:
// CALLDATALOAD(0) >> 224 → selector
// if selector == 0xdeadbeef: PC + 0x0A → JUMP (legitimate swap function)
// if selector == 0x00000001: PC + 0x1F → JUMP (hidden backdoor drain)
//
// Decompilers see the first branch but fail to resolve the second because
// the jump target is computed as PC + 0x1F, which requires knowing the exact
// PC value at that instruction. Without concrete execution, the decompiler
// marks the second branch as "unreachable" or "unknown."
//
// The backdoor at PC + 0x1F transfers the contract's entire ETH and token
// balances to a hardcoded address.
//
// A protocol that integrates this contract (e.g., as a DEX aggregator route)
// cannot audit the hidden function. The operator can call selector 0x00000001
// at any time to drain funds routed through the contract.
Scenario C: PC as Broken Randomness
contract VulnerableNFTMint {
uint256 public nextTokenId;
mapping(uint256 => uint256) public tokenRarity;
function mint() external payable {
require(msg.value == 0.1 ether);
// VULNERABLE: pc() returns the same value for every call to mint()
uint256 pcVal;
assembly {
pcVal := pc()
}
// Developer thinks pcVal adds entropy. It doesn't -- it's a constant.
uint256 rarity = uint256(keccak256(abi.encodePacked(
pcVal, msg.sender, block.timestamp, block.prevrandao
))) % 100;
tokenRarity[nextTokenId] = rarity;
nextTokenId++;
}
// Attack: Deploy an attacker contract that computes the same hash.
// The attacker reads the deployed bytecode of VulnerableNFTMint to find
// the exact PC value (it's a constant in the bytecode). Then:
//
// contract Attacker {
// function snipe(VulnerableNFTMint target) external payable {
// uint256 PC_CONSTANT = 0x1A3; // read from disassembly
// uint256 rarity = uint256(keccak256(abi.encodePacked(
// PC_CONSTANT, address(this), block.timestamp, block.prevrandao
// ))) % 100;
// // Only mint if we'll get a legendary (rarity > 95)
// if (rarity > 95) {
// target.mint{value: 0.1 ether}();
// }
// }
// }
}Scenario D: PC-Derived Jump Creating an Infinite Gas Drain
// Hand-written bytecode with a PC-derived jump bug:
//
// Offset 0x00: JUMPDEST (0x5B)
// Offset 0x01: PC (0x58) → pushes 0x01
// Offset 0x02: PUSH1 0x00 (0x60 0x00)
// Offset 0x04: ADD (0x01) → stack: 0x01
// Offset 0x05: JUMP (0x56) → jumps to 0x01
//
// This creates an infinite loop: PC at offset 0x01 always pushes 0x01,
// ADD with 0 gives 0x01, JUMP goes to offset 0x01 (which is the PC instruction,
// not a JUMPDEST). The EVM reverts with an invalid jump destination error.
//
// If the developer intended to jump to 0x00 (the JUMPDEST), they needed:
// PC (pushes 0x01) PUSH1 0x01 SUB JUMP → jumps to 0x00
//
// The off-by-one between "PC returns its own offset" and "the intended
// destination is one byte earlier" is a common pitfall in hand-written bytecode.
// This wastes all gas supplied to the transaction.
Mitigations
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: Position-dependent logic | Never use PC values for branching, storage, or cross-call comparisons | Replace pc() with explicit constants, function selectors, or compiler-generated identifiers. Use keccak256 of function signatures for unique identifiers instead. |
| T2: Obfuscation hiding vulnerabilities | Require source verification for all integrated contracts | Use Etherscan/Sourcify verification; refuse to integrate contracts that cannot be decompiled to auditable control flow. Run symbolic execution tools (Mythril, Halmos) against all dependencies. |
| T2: Anti-analysis in MEV bots | Use formal verification alongside obfuscation | If obfuscation is necessary for competitive reasons, apply formal verification to the pre-obfuscation source (Huff or assembly). Maintain private source code with verified build reproducibility. |
| T3: PC for randomness | Use verifiable randomness (Chainlink VRF) or commit-reveal | Chainlink VRF v2+ for provably fair randomness. PC adds zero entropy; remove it from any hash input. |
| T4: Compiler-version sensitivity | Pin compiler versions and optimizer settings in build config | Use solc version pinning in foundry.toml or hardhat.config.js. Verify that deployed bytecode matches expected bytecode hash before interacting with PC-dependent code. |
| T5: Audit tool limitations | Supplement automated analysis with manual bytecode review | For contracts with inline assembly containing pc(), require manual opcode-level audit. Use trace-based analysis (Tenderly, cast run) to verify all reachable code paths. |
| P1: EOF migration | Replace PC with static relative jumps or explicit constants | Use RJUMP/RJUMPI (EIP-4200) for control flow. Replace pc() in Solidity with compiler-generated constants. Plan migration before EOF activation. |
| General: Legacy bytecode | Avoid pc() in new code entirely | Solidity has deprecated pc() since 0.7.x. Use named labels in Huff/assembly. Prefer high-level control flow constructs that the compiler manages. |
Compiler/EIP-Based Protections
- Solidity >= 0.7.x: The
pc()instruction in inline assembly is marked deprecated. The compiler emits a warning whenpc()is used, discouraging new adoption. - EIP-3670 (EOF - Code Validation): Rejects PC (0x58) as an invalid instruction in EOF containers at deploy time. Prevents all PC-dependent patterns in EOF bytecode.
- EIP-4200 (Static Relative Jumps): Introduces
RJUMP,RJUMPI, andRJUMPVwith signed immediate offsets, providing position-independent control flow that replaces PC-derived jump patterns. - EIP-7692 (EOFv1 Meta): Consolidates all EOF-related EIPs. Once activated, new contracts targeting EOF cannot use PC, JUMP, or JUMPI, forcing static analyzability of all control flow.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | High | Medium | Silent breakage on compiler upgrades; no single large exploit, but a systemic risk across hand-written bytecode |
| T2 | Smart Contract | Medium | High | MEV bot obfuscation is pervasive; 0xbaDc0dE exploit ($1.45M) enabled by unauditable obfuscated code |
| T3 | Smart Contract | Medium | Low | Gambling DApps using bytecode properties for randomness (2017-2019); largely superseded by block variable misuse |
| T4 | Smart Contract | High | Medium | Compiler-version sensitivity affects any contract using pc() in inline assembly |
| T5 | Smart Contract | Medium | Medium | Automated scanner false negatives on obfuscated contracts; integration risk in DeFi aggregation |
| P1 | Protocol | Medium | High | EOF is on the Ethereum roadmap; PC will be invalid in all new EOF contracts post-activation |
Related Opcodes
| Opcode | Relationship |
|---|---|
| JUMP (0x56) | Unconditional jump to a stack-provided destination. PC is often used to compute JUMP targets dynamically (PC + offset → JUMP), creating position-dependent control flow. Under EOF, both JUMP and PC are replaced by static relative jumps. |
| JUMPI (0x57) | Conditional jump to a stack-provided destination. Like JUMP, can consume PC-derived values as destinations. PC-computed JUMPI targets defeat static analysis of conditional branches. |
| CODESIZE (0x38) | Returns the byte length of the executing contract’s code. Combined with PC, enables a contract to determine its own position relative to the end of its bytecode (e.g., CODESIZE - PC = bytes remaining). Both are code introspection opcodes deprecated under EOF. |
| JUMPDEST (0x5B) | Marks a valid jump destination. PC-derived JUMP targets must still land on a JUMPDEST; if the PC computation is off by even one byte, the jump is invalid and the transaction reverts. This is the most common failure mode of hand-written PC-dependent bytecode. |
| CODECOPY (0x39) | Copies the executing contract’s bytecode to memory. Combined with PC, allows reading bytecode at position-relative offsets, enabling self-referencing patterns and data embedded in code. |