6. Delegation
Chain: Goerli
Difficulty: ●●○○○
Level: https://ethernaut.openzeppelin.com/level/0x31C4D3a9e0ED12A409cF3C84ad145331aB487D3F
要求
这一关的目标是申明你对你创建实例的所有权.
这可能有帮助
- 仔细看solidity文档关于
delegatecall的低级函数, 他怎么运行的, 他如何将操作委托给链上库, 以及他对执行的影响. - Fallback 方法
- 方法 ID
分析
1 | // SPDX-License-Identifier: MIT |
首先我们根据解题提示和分析合约,有3个知识点。以下内容引用自:WTF Academy
delegatecall 和 call
delegatecall与call类似,是solidity中地址类型的低级成员函数。
当用户A通过合约B来call合约C的时候,执行的是合约C的函数,语境(Context,可以理解为包含变量和状态的环境)也是合约C的:msg.sender是B的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约C的变量上。

而当用户A通过合约B来delegatecall合约C的时候,执行的是合约C的函数,但是语境仍是合约B的:msg.sender是A的地址,并且如果函数改变一些状态变量,产生的效果会作用于合约B的变量上。

大家可以这样理解:一个富商把它的资产(状态变量)都交给一个VC代理(目标合约的函数)来打理。执行的是VC的函数,但是改变的是富商的状态。
delegatecall语法和call类似,也是:
1 | 目标合约地址.delegatecall(二进制编码); |
其中二进制编码利用结构化编码函数abi.encodeWithSignature获得:
1 | abi.encodeWithSignature("函数签名", 逗号分隔的具体参数) |
函数签名为"函数名(逗号分隔的参数类型)"。例如abi.encodeWithSignature("f(uint256,address)", _x, _addr)。
和call不一样,delegatecall在调用合约时可以指定交易发送的gas,但不能指定发送的ETH数额
注意:delegatecall有安全隐患,使用时要保证当前合约和目标合约的状态变量存储结构相同,并且目标合约安全,不然会造成资产损失。
Fallback
fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract。fallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { ... }。
我们定义一个fallback()函数,被触发时候会释放fallbackCalled事件,并输出msg.sender,msg.value和msg.data:
1 | // fallback |
方法 ID
ABI (Application Binary Interface,应用二进制接口)是与以太坊智能合约交互的标准。数据基于他们的类型编码;并且由于编码后不包含类型信息,解码时需要注明它们的类型。
Solidity中,ABI编码有4个函数:abi.encode, abi.encodePacked, abi.encodeWithSignature, abi.encodeWithSelector。而ABI解码有1个函数:abi.decode,用于解码abi.encode的数据。
abi.encode
将给定参数利用ABI规则编码。ABI被设计出来跟智能合约交互,他将每个参数填充为32字节的数据,并拼接在一起。如果你要和合约交互,你要用的就是abi.encode。
1 | function encode() public view returns(bytes memory result) { |
abi.encodePacked
将给定参数根据其所需最低空间编码。它类似 abi.encode,但是会把其中填充的很多0省略。比如,只用1字节来编码uint类型。当你想省空间,并且不与合约交互的时候,可以使用abi.encodePacked,例如算一些数据的hash时。
1 | function encodePacked() public view returns(bytes memory result) { |
abi.encodeWithSignature
与abi.encode功能类似,只不过第一个参数为函数签名,比如"foo(uint256,address)"。当调用其他合约的时候可以使用。
1 | function encodeWithSignature() public view returns(bytes memory result) { |
等同于在abi.encode编码结果前加上了4字节的函数选择器。 说明: 函数选择器就是通过函数名和参数进行签名处理(Keccak–Sha3)来标识函数,可以用于不同合约之间的函数调用。
abi.encodeWithSelector
与abi.encodeWithSignature功能类似,只不过第一个参数为函数选择器,为函数签名Keccak哈希的前4个字节。
1 | function encodeWithSelector() public view returns(bytes memory result) { |
我们再来分析下合约,就很明了。通过触发合约的 fallback 调用 delegatecall 执行 pwn 方法,就可以将合约的owner更改为 player。
解题
首先打开Console,获取当前关卡合约实例地址
contract;执行JS,调用sendTransaction:
1
contract.sendTransaction({data: web3.eth.abi.encodeFunctionSignature('pwn()')});
最后提交,本关完成。
后记
使用delegatecall 是很危险的,而且历史上已经多次被用于进行 attack vector。使用它,你对合约相当于在说 “看这里, 其他合约或是其它库,来对我的状态为所欲为吧”。代理对你合约的状态有完全的控制权。 delegatecall 函数是一个很有用的功能,但是也很危险,所以使用的时候需要非常小心。
请参见 The Parity Wallet Hack Explained 这篇文章, 他详细解释了这个方法是如何窃取三千万美元的。