通关条件
部署一个只有 10 个 opcode 的合约,该合约在调用后返回 42 题目合约
pragma solidity ^0.5.0;
contract MagicNum {
address public solver;
constructor() public {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///__
*/
}
解题过程
创建合约的交易的 bytecode 主要由初始化代码和运行时代码两部分组成。初始化代码用于创建合约,并存储运行时代码;运行时代码则是合约的实际逻辑。
首先考虑运行时代码。我们需要将 42(0x2A)存放到内存中,再返回给调用者。第一个步骤需要使用 MSTORE(0x52,用于将一个 (u)int256 写入内存,0x52),第二个步骤需要使用 RETURN(0xF3,返回合约调用的结果)。
第一步:
PUSH1 0x2A(PUSH1,0x60,用于将 1 byte 的值推入插槽栈中。这里我们需要存储 42)
PUSH1 0x80(我们将 0x2A 存入 slot 0x80)
MSTORE(以前两个元素作为参数调用 MSTORE)
所以该步骤的代码为 0x602A608052
第二步:
PUSH1 0x20(返回值的长度,我们设置为 32 bytes)
PUSH1 0x80(返回值存储在 slot 0x80)
RETURN(以前两个元素为参数调用 RETURN)
所以该步骤的代码为 0x60206080F3
两个步骤结合起来得到我们的运行时代码 0x602A60805260206080F3,刚好 10 个 opcode,同时也是 10 bytes。
现在考虑初始化代码。初始化代码需要拷贝运行时代码并返回给 EVM。第一个步骤需要使用 CODECOPY(0x39,用于拷贝运行时代码),第二个步骤也是 RETURN。
执行第一步:
PUSH1 0x0A(运行时代码的大小,10 bytes)
PUSH1 0x??(运行时代码目前的位置,现在还是未知)
PUSH1 0x00(运行时代码存储的目标位置,我们设定为 slot 0x00)
CODECOPY(以前三个元素为参数调用 CODECOPY)
所以该步骤代码为 0x600A60??600039
执行第二步:
PUSH1 0x0A(运行时代码的长度,10 bytes)
PUSH1 0x00(运行时代码存储的位置,slot 0x00)
RETURN(以前两个元素为参数调用 RETURN)
所以该步骤代码为 0x600A6000F3
结合以上两步我们可以得到初始化代码一共 12 bytes,运行时代码会接在初始化代码之后,所以上面的 0x?? 实际上是 0x0C(运行时代码在 bytecode 中的起始索引为 12)。由此得到我们的初始化代码 0x600A600C600039600A6000F3
将初始化代码和运行时代码组合起来就得到了我们的 bytecode:0x600A600C600039600A6000F3602A60805260206080F3。
接下来部署我们的合约并设置为 Solver:
let bytecode = "0x600A600C600039600A6000F3602A60805260206080F3";
web3.eth.sendTransaction({from: player, data: bytecode});
// 通关 Etherscan 得到合约地址 contractAddress
await contract.setSolver("contractAddress");
另一篇参考文献:Ethernaut Lvl 19 MagicNumber Walkthrough: How to deploy contracts using raw assembly opcodes | by Nicole Zhu | Coinmonks | Medium
|