跳到主要内容

Level 0 - Hello Ethernaut

来源:ethernaut.openzeppelin.com/level/0

Hello Ethernaut 是一道入门关,但它已经在教接下来全程都用得上的一个习惯: 别再相信 UI,打开控制台,看清楚合约真正暴露出了什么。

这一关算不上真正意义上的利用挑战。它更像是 Ethernaut 在教你 —— 它希望你怎么玩这套东西。

准备环境​

如果页面以只读模式打开,说明 Ethernaut 还没看到你的钱包。处理方式很直接: 装一个 MetaMask,把它连上,切到 Sepolia 网络,然后给账户充一点测试 ETH。

这里不打算展开钱包的完整教程。这一关需要确保的,只有以下几点:

  • MetaMask 已安装并已解锁
  • 网络已经切到 Sepolia
  • 账户里有足够支付 gas 的测试 ETH

切到 Sepolia 的顺序是: 先把"显示测试网络"打开,再从钱包主界面切换网络。

打开 MetaMask 设置

MetaMask 管理网络

打开 MetaMask 网络选择器

在 MetaMask 中选中 Sepolia

同一个钱包地址在不同网络上保持一致

第一次接触时容易踩到的一个小点: 你的地址在以太坊主网和 Sepolia 上一般是同一个。变的是网络状态,而不是地址本身。每个网络的余额、交易、合约都是各自独立的。

Faucet 备注​

大多数 Sepolia 水龙头平时都能用,直到突然不能用。最常见的麻烦来自反滥用策略: 有的水龙头拒绝全新钱包,有的要求登录账号,还有的会检查你主网上是否有一点点 ETH。

实操上的应对很简单:

  • 先试一个正常的水龙头
  • 如果被拒,换一个备选
  • 如果钱包是全新的,基于浏览器的 Sepolia PoW 水龙头 通常是阻力最小的路径

可用资源:

控制台才是真正的操作界面​

钱包接上以后,把浏览器控制台打开。从这一刻起,Ethernaut 不再是一个网页,而像一个合约游乐场。

你应该立刻能看到关卡横幅、提示文字,以及当前关卡的合约地址。

Ethernaut 控制台总览

第一个值得敲一下的辅助是 player:

player

它会返回当前连接账户的地址。

Ethernaut player 地址输出

接着确认账户里确实有 gas:

await getBalance(player)

Ethernaut getBalance 输出

最后问问 Ethernaut 都提供了哪些辅助方法:

help()

Ethernaut help 输出

这一条命令几乎把这个平台的工作面都告诉你了: player 地址、当前关卡地址、当前实例、合约包装器,以及几个顺手的工具方法。

真正重要的两个对象​

到这一步,真正值得关心的对象只有两个。

ethernaut​

它是全局的游戏合约包装器,而不是当前关卡的合约实例。它负责管理所有关卡以及通关校验。

ethernaut
await ethernaut.owner()

Ethernaut owner 调用

这一步主要起到熟悉作用。重点是让自己习惯一件事: 这些"游戏对象"就是普通的合约包装器,而不是某种只存在于 UI 里的、有魔法的抽象。

contract​

它是你请求出来的当前关卡实例。真正用来解题的,是这个对象。

通关步骤​

1. 连接站点​

先在 MetaMask 里通过 Ethernaut 发起的钱包连接请求。

Ethernaut 钱包连接请求

如果 MetaMask 询问网络权限,通过 Sepolia 这一项。

Ethernaut 网络权限请求

2. 请求一个关卡实例​

点击 Get New Instance,在 MetaMask 里通过那笔交易。部署完成后,Ethernaut 会把实例地址打印到控制台。

Ethernaut 请求实例输出

3. 顺着合约留的面包屑走​

这一关本质上是一次贯穿合约接口的引导式游览。合约会一路告诉你下一步该去哪儿,你要做的只是注意听它说什么。

整条交互链是:

await contract.info()
await contract.info1()
await contract.info2('hello')
await contract.infoNum()
await contract.info42()
await contract.theMethodName()
await contract.method7123949()
await contract.password()
await contract.authenticate('ethernaut0')

Ethernaut 合约 info 链

这里没有什么微妙之处。Ethernaut 是在教你: 读合约,而不是盯着按钮猜。

4. 提交已通关的实例​

