7. Force
Chain: Goerli
Difficulty: ●●●○○
Level: https://ethernaut.openzeppelin.com/level/0x20B5c742dD8A63400644Ba85dd48E8FDB6908A7A
要求
有些合约就是拒绝你的付款,就是这么任性 ¯\_(ツ)_/¯
这一关的目标是使合约的余额大于0
这可能有帮助:
- Fallback 方法
- 有时候攻击一个合约最好的方法是使用另一个合约.
- 阅读上方的帮助页面, “控制台之外” 部分
分析
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
首先引入一个知识点 fallback。上一关已经简单介绍过 fallback方法,这次再扩展下。
Solidity
支持两种特殊的回调函数,receive()
和fallback()
,他们主要在两种情况下被使用:
- 接收ETH
- 处理合约中不存在的函数调用(代理合约proxy contract)
接收ETH函数 receive
receive()
只用于处理接收ETH
。一个合约最多有一个receive()
函数,声明方式与一般函数不一样,不需要function
关键字:receive() external payable { ... }
。receive()
函数不能有任何的参数,不能返回任何值,必须包含external
和payable
。
// 定义事件
event Received(address Sender, uint Value);
// 接收ETH时释放Received事件
receive() external payable {
emit Received(msg.sender, msg.value);
}
回退函数 fallback
fallback()
函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract
。fallback()
声明时不需要function
关键字,必须由external
修饰,一般也会用payable
修饰,用于接收ETH:fallback() external payable { ... }
。
// fallback
fallback() external payable{
emit fallbackCalled(msg.sender, msg.value, msg.data);
}
receive和fallback的区别
receive
和fallback
都能够用于接收ETH
,他们触发的规则如下:
触发fallback() 还是 receive()?
接收ETH
|
msg.data是空?
/ \
是 否
/ \
receive()存在? fallback()
/ \
是 否
/ \
receive() fallback()
简单来说,合约接收ETH
时,msg.data
为空且存在receive()
时,会触发receive()
;msg.data
不为空或不存在receive()
时,会触发fallback()
,此时fallback()
必须为payable
。
receive()
和payable fallback()
均不存在的时候,向合约发送ETH
将会报错。
我们再来看 Force 合约,这里只定义了合约,没有定义 receive
和 fallback
方法,到这里从调用不存在的合约方法触发 fallback 走不通,那还有没有其他方法可以往一个合约转账,还真有一个方法 selfdestruct
。
selfdestruct
命令可以用来删除智能合约,并将该合约剩余ETH
转到指定地址。selfdestruct
是为了应对合约出错的极端情况而设计的。
selfdestruct
使用起来非常简单:
selfdestruct(address payable recipient)
其中recipient
是接收合约中剩余ETH
的地址。
contract DeleteContract {
uint public value = 10;
constructor() payable {}
receive() external payable {}
function deleteContract() external {
// 调用selfdestruct销毁合约,并把剩余的ETH转给msg.sender
selfdestruct(payable(msg.sender));
}
function getBalance() external view returns(uint balance){
balance = address(this).balance;
}
}
解题
首先打开Console,获取当前关卡合约实例地址
instance
;打开 Remix IDE,创建文件
7_Force.sol
,粘贴以下代码:// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; contract HackForce { address levelInstance; constructor(address _levelInstance) payable { levelInstance = _levelInstance; } function give() external payable { selfdestruct(payable(levelInstance)); } }
在constructor 填入当前关卡合约实例地址后部署。
VALUE
填入任意值。再调用
give
函数,完成关卡。
后记
在solidity中,如果一个合约要接受 ether,fallback 方法必须设置为 payable
。
但是,并没有发什么办法可以阻止攻击者通过自毁的方法向合约发送 ether, 所以, 不要将任何合约逻辑基于 address(this).balance == 0
之上。