Skip to main content
This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.

Overview

Opcode: 0xf3 Introduced: Frontier (EVM genesis) RETURN halts execution successfully and returns output data to the caller. All state changes are preserved, and the specified memory range is copied to the return buffer. This is the standard way to complete execution with a return value in the EVM.

Specification

Stack Input:
offset (top) - Memory offset of return data
length       - Length of return data in bytes
Stack Output: None Gas Cost: Memory expansion cost (dynamic) Operation:
1. Pop offset and length from stack
2. Charge gas for memory expansion
3. Copy memory[offset:offset+length] to output
4. Set stopped = true
5. Return to caller

Behavior

RETURN terminates execution with output:
  1. Pops offset from stack (top)
  2. Pops length from stack (second)
  3. Validates offset and length fit in u32
  4. Charges gas for memory expansion to offset+length
  5. Copies length bytes from memory[offset] to output buffer
  6. Sets execution state to stopped
  7. Returns control to caller with success status
State Effects:
  • All state changes preserved (storage, logs, balance transfers)
  • Output data available to caller
  • Remaining gas NOT refunded (consumed by transaction)
  • Execution marked as successful

Examples

Basic Return

import { createFrame } from '@tevm/voltaire/evm/Frame';
import { handler_0xf3_RETURN } from '@tevm/voltaire/evm/control';

// Return 32 bytes from memory offset 0
const frame = createFrame({
  stack: [32n, 0n],  // length=32, offset=0
  memory: Bytes64(),
  gasRemaining: 1000n
});

// Write data to memory
frame.memory[0] = 0x42;

const err = handler_0xf3_RETURN(frame);

console.log(err);             // null (success)
console.log(frame.stopped);   // true
console.log(frame.output);    // Uint8Array([0x42, 0x00, ...]) (32 bytes)

Return Value

function getValue() external pure returns (uint256) {
    assembly {
        // Store return value in memory
        mstore(0, 42)

        // Return 32 bytes from offset 0
        return(0, 32)
    }
}
Compiled to:
PUSH1 42
PUSH1 0
MSTORE         // memory[0] = 42

PUSH1 32       // length
PUSH1 0        // offset
RETURN         // return memory[0:32]

Return String

function getString() external pure returns (string memory) {
    return "Hello";
}
Compiled (simplified):
// ABI encoding: offset, length, data
PUSH1 0x20     // offset of string data
PUSH1 0
MSTORE

PUSH1 5        // string length
PUSH1 0x20
MSTORE

PUSH5 "Hello"  // string data
PUSH1 0x40
MSTORE

PUSH1 0x60     // total length
PUSH1 0        // offset
RETURN

Empty Return

// Return 0 bytes (void function)
const frame = createFrame({
  stack: [0n, 0n],  // length=0, offset=0
  gasRemaining: 1000n
});

handler_0xf3_RETURN(frame);

console.log(frame.output);  // undefined (no output)
console.log(frame.stopped); // true

Constructor Return

contract Example {
    constructor() {
        // Constructor code
    }
}
Constructor bytecode:
// Initialization code
<setup code>

// Return runtime bytecode
PUSH1 runtime_size
PUSH1 runtime_offset
RETURN           // Return deployed code

Gas Cost

Cost: Memory expansion cost (dynamic) Formula:
memory_size_word = (offset + length + 31) / 32
memory_cost = (memory_size_word ^ 2) / 512 + (3 * memory_size_word)
gas = memory_cost - previous_memory_cost
Examples: Return 32 bytes (1 word):
const frame = createFrame({
  stack: [32n, 0n],
  memorySize: 0,
});

// New memory size: 32 bytes = 1 word
// Cost: (1^2)/512 + 3*1 = 0 + 3 = 3 gas
Return 256 bytes (8 words):
const frame = createFrame({
  stack: [256n, 0n],
  memorySize: 0,
});

// New memory size: 256 bytes = 8 words
// Cost: (8^2)/512 + 3*8 = 0.125 + 24 = 24 gas (rounded)
Return from existing memory (no expansion):
const frame = createFrame({
  stack: [32n, 0n],
  memorySize: 64,  // Already expanded
});

// No expansion needed: 0 gas

Edge Cases

Zero Length Return

// Return 0 bytes
const frame = createFrame({
  stack: [0n, 0n],
  gasRemaining: 1000n
});

handler_0xf3_RETURN(frame);

console.log(frame.output);     // undefined
console.log(frame.stopped);    // true
console.log(frame.gasRemaining); // ~1000n (minimal gas consumed)

