导语:可编程的钱
智能合约是运行在区块链上的程序,一旦部署就不可篡改、透明可验证、自动执行。它们不只是"代码"——它们持有真实的资产,控制真实的权限。
Solidity 是以太坊生态最主要的智能合约语言,语法接近 JavaScript/C++,但增加了许多区块链特有的概念:gas 限制、状态持久化、不可变性、公开透明。
一句话总结:Solidity 让开发者可以用代码定义金融规则——转账、借贷、投票、NFT 铸造,全部在无中介的情况下自动执行。
1. Solidity 语言基础
1.1 第一个合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract HelloWorld {
string public message;
address public owner;
constructor() {
message = "Hello, Solidity!";
owner = msg.sender;
}
function setMessage(string memory _message) public {
message = _message;
}
function getMessage() public view returns (string memory) {
return message;
}
}
核心语法元素:
| 元素 | 说明 | 示例 |
|---|---|---|
pragma | 版本声明 | ^0.8.20 = 0.8.x 但不低于 0.8.20 |
contract | 合约定义 | 类似 class,可包含状态变量和函数 |
address | 以太坊地址类型 | msg.sender = 调用者地址 |
public | 可见性/自动 getter | 状态变量声明后自动生成读取函数 |
view | 不修改状态的函数 | 不消耗 gas(调用)/ gas 低廉(部署后读取) |
memory | 数据位置 | 局部变量存在 memory 中(离开函数即销毁) |
msg.sender | 当前调用者 | 交易发起者的钱包地址 |
1.2 值类型与引用类型
// 值类型(赋值时拷贝)
bool isActive = true;
uint256 amount = 1000; // 无符号整数,最大 2^256-1
int256 temperature = -273; // 有符号整数
address wallet = 0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B;
bytes32 hash = keccak256(abi.encodePacked("data"));
// 引用类型(赋值时可能引用同一块内存)
string memory name = "Solidity"; // 动态大小字节数组
bytes memory data = new bytes(64);
uint256[] memory dynamicArray;
// 状态变量自动存储在 storage(合约持久化状态)
uint256[] public values; // ← 存储在 storage
1.3 函数修饰符
modifier onlyOwner() {
require(msg.sender == owner, "Not authorized");
_; // ← 被修饰函数的代码插入此处
}
modifier validAddress(address _addr) {
require(_addr != address(0), "Invalid address");
_;
}
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
可见性修饰符:
| 修饰符 | 合约内部 | 继承合约 | 外部调用 | 用途 |
|---|---|---|---|---|
private | ✅ | ❌ | ❌ | 内部工具函数 |
internal | ✅ | ✅ | ❌ | 子合约可复用 |
public | ✅ | ✅ | ✅ | 接口函数 |
external | ❌ | ❌ | ✅ | 仅外部调用 |
一句话总结:Solidity 的类型系统围绕以太坊地址和加密货币设计,
storage/memory/calldata三种数据位置直接关联 gas 成本。
2. Remix IDE 开发流程
Remix(remix.ethereum.org)是 Solidity 开发的最简单入口,无需安装任何本地工具。
2.1 标准开发步骤
1. 打开 remix.ethereum.org
2. 左侧 File Explorer → 创建文件 Hello.sol
3. 编写合约代码
4. 点击 "Compile"(自动下载对应编译器版本)
5. 切换到 "Deploy & Run Transactions"
6. Environment 选择:
├─ JavaScript VM (London/Berlin) ← 本地沙箱,即时免费
├─ Injected Web3 ← 连接 MetaMask
└─ Hardhat/Foundry Provider ← 连接本地开发网络
7. 点击 "Deploy"
8. 下方展开合约实例,调用函数
2.2 调试与测试
Remix 内置 Solidity 单元测试框架:
// contracts/Hello_test.sol
import "remix_tests.sol";
import "../contracts/Hello.sol";
contract HelloWorldTest {
HelloWorld hello;
function beforeAll() public {
hello = new HelloWorld();
}
function checkInitialMessage() public {
Assert.equal(hello.message(), "Hello, Solidity!", "初始消息不对");
}
function checkSetMessage() public {
hello.setMessage("Test");
Assert.equal(hello.message(), "Test", "设置失败");
}
}
一句话总结:Remix 是 Solidity 的"游乐场"——零配置即可编写、编译、部署和调试合约,是学习阶段的最佳环境。
3. 事件与日志
3.1 事件定义与触发
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
function transfer(address to, uint256 amount) public returns (bool) {
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount); // 触发事件
return true;
}
3.2 前端读取事件日志
// ethers.js v6
const provider = new ethers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_KEY');
const contract = new ethers.Contract(tokenAddress, ERC20_ABI, provider);
// 监听新事件
contract.on('Transfer', (from, to, value, event) => {
console.log(`${from} -> ${to}: ${ethers.formatUnits(value, 18)} tokens`);
});
// 查询历史事件
const filter = contract.filters.Transfer(null, myAddress); // 接收方为我的地址
const events = await contract.queryFilter(filter, -10000); // 最近 10000 个区块
3.3 为什么事件便宜
事件数据不存储在合约 storage 中(storage 是链上最昂贵的资源),而是写入日志(log)数据结构:
- EVM 日志存储在交易收据中,gas 成本约为每字节 8 gas(topic 中 indexed 参数每字节约 375 gas)
- 合约存储每 32 字节槽位约 20,000 gas(第一次写入)
一句话总结:事件日志是 Solidity 的"print 调试 + 前端通知"系统,成本远低于 storage,是合约与 DApp 交互的核心通道。
4. 完整实战:计数器合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface ICounter {
function increment() external;
function decrement() external;
function getCount() external view returns (uint256);
}
contract Counter is ICounter {
uint256 private count;
address public owner;
event CountChanged(uint256 newCount, address changer);
error Unauthorized(address caller);
error Underflow();
modifier onlyOwner() {
if (msg.sender != owner) {
revert Unauthorized(msg.sender);
}
_;
}
constructor(uint256 initialCount) {
count = initialCount;
owner = msg.sender;
}
function increment() public {
unchecked {
count += 1;
}
emit CountChanged(count, msg.sender);
}
function decrement() public onlyOwner {
if (count == 0) revert Underflow();
unchecked {
count -= 1;
}
emit CountChanged(count, msg.sender);
}
function getCount() public view returns (uint256) {
return count;
}
// 接收 ETH
receive() external payable {}
// 提取合约中的 ETH
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
}
一句话总结:一个完整的 Solidity 合约包含:版本声明 → 接口 → 状态变量 → 事件/错误 → 修饰符 → 构造函数 → 业务函数 → fallback/receive 函数。
5. Solidity 开发关键原则
| 原则 | 说明 |
|---|---|
| Checks-Effects-Interactions | 先校验、再修改状态、最后外部调用(防重入攻击) |
| 最小权限 | 函数可见性设为最低必要级别 |
| gas 优化 | 用 calldata 替代 memory、打包变量、使用 unchecked |
| 不要信任输入 | 假设所有外部调用者都是恶意的 |
| 使用成熟库 | OpenZeppelin 的安全合约库经过广泛审计 |
一句话总结:Solidity 开发不是普通编程——每行代码都可能涉及真实资产,安全模式不是可选优化而是生存底线。
6. 总结
Solidity 的核心概念:
- 合约即是程序:可持有 ETH、可转账、可被其他合约调用
- gas 是成本计量:所有操作消耗 gas,优化即省钱
- 不可变是基础:部署后代码不能改,设计时必须考虑升级策略
- 事件是桥梁:连接链上事件与链下 DApp 的用户界面
延伸阅读:
- ERC-20 与 ERC-721 标准详解 — 理解代币和 NFT 合约标准
- Hardhat 与 Foundry 开发工具链 — 从 Remix 到专业开发环境
- 智能合约安全审计与常见漏洞 — 安全是智能合约的生命线
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。