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: 0x33
Introduced: Frontier (EVM genesis)
CALLER pushes the address of the immediate caller onto the stack. This is the address that directly invoked the current execution context, changing with each call in the call chain.
Specification
Stack Input:
Stack Output:
caller (uint160 as uint256)
Gas Cost: 2 (GasQuickStep)
Operation:
stack.push(execution_context.caller)
Behavior
CALLER provides the address that made the current call. Unlike ORIGIN which remains constant, CALLER changes with each contract call in the execution chain.
Key characteristics:
- Changes with each call (CALL, STATICCALL, DELEGATECALL)
- Can be either EOA or contract address
- Used for authentication and access control
- Safe for authorization checks
Examples
Basic Usage
import { caller } from '@tevm/voltaire/evm/context';
import { createFrame } from '@tevm/voltaire/evm/Frame';
import * as Address from '@tevm/voltaire/Address';
// Immediate caller address
const callerAddr = Address('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb');
const frame = createFrame({
caller: callerAddr,
stack: []
});
const err = caller(frame);
console.log(frame.stack[0]); // 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbn
Access Control
contract Ownable {
address public owner;
constructor() {
owner = msg.sender; // Uses CALLER opcode
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner"); // SAFE
_;
}
function restricted() public onlyOwner {
// Only owner can call
}
}
Call Chain Tracking
contract ContractC {
function whoCalledMe() public view returns (address) {
return msg.sender; // Returns ContractB's address
}
}
contract ContractB {
function callC(ContractC c) public returns (address) {
return c.whoCalledMe(); // msg.sender in C = address(this)
}
}
// User (0xAAA) → ContractB (0xBBB) → ContractC (0xCCC)
// In ContractC: msg.sender = 0xBBB
Gas Cost
Cost: 2 gas (GasQuickStep)
Same cost as other environment access opcodes:
- ADDRESS (0x30): 2 gas
- ORIGIN (0x32): 2 gas
- CALLVALUE (0x34): 2 gas
Common Usage
Ownership Pattern
contract Owned {
address public owner;
constructor() {
owner = msg.sender;
}
function transferOwnership(address newOwner) public {
require(msg.sender == owner, "Not owner");
require(newOwner != address(0), "Invalid address");
owner = newOwner;
}
}
Access Control Lists
contract ACL {
mapping(address => bool) public authorized;
modifier onlyAuthorized() {
require(authorized[msg.sender], "Not authorized");
_;
}
function grantAccess(address account) public onlyAuthorized {
authorized[account] = true;
}
function revokeAccess(address account) public onlyAuthorized {
authorized[account] = false;
}
}
Payment Tracking
contract PaymentTracker {
mapping(address => uint256) public payments;
receive() external payable {
payments[msg.sender] += msg.value;
}
function refund() public {
uint256 amount = payments[msg.sender];
require(amount > 0, "No payment");
payments[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
Delegation Pattern
contract Delegator {
mapping(address => address) public delegates;
function setDelegate(address delegate) public {
delegates[msg.sender] = delegate;
}
function actAsDelegate(address principal) public view returns (bool) {
return delegates[principal] == msg.sender;
}
}
Security
CALLER vs ORIGIN
SAFE pattern - use msg.sender (CALLER):
contract Safe {
address public owner;
function withdraw() public {
require(msg.sender == owner, "Not owner"); // ✓ SAFE
payable(owner).transfer(address(this).balance);
}
}
UNSAFE pattern - use tx.origin (ORIGIN):
contract Unsafe {
address public owner;
function withdraw() public {
require(tx.origin == owner, "Not owner"); // ✗ DANGEROUS
payable(owner).transfer(address(this).balance);
}
}
DELEGATECALL Context Preservation
contract Implementation {
address public owner;
function whoIsOwner() public view returns (address) {
return msg.sender; // Returns caller in delegatecall context
}
}
contract Proxy {
address public implementation;
fallback() external payable {
address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
// User calls Proxy.whoIsOwner() via delegatecall
// msg.sender in Implementation = User's address (not Proxy)
Reentrancy Protection
contract ReentrancyGuard {
mapping(address => bool) private locked;
modifier nonReentrant() {
require(!locked[msg.sender], "Reentrant call");
locked[msg.sender] = true;
_;
locked[msg.sender] = false;
}
function withdraw() public nonReentrant {
// Protected from reentrancy
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
Authorization Checks
contract MultiSig {
mapping(address => bool) public isSigner;
mapping(bytes32 => mapping(address => bool)) public approved;
function approve(bytes32 txHash) public {
require(isSigner[msg.sender], "Not a signer"); // ✓ SAFE
approved[txHash][msg.sender] = true;
}
function execute(bytes32 txHash) public {
require(approved[txHash][msg.sender], "Not approved");
// Execute transaction
}
}
Implementation
import { consumeGas } from "../Frame/consumeGas.js";
import { pushStack } from "../Frame/pushStack.js";
import { toU256 } from "../../primitives/Address/AddressType/toU256.js";
/**
* CALLER opcode (0x33) - Get caller address
*
* Stack: [] => [caller]
* Gas: 2 (GasQuickStep)
*/
export function caller(frame: FrameType): EvmError | null {
const gasErr = consumeGas(frame, 2n);
if (gasErr) return gasErr;
const callerU256 = toU256(frame.caller);
const pushErr = pushStack(frame, callerU256);
if (pushErr) return pushErr;
frame.pc += 1;
return null;
}
Edge Cases
Contract as Caller
// Caller can be a contract address
const contractCaller = Address('0xContractAddress...');
const frame = createFrame({ caller: contractCaller });
caller(frame);
console.log(frame.stack[0]); // Contract address as u256
Stack Overflow
const frame = createFrame({
caller: callerAddr,
stack: new Array(1024).fill(0n)
});
const err = caller(frame);
console.log(err); // { type: "StackOverflow" }
Out of Gas
const frame = createFrame({
caller: callerAddr,
gasRemaining: 1n
});
const err = caller(frame);
console.log(err); // { type: "OutOfGas" }
Best Practices
✅ DO: Use for access control
require(msg.sender == owner);
✅ DO: Track caller identity
mapping(address => uint256) public balances;
balances[msg.sender] += amount;
✅ DO: Validate caller
require(authorizedCallers[msg.sender], "Unauthorized");
❌ DON’T: Confuse with tx.origin
// WRONG
require(tx.origin == owner);
// CORRECT
require(msg.sender == owner);
References