Large Return Data

// Return 1 KB
const frame = createFrame({
  stack: [1024n, 0n],
  memory: new Uint8Array(2048),
  gasRemaining: 10000n
});

handler_0xf3_RETURN(frame);

console.log(frame.output.length); // 1024
// Gas consumed for 32 words memory expansion

Out of Bounds

// Offset + length overflow u32
const frame = createFrame({
  stack: [0x100000000n, 0n],  // length > u32::MAX
  gasRemaining: 1000n
});

const err = handler_0xf3_RETURN(frame);
console.log(err); // { type: "OutOfBounds" }

Offset Overflow

// offset + length causes overflow
const frame = createFrame({
  stack: [100n, 0xffffffffn],  // offset + length overflows
  gasRemaining: 1000n
});

const err = handler_0xf3_RETURN(frame);
console.log(err); // { type: "OutOfBounds" }

Stack Underflow

// Need 2 stack items
const frame = createFrame({
  stack: [32n],  // Only 1 item
  gasRemaining: 1000n
});

const err = handler_0xf3_RETURN(frame);
console.log(err); // { type: "StackUnderflow" }

Out of Gas

// Insufficient gas for memory expansion
const frame = createFrame({
  stack: [1000n, 0n],  // Large return data
  gasRemaining: 1n,    // Insufficient gas
});

const err = handler_0xf3_RETURN(frame);
console.log(err); // { type: "OutOfGas" }

Common Usage

Function Return Values

function add(uint256 a, uint256 b) external pure returns (uint256) {
    return a + b;
}
Compiled to:
// Add a + b
PUSH1 0x04
CALLDATALOAD
PUSH1 0x24
CALLDATALOAD
ADD

// Store result in memory
PUSH1 0
MSTORE

// Return 32 bytes
PUSH1 32
PUSH1 0
RETURN

Multiple Return Values

function swap(uint256 a, uint256 b) external pure returns (uint256, uint256) {
    return (b, a);
}
Compiled to:
// Store first return value
PUSH1 0x24
CALLDATALOAD
PUSH1 0
MSTORE

// Store second return value
PUSH1 0x04
CALLDATALOAD
PUSH1 0x20
MSTORE

// Return 64 bytes
PUSH1 64
PUSH1 0
RETURN

Constructor Deployment

contract Example {
    uint256 public value;

    constructor(uint256 _value) {
        value = _value;
    }
}
Constructor ends with RETURN containing runtime bytecode:
// Initialization
PUSH1 _value
PUSH1 0
SSTORE

// Copy runtime code to memory
PUSH1 runtime_size
PUSH1 runtime_offset
PUSH1 0
CODECOPY

// Return runtime code
PUSH1 runtime_size
PUSH1 0
RETURN

View Function

function getBalance(address account) external view returns (uint256) {
    return balances[account];
}
Compiled to:
// Load balance
PUSH1 0x04
CALLDATALOAD
PUSH1 0
SLOAD

// Return balance
PUSH1 0
MSTORE
PUSH1 32
PUSH1 0
RETURN

Implementation

import { popStack } from "../Frame/popStack.js";
import { consumeGas } from "../Frame/consumeGas.js";
import { memoryExpansionCost } from "../Frame/memoryExpansionCost.js";
import { readMemory } from "../Frame/readMemory.js";

/**
 * RETURN opcode (0xf3) - Halt execution and return output data
 *
 * @param frame - Frame instance
 * @returns Error if operation fails
 */
export function handler_0xf3_RETURN(frame: FrameType): EvmError | null {
  const offsetResult = popStack(frame);
  if (offsetResult.error) return offsetResult.error;
  const offset = offsetResult.value;

  const lengthResult = popStack(frame);
  if (lengthResult.error) return lengthResult.error;
  const length = lengthResult.value;

  // Check if offset + length fits in u32
  if (offset > 0xffffffffn || length > 0xffffffffn) {
    return { type: "OutOfBounds" };
  }

  const off = Number(offset);
  const len = Number(length);

  if (length > 0n) {
    // Check for overflow
    const endOffset = off + len;
    if (endOffset < off) {
      return { type: "OutOfBounds" };
    }

    // Charge memory expansion
    const memCost = memoryExpansionCost(frame, endOffset);
    const gasErr = consumeGas(frame, memCost);
    if (gasErr) return gasErr;

    const alignedSize = wordAlignedSize(endOffset);
    if (alignedSize > frame.memorySize) {
      frame.memorySize = alignedSize;
    }

    // Copy memory to output
    frame.output = new Uint8Array(len);
    for (let idx = 0; idx < len; idx++) {
      const addr = off + idx;
      frame.output[idx] = readMemory(frame, addr);
    }
  }

  frame.stopped = true;
  return null;
}

