HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • Web3 完整技术体系

    • Web3 完整技术体系
    • 区块链基础与密码学
    • 第一章:比特币原理与实现
    • 第二章:以太坊架构与核心概念
    • Solidity智能合约开发基础
    • 04-Solidity进阶与安全
    • 05-ERC标准详解
    • 06-DeFi核心协议-去中心化交易所
    • 07-DeFi核心协议-借贷协议
    • 08-DeFi核心协议-稳定币
    • NFT与元宇宙
    • Layer2扩容方案
    • 跨链技术
    • Web3前端开发
    • 链下数据与预言机
    • 智能合约测试与部署
    • MEV与交易优化
    • DAO治理
    • 项目实战:完整DeFi借贷协议

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(); // 等待交易确认

关键概念:

  1. SPDX许可证:
// SPDX-License-Identifier: MIT
// 必须写,否则编译器会警告
// 常见的:MIT、GPL-3.0、UNLICENSED
  1. 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: 范围版本
  1. 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事件

为什么需要事件?

  1. 记录历史:
// 智能合约没有历史记录,只有当前状态
// 事件可以记录所有历史操作

event UserRegistered(address indexed user, uint timestamp);

function register() public {
    emit UserRegistered(msg.sender, block.timestamp);
    // 链下可以查询所有注册记录
}
  1. 节省Gas:
// 方案1:存储在状态变量中(昂贵)
Transfer[] public transfers; // 每次push消耗大量Gas

// 方案2:使用事件(便宜)
event Transfer(...);
emit Transfer(...); // Gas成本低很多
  1. 前端监听:
// 监听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")
        );
    });
});

Prev
第二章:以太坊架构与核心概念
Next
04-Solidity进阶与安全