第二个合约:Hello World

好,咱们正式开始写第一个智能合约。

说实话,我当年学智能合约时,第一个程序也是 Hello World。这就像学编程语言的 tradition,你懂的。不过区块链上的 Hello World 有点不一样——它跑在链上,谁都能查,谁也改不了。

创建文件

先建个文件夹,叫 hello-world。在里面新建一个文件,命名为 HelloWorld.sol

注意后缀名是 .sol,这是 Solidity 的标准扩展名。我见过有人写成 .txt 然后编译报错,嗯,这种坑我踩过。

编写基础合约结构

打开文件,敲入以下代码:

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

contract HelloWorld {
    string public greeting;

    constructor() {
        greeting = "Hello, World!";
    }

    function setGreeting(string memory _newGreeting) public {
        greeting = _newGreeting;
    }

    function getGreeting() public view returns (string memory) {
        return greeting;
    }
}

咱们一行行拆开看。

第一行:// SPDX-License-Identifier: MIT
这是开源许可证声明。说白了就是告诉别人:这代码你可以随便用,遵循 MIT 协议。我个人习惯每个文件都加上,省得后面审计时被人问。

第二行:pragma solidity ^0.8.0;
指定编译器版本。^0.8.0 表示兼容 0.8.x 系列。为什么这么写?因为 0.8 版本之后 Solidity 做了很多安全改进,比如默认检查整数溢出。我之前用 0.7 写过一个合约,结果上线后被人利用溢出漏洞……从那以后我再也不用低于 0.8 的版本了。

第三行:contract HelloWorld { ... }
合约定义。你可以把它理解成一个类(class),里面可以存数据、写函数。

状态变量:string public greeting;
这个 public 关键字很关键。它自动生成一个 getter 函数,外部可以直接读取。你想想看,省了你写一个 view 函数的功夫。

构造函数:constructor()
部署时自动执行一次。这里我们把 greeting 初始化为 "Hello, World!"。

两个函数:

  • setGreeting:修改 greeting 的值。注意参数用了 memory 关键字,这是 Solidity 的数据位置,后面会细讲。
  • getGreeting:读取当前 greeting。加了 view 表示不消耗 gas,只读操作。
小技巧:如果你只是想让别人读取 greeting,直接用 public 就够了。我写 getGreeting 函数纯粹是为了演示,实际项目中很少这么干。

编译与部署

写完了代码,下一步就是编译。我用的是 Hardhat 工具链,你也可以用 Remix(在线 IDE)。

用 Hardhat 编译:

npx hardhat compile

如果一切顺利,你会看到 Compiled 1 Solidity file successfully。如果报错,多半是版本问题,检查一下 hardhat.config.js 里的 Solidity 版本是否匹配。

部署脚本:

// scripts/deploy.js
async function main() {
  const HelloWorld = await ethers.getContractFactory("HelloWorld");
  const hello = await HelloWorld.deploy();
  await hello.deployed();
  console.log("合约部署地址:", hello.address);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

运行部署:

npx hardhat run scripts/deploy.js --network localhost

部署成功后,你会得到一个合约地址。这个地址就是你在链上的“门牌号”。

注意:部署到主网需要真金白银的 gas 费。测试网可以用水龙头领测试币。我曾经在 Ropsten 测试网部署时,忘了领币,结果一直报 insufficient funds,折腾了半小时才发现……别学我。

在链上交互

合约部署上链后,怎么跟它玩?两种方式:

方式一:用 Hardhat 控制台

npx hardhat console --network localhost

// 获取合约实例
const HelloWorld = await ethers.getContractFactory("HelloWorld");
const hello = await HelloWorld.attach("0x你的合约地址");

// 读取 greeting
await hello.getGreeting();
// 输出: "Hello, World!"

// 修改 greeting
await hello.setGreeting("你好,区块链!");

// 再次读取
await hello.getGreeting();
// 输出: "你好,区块链!"

方式二:用 ethers.js 写脚本

// scripts/interact.js
const { ethers } = require("hardhat");

async function main() {
  const hello = await ethers.getContractAt("HelloWorld", "0x你的合约地址");
  
  // 读取
  let msg = await hello.getGreeting();
  console.log("当前消息:", msg);
  
  // 修改
  let tx = await hello.setGreeting("从零构建智能合约");
  await tx.wait(); // 等待交易确认
  
  // 再读
  msg = await hello.getGreeting();
  console.log("修改后消息:", msg);
}

main();

运行交互脚本:

npx hardhat run scripts/interact.js --network localhost
核心概念:每次调用 setGreeting 都会产生一笔交易,需要 gas 费。而 getGreeting 是只读操作,免费。这就是区块链的“写要钱,读免费”原则。

避坑指南

  • 字符串比较:Solidity 不支持 == 直接比较字符串,需要用 keccak256 哈希后比较。我早期写过一个投票合约,因为字符串比较写错了,导致结果全乱套。
  • Gas 估算:部署合约时,gas 上限设高一点。我习惯设 300000,不够再加。
  • 版本兼容:0.8.x 和 0.7.x 的语法有差异,比如 now 被移除了,改用 block.timestamp

总结

第一个合约跑通了,恭喜你!

说白了,智能合约就是一个“永不宕机的数据库+执行逻辑”。你写进去的数据,没人能删,没人能改(除非你留了后门)。

下一章咱们会深入 Solidity 的数据类型和变量,到时候你会看到更多有意思的东西。

课后练习:试着在合约里加一个计数器,每次调用 setGreeting 时自动加 1。提示:用 uint256 public count