Solidity智能合约开发基础
章节导读
Solidity看起来像JavaScript,但思维方式完全不同。传统开发关注的是性能和用户体验,而智能合约开发最关注的是安全和Gas优化。这是学习Solidity时需要特别注意的核心差异。
本章通过大量实战代码,从零开始讲解Solidity的核心概念。不仅教语法,更重要的是教如何写出安全、省Gas的合约。
学习路线
基础语法(必须掌握)
├── 合约结构
├── 数据类型
├── 函数
└── 修饰器
核心概念(重点理解)
├── 状态变量与存储
├── 事件与日志
├── 错误处理
└── 权限控制
实战技巧(加分项)
├── Gas优化
├── 安全实践
└── 常见模式
一、第一个智能合约
1.1 Hello World
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HelloWorld {
// 状态变量:存储在区块链上
string public message;
// 构造函数:部署时执行一次
constructor(string memory _message) {
message = _message;
}
// 修改状态的函数:需要发送交易
function setMessage(string memory _message) public {
message = _message;
}
// 只读函数:不消耗Gas
function getMessage() public view returns (string memory) {
return message;
}
}
部署和调用流程:
// 1. 部署合约
const HelloWorld = await ethers.getContractFactory("HelloWorld");
const hello = await HelloWorld.deploy("Hello, World!");
await hello.deployed();
// 2. 读取message(不消耗Gas)
const msg = await hello.getMessage();
console.log(msg); // "Hello, World!"
// 3. 修改message(消耗Gas)
const tx = await hello.setMessage("New Message");
await tx.wait(); // 等待交易确认
关键概念:
- SPDX许可证:
// SPDX-License-Identifier: MIT
// 必须写,否则编译器会警告
// 常见的:MIT、GPL-3.0、UNLICENSED
- pragma版本声明:
pragma solidity ^0.8.0;
// ^0.8.0: 兼容0.8.x,但不兼容0.9.0
// 0.8.0: 只能用0.8.0
// >=0.8.0 <0.9.0: 范围版本
- public修饰符:
string public message;
// 编译器自动生成getter函数:
// function message() public view returns (string memory)
1.2 合约的生命周期
部署 → 初始化 → 使用 → (可选)销毁
部署:
- 将字节码发送到区块链
- 执行构造函数
- 返回合约地址
初始化:
- 构造函数只执行一次
- 设置初始状态
使用:
- 调用合约函数
- 修改状态或读取状态
销毁:
- 调用selfdestruct()
- 合约代码和状态被删除
- 余额转移到指定地址
二、数据类型
2.1 值类型
布尔类型
bool public isActive = true;
function toggle() public {
isActive = !isActive;
}
// 布尔运算
bool result = true && false; // false
bool result = true || false; // true
bool result = !true; // false
整数类型
// 有符号整数:int8, int16, ..., int256
int public temperature = -10;
int256 public bigNumber = -1000000;
// 无符号整数:uint8, uint16, ..., uint256
uint public age = 25;
uint256 public population = 1000000;
// uint和uint256是等价的
uint public count = 0;
整数运算:
uint a = 10;
uint b = 3;
uint sum = a + b; // 13
uint diff = a - b; // 7
uint product = a * b; // 30
uint quotient = a / b; // 3 (向下取整)
uint remainder = a % b; // 1
// 注意:Solidity 0.8.0之后,溢出会自动revert
uint max = type(uint256).max; // 2^256 - 1
uint overflow = max + 1; // revert
// 如果需要溢出回绕,使用unchecked
unchecked {
uint overflow = max + 1; // 0
}
地址类型
// 地址:20字节(160位)
address public owner;
address payable public wallet; // 可以接收以太币的地址
// 地址的属性
owner.balance; // 该地址的以太币余额(wei)
owner.code; // 该地址的合约代码
// 地址的方法
wallet.transfer(1 ether); // 转账,失败会revert
wallet.send(1 ether); // 转账,失败返回false
wallet.call{value: 1 ether}(""); // 低级调用,返回(bool, bytes)
实战示例:提现函数
contract Wallet {
address payable public owner;
constructor() {
owner = payable(msg.sender); // msg.sender是address,需要转换
}
// 接收以太币
receive() external payable {}
// 提现
function withdraw() public {
require(msg.sender == owner, "Not owner");
uint balance = address(this).balance;
owner.transfer(balance);
}
// 查询余额
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
字节类型
// 定长字节数组:bytes1, bytes2, ..., bytes32
bytes32 public hash;
bytes1 public b = 0xff;
// 变长字节数组
bytes public data;
// 字符串(本质是bytes)
string public name = "Alice";
// 字节数组操作
bytes32 b32 = keccak256("hello"); // 计算哈希
bytes memory b = new bytes(10); // 创建变长字节数组
b[0] = 0xff; // 修改
2.2 引用类型
数组
// 定长数组
uint[5] public fixedArray = [1, 2, 3, 4, 5];
// 变长数组
uint[] public dynamicArray;
// 数组操作
function arrayOps() public {
// 添加元素
dynamicArray.push(1);
dynamicArray.push(2);
// 访问元素
uint first = dynamicArray[0];
// 删除元素(不减少长度,只是将该位置置为0)
delete dynamicArray[0];
// 删除最后一个元素(减少长度)
dynamicArray.pop();
// 获取长度
uint len = dynamicArray.length;
}
// 数组作为参数
function sum(uint[] memory arr) public pure returns (uint) {
uint total = 0;
for (uint i = 0; i < arr.length; i++) {
total += arr[i];
}
return total;
}
数组的存储位置:
// storage:状态变量,存储在区块链上
uint[] public storageArray;
// memory:临时变量,函数执行后释放
function memoryExample() public pure {
uint[] memory memArray = new uint[](5);
memArray[0] = 1;
// 函数结束后,memArray被释放
}
// calldata:只读,用于函数参数
function calldataExample(uint[] calldata arr) external pure returns (uint) {
// arr不能修改,只能读取
return arr[0];
}
结构体
struct User {
address addr;
string name;
uint age;
}
// 状态变量
User public admin;
User[] public users;
// 创建结构体
function createUser() public {
// 方式1:按顺序赋值
User memory user1 = User(msg.sender, "Alice", 25);
// 方式2:命名赋值
User memory user2 = User({
addr: msg.sender,
name: "Bob",
age: 30
});
// 方式3:分步赋值
User memory user3;
user3.addr = msg.sender;
user3.name = "Charlie";
user3.age = 35;
// 添加到数组
users.push(user1);
}
映射(Mapping)
// mapping(键类型 => 值类型)
mapping(address => uint) public balances;
mapping(address => bool) public whitelist;
mapping(address => mapping(address => uint)) public allowances;
// 映射操作
function mappingOps() public {
// 写入
balances[msg.sender] = 100;
// 读取(默认值为0)
uint balance = balances[msg.sender];
// 删除(恢复为默认值)
delete balances[msg.sender];
// 注意:mapping没有length,无法遍历
}
实战示例:ERC20余额映射
contract Token {
mapping(address => uint) public balances;
mapping(address => mapping(address => uint)) public allowances;
function transfer(address to, uint amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
function approve(address spender, uint amount) public {
allowances[msg.sender][spender] = amount;
}
function transferFrom(address from, address to, uint amount) public {
require(balances[from] >= amount, "Insufficient balance");
require(allowances[from][msg.sender] >= amount, "Insufficient allowance");
balances[from] -= amount;
balances[to] += amount;
allowances[from][msg.sender] -= amount;
}
}
三、函数
3.1 函数的可见性
contract Visibility {
// public:任何人都可以调用
function publicFunc() public {}
// external:只能从外部调用,不能在内部调用
function externalFunc() external {}
// internal:只能在本合约和子合约中调用
function internalFunc() internal {}
// private:只能在本合约中调用
function privateFunc() private {}
function test() public {
publicFunc(); //
// externalFunc(); // 不能在内部调用
this.externalFunc(); // 通过this调用
internalFunc(); //
privateFunc(); //
}
}
contract Child is Visibility {
function childTest() public {
publicFunc(); //
// externalFunc(); //
this.externalFunc(); //
internalFunc(); //
// privateFunc(); // 父合约的private函数不能调用
}
}
什么时候用external?
// external比public省Gas,因为参数在calldata中,不会复制到memory
function processData(uint[] calldata data) external {
// data在calldata中,只读
}
// public会将参数复制到memory,多消耗Gas
function processData2(uint[] memory data) public {
// data在memory中,可以修改
}
3.2 函数的状态修饰符
contract StateModifiers {
uint public count = 0;
// view:只读,不修改状态
function getCount() public view returns (uint) {
return count; // 只读count
}
// pure:不读不写状态
function add(uint a, uint b) public pure returns (uint) {
return a + b; // 不涉及状态变量
}
// 默认:可读可写状态
function increment() public {
count++; // 修改状态
}
}
什么时候用view?
// 情况1:读取状态变量
uint public totalSupply = 1000000;
function getTotalSupply() public view returns (uint) {
return totalSupply;
}
// 情况2:读取其他合约的状态
function getBalance(address token) public view returns (uint) {
return IERC20(token).balanceOf(address(this));
}
什么时候用pure?
// 情况1:纯计算
function multiply(uint a, uint b) public pure returns (uint) {
return a * b;
}
// 情况2:编码/解码
function encode(address addr, uint value) public pure returns (bytes memory) {
return abi.encode(addr, value);
}
// 情况3:哈希计算
function hash(string memory str) public pure returns (bytes32) {
return keccak256(bytes(str));
}
3.3 函数修饰器(Modifier)
contract Modifiers {
address public owner;
constructor() {
owner = msg.sender;
}
// 定义修饰器
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_; // 继续执行被修饰的函数
}
// 使用修饰器
function sensitiveOperation() public onlyOwner {
// 只有owner能执行
}
// 带参数的修饰器
modifier minAmount(uint amount) {
require(msg.value >= amount, "Insufficient amount");
_;
}
function deposit() public payable minAmount(1 ether) {
// msg.value必须>= 1 ether
}
// 多个修饰器
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
bool public paused = false;
function emergencyWithdraw() public onlyOwner whenNotPaused {
// 必须是owner且合约未暂停
}
}
常见修饰器模式:
// 1. 重入保护
modifier nonReentrant() {
require(!locked, "Reentrant call");
locked = true;
_;
locked = false;
}
// 2. 有效地址检查
modifier validAddress(address addr) {
require(addr != address(0), "Invalid address");
_;
}
// 3. 有效金额检查
modifier validAmount(uint amount) {
require(amount > 0, "Amount must be positive");
_;
}
四、事件
4.1 事件的定义和使用
contract Events {
// 定义事件
event Transfer(address indexed from, address indexed to, uint value);
event Approval(address indexed owner, address indexed spender, uint value);
// 触发事件
function transfer(address to, uint value) public {
// 业务逻辑
balances[msg.sender] -= value;
balances[to] += value;
// 触发事件
emit Transfer(msg.sender, to, value);
}
}
indexed关键字:
// indexed参数可以被高效检索
event Transfer(address indexed from, address indexed to, uint value);
// 前端可以这样过滤:
const filter = contract.filters.Transfer(fromAddress, null, null);
// 查询所有from=fromAddress的Transfer事件
为什么需要事件?
- 记录历史:
// 智能合约没有历史记录,只有当前状态
// 事件可以记录所有历史操作
event UserRegistered(address indexed user, uint timestamp);
function register() public {
emit UserRegistered(msg.sender, block.timestamp);
// 链下可以查询所有注册记录
}
- 节省Gas:
// 方案1:存储在状态变量中(昂贵)
Transfer[] public transfers; // 每次push消耗大量Gas
// 方案2:使用事件(便宜)
event Transfer(...);
emit Transfer(...); // Gas成本低很多
- 前端监听:
// 监听Transfer事件
contract.on("Transfer", (from, to, value) => {
console.log(`${from} 转账 ${value} 给 ${to}`);
});
4.2 实战示例:订单系统
contract OrderSystem {
enum Status { Created, Paid, Shipped, Completed, Cancelled }
struct Order {
uint id;
address buyer;
uint amount;
Status status;
}
mapping(uint => Order) public orders;
uint public orderCount;
// 定义各种事件
event OrderCreated(uint indexed orderId, address indexed buyer, uint amount);
event OrderPaid(uint indexed orderId);
event OrderShipped(uint indexed orderId);
event OrderCompleted(uint indexed orderId);
event OrderCancelled(uint indexed orderId);
function createOrder() public payable {
orderCount++;
orders[orderCount] = Order({
id: orderCount,
buyer: msg.sender,
amount: msg.value,
status: Status.Created
});
emit OrderCreated(orderCount, msg.sender, msg.value);
}
function payOrder(uint orderId) public payable {
Order storage order = orders[orderId];
require(order.status == Status.Created, "Invalid status");
require(msg.value == order.amount, "Invalid amount");
order.status = Status.Paid;
emit OrderPaid(orderId);
}
// 前端可以监听这些事件,实时更新UI
}
五、错误处理
5.1 三种错误处理方式
contract ErrorHandling {
uint public balance = 100;
// 1. require:条件检查,失败回滚并返回剩余Gas
function withdraw1(uint amount) public {
require(amount <= balance, "Insufficient balance");
balance -= amount;
}
// 2. revert:主动回滚,返回剩余Gas
function withdraw2(uint amount) public {
if (amount > balance) {
revert("Insufficient balance");
}
balance -= amount;
}
// 3. assert:内部错误检查,失败消耗所有Gas
function withdraw3(uint amount) public {
balance -= amount;
assert(balance >= 0); // 理论上不会失败,如果失败说明有bug
}
}
什么时候用哪种?
// require:用于验证输入和条件
require(msg.sender == owner, "Not owner");
require(amount > 0, "Amount must be positive");
require(balance >= amount, "Insufficient balance");
// revert:用于复杂的条件判断
if (condition1 || condition2 || condition3) {
revert("Complex condition failed");
}
// assert:用于检查不变量(应该永远为真)
assert(totalSupply >= balances[msg.sender]);
assert(address(this).balance >= 0);
5.2 自定义错误(Gas优化)
Solidity 0.8.4引入了自定义错误,比字符串错误更省Gas:
// 定义自定义错误
error InsufficientBalance(uint requested, uint available);
error Unauthorized(address caller);
contract CustomErrors {
mapping(address => uint) public balances;
function withdraw(uint amount) public {
uint balance = balances[msg.sender];
if (amount > balance) {
revert InsufficientBalance({
requested: amount,
available: balance
});
}
balances[msg.sender] -= amount;
}
}
Gas对比:
// 方式1:字符串错误(昂贵)
require(amount <= balance, "Insufficient balance"); // ~24,000 gas
// 方式2:自定义错误(便宜)
if (amount > balance) {
revert InsufficientBalance(amount, balance); // ~15,000 gas
}
// 节省约40% Gas!
六、实战项目:众筹合约
综合运用本章所学,实现一个众筹合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Crowdfunding {
// 状态变量
address public owner;
uint public goal;
uint public deadline;
uint public totalFunds;
bool public finalized;
mapping(address => uint) public contributions;
// 事件
event Contribute(address indexed contributor, uint amount);
event GoalReached(uint totalFunds);
event Withdraw(address indexed owner, uint amount);
event Refund(address indexed contributor, uint amount);
// 自定义错误
error DeadlinePassed();
error GoalNotReached();
error AlreadyFinalized();
error NotOwner();
// 修饰器
modifier onlyOwner() {
if (msg.sender != owner) revert NotOwner();
_;
}
modifier beforeDeadline() {
if (block.timestamp > deadline) revert DeadlinePassed();
_;
}
// 构造函数
constructor(uint _goal, uint _duration) {
owner = msg.sender;
goal = _goal;
deadline = block.timestamp + _duration;
}
// 贡献
function contribute() public payable beforeDeadline {
require(msg.value > 0, "Amount must be positive");
contributions[msg.sender] += msg.value;
totalFunds += msg.value;
emit Contribute(msg.sender, msg.value);
// 检查是否达到目标
if (totalFunds >= goal) {
emit GoalReached(totalFunds);
}
}
// 提现(目标达成)
function withdraw() public onlyOwner {
if (block.timestamp < deadline) revert("Deadline not passed");
if (totalFunds < goal) revert GoalNotReached();
if (finalized) revert AlreadyFinalized();
finalized = true;
uint amount = address(this).balance;
(bool success, ) = owner.call{value: amount}("");
require(success, "Transfer failed");
emit Withdraw(owner, amount);
}
// 退款(目标未达成)
function refund() public {
if (block.timestamp < deadline) revert("Deadline not passed");
if (totalFunds >= goal) revert("Goal reached");
uint amount = contributions[msg.sender];
require(amount > 0, "No contribution");
contributions[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Refund failed");
emit Refund(msg.sender, amount);
}
// 查询剩余时间
function timeLeft() public view returns (uint) {
if (block.timestamp >= deadline) {
return 0;
}
return deadline - block.timestamp;
}
// 查询进度百分比
function progress() public view returns (uint) {
return (totalFunds * 100) / goal;
}
}
测试脚本:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Crowdfunding", function () {
it("Should allow contributions", async function () {
const [owner, addr1] = await ethers.getSigners();
const Crowdfunding = await ethers.getContractFactory("Crowdfunding");
const crowdfunding = await Crowdfunding.deploy(
ethers.utils.parseEther("10"), // goal: 10 ETH
7 * 24 * 60 * 60 // duration: 7 days
);
// 贡献1 ETH
await crowdfunding.connect(addr1).contribute({
value: ethers.utils.parseEther("1")
});
expect(await crowdfunding.totalFunds()).to.equal(
ethers.utils.parseEther("1")
);
});
});