一道不错的区块链题目,主要是 Minimal Proxy 合约的学习。
题目源码信息
1 | // contracts/D3Casino.sol |
合约逻辑分析
合约整体逻辑比较清楚,我与目标合约的 bet
函数进行交互,需要完成 Solve
的条件,首先看一下逻辑限制(按照合约顺序):
line 17
: 限制单个块内只能交易一次,也就是单个区块内只能与目标合约的 bet 函数交易一次。line 18-21
: 每个合约只能成功调用bet
函数一次。line 23-28
:调用bet
的合约 runtime code 长度不超过 0x64 字节。line 41
:使用 staticcall 回调 msg.sender 的fallback
函数,限制了 gas 费不超过 1000。
总结一句话就是必须使用字节码不超过 0x64 的小合约与 casino 合约交互,并且杜绝了重入的可能。
再看获得 score 的条件,首先需要猜测一个由区块信息和 msg.sender
完全确定的随机数 rand
,猜测的结果通过我们合约的 fallback 函数返回给 casino,猜对了才能继续下面判定,否则 socre 清 0。随机数猜对后,如果msg.sender
(我们部署的合约地址)与 tx.origin
(我们的账户地址)在相同的位置都有 00
字节,则积一分,可多次积分,连续10 次满足以上条件就可以成功调用 Solve
获取 flag,因此我们同一个账户创建最多十个合约成功 bet
即可。
Nice Address
获取 score 的条件要求我们账户的地址含有比较多的 00 ,以及我们用于交互的合约(记为 solver合约)地址也有 00。账户地址可以使用 python web3 穷举,或者直接 vanity 生成前缀 00 地址即可。
1 | # find good address (with many 00 in its public address) in web3 eth |
如果我们想要部署的合约地址和账户的地址在相同的位置有 00 字节,就必须通过一个代理合约使用 create/create2 来创建实际交互合约 solver,使用 create2 ,调整 salt 值即可完全预测合约部署的地址,因此可以链下穷举 salt ,找到符合条件的 salt 值,再使用 create2 创建合约上链。
solver 合约
最终 solver 合约如下 ,这个合约编译出来的字节码肯定大于 0x64 ,即使开了 999 优化编译也有 1000 字节左右,不能直接与 casino 合约交互,绕过部分参考下一节。
1 | pragma solidity 0.8.17; |
我们需要定义一个带参数和返回值、non-payable、不能改变链上状态的 fallback 函数,并且不能有复杂的计算,因此 ans 的计算必须放在 fallback 之外,放在交互的合约 solve 内计算是最好的。
Minimal Proxy 合约
这题的最难点就是如何绕过字节码长度的限制。一个比较简单的想法就是通过在 constructor 内部完成所有交互过程,即将上节的 solver 内的 solve
函数改成 constructor
即可,因为在 constructor 内部调用目标合约,我们的 solver 还没完成初始化,只有 init/deploy 字节码,其 runtime 字节码长度是 0 ,即可绕过 runtime 字节码长度限制。
但是很可惜,上述方法不行。原因在于 casino 合约的 bet
函数会回调 solver 合约的 fallback
函数,而如果我们 在 constructor
内交互, 实际 solver 的 fallback
函数还没初始化,casino 内的 staticcall
虽然可以调用成功,但是返回值为空 , 后续 decode 会发生 revert。
最终的解法是 ERC-1167: Minimal Proxy Contract ,详细的构造可以参考 OpenZippelin 的博客 和 实现。其核心原理就是我们先部署一个 solver 合约上链,作为我们的 implementation 合约,后续我们通过 Mini Proxy 合约不断克隆 solver 得到新合约,新合约实际逻辑通过 DELEGATECALL
与 solver 保持完全一样,而新合约本身只需要实现简单的 call data 转发和结果回传功能即可,实现逻辑如下:
- Receive some data
- Forward the received data to an implementation contract using the DELEGATECALL instruction.
- Get the result of the external call (i.e., the result of DELEGATECALL)
- Return the external call’s result to the caller if step 3 succeeded, or revert the transaction in any other case.
Minimal Proxy Contract 生成的合约是用汇编写的,典型的 runtime code 如下,仅 55 个字节,满足题目条件:
1 | 3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 |
但是实际上用 OpenZippelin 的 Clone 合约部署的克隆合约字节码如下,仅 45 个字节:
1 | 363d3d373d3d3d363d731ffbd6041951b0641bc171adefb8c87bf596172b5af43d82803e903d91602b57fd5bf3 |
All in One
最后把上面的所有的部分串起来(使用 python web3 完成链下部分):
- 生成含有比较多 00 字节的账户地址
- 部署 solver 合约, clone 合约
- 穷举 salt 值,直到预测的合约地址与账户地址有相同位置的 00 字节
- 使用 salt ,调用 clone 合约使用 create2 克隆 solver 合约得到 cloned solver 合约
- 通过 cloned solver 合约与 casino 合约交互,重复 Step 3 - 5,直到 score 大于 10 分。
脚本
python exp/local solver (读者可以使用 ganache + remixd在本地验证)
1 | # ganache-cli --account "0x7207dccc6b59a6945ca0c8431bec866265a37217f3fde378cd0d33bc5f64a428,300000000000000000000" |
exp_sol.sol
1 | // SPDX-License-Identifier: MIT |