Day 41:
Code
// 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;
}
}
Code Breakdown
This contract is fairly well commented out so a few key callouts:
Motorbike contract
- import of the intializable.sol contract imports a contract to support initializer functions. To use it, replace the constructor with a function that has the
initializer
modifier; struct AddressSlot
: wraps an address in a struct so that it can be passed around as a storage pointer. For further context,struct
is user-defined data types that allow us to group multiple variables of different types under a single name;- `_logic' is a check to ensure that the implementation contract exists and subsequent to confirming its existence, stores it at the relevant slot. It then calls delegatecall to the implementation contract where initialize is called); and
delegate
function delegates to the engine contract, however I'd be grateful for a further breakdown of theassembly
and subsequent code because this is way above my level of understanding.
Engine contract
initialize
has aninitalizer
modifier that inherits from OZ's initializeable contract. This is an attempt by the user to (from my limited understanding) implement a Universal Upgradeable Proxy Standard (UUPS) pattern, which supposedly makes smart contracts more gas efficient and ensures that storage collisions are less likely to occur; andupgradeToAndCall
is how anupgrader
is able to update state variables within a contract. Note that this is linked with theauthorizeUpgrade
function whereby there is a requirement that only the upgrader can authorise an upgrade.
Problem
You have to selfdestruct the engine. So we have to takeover the upgrader
role to authorize the upgrade to a malicious contract with selfdestruct
function to make the proxy contract useless.
Note: Motorbike is the Proxy contract that delegates delegatecall to the Engine contract (also known as the Implementation contract).
Solution
Call the initialize function to make our player the upgrader
. We can then call the upgradeToAndCall with the contract address of the below malicious contract.
As a result the selfdestruct on the new implementation contract is executed:
// SPDX-License-Identifier: MIT
pragma solidity <0.7.0;
contract Boom {
function explode() public {
selfdestruct(address(0));
}
}