function wordAlignedSize(bytes: number): number {
  const words = Math.ceil(bytes / 32);
  return words * 32;
}

Testing

Test Coverage

import { describe, it, expect } from 'vitest';
import { handler_0xf3_RETURN } from './0xf3_RETURN.js';

describe('RETURN (0xf3)', () => {
  it('returns data from memory', () => {
    const memory = Bytes64();
    memory[0] = 0x42;
    memory[1] = 0x43;

    const frame = createFrame({
      stack: [2n, 0n],
      memory,
      gasRemaining: 1000n,
    });

    const err = handler_0xf3_RETURN(frame);

    expect(err).toBeNull();
    expect(frame.stopped).toBe(true);
    expect(frame.output).toEqual(new Uint8Array([0x42, 0x43]));
  });

  it('handles zero-length return', () => {
    const frame = createFrame({
      stack: [0n, 0n],
      gasRemaining: 1000n,
    });

    handler_0xf3_RETURN(frame);

    expect(frame.stopped).toBe(true);
    expect(frame.output).toBeUndefined();
  });

  it('charges memory expansion gas', () => {
    const frame = createFrame({
      stack: [32n, 0n],
      memorySize: 0,
      gasRemaining: 1000n,
    });

    handler_0xf3_RETURN(frame);

    expect(frame.gasRemaining).toBeLessThan(1000n);
  });

  it('rejects out of bounds offset', () => {
    const frame = createFrame({
      stack: [1n, 0x100000000n],
    });

    expect(handler_0xf3_RETURN(frame)).toEqual({ type: 'OutOfBounds' });
  });

  it('rejects overflow', () => {
    const frame = createFrame({
      stack: [100n, 0xffffffffn],
    });

    expect(handler_0xf3_RETURN(frame)).toEqual({ type: 'OutOfBounds' });
  });
});

Security

Return Data Size

Large return data consumes significant gas:
// EXPENSIVE: Return 10 KB
function getLargeData() external pure returns (bytes memory) {
    bytes memory data = new bytes(10000);
    return data;  // High gas cost for RETURN
}
Gas cost grows quadratically with memory expansion:
  • 1 KB: ~100 gas
  • 10 KB: ~1,500 gas
  • 100 KB: ~150,000 gas

Memory Expansion Attack

Attacker cannot cause excessive memory expansion via RETURN:
  • Gas limit prevents unlimited expansion
  • Quadratic cost makes large expansion expensive
  • Out-of-gas reverts transaction

Return Data Validation

Caller must validate returned data:
// VULNERABLE: Trusts return data
function unsafeCall(address target) external {
    (bool success, bytes memory data) = target.call("");
    require(success);

    uint256 value = abi.decode(data, (uint256));  // DANGEROUS
    // No validation - could be malicious
}
Safe pattern:
function safeCall(address target) external {
    (bool success, bytes memory data) = target.call("");
    require(success);
    require(data.length == 32, "Invalid return size");

    uint256 value = abi.decode(data, (uint256));
    require(value <= MAX_VALUE, "Value out of range");
    // Validated return data
}

State Finality

RETURN makes all state changes final:
function unsafeUpdate(uint256 value) external {
    balance = value;  // State changed

    // RETURN makes this final - no further validation possible
    assembly {
        mstore(0, value)
        return(0, 32)
    }
}
Better: Validate before state changes.

Compiler Behavior

Function Returns

Solidity encodes return values using ABI encoding:
function getValues() external pure returns (uint256, address) {
    return (42, address(0x123));
}
Compiled to:
// Encode first return value
PUSH1 42
PUSH1 0
MSTORE

// Encode second return value
PUSH20 0x123...
PUSH1 0x20
MSTORE

// Return 64 bytes
PUSH1 64
PUSH1 0
RETURN

Constructor Pattern

Every constructor ends with RETURN:
constructor() {
    owner = msg.sender;
}
Bytecode structure:
<constructor code>
CODECOPY        // Copy runtime code to memory
RETURN          // Return runtime code

<runtime code>

View Functions

View functions use RETURN to provide read-only data:
function balanceOf(address account) external view returns (uint256) {
    return balances[account];
}
Staticcall context + RETURN = gas-efficient reads.

References