3、智能合约基础:Solidity语法速览、合约结构、状态变量与函数、事件与修饰符
好,咱们进入第三章节。说实话,很多同学学NFT开发,一看到Solidity就头大。我当年也是,觉得这玩意儿跟JavaScript长得像,但用起来处处是坑。别急,今天我用一节课的时间,带你快速过一遍Solidity的核心骨架。
你想想看,智能合约说白了就是跑在区块链上的程序。它不能重启,不能随便改,部署了就跟你签了合同一样。所以,写合约的时候,每一行代码都得想清楚。
3.1 Solidity语法速览
Solidity是一门静态类型语言,语法上跟JavaScript、C++都有点像。我个人习惯把它理解为「带类型检查的JavaScript,但跑在以太坊上」。
先看一个最简单的合约骨架:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HelloNFT {
string public name = "MyFirstNFT";
uint256 public totalSupply = 10000;
}
这里有几个关键点:
- SPDX许可证:开源协议声明,不加的话有些编译器会报警告。我一般用MIT。
- pragma solidity:指定编译器版本。^0.8.0表示兼容0.8.x系列。嗯,这里要注意,版本不对应会导致部署失败。
- contract:合约关键字,相当于Java里的class。
我的小习惯:每次新建合约文件,第一件事就是写版本声明。曾经有一次我忘了写,结果用Remix编译时报了一堆看不懂的错误,排查了半小时才发现是版本问题。
3.2 合约结构
一个完整的Solidity合约,通常包含以下几部分:
| 组成部分 | 说明 | 是否必须 |
|---|---|---|
| SPDX许可证 | 开源协议标识 | 推荐 |
| pragma指令 | 编译器版本声明 | 必须 |
| import语句 | 引入其他合约或库 | 按需 |
| contract定义 | 合约主体 | 必须 |
| 状态变量 | 存储在链上的数据 | 按需 |
| 函数 | 合约的行为逻辑 | 按需 |
| 事件 | 链上日志输出 | 按需 |
| 修饰符 | 函数访问控制 | 按需 |
我建议你写合约时,按照这个顺序来组织代码。这样别人读你的代码,一眼就能找到想要的东西。
3.3 状态变量与函数
状态变量是合约的灵魂。它们被永久存储在区块链上,每次读写都要花Gas。说白了,这就是你的「数据库」。
contract NFTMarket {
// 状态变量
address public owner;
uint256 public listingFee = 0.01 ether;
mapping(uint256 => address) public tokenOwner;
// 构造函数:部署时执行一次
constructor() {
owner = msg.sender;
}
// 函数:修改状态
function mint(address to, uint256 tokenId) public {
require(tokenOwner[tokenId] == address(0), "Token already exists");
tokenOwner[tokenId] = to;
}
// 函数:只读,不花Gas
function getOwner(uint256 tokenId) public view returns (address) {
return tokenOwner[tokenId];
}
}
这里有几个关键概念:
- public:自动生成getter函数,外部可以直接读取。我建议状态变量尽量用public,方便调试。
- mapping:键值对存储,相当于哈希表。NFT项目里90%的数据都用mapping存。
- msg.sender:调用者地址。这是Solidity里最重要的全局变量之一。
- view:声明函数不修改状态,调用不花Gas。但注意,如果被另一个修改状态的函数调用,还是会花Gas。
我曾经踩过的坑:有一次我把一个需要修改状态的函数忘了加payable,结果用户转ETH进来直接revert。记住:凡是涉及转账的函数,必须加payable关键字。
3.4 事件与修饰符
事件是合约向外部世界「喊话」的方式。前端DApp通过监听事件,实时获取链上状态变化。
contract NFTEvent {
// 定义事件
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Mint(address indexed owner, uint256 tokenId, uint256 price);
function mintNFT(uint256 tokenId) public {
// ... 铸造逻辑
emit Mint(msg.sender, tokenId, 0.1 ether);
}
function transferNFT(address to, uint256 tokenId) public {
// ... 转账逻辑
emit Transfer(msg.sender, to, tokenId);
}
}
事件里的indexed关键字很重要。加了indexed的参数可以被前端过滤查询。我一般把地址和ID都设为indexed,方便前端做搜索。
修饰符是Solidity里很优雅的设计。它让你把重复的权限检查抽出来,代码干净很多。
contract WithModifier {
address public owner;
constructor() {
owner = msg.sender;
}
// 定义修饰符
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_; // 继续执行原函数
}
modifier validTokenId(uint256 tokenId) {
require(tokenId > 0, "Invalid token ID");
_;
}
// 使用修饰符
function withdraw() public onlyOwner {
// 只有owner能调用
payable(owner).transfer(address(this).balance);
}
function burn(uint256 tokenId) public onlyOwner validTokenId(tokenId) {
// 多个修饰符叠加使用
// 销毁逻辑
}
}
修饰符的执行顺序是从左到右。上面的例子中,先检查onlyOwner,再检查validTokenId。如果第一个检查失败,后面的就不会执行了。
核心要点:
- 状态变量是链上存储,读写都花Gas
- 事件是链上日志,前端通过监听事件获取数据
- 修饰符用于权限控制和参数校验
- view函数不花Gas,但只能读不能写
- payable函数才能接收ETH
好了,这一章的内容就到这里。说实话,Solidity入门不难,难的是写出安全、高效的合约。我建议你每学一个知识点,就去Remix里敲一遍代码,亲手部署试试。下一章我们会讲ERC-721标准,那是NFT的核心协议,敬请期待。
课后练习:写一个简单的NFT合约,包含mint函数和transfer函数,加上onlyOwner修饰符,并定义Transfer事件。部署到测试网试试看。