Opcode Summary

PropertyValue
Opcode0x1A
MnemonicBYTE
Gas3
Stack Inputi, x
Stack Output(x >> (248 - i*8)) & 0xFF
BehaviorExtract the ith byte from a 256-bit value x, where i=0 is the most significant (leftmost) byte. Returns 0 if i >= 32.

Threat Surface

BYTE extracts a single byte from a 256-bit word using big-endian byte ordering. This is the EVM’s native byte order, but it conflicts with the little-endian conventions used by most modern CPUs and many developer mental models. The i=0 position refers to the most significant byte (bits 248-255), not the least significant byte that most programmers would expect.

This big-endian convention is the root of most BYTE-related vulnerabilities. Developers who think in little-endian (or who work with Solidity’s abi.encodePacked which produces big-endian output) frequently extract the wrong byte:

  • Address extraction: An Ethereum address occupies bytes 12-31 of a 32-byte word (right-aligned). BYTE(0, addr) returns 0 (the padding), not the first byte of the address. The correct first address byte is at BYTE(12, addr).
  • Hash analysis: Keccak256 produces big-endian output. BYTE(0, hash) returns the most significant byte, which is the first byte displayed in hex notation. This is correct but unintuitive.
  • Selector extraction: Function selectors are the first 4 bytes of calldata. They occupy bytes 0-3, so BYTE(0) through BYTE(3) on a CALLDATALOAD(0) correctly extracts them.

The secondary threat surface is BYTE’s silent zero-return for out-of-range indices. BYTE(32, x) or BYTE(MAX_UINT256, x) returns 0 without any error or revert. Code that uses BYTE with a user-controlled index can be tricked into reading “zero” from any out-of-range position, potentially bypassing checks that expect non-zero bytes.


Smart Contract Threats

T1: Big-Endian Byte Ordering Confusion (High)

The most common BYTE error: developers expect byte 0 to be the least significant byte (little-endian convention) when it is actually the most significant. This produces systematically wrong extractions:

// Want: extract the least significant byte of x
// Wrong: BYTE(0, x) returns the MOST significant byte
// Right: BYTE(31, x) returns the least significant byte
 
assembly {
    let lsb := byte(31, x)  // Correct: byte 31 is the rightmost
    let msb := byte(0, x)   // Correct: byte 0 is the leftmost
}

For addresses (which are right-aligned in 32-byte words):

  • The address 0xdead...beef stored as a uint256 is 0x000000000000000000000000dead...beef
  • BYTE(0) through BYTE(11) return 0 (padding)
  • BYTE(12) returns the first byte of the actual address
  • BYTE(31) returns the last byte of the address

T2: Out-of-Range Index Returns Zero (High)

BYTE silently returns 0 for any index >= 32. This creates a dangerous silent failure mode when the index is:

  • Computed from user input and not bounds-checked
  • The result of an arithmetic operation that overflows
  • A value from an untrusted source
assembly {
    let idx := calldataload(0)  // User-controlled index
    let b := byte(idx, someValue)  // If idx >= 32, returns 0
    // Zero might bypass a != 0 check, or match an expected zero byte
}

T3: Wrong Bytes Extracted from Addresses and Hashes (High)

Contract logic that extracts specific bytes from addresses or keccak256 hashes for routing, sharding, or vanity-address verification must use the correct byte positions. Errors here can:

  • Route tokens to the wrong shard/chain (if using address byte for chain selection)
  • Accept invalid vanity addresses (if checking the wrong byte for zero)
  • Produce incorrect address checksums
// Checking if an address starts with zero bytes (CREATE2 vanity mining)
assembly {
    let firstByte := byte(0, addr)  // Bug: this is the padding, always 0
    // Should be: byte(12, addr) for the first actual address byte
}

T4: BYTE for Signature Parsing (Medium)

Some contracts use BYTE to extract v, r, s components from packed ECDSA signatures. If the byte positions are wrong, the extracted components are incorrect, producing either invalid signatures (which revert safely) or, worse, valid signatures for a different signer.

T5: Nibble Extraction Errors (Low)

Contracts that extract individual nibbles (4-bit values) from bytes using BYTE combined with AND and SHR can produce incorrect nibble ordering if the byte position is wrong:

assembly {
    let b := byte(idx, value)
    let highNibble := shr(4, b)  // Upper 4 bits
    let lowNibble := and(b, 0x0f)  // Lower 4 bits
    // If idx is wrong, both nibbles come from the wrong byte
}

Protocol-Level Threats

P1: No DoS Vector (Low)

BYTE costs a fixed 3 gas regardless of operand values. It operates purely on the stack.

P2: Consensus Safety (Low)

