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:
- Pops offset from stack (top)
- Pops length from stack (second)
- Validates offset and length fit in u32
- Charges gas for memory expansion to offset+length
- Copies length bytes from memory[offset] to output buffer
- Sets execution state to stopped
- 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