Ethernaut 题库闯关 - 第 4 关题解。
今天这篇是Ethernaut 题库闯关连载的第4篇,难度等级:简单。
Ethernaut 题库闯关我已经整理为一个专栏了, 欢迎大家订阅专栏。
本次挑战是要求获得Telephone合约的所有权
Telephone 合约的代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Telephone {
address public owner;
constructor() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
在查看解题思路之前,可以先自己想一想,自己会怎么做?
本题主要考察的是对 tx.origin
及 msg.sender
的理解。
Telephone
合约很小,相信大家可以快速阅读并理解如何解决这个挑战。
owner
状态变量在构造函数
中被初始化。唯一会更新 owner
的函数是 changeOwner
。
function changeOwner(address _owner) public
这是一个公共函数,只需要一个参数address _owner
。如果tx.origin
值与msg.sender
不同,它将用函数输入参数_owner
更新owner
。
为了解决这个难题,我们需要了解什么是msg.sender
和tx.origin
。
如果我们看一下Solidity官方文档中的区块和交易属性文档页,可以找到他们的定义:
tx.origin
(address
): 交易的发起者(完整的调用链)msg.sender
(address
): 消息的发送者(当前调用)tx.origin
和msg.sender
都是 "特殊变量",始终存在于全局命名空间,主要用于提供区块链的信息,或者是通用的实用函数。
但我们需要注意的是:
msg
的所有成员,包括msg.sender
和msg.value
的值可以在每一个外部函数调用中改变。这包括对库函数的调用。tx.origin
将返回最初发送交易的地址,而msg.sender
将返回发起external
调用的地址。这意味着什么呢?
让我们举个例子,看看这两者的不同值:
情景A:Alice (EOA,即外部账号)直接调用Telephone.changeOwner(Bob)
。
tx.origin
: Alice的地址msg.sender
: Alice的地址情景B: Alice (EOA)调用智能合约Forwarder.forwardChangeOwnerRequest(Bob)
,该合约将调用Telephone.changeOwner(Bob)
。
在Forwarder.forwardChangeOwnerRequest
里
tx.origin
: Alice的地址msg.sender
: Alice的地址在Telephone.changeOwner(Bob)
里面
tx.origin
: Alice的地址msg.sender
: Forwarder(合约)的地址这是因为,虽然tx.origin
将总是返回创建交易的地址,msg.sender
将返回进行最后一次外部调用的地址。
我们只需要创建一个合约,在合约你调用Telephone
合约:
contract Exploiter {
function exploit(Telephone level) public {
level.changeOwner(msg.sender);
}
}
而在我们的解决方案代码中,只要部署它并调用它即可:
function exploitLevel() internal override {
vm.startPrank(player, player);
Exploiter exploiter = new Exploiter();
vm.stopPrank();
}
之前我们在文章里有过介绍,vm.startPrank
Foundry 开发框架添加的用于测试的作弊代码,这次的 startPrank
是另一个重载版本。
// Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called
function startPrank(address) external;
// Sets all subsequent calls' msg.sender to be the input address until `stopPrank` is called, and the tx.origin to be the second input
function startPrank(address, address) external;
在这种情况下,我们使用第二个版本,因为我们还需要覆盖初始的tx.orgin
,否则就是address(this)
:测试合约本身的地址!
你可以打开Telephone.t.sol阅读该挑战的完整解决方案代码。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!