BYTE is deterministic: the same (i, x) pair always produces the same result across all EVM implementations. The big-endian convention is unambiguous in the specification. No consensus bugs have occurred due to BYTE.

P3: zkEVM Constraint Gaps (Medium)

The PSE/Scroll zkEVM had a missing constraint vulnerability related to byte-level operations. In zero-knowledge EVM implementations, each opcode must be constrained to produce the correct output for all possible inputs. Byte extraction operations require careful constraint generation because the “which byte” selection involves position-dependent masking. A missing or incorrect constraint could allow a malicious prover to claim an incorrect BYTE result was correct.


Edge Cases

Edge CaseBehaviorSecurity Implication
BYTE(0, x)Returns most significant byteBig-endian; not the “first” byte in little-endian sense
BYTE(31, x)Returns least significant byteThis is x & 0xFF
BYTE(32, x)Returns 0Silent zero; no revert or error
BYTE(MAX_UINT256, x)Returns 0Any index >= 32 returns 0
BYTE(i, 0)Returns 0 for all iZero has no non-zero bytes
BYTE(0, MAX_UINT256)Returns 0xFFAll bytes of MAX_UINT256 are 0xFF
BYTE(12, address)Returns first byte of a right-aligned addressCorrect way to get byte 0 of a 20-byte address in a 32-byte word
BYTE(i, hash)Returns byte i of keccak256Direct extraction; keccak256 output is already big-endian
Negative index (as uint256)Returns 0 (index >> 31)Interpreted as very large unsigned number

Real-World Exploits

Exploit 1: PSE/Scroll zkEVM Missing Constraint in Byte Operations (2023)

Root cause: Missing or incomplete constraints in the zero-knowledge circuit for byte-level operations, allowing a malicious prover to potentially forge incorrect execution results.

Details: In the PSE (Privacy and Scaling Explorations) and Scroll zkEVM implementations, byte extraction and manipulation operations required careful constraint generation. A vulnerability was found where the constraints for byte-level operations were incomplete, meaning a malicious prover could potentially construct a valid proof for an incorrect BYTE operation result. This would allow forging state transitions in the ZK rollup.

BYTE’s role: Byte extraction operations require position-dependent masking in ZK circuits. Each possible byte position (0-31) must be properly constrained, and the out-of-range (>=32) case must return zero. Missing constraints for any of these cases creates a soundness bug.

Impact: No exploitation occurred; the bug was found during audit and fixed before mainnet deployment.

References:


Exploit 2: Address Byte Extraction Errors in Vanity Address Verification (Recurring Pattern)

Root cause: Checking byte 0 instead of byte 12 when verifying that an address has specific byte patterns (e.g., leading zeros for CREATE2 vanity addresses).

Details: Vanity address mining (particularly for CREATE2-deployed contracts) involves finding salt values that produce addresses with specific byte patterns — typically leading zero bytes for gas-efficient address comparison. Contracts that verify these patterns on-chain sometimes check BYTE(0, address) instead of BYTE(12, address), failing to account for the 12-byte right-padding of addresses in 256-bit words.

Since the leading 12 bytes of a properly-encoded address are always zero, checking BYTE(0, addr) always passes (returns 0), making the vanity address verification meaningless. Any address would be accepted.

BYTE’s role: The bug is a direct misuse of BYTE’s big-endian indexing applied to right-aligned address data.

Impact: No specific public exploit reported, but the pattern has been flagged in multiple private audits. Warden findings in Code4rena and Sherlock competitions have identified this pattern in address verification logic.


Exploit 3: Function Selector Extraction Errors in Proxy Contracts (Recurring Pattern)

Root cause: Incorrect BYTE extraction of the 4-byte function selector from calldata, causing proxy contracts to route calls to wrong implementation functions.

Details: Proxy contracts that route calls based on the function selector must extract bytes 0-3 from calldata. In assembly, this is done by loading the first 32 bytes (CALLDATALOAD(0)) and extracting the selector using either SHR(224, ...) or BYTE(0..3). When BYTE is used, extracting at the wrong position produces an incorrect selector, causing the proxy to match the wrong function or fail to match any function (falling through to the fallback).

BYTE’s role: Incorrect byte position in selector extraction produces wrong 4-byte values, silently misrouting calls.


Attack Scenarios

Scenario A: Address Byte Check Bypass

contract VanityVerifier {
    function verifyLeadingZeros(address addr, uint8 requiredZeros) external pure returns (bool) {
        for (uint8 i = 0; i < requiredZeros; i++) {
            uint8 b;
            assembly {
                // Bug: byte(0..n) reads padding, which is ALWAYS zero
                b := byte(i, addr)
            }
            if (b != 0) return false;
        }
        return true;  // Always returns true for requiredZeros <= 12!
    }
    
    // Correct: start from byte 12 for the actual address bytes
    // b := byte(add(12, i), addr)
}

