概述
Solidity是以太坊智能合约的主要编程语言,是一种静态类型、面向合约的高级语言。本文整理了Solidity开发中的核心概念,包括数据类型、地址操作、ABI编码、存储机制、Gas优化以及函数修饰符等关键知识点,适合初学者系统学习和开发者快速参考。
一、基本数据类型
1.1 整型(Integer Types)
Solidity提供有符号整型(int)和无符号整型(uint),支持8到256位的多种长度:
| 类型 | 别名 | 范围 |
|---|---|---|
uint |
uint256 |
0 到 2²⁵⁶-1 |
int |
int256 |
-2²⁵⁵ 到 2²⁵⁵-1 |
uint8 |
- | 0 到 255 |
uint32 |
- | 0 到 2³²-1 |
uint128 |
- | 0 到 2¹²⁸-1 |
重要提示:在Solidity 0.8.0之前,整型运算溢出会被静默截断(truncate)。从0.8.0开始,溢出会自动触发revert。如需旧版行为,可使用unchecked块。
// Solidity 0.8.0+ 溢出处理示例
uint8 a = 255;
uint8 b = a + 1; // 会revert
unchecked {
uint8 c = a + 1; // c = 0,溢出回绕
}
1.2 布尔型(Boolean)
bool public isActive = true;
bool public isPaused = false;
// 支持的运算符:!、&&、||、==、!=
1.3 字节类型(Bytes)
| 类型 | 说明 | 使用场景 |
|---|---|---|
bytes1 到 bytes32 |
固定长度字节数组 | 存储哈希值、签名等 |
bytes |
动态长度字节数组 | 任意长度二进制数据 |
string |
动态长度UTF-8字符串 | 文本数据 |
bytes32 public hash = keccak256("hello");
bytes public data = hex"001122";
string public name = "MyToken";
二、地址类型详解
2.1 address vs address payable
Solidity中的地址类型用于存储20字节的以太坊地址,分为两种:
| 类型 | 说明 | 可用成员 |
|---|---|---|
address |
普通地址,不能直接接收ETH | balance, code, codehash, call, delegatecall, staticcall |
address payable |
可支付地址,可以接收ETH | 上述所有 + transfer, send |
2.2 ETH转账方法对比
| 方法 | Gas限制 | 失败处理 | 推荐程度 |
|---|---|---|---|
transfer() |
2300 gas | 自动revert | ⚠️ 不推荐(Gas不足风险) |
send() |
2300 gas | 返回bool | ⚠️ 不推荐 |
call{value:}("") |
可配置 | 返回(bool, bytes) | ✅ 推荐使用 |
2.3 地址操作完整示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract AddressDemo {
address public owner;
address payable public treasury;
// 记录各地址余额快照
uint public ownerBalance;
uint public treasuryBalance;
uint public contractBalance;
constructor() payable {
owner = msg.sender;
treasury = payable(0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2);
_updateBalances();
}
// 使用transfer转账(不推荐,Gas固定2300)
function sendWithTransfer() public payable {
treasury.transfer(1 ether);
_updateBalances();
}
// 使用call转账(推荐方式)
function sendWithCall() public payable {
(bool success, ) = treasury.call{value: 1 ether}("");
require(success, "Transfer failed");
_updateBalances();
}
// 更新余额快照
function _updateBalances() private {
ownerBalance = owner.balance;
treasuryBalance = treasury.balance;
contractBalance = address(this).balance;
}
// 接收ETH
receive() external payable {}
}
三、ABI编码与函数选择器
3.1 什么是ABI?
ABI(Application Binary Interface,应用二进制接口)是以太坊生态系统中与智能合约交互的标准接口。它定义了:
- 如何编码函数调用数据
- 如何解码返回值
- 事件的编码格式
3.2 函数选择器(Function Selector)
函数调用数据的前4个字节是函数选择器,用于标识要调用的函数:
// 函数选择器 = keccak256("函数签名")的前4字节
bytes4 selector = bytes4(keccak256("transfer(address,uint256)"));
// selector = 0xa9059cbb
3.3 ABI编码函数
| 函数 | 说明 | 使用场景 |
|---|---|---|
abi.encode(...) |
标准ABI编码 | 一般数据编码 |
abi.encodePacked(...) |
紧凑编码,无填充 | 哈希计算、节省空间 |
abi.encodeWithSelector(selector, ...) |
带选择器的编码 | 底层调用 |
abi.encodeWithSignature(sig, ...) |
带签名的编码 | 底层调用 |
abi.decode(data, (types)) |
解码ABI数据 | 解析返回值 |
3.4 安全转账实现示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SafeTransferDemo {
address public owner;
mapping(address => uint256) public balances;
// 预计算函数选择器,节省Gas
bytes4 private constant TRANSFER_SELECTOR =
bytes4(keccak256(bytes("transfer(address,uint256)")));
constructor() {
owner = msg.sender;
balances[msg.sender] = 10000;
balances[address(this)] = 10000;
}
// 标准转账函数
function transfer(address to, uint256 value) public returns (bool) {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] -= value;
balances[to] += value;
return true;
}
// 安全转账:通过底层call调用,处理返回值
function _safeTransfer(
address token,
address to,
uint256 value
) internal {
// 编码调用数据
(bool success, bytes memory data) = token.call(
abi.encodeWithSelector(TRANSFER_SELECTOR, to, value)
);
// 检查调用是否成功
// 兼容不返回值的代币(如USDT)
require(
success && (data.length == 0 || abi.decode(data, (bool))),
"Transfer failed"
);
}
// 测试安全转账
function testSafeTransfer() public {
_safeTransfer(
address(this),
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,
100
);
}
}
四、CREATE2与合约部署
4.1 CREATE vs CREATE2
| 特性 | CREATE | CREATE2 |
|---|---|---|
| 地址计算 | sender + nonce | sender + salt + bytecode |
| 地址可预测 | ❌ 依赖nonce | ✅ 完全确定 |
| 使用场景 | 普通部署 | 工厂合约、状态通道 |
4.2 CREATE2地址计算公式
address = keccak256(0xff ++ deployerAddress ++ salt ++ keccak256(bytecode))[12:]
4.3 type(C).creationCode
type(C).creationCode返回合约C的创建字节码,常用于CREATE2部署:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// 交易对合约
contract Pair {
address public token0;
address public token1;
function initialize(address _token0, address _token1) external {
token0 = _token0;
token1 = _token1;
}
}
// 工厂合约
contract PairFactory {
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
// 预计算INIT_CODE_HASH,用于链下计算Pair地址
bytes32 public constant INIT_CODE_HASH =
keccak256(abi.encodePacked(type(Pair).creationCode));
event PairCreated(
address indexed token0,
address indexed token1,
address pair,
uint256 index
);
function createPair(
address tokenA,
address tokenB
) external returns (address pair) {
require(tokenA != tokenB, "Identical addresses");
// 排序token地址,确保唯一性
(address token0, address token1) = tokenA < tokenB
? (tokenA, tokenB)
: (tokenB, tokenA);
require(token0 != address(0), "Zero address");
require(getPair[token0][token1] == address(0), "Pair exists");
// 获取Pair合约的创建字节码
bytes memory bytecode = type(Pair).creationCode;
// 使用两个token地址作为salt
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
// 使用CREATE2部署合约
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
// 初始化Pair合约
Pair(pair).initialize(token0, token1);
// 更新映射
getPair[token0][token1] = pair;
getPair[token1][token0] = pair;
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
// 链下计算Pair地址(无需调用合约)
function computePairAddress(
address token0,
address token1
) external view returns (address) {
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
return address(uint160(uint256(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
INIT_CODE_HASH
)))));
}
}
五、数据存储位置
5.1 Storage vs Memory vs Calldata
| 位置 | 持久性 | Gas成本 | 使用场景 |
|---|---|---|---|
storage |
永久存储在区块链 | 高(SSTORE: 20000 gas) | 状态变量 |
memory |
函数执行期间 | 中等 | 函数内临时变量 |
calldata |
只读,函数执行期间 | 最低 | external函数参数 |
5.2 存储位置规则
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StorageDemo {
// 状态变量:自动存储在storage
string public name = "StorageDemo";
uint256[] public numbers;
struct User {
string name;
uint256 balance;
}
mapping(address => User) public users;
// 函数参数:string/bytes/数组/结构体需要指定位置
function updateName(string memory newName) public {
// memory变量:函数内临时使用
string memory tempName = newName;
// 写入storage(永久保存)
name = tempName;
}
// calldata:只读,Gas更低
function processData(string calldata data) external pure returns (uint256) {
return bytes(data).length;
}
// storage引用:直接修改状态变量
function updateUser(address addr, uint256 newBalance) public {
// 获取storage引用,修改会直接影响状态
User storage user = users[addr];
user.balance = newBalance;
}
// memory复制:修改不影响原数据
function getUserCopy(address addr) public view returns (User memory) {
// 复制到memory,返回副本
User memory user = users[addr];
return user;
}
}
5.3 常见错误与解决方案
// ❌ 错误:值类型不能指定memory
// uint memory var1 = 0;
// TypeError: Storage location can only be given for array or struct types.
// ✅ 正确:值类型直接声明
uint256 var1 = 0;
// ❌ 错误:函数内string未指定位置
// function test(string name) { }
// TypeError: Data location must be specified
// ✅ 正确:指定memory或calldata
function test1(string memory name) public { }
function test2(string calldata name) external { }
六、函数修饰符详解
6.1 view、pure、payable对比
| 修饰符 | 读取状态 | 修改状态 | 接收ETH | Gas消耗 |
|---|---|---|---|---|
| (无) | ✅ | ✅ | ❌ | 需要Gas |
view |
✅ | ❌ | ❌ | 外部调用免费 |
pure |
❌ | ❌ | ❌ | 外部调用免费 |
payable |
✅ | ✅ | ✅ | 需要Gas |
6.2 view函数
view函数承诺只读取状态,不修改状态:
contract ViewDemo {
uint256 public value = 100;
// view函数:可以读取状态变量
function getValue() public view returns (uint256) {
return value;
}
// view函数不能做的事:
// - 写入状态变量
// - 发出事件
// - 创建合约
// - 使用selfdestruct
// - 发送ETH
// - 调用非view/pure函数
}
6.3 pure函数
pure函数是最严格的,既不读取也不修改状态:
// SafeMath库:所有函数都是pure的
library SafeMath {
function add(uint256 x, uint256 y) internal pure returns (uint256 z) {
require((z = x + y) >= x, "SafeMath: addition overflow");
}
function sub(uint256 x, uint256 y) internal pure returns (uint256 z) {
require((z = x - y) <= x, "SafeMath: subtraction underflow");
}
function mul(uint256 x, uint256 y) internal pure returns (uint256 z) {
require(y == 0 || (z = x * y) / y == x, "SafeMath: multiplication overflow");
}
function div(uint256 x, uint256 y) internal pure returns (uint256 z) {
require(y > 0, "SafeMath: division by zero");
z = x / y;
}
}
contract PureDemo {
// pure函数不能做的事(除了view的限制外):
// - 读取状态变量
// - 访问address(this).balance
// - 访问block、tx、msg的成员(msg.sig和msg.data除外)
// - 调用非pure函数
function calculate(uint256 a, uint256 b) public pure returns (uint256) {
return a * b + a / b;
}
function hash(string memory input) public pure returns (bytes32) {
return keccak256(abi.encodePacked(input));
}
}
6.4 payable函数
contract PayableDemo {
event Received(address sender, uint256 amount);
// payable构造函数:部署时可以发送ETH
constructor() payable { }
// payable函数:可以接收ETH
function deposit() public payable {
emit Received(msg.sender, msg.value);
}
// receive函数:接收纯ETH转账
receive() external payable {
emit Received(msg.sender, msg.value);
}
// fallback函数:处理未匹配的调用
fallback() external payable {
emit Received(msg.sender, msg.value);
}
}
七、Gas估算与优化
7.1 Gas估算方法
在Remix中估算Gas:
// Remix控制台脚本
(async () => {
try {
const result = await web3.eth.estimateGas({
to: "0xContractAddress",
data: "0xFunctionSelectorAndParams"
});
console.log("Estimated Gas:", result);
} catch (e) {
console.log("Error:", e.message);
}
})();
在Node.js中估算Gas:
const Web3 = require('web3');
const web3 = new Web3('https://mainnet.infura.io/v3/YOUR_PROJECT_ID');
async function estimateGas() {
const gas = await web3.eth.estimateGas({
to: "0xContractAddress",
data: "0xFunctionSelectorAndParams"
});
console.log("Estimated Gas:", gas);
}
estimateGas();
7.2 Gas优化技巧
| 优化技巧 | 说明 | Gas节省 |
|---|---|---|
使用calldata替代memory |
只读参数使用calldata | ~60 gas/参数 |
| 打包存储变量 | 将小于32字节的变量放在一起 | ~20000 gas/slot |
使用++i替代i++ |
前置递增更高效 | ~5 gas/次 |
| 缓存数组长度 | 循环前缓存array.length |
~100 gas/循环 |
使用immutable |
部署时设置,后续读取便宜 | ~200 gas/读取 |
使用constant |
编译时常量 | ~200 gas/读取 |
使用unchecked |
跳过溢出检查(确保安全时) | ~40 gas/运算 |
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GasOptimization {
// ✅ 使用constant和immutable
uint256 public constant FIXED_VALUE = 100;
address public immutable OWNER;
uint256[] public data;
constructor() {
OWNER = msg.sender;
}
// ❌ 未优化版本
function sumBad(uint256[] memory arr) public pure returns (uint256) {
uint256 total = 0;
for (uint256 i = 0; i < arr.length; i++) {
total = total + arr[i];
}
return total;
}
// ✅ 优化版本
function sumGood(uint256[] calldata arr) public pure returns (uint256) {
uint256 total = 0;
uint256 len = arr.length; // 缓存长度
for (uint256 i = 0; i < len; ) {
unchecked {
total += arr[i];
++i; // 前置递增
}
}
return total;
}
}
八、常用全局变量
8.1 区块和交易属性
| 变量 | 类型 | 说明 |
|---|---|---|
block.number |
uint256 |
当前区块号 |
block.timestamp |
uint256 |
当前区块时间戳(秒) |
block.basefee |
uint256 |
当前区块基础费用 |
block.chainid |
uint256 |
链ID |
block.coinbase |
address payable |
区块矿工地址 |
msg.sender |
address |
消息发送者 |
msg.value |
uint256 |
发送的ETH数量(wei) |
msg.data |
bytes calldata |
完整的calldata |
msg.sig |
bytes4 |
函数选择器 |
tx.origin |
address |
交易发起者(不推荐使用) |
tx.gasprice |
uint256 |
交易Gas价格 |
gasleft() |
uint256 |
剩余Gas |
九、参考资源
- Solidity官方文档:https://docs.soliditylang.org/
- OpenZeppelin合约库:https://github.com/OpenZeppelin/openzeppelin-contracts
- CREATE2详解:https://hackernoon.com/using-ethereums-create2-nw2137q7
- Pure和View详解:https://cryptomarketpool.com/pure-and-view-in-solidity-smart-contracts/
- Gas优化技巧:https://www.rareskills.io/post/gas-optimization
总结
本文涵盖了Solidity开发的核心知识点:
- 数据类型:理解整型范围和溢出处理,正确使用字节和字符串类型
- 地址操作:区分
address和address payable,优先使用call进行ETH转账 - ABI编码:掌握函数选择器计算和各种编码函数的使用场景
- CREATE2:理解确定性地址部署的原理和应用
- 存储位置:正确选择
storage、memory、calldata - 函数修饰符:合理使用
view、pure、payable优化Gas消耗 - Gas优化:应用各种技巧降低合约执行成本
掌握这些基础知识,将为进一步学习DeFi协议、NFT开发和高级智能合约设计打下坚实基础。