{"content":{"title":"以太坊EVM Puzzle测试（一共10道题，看看你能过几关）","body":"以太坊在内部实现了一个基于栈的虚拟机，我们称之为[EVM](https://learnblockchain.cn/article/4800)（Ethereum Virtual Machine），用户所有的操作最终都会转化为操作码（OPCODE）然后在EVM中执行，下图为整个执行流程，目前我们对EVM的讲解不多，后续会陆续补上。\r\n\r\n\r\n\r\n![image-20221031203407316.png](https://img.learnblockchain.cn/attachments/2022/11/XYPYatUH63606a748a714.png)\r\n\r\n本文将介绍10道EVM题目，每道题都会要求用户填写部分内容：使得程序正常退出，而不允许执行REVERT。\r\n\r\n**注意：本文针对道是有EVM基础的同学，对于尚未接触过OPCODE的同学**，不要着急，在这里可以先看看EVM里面都有啥，有助于后续深入学习。\r\n\r\n\r\n\r\n## 启动\r\n\r\n**运行游戏**：https://github.com/fvictorio/evm-puzzles\r\n\r\n```sh\r\nnpm i\r\nnpx hardhat play\r\n```\r\n\r\n\r\n\r\n## Puzzle#1：CALLVALUE\r\n\r\n```sh\r\n############\r\n# Puzzle 1 #\r\n############\r\n\r\n00      34      CALLVALUE\t#[msg.value]\r\n01      56      JUMP\t#跳转下一个evm字节，查看JUMPDEST，发现目的地是08，所以msg.value为8\r\n02      FD      REVERT\r\n03      FD      REVERT\r\n04      FD      REVERT\r\n05      FD      REVERT\r\n06      FD      REVERT\r\n07      FD      REVERT\r\n08      5B      JUMPDEST\r\n09      00      STOP\r\n\r\n? Enter the value to send: \r\n```\r\n\r\n- callvalue是输入的ether值，会存储到栈顶\r\n- jump会读取栈顶值，并跳转到相应的字节地址，由JUMPDEST承接\r\n- 由于目的地JUMPDEST是08，所以输入值为：8\r\n\r\n\r\n\r\n## Puzzle#2：CODESIZE\r\n\r\n```sh\r\n############\r\n# Puzzle 2 #\r\n############\r\n\r\n00      34      CALLVALUE\t#[msg.value]\r\n01      38      CODESIZE\t#[10, msg.value]，codesize为当前evm中的操作码的数量，每一个是1字节\r\n02      03      SUB\t#[sub_result]， 需要跳转到06，所以 10 - msg.value = 6\r\n03      56      JUMP\r\n04      FD      REVERT\r\n05      FD      REVERT\r\n06      5B      JUMPDEST\r\n07      00      STOP\r\n08      FD      REVERT\r\n09      FD      REVERT\r\n\r\n? Enter the value to send: \r\n```\r\n\r\n- CALLVALUE存储值到栈顶，STACK：[x]\r\n- CODESIZE获取当前EVM环境中的操作码SIZE，每个OPCODE为1byte，我们此时内存中共10个操作码，因此执行CODESIZE后，会向栈顶存入10，STACK：[10, x]\r\n- SUB会执行减法操作，并将结果如栈，STACK：[10 - x]\r\n- JUMP会跳到06字节，因此我们需要 10 - x = 6，推导出：x = 4\r\n\r\n\r\n\r\n## Puzzle#3：CALLDATASIZE\r\n\r\n```sh\r\n############\r\n# Puzzle 3 #\r\n############\r\n\r\n00      36      CALLDATASIZE\t#[datasize]\r\n01      56      JUMP\t#跳转到04，所以我们只要保证calldata到size为4即可，内容不限。\r\n02      FD      REVERT\r\n03      FD      REVERT\r\n04      5B      JUMPDEST\r\n05      00      STOP\r\n\r\n? Enter the calldata: 0x11223344\r\n```\r\n\r\n- CALLDATASIZE返回INTPUT的字节数，并存入栈顶，STACK：[0x...]\r\n- 为了JUMP到04，因此我们的输入需要大小为4字节，因此随便输入一个四字节data即可！\r\n\r\n\r\n\r\n## Puzzle#4：XOR\r\n\r\n```sh\r\n############\r\n# Puzzle 4 #\r\n############\r\n\r\n00      34      CALLVALUE\t#[msg.value]\r\n01      38      CODESIZE\t#[12, msg.value]\r\n02      18      XOR\r\n03      56      JUMP\r\n04      FD      REVERT\r\n05      FD      REVERT\r\n06      FD      REVERT\r\n07      FD      REVERT\r\n08      FD      REVERT\r\n09      FD      REVERT\r\n0A      5B      JUMPDEST\r\n0B      00      STOP\r\n\r\n? Enter the value to send: \r\n```\r\n\r\n- CALLVALUE获取数值后入栈，STACK：[x]\r\n- CODESIZE获取数值为12入栈，STACK：[12, x]\r\n- 异或操作：\r\n  - 12：0000，1100\r\n  - 0A：0000，1010\r\n  - x：   0000，0110 =》6，所以答案为：6\r\n\r\n\r\n\r\n## Puzzle#5：JUMPI\r\n\r\n```sh\r\n############\r\n# Puzzle 5 #\r\n############\r\n\r\n00      34          CALLVALUE\t#[msg.value]\r\n01      80          DUP1\t#[msg.value, msg.value]\r\n02      02          MUL\t#[mul_result]\r\n03      610100      PUSH2 0100\t#[0100, mul_result]，反推mul_result为: 0100\r\n06      14          EQ\t#[0或1]，反推：1\r\n07      600C        PUSH1 0C\t#[0C, 0或1]，为了能跳转，此时必须为：1\r\n09      57          JUMPI\t#[]，stack2为1时，跳转到stack1的位置\r\n0A      FD          REVERT\r\n0B      FD          REVERT\r\n0C      5B          JUMPDEST\r\n0D      00          STOP\r\n0E      FD          REVERT\r\n0F      FD          REVERT\r\n\r\n? Enter the value to send: (0)：\r\n```\r\n\r\n- CALLVALUE，STACK-> [x]\r\n- DUP1：STACK-> [x，x]\r\n- MUL：STACK-> [mul_result]\r\n- PUSH2 0100：STACK-> [0100，mul_result]\r\n- EQ：判断stack1和stack2是否相等，若相等，则清除这两个值，并向栈顶存入1，否则存入0\r\n- PUSH1 0C：STACK-> [0C, 1]\r\n- JUMPI：读取stack2的值，如果为1，则跳转到stack1的位置，即0C，满足条件！\r\n- 因此我们需要使得：0100 = x*x，0x0100十进制为256，所以x = 16\r\n\r\n\r\n\r\n## Puzzle#6：CALLDATALOAD\r\n\r\n```sh\r\n############\r\n# Puzzle 6 #\r\n############\r\n\r\n00      6000      PUSH1 00\t#[00]\r\n02      35        CALLDATALOAD\t#[calldata, 00]\r\n03      56        JUMP\t#跳转到0A，所以stack1为：0A\r\n04      FD        REVERT\r\n05      FD        REVERT\r\n06      FD        REVERT\r\n07      FD        REVERT\r\n08      FD        REVERT\r\n09      FD        REVERT\r\n0A      5B        JUMPDEST\r\n0B      00        STOP\r\n```\r\n\r\n- PUSH1 00，STACK：【0x00】\r\n- CALLDATALOAD：获取input的数据，即calldata，参数为0x00，即从第00位置开始加载\r\n- JUMP想跳转到0A处，所以calldata的值为0x0a，如果我们直接输入0x0a，此时会被转化为：a00000000000000000000000000000000000000000000000000000000000000，这是错的；\r\n- 由于calldata的数值总为32字节的倍数，所以此处应该为：0x000000000000000000000000000000000000000000000000000000000000000a\r\n\r\n\r\n\r\n## Puzzle#7：EXTCODESIZE\r\n\r\n```sh\r\n############\r\n# Puzzle 7 #\r\n############\r\n\r\n00      36        CALLDATASIZE\t# [datasize]\r\n01      6000      PUSH1 00\t# [00, datasize]\r\n03      80        DUP1\t\t# [00, 00, datasize]\r\n04      37        CALLDATACOPY\t# [] data被copy到memory中，栈被清空\r\n05      36        CALLDATASIZE\t# [datasize]\r\n06      6000      PUSH1 00\t\t# [00, datasize]\r\n08      6000      PUSH1 00\t\t# [00, 00, datasize]\r\n0A      F0        CREATE\t# [deployed_address] 栈被清空，从内存中读取数据，创建合约，返回地址 \r\n0B      3B        EXTCODESIZE\t# [address_code_size] 输入地址，返回合约的size\r\n0C      6001      PUSH1 01\t# [01, address_code_size]\r\n0E      14        EQ\t# [1] address_code_size必须为1，后续的才成立\r\n0F      6013      PUSH1 13\r\n11      57        JUMPI\r\n12      FD        REVERT\r\n13      5B        JUMPDEST\r\n14      00        STOP\r\n? Enter the calldata: \r\n```\r\n\r\n- CREATE：三个参数（value，offset，size）\r\n  - value是wei单位\r\n  - offset是bytecode在内存中的起始位置\r\n  - size是bytecode的size\r\n- 注意⚠️：offset开始的bytecode是包含部署逻辑的数据，并非是新合约的bytecode，其返回值才是新合约的bytecode。\r\n- 所以结果为：0x60**01**600052600160**1ff3**，[点击验证](https://www.evm.codes/playground?callValue=0&unit=Wei&codeType=Bytecode&code=%2760016000526001601ff3%27_&ref=hackernoon.com&fork=grayGlacier)\r\n\r\n\r\n![image-20221031173928192.png](https://img.learnblockchain.cn/attachments/2022/11/5LJIesIf63606a9f0dc14.png)\r\n\r\n\r\n\r\n## Puzzle#8：SWAP\r\n\r\n```sh\r\n############\r\n# Puzzle 8 #\r\n############\r\n\r\n00      36        CALLDATASIZE\t# [datasize]\r\n01      6000      PUSH1 00\t# [00, datasize]\r\n03      80        DUP1\t# [00, 00, datasize]\r\n04      37        CALLDATACOPY\t# []  copy到内存中\r\n05      36        CALLDATASIZE\t# [datasize]，直接生成数据，不需要栈参数\r\n06      6000      PUSH1 00\t# [00, datasize]\r\n08      6000      PUSH1 00\t# [00, 00, datasize]\r\n0A      F0        CREATE\t# [deployed_address]\r\n0B      6000      PUSH1 00\t# [00, deployed_address]\r\n0D      80        DUP1\t# [00, 00, deployed_address]\r\n0E      80        DUP1\t# [00, 00, 00, deployed_address]\r\n0F      80        DUP1\t# [00, 00, 00, 00, deployed_address]\r\n10      80        DUP1\t# [00, 00, 00, 00, 00, deployed_address]\r\n11      94        SWAP5\t# [deployed_address, 00, 00, 00, 00, 00]，兑换1st 和 6th，你没有看错1和6，不是5\r\n12      5A        GAS\t# [gasAvail, deployed_address, 00, 00, 00, 00, 00] // 7个参数\r\n13      F1        CALL\t# [0或1]调用函数，需要是0，0表示失败，1表示成功！（反推的）\r\n14      6000      PUSH1 00\t# [00, 0或1]，需要是0\r\n16      14        EQ\t# [0或1]，需要是1\r\n17      601B      PUSH1 1B\t# [1B, 0或1]，需要是1\r\n19      57        JUMPI\t\t\t\t\t\t\r\n1A      FD        REVERT\r\n1B      5B        JUMPDEST\r\n1C      00        STOP\r\n\r\n? Enter the calldata:\r\n```\r\n\r\n- 我们需要做的是让call失败，一共要三种方式：\r\n  - gas不足\r\n  - 栈空间不足\r\n  - 使用了STATICCALL但是value不为0，（注意，如果codesize是0，call方法是一直返回成功的）\r\n- 我们需要传入数据，执行CRATE后返回的bytecode参与到后续的计算中，从而造成revert\r\n- 具体做法为，根据Puzzle#8，我们传入：0x60016000526001601ff3，返回值为01，这个是OPCODE中的：ADD操作\r\n- 调用CALL的时候，由于栈中的操作数已经不足（当前是0，ADD需要2个），因此会调用失败，REVERT。\r\n- 因此答案为：0x60016000526001601ff3\r\n\r\n\r\n\r\n## Puzzle#9：LT\r\n\r\n```sh\r\n############\r\n# Puzzle 9 #\r\n############\r\n\r\n00      36        CALLDATASIZE\t# [datasize]\r\n01      6003      PUSH1 03\t# [03, datasize]\r\n03      10        LT\t# [1], stack(1) < stack(2),\r\n04      6009      PUSH1 09\t# [09, 1]\r\n06      57        JUMPI\r\n07      FD        REVERT\r\n08      FD        REVERT\r\n09      5B        JUMPDEST\t# [] 跳转到这里\r\n0A      34        CALLVALUE\t\t# [msgvalue]\r\n0B      36        CALLDATASIZE\t# [datasize, msgvalue]\r\n0C      02        MUL\t\t# [mul_result]\r\n0D      6008      PUSH1 08\t# [08, mul_result]\r\n0F      14        EQ\t# [1], 必须是0\r\n10      6014      PUSH1 14\t# [14, 1]\r\n12      57        JUMPI\r\n13      FD        REVERT\r\n14      5B        JUMPDEST\t# 跳转到这里，结束！\r\n15      00        STOP\r\n\r\n? Enter the value to send: \r\n? Enter the calldata: \r\n```\r\n\r\n- mul_result应该是8\r\n- datasize * msgvalue = 8\r\n- 由于在03字节中， stack(1) < stack(2)，所以datasize大于3，且小于8\r\n- 考虑到乘法，所以datasize应该为：4，msgvalue 为：2\r\n- 因此答案为：\r\n  - 2\r\n  - 0xffffffff\r\n\r\n\r\n\r\n## Puzzle#10：ISZERO\r\n\r\n```sh\r\n#############\r\n# Puzzle 10 #\r\n#############\r\n\r\n00      38          CODESIZE\t# [23]\r\n01      34          CALLVALUE\t# [msgvalue, 23]\r\n02      90          SWAP1\t# [23, msgvalue]\r\n03      11          GT\t# [1], msgvalue < 23\r\n04      6008        PUSH1 08\t# [08, 1]\r\n06      57          JUMPI\r\n07      FD          REVERT\r\n08      5B          JUMPDEST\t# 跳到这里\r\n09      36          CALLDATASIZE\t# [datasize]\r\n0A      610003      PUSH2 0003\t# [0003, datasize]\r\n0D      90          SWAP1\t# [datasize, 0003]\r\n0E      06          MOD\t\t# [N]，取余数，N为余数，必须是：0\r\n0F      15          ISZERO\t# [0或1]，必须是1\r\n10      34          CALLVALUE\t# [msgvalue, 0或1]，必须是：1\r\n11      600A        PUSH1 0A\t# [0A，msgvalue, 0或1]，反推：msgvalue = 0x0f\r\n13      01          ADD\t# [add_result, 0或1]，下面是跳转了，所以栈值为：[0x19, 1]\r\n14      57          JUMPI\r\n15      FD          REVERT\r\n16      FD          REVERT\r\n17      FD          REVERT\r\n18      FD          REVERT\r\n19      5B          JUMPDEST\r\n1A      00          STOP\r\n? Enter the value to send: \r\n? Enter the calldata: \r\n```\r\n\r\n- ~~0A + msgvalue = 19，所以msgvalue为：9~~（这里犯错了，19是16进制的，我错误的当成了10进制相减了）。\r\n- 0x19 - 0x0a = 0x0f，即十进制值：15\r\n- calldata字段，需要满足size对3取余数为0即可，我们选择：0xffffff\r\n- All puzzles are solved!\r\n\r\n\r\n\r\n## 错误记录\r\n\r\n1. push2：表示存入一个2字节的数据入栈，而不是入栈两次\r\n2. Swap5：表示1st和6th兑换，而不是1st和5yh兑换\r\n3. 1表示成功，0表示失败，JUMIPI和CALL都如此\r\n4. OPCODE的字节序是16进制，而不是10进制，注意转化\r\n\r\n\r\n\r\n## 小结\r\n\r\n接下来会花几节课来讲解EVM相关内容，以及反汇编内容，敬请关注！\r\n\r\n\r\n\r\n## 其他知识\r\n\r\n- 答案：https://hackernoon.com/evm-puzzles-learn-ethereum-by-solving-interactive-puzzles\r\n- 了解存储相关：https://www.evm.codes/about\r\n\r\n\r\n\r\n\r\n\r\n---\r\n\r\n加V入群：Adugii，公众号：[阿杜在新加坡](https://mp.weixin.qq.com/s/kjBUa2JHCbOI_2UKmZxjJQ)，一起抱团拥抱web3，下期见！\r\n\r\n\r\n\r\n> 关于作者：国内第一批区块链布道者；2017年开始专注于区块链教育(btc, eth, fabric)，目前base新加坡，专注海外defi,dex,元宇宙等业务方向。"},"author":{"user":"https://learnblockchain.cn/people/3162","address":null},"history":"QmVRta4pnPAVqKSmdNPb7QgsEMQzy7G5qRjxu1e6qaTVbH","timestamp":1667458569,"version":1}