交互链跑完之后,点 Submit Instance,在 MetaMask 里通过这笔交易。

Ethernaut 实例操作按钮

Ethernaut 在 MetaMask 中提交交易请求

如果一切正常,Ethernaut 会把这一关标记为已通过。

Ethernaut 通关输出

看完源码之后,有些事就很明显了​

通关之后,Ethernaut 会把源码公开出来。这才是这一关真正值得回味的地方。

下面是同一份合约,加上了内联注解:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Instance {
// 声明为 public 意味着 Solidity 会自动生成一个 password() 取值方法。
// 换句话说,这个"秘密"任何人都可以读到。
string public password;

// 这是一条面包屑。值 42 是为了把玩家引向 info42()。
uint8 public infoNum = 42;

// 又一条面包屑。因为它是 public 的,玩家其实也可以直接把它读出来。
string public theMethodName = "The method name is method7123949.";

// 这才是这一关真正的通关状态。
bool private cleared = false;

// 构造函数
// 关卡实例在部署时被赋予一个初始 password。
constructor(string memory _password) {
password = _password;
}

// 链条上的第一条提示。
function info() public pure returns (string memory) {
return "You will find what you need in info1().";
}

// 链条上的第二条提示。
function info1() public pure returns (string memory) {
return 'Try info2(),but with "hello" as a parameter.';
}

// 玩家第一次需要传入参数的地方。
// 只有传入 "hello",才能拿到下一条线索。
function info2(string memory param) public pure returns (string memory) {
if (keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked("hello"))) {
return "The property infoNum holds the number of the next info method to call.";
}
return "Wrong parameter.";
}

// infoNum 指向这里。
function info42() public pure returns (string memory) {
return "theMethodName is the name of the next method.";
}

// theMethodName 指向这里。
function method7123949() public pure returns (string memory) {
return "If you know the password,submit it to authenticate().";
}

// 如果传入的值与 password 一致,这一关就算通过。
function authenticate(string memory passkey) public {
if (keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
cleared = true;
}
}

// 检查这一关是否已被通关的辅助方法。
function getCleared() public view returns (bool) {
return cleared;
}
}

整个流程被刻意设计得很简单。一个函数指向下一个,直到合约最终告诉你: 读出 password,然后传进 authenticate()。

真正的关键就在这一行:

string public password;

整道题的精髓就在这里。合约把 password 写得像一个秘密,但只要它被声明成 public,Solidity 就会自动暴露出一个 getter。它不是隐藏的,也不是被保护的,更不是任何意义上的 private —— 你可以直接读,然后把它喂回 authenticate()。

这也是为什么这道看似简单的题依然值得认真做: 它逼你去注意,UI 里描述的样子,跟合约在链上真正暴露的内容,是两回事。

这一关真正想教的是什么​

最显而易见的那句话当然是"public 状态本来就是公开的"。但底下其实还有一层更宽的意思:

  • 把合约接口当成事实来源
  • 不要因为某个变量"听起来像个秘密"就以为它真的是秘密
  • 仔细读输出 —— 合约经常把下一步的提示直接摆在你面前
  • 区分清楚: 什么是前端层隐藏的,什么是链上真正隐藏的

这是真正的智能合约审计习惯,不是游戏化的技巧。

这一关为什么仍然重要​

题本身刻意简单,但它教的工作流,恰恰是后面每一关都会反复出现的:

  1. 接钱包
  2. 切到对的网络
  3. 给账户充值
  4. 摸清楚控制台里的辅助方法
  5. 读懂公开接口
  6. 创建实例
  7. 与合约交互
  8. 提交结果

一旦这套循环开始变得自然,后面的关卡也就不再那么生疏。

通关标准​

只要做到下面这些,这一关就算过了:

  • 钱包成功连接
  • 切到 Sepolia,并有足够的 gas
  • 成功创建关卡实例
  • 顺着合约公开的提示链走完一遍
  • 提交已解的实例

关键收获​

  • 这是一道入门关,而不是漏洞利用题。
  • 浏览器控制台是这场游戏的一部分,不是可选项。
  • public 变量并不是秘密。
  • 读合约,通常比盯着 UI 猜要划算得多。
  • 把钱包、网络与水龙头一次配好,后面省下的时间会非常可观。

参考链接​