Attack: Any address passes the vanity verification because the check reads zero-padding instead of actual address bytes.

Scenario B: Out-of-Range Index Zeroing

contract ByteRouter {
    function route(uint256 data, uint256 byteIndex) external {
        uint8 routeId;
        assembly {
            routeId := byte(byteIndex, data)  // If byteIndex >= 32, routeId = 0
        }
        
        if (routeId == 0) {
            // "Default" route -- perhaps a privileged path
            _handleDefault(data);
        } else {
            _handleRoute(routeId, data);
        }
    }
}

Attack: Pass byteIndex = 32 or higher. BYTE returns 0. Attacker accesses the default route regardless of the actual data content.

Scenario C: Wrong Hash Byte Extraction for Difficulty Verification

contract ProofOfWork {
    function verifyWork(bytes32 hash, uint8 difficulty) external pure returns (bool) {
        // Check that the first 'difficulty' bytes of hash are zero
        for (uint8 i = 0; i < difficulty; i++) {
            // Correct for keccak256 output (big-endian): byte(i, hash) IS the ith byte
            // But wrong if hash was produced by a little-endian system
            if (uint8(hash[i]) != 0) return false;
        }
        return true;
    }
}

Scenario D: Signature Component Extraction Error

function extractV(bytes32 packed) internal pure returns (uint8 v) {
    assembly {
        // Bug: byte(0) is MSB of packed, not the v value
        // v is typically stored in the LSB (byte 31) or at a specific offset
        v := byte(0, packed)  // Returns wrong byte
    }
}

Mitigations

ThreatMitigationImplementation
T1: Endianness confusionDocument byte positions explicitly; use Solidity’s bytes32[i] syntax instead of assemblyuint8(hash[0]) instead of byte(0, hash) in assembly
T1: Address bytesRemember: address byte 0 is at BYTE(12), not BYTE(0)Comment with byte layout: // [0-11: padding][12-31: address]
T2: Out-of-range indexValidate index < 32 before BYTE extractionrequire(idx < 32, "out of range")
T2: User-controlled indexNever pass user input directly as BYTE indexSanitize: idx := and(idx, 0x1f) (mod 32)
T3: Address/hash extractionUse Solidity’s type system for byte extractionbytes20(address), bytes32(hash) with array indexing
T4: Signature parsingUse OpenZeppelin’s ECDSA libraryECDSA.recover() handles byte extraction correctly
T5: Nibble extractionTest with values containing non-zero nibbles at all positionsFuzz with random 256-bit values

Best Practices for Byte Extraction

  • Use Solidity when possible: bytes32(value)[i] uses the same big-endian convention as BYTE but is type-checked.
  • Prefer SHR over BYTE for multi-byte extraction: SHR(248, x) extracts the MSB; AND(x, 0xFF) extracts the LSB. Both are clearer than BYTE for common patterns.
  • Document the byte layout: When working with packed data, include a comment showing which bytes correspond to which fields.
  • Test with non-zero padding: Include test cases where the “padding” bytes (0-11 for addresses) are non-zero to catch extraction position errors.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractHighHighWidespread endianness confusion in assembly
T2Smart ContractHighMediumSilent zero return bypasses checks
T3Smart ContractHighMediumAddress byte extraction errors in audits
T4Smart ContractMediumLowSignature parsing bugs
T5Smart ContractLowLowNibble extraction in specialized contracts
P1ProtocolLowN/A
P2ProtocolLowN/A
P3ProtocolMediumLowPSE/Scroll zkEVM constraint gaps

OpcodeRelationship
AND (0x16)AND(x, 0xFF) extracts the least significant byte; BYTE extracts by position. Both used for byte-level extraction
SHR (0x1C)SHR(248, x) extracts the most significant byte, equivalent to BYTE(0, x). SHR is more flexible for multi-byte extraction
SHL (0x1B)Positions bytes before extraction; BYTE(i, SHL(n, x)) extracts shifted bytes
CALLDATALOAD (0x35)Loads 32 bytes from calldata; BYTE extracts individual bytes from the loaded word
SIGNEXTEND (0x0B)Extends the sign bit of a sub-256-bit value; BYTE can extract the sign byte for manual sign extension
MLOAD (0x51)Loads 32 bytes from memory; BYTE extracts individual bytes from loaded words
KECCAK256 (0x20)Produces 32-byte hashes; BYTE extracts individual bytes of the hash for difficulty checks or routing