Solidity 智能合约基础
Solidity 简介与环境搭建
什么是 Solidity
Solidity 是一种面向合约的高级编程语言,主要用于在以太坊虚拟机(EVM)上编写智能合约。它的语法受到 JavaScript、C++ 和 Python 的影响,设计目标是让开发者能够编写去中心化应用(DApp)的逻辑。
智能合约是运行在区块链上的自执行程序,一旦部署,其代码和状态就不可篡改,这赋予了它无需可信第三方的自治能力。Solidity 是目前以太坊生态中使用最广泛的智能合约语言。
开发环境准备
初学者无需搭建本地区块链环境,可以直接使用在线集成开发环境 Remix IDE(https://remix.ethereum.org)。Remix 提供了代码编辑、编译、部署和调试的全套功能,内置一个 JavaScript 虚拟机模拟以太坊网络,可以零成本开始学习。
如果你希望本地开发,常用工具链包括:
- Node.js + npm:管理项目依赖
- Hardhat 或 Truffle:本地开发框架,提供编译、测试、部署脚本
- Ganache:本地个人区块链,用于快速测试
本教程将以 Remix 为主要演示环境。
第一个合约:Hello World
在 Remix 中创建一个新文件 HelloWorld.sol,输入以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract HelloWorld {
string public greeting = "Hello, Solidty!";
function getGreeting() public view returns (string memory) {
return greeting;
}
function setGreeting(string memory _newGreeting) public {
greeting = _newGreeting;
}
}
- SPDX 许可证标识:第一行注释声明许可证,是 Solidity 社区的规范。
- 版本声明:
pragma solidity ^0.8.0;表示使用 0.8.0 及以上版本编译器,避免语法兼容问题。 - contract 关键字:类似面向对象中的类,一个合约可以包含状态变量和函数。
- 状态变量
greeting:永久存储在区块链上,public修饰符自动生成一个同名的 getter 函数。 - 函数:
getGreeting读取数据(view表示不修改状态),setGreeting修改数据。
部署到 Remix 的虚拟机后,点击按钮就可以读写问候语,这让你直观感受到合约的存储与交互。
数据类型与结构
基本值类型
Solidity 提供丰富的值类型,它们是按值传递的。
| 类型 | 说明 | 示例 |
|---|---|---|
bool |
布尔值,取 true 或 false |
bool public isReady = true; |
int / uint |
有符号/无符号整数,从 8 位到 256 位,以 8 位为步长 | uint256 public count = 100; |
address |
20 字节的以太坊地址,拥有余额和转账相关成员 | address public owner = 0xAb8...; |
bytes1 到 bytes32 |
固定长度字节数组 | bytes32 public hash = "0x12..."; |
string |
动态长度 UTF-8 字符串 | string public name = "Alice"; |
注意:在 Solidity 0.8.0 之后,整数运算默认带有溢出检查,安全度大幅提升。
引用类型与数据位置
引用类型包括数组、结构体和映射。它们的使用方式依赖于 数据位置(data location):
storage:状态变量的默认位置,数据永久存储在链上,读写开销大。memory:临时变量,函数调用期间存在,调用后释放,用于局部变量。calldata:函数参数的特殊数据位置,不可修改,直接来自交易输入数据,成本低。
代码示例:
uint[] public storageArray; // storage 数组
function testMemory(uint[] memory _input) public pure returns (uint) {
uint[] memory memArray = new uint[](3); // memory 数组
return memArray.length;
}
结构体与映射
结构体(struct) 允许自定义组合类型:
struct Person {
string name;
uint age;
address wallet;
}
Person public person;
映射(mapping) 是一种键值对存储结构,类似于哈希表,语法为 mapping(keyType => valueType)。映射只能存在于 storage 中,不能作为函数参数的 memory 类型。
mapping(address => uint) public balances;
操作映射:
balances[msg.sender] = 100;
uint userBalance = balances[msg.sender];
函数与修饰器
函数基本语法
function 函数名(参数列表) 可见性修饰符 状态修饰符 returns (返回类型) {
// 逻辑
}
常见可见性修饰符:
public:内部和外部均可访问private:仅当前合约内部可见internal:当前合约和派生合约可见external:仅外部调用(不能通过this调用,但可被外部交易触发)
状态修饰符:
view:不修改状态,可读取状态pure:既不读取也不修改状态,纯计算payable:允许函数接收以太币
函数返回与解构
Solidity 支持多返回值:
function getInfo() public view returns (string memory, uint) {
return ("Alice", 30);
}
function useInfo() public view {
(string memory name, uint age) = getInfo(); // 元组解构
}
修饰器(modifier)
修饰器用于在函数执行前后进行条件检查,增强可读性和复用性。常用场景:权限控制、重入锁等。
modifier onlyOwner() {
require(msg.sender == owner, "Not the contract owner");
_; // 代表函数体执行位置
}
function changeOwner(address _newOwner) public onlyOwner {
owner = _newOwner;
}
修饰器可以带参数,实现更灵活的校验。
合约继承与接口
继承
Solidity 支持多重继承,使用 is 关键字。子合约可以重写父合约函数,必须在函数上标记 virtual 和 override。
contract Ownable {
address public owner;
modifier onlyOwner() { ... }
}
contract Pausable is Ownable {
bool public paused;
function pause() public onlyOwner { ... }
}
抽象合约与接口
抽象合约 包含至少一个未实现的函数(没有函数体的 virtual 函数),不能直接部署,必须被继承并实现。
abstract contract Animal {
function makeSound() public virtual returns (string memory);
}
接口(interface) 是完全抽象的合约,不能包含任何实现,也不能定义状态变量和构造函数。接口是与其他合约交互的标准方式。
interface IERC20 {
function transfer(address to, uint amount) external returns (bool);
}
接口中的函数必须是 external,所有声明自动为 virtual。
事件、回退与接收器
事件(Events)
事件允许将数据记录到区块链的交易日志中,成本低于存储,前端可以监听这些日志。语法:
event Transfer(address indexed from, address indexed to, uint value);
function transfer(address to, uint value) public {
emit Transfer(msg.sender, to, value);
}
indexed 参数最多三个,可作为过滤条件索引。
错误处理与 require
除了传统的 require、assert、revert,Solidity 0.8.4 后支持自定义错误,可以节省 gas。
error InsufficientBalance(uint requested, uint available);
function withdraw(uint amount) public {
if (balances[msg.sender] < amount)
revert InsufficientBalance(amount, balances[msg.sender]);
// 转账逻辑
}
回退函数与接收器
合约可以定义 fallback 和 receive 来处理直接转账或调用不存在的函数。
receive():合约接收纯以太币转帐时触发,必须声明为external payable。fallback():当调用的函数签名不匹配,或转账数据非空时触发。
event Received(address sender, uint amount);
receive() external payable {
emit Received(msg.sender, msg.value);
}
fallback() external payable { }
实战:发行简单代币(ERC-20 标准)
标准接口概览
ERC-20 是以太坊同质化代币的技术标准,规定了必须实现的事件和函数:
totalSupply()
balanceOf(address)
transfer(address,uint)
transferFrom(address,address,uint)
approve(address,uint)
allowance(address,address)
事件:Transfer, Approval。
完整实现示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleToken {
mapping(address => uint) public balanceOf;
mapping(address => mapping(address => uint)) public allowance;
uint public totalSupply;
string public name = "My Token";
string public symbol = "MTK";
uint8 public decimals = 18;
event Transfer(address indexed from, address indexed to, uint value);
event Approval(address indexed owner, address indexed spender, uint value);
constructor(uint _initialSupply) {
totalSupply = _initialSupply * (10 ** uint(decimals));
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address to, uint value) external returns (bool) {
require(balanceOf[msg.sender] >= value, "Insufficient balance");
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
return true;
}
function approve(address spender, uint value) external returns (bool) {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transferFrom(address from, address to, uint value) external returns (bool) {
require(balanceOf[from] >= value, "Insufficient balance");
require(allowance[from][msg.sender] >= value, "Not allowed");
allowance[from][msg.sender] -= value;
balanceOf[from] -= value;
balanceOf[to] += value;
emit Transfer(from, to, value);
return true;
}
}
部署后,你可以使用 Remix 的交互面板查看总供应、转账、授权等操作。这是理解 DeFi 应用的基础。
安全注意事项与最佳实践
常见漏洞与防范
- 重入攻击:攻击者在转账回调中重复调用合约。解决:使用“检查-效果-交互”模式,或添加重入锁修饰器。
- 整数溢出/下溢:0.8.0 之后编译器默认检查,但使用
unchecked块时需谨慎。 - 访问控制缺陷:检查
onlyOwner等修饰器是否正确,避免误用tx.origin做鉴权。 - 前端运行与抢先交易:依赖精确时间戳或区块高度可能导致问题,应使用随机性更强的方案或预言机。
- 未初始化存储指针:结构体、数组在 storage 中声明时需显式初始化,否则可能覆盖意外位置的存储。
开发习惯建议
- 使用最新稳定版编译器,并采用 0.8.x 以获得默认安全检查。
- 状态变量声明时一律赋予初始值。
- 充分利用自定义错误节省 gas。
- 编写完备的测试用例(Hardhat + Mocha 或 Truffle 测试)。
- 主网部署前,务必在测试网(如 Sepolia)进行充分测试并经过安全审计。
学习路径与后续进阶
掌握本教程内容后,你已经具备编写基本智能合约的能力。下一步可以深入以下方向:
- OpenZeppelin 合约库:使用经过审计的安全合约模板。
- NFT 标准 (ERC-721/1155):学习非同质化代币的实现。
- DeFi 协议:研究 Uniswap V2/V3 的代码结构,理解流动性池和 AMM。
- 升级合约模式:代理模式(Proxy)与钻石标准(EIP-2535)。
- Gas 优化:深入理解 EVM 操作码,优化存储和循环。
- 预言机(Chainlink):将链下数据安全引入合约。
Solidity 的学习需要大量的编码实践,建议持续在 Remix 或本地框架中尝试编写、测试和部署合约,逐步积累经验。