学习资料
solidity
学习
// 使用0.4.0以上版本运行没问题(最高到0.5.0,但是不包含0.5.0)
pragma solidity ^0.4.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}
Solidity中合约的含义就是一组代码(它的 函数 )和数据(它的 状态 ),它们位于以太坊区块链的一个特定地址上。 代码行
uint storedData;声明一个类型为uint(256位无符号整数)的状态变量,叫做storedData。 你可以认为它是数据库里的一个位置,可以通过调用管理数据库代码的函数进行查询和变更。对于以太坊来说,上述的合约就是拥有合约(owning contract)。在这种情况下,函数set和get可以用来变更或取出变量的值。
Solidity 0.8.0 以前的自动溢出 - 致命bug
1. 什么是「整数溢出」?
Solidity 里的数字有上限,超过上限就会乱变,这就叫溢出。
举个最最简单的例子:
uint8 是 8 位整数,只能存 0 ~ 255
就像一个最多装 255 个球的盒子:
- 装 255 个 → 满了
- 再加 1 个 → 盒子直接变空(变成 0) 这就是溢出(数字爆了,乱跳)
2. 什么是「下溢」?
和溢出相反:数字小于 0,直接跳成最大值,比如 uint8 是 0,再减 1 → 直接变成 255
溢出 255 + 1 -> 0 下溢 0 - 1 -> 255
Solidity 0.8.0 之前(危险!)
规则:溢出 / 下溢 → 不报错、不提醒、直接乱变数字;黑客就是靠这个:让余额下溢变成无限大,偷光合约里的钱
自动溢出检查
编译器自动检查,溢出直接报错、交易作废,绝对安全
编译器会自动针对 + - * / % ++ -- 做一些判断,强制类型转换不支持。
但这需要额外的 gas 费,
如果不需要 可以使用 unchecked 关闭自动检查,可以节省 gas 费用!
pragma solidity ^0.8.0;
contract Test {
uint256 public num = 100;
function add() public {
// 明确告诉编译器:这里不会溢出,别检查
unchecked {
num = num + 1;
}
}
}
数据类型:
1. 值类型(赋值复制,最常用 | 共 9 种)
变量存的是数据本身,修改副本不会影响原数据
bool(布尔):true/falseuint(无符号整数):正整数(你最熟的uint256属于它)int(有符号整数):正负整数address(普通地址):钱包 / 合约地址address payable(可支付地址):能收 ETH 的地址bytes1~bytes32(定长字节):固定长度的二进制数据enum(枚举):自定义常量列表ufixed(无符号小数):极冷门fixed(有符号小数):极冷门
2. 引用类型(赋值传地址 | 共 3 种)
变量存的是数据的位置,修改会直接改原数据
bytes(动态字节):不固定长度的二进制string(字符串):存文字、中文array(数组):存一组数据([1,2,3])
3. 映射类型(键值对 | 仅 1 种)
区块链最核心存储结构,类似字典
mapping:mapping(address => uint256)查余额必备
4. 函数类型(仅 1 种)
function:定义合约的方法
5. 自定义 / 特殊类型(共 4 种)
struct(结构体):自定义复合数据(打包多个变量)contract(合约类型):合约本身interface(接口):合约交互标准library(库):工具函数集合
8种高频
booluintaddressaddress payablestringarraymappingstruct
mapping
mapping(address => uint256) public balances; // 输入地址 → 立刻查到余额
// 标准格式
mapping(键类型 => 值类型) 变量名;
// 必须写在合约内,作为【状态变量】(存在链上)
键 (Key) 只能用:值类型(
uint/address/bool/bytes32/enum) ❌ 键不能用:string/数组/struct✅ 值 (Value) 随便用:所有类型都可以(
uint/address/struct/数组/另一个mapping)
五大规则
规则 1: 读取不存在的键 → 不报错,返回「默认值」
- 没给地址存过余额 → 读取返回
0 - 没存过布尔值 → 返回
false - 没存过地址 → 返回
0x0000000000000000000000000000000000000000
⚠️ 危险提醒:这不是 bug,是默认行为!
规则 2:永久存在链上(storage)
- 只要赋值,数据永远保存在区块链上
- 关浏览器、重启节点,数据都不会丢
- gas 费用极低(EVM 专门优化的结构)
规则 3:默认不支持遍历!
这是最大的坑!
✅ 能做:地址 → 余额(查单个)
❌ 不能做:获取所有地址的余额(遍历全部)
✅ 解决遍历的标准方案:配合数组存储所有键
address[] public allUsers; // 用数组存所有键
mapping(address => uint256) public balances;
规则 4:声明为 public → 自动生成查询函数 你写:
mapping(address => uint256) public balances;
编译器自动帮你生成这个函数,不用自己写:
function balances(address account) public view returns(uint256) {
return balances[account];
}
规则 5:支持嵌套 mapping(超级常用) 用于授权、多维度数据,比如 ERC20 授权:
// 授权:owner => spender => 额度
mapping(address => mapping(address => uint256)) public allowance;
mapping 遍历
pragma solidity ^0.8.0;
contract MappingIterate {
// 1. 核心 mapping:地址 => 余额
mapping(address => uint256) public balances;
// 2. 数组:存储所有 key(地址),专门用来遍历
address[] public allAccounts;
// 3. 新增/更新数据(必须同时写 mapping + 数组)
function setBalance(address account, uint256 amount) public {
// 第一次设置时,才把地址加入数组(避免重复)
if (balances[account] == 0) {
allAccounts.push(account);
}
// 更新 mapping
balances[account] = amount;
}
// ✅ 【遍历方法】返回所有地址 + 对应余额
function getAllBalances() public view returns(address[] memory, uint256[] memory) {
uint256 length = allAccounts.length;
// 创建临时数组存储结果
address[] memory addrs = new address[](length);
uint256[] memory vals = new uint256[](length);
// 遍历数组,逐个读取 mapping 值
for (uint256 i = 0; i < length; i++) {
addrs[i] = allAccounts[i];
vals[i] = balances[allAccounts[i]];
}
return (addrs, vals);
}
// 获取数组长度(知道有多少条数据)
function getAccountCount() public view returns(uint256) {
return allAccounts.length;
}
}
interface 详解
- 只有合约地址(address) 能绑定 interface / 合约,其他类型都不行;
- 一个地址 可以绑定无数个 interface(只要合约实现了对应函数);
- address 还可以绑定 contract
数据位置
storage/memory/calldata
his 是合约自己, 类型 payable 地址; 调用 external 必须用, 获取地址全靠它; 内部调用省 gas, this 调用算外部!
this= 当前合约- 类型 =
address payable - 唯一必须用的场景:调用自己的 external 函数
this.func()= 外部调用,触发所有校验- 不要滥用,仅在必要时使用
function
// 完整格式([] 表示可选)
function 函数名(参数列表) [可见性] [状态可变性] [modifier] [returns(返回值列表)] {
// 业务逻辑
}
// 加法函数:接收两个uint,返回它们的和
function add(uint256 a, uint256 b) public pure returns(uint256) {
return a + b;
}
引用类型
只有这 5 种,全部需要指定存储位置:
- 动态数组:
uint[]/address[] - 固定长度数组:
uint[10] - 字符串:
string(本质是特殊的字节数组) - 动态字节数组:
bytes - 结构体:
struct - 特殊:
mapping(只能存在storage,不能用memory/calldata)
铁律 1:必须指定存储位置
只要是引用类型,且不是合约状态变量,就必须写 storage/memory/calldata
不写 → 直接编译报错!
铁律 2:默认都是浅拷贝(共享数据)
只有 storage → memory / calldata → memory 是深拷贝,其余全是浅拷贝。
铁律 3:存储位置 = 生命周期 + Gas 成本
| 位置 | 生命周期 | 可修改 | Gas | 适用场景 |
|---|---|---|---|---|
storage | 永久链上 | ✅ | 最贵 | 状态变量 |
memory | 函数执行期间 | ✅ | 中等 | 临时变量 / 函数参数 |
calldata | 函数执行期间 | ❌ 只读 | 最低 | external 函数参数 |
call vs delegatecall
| 特性 | A.call(B) | A.delegatecall(B) |
|---|---|---|
| 执行上下文 | 目标合约 B | 当前合约 A |
msg.sender | 合约 A | 原始调用者(谁调了 A,谁就是 msg.sender) |
address(this) | 合约 B | 合约 A |
msg.value | 可以发送 ETH | 不能发送 ETH(保留原始调用值) |
| 存储 (Storage) | 修改 B 的状态 | 修改 A 的状态 |
合约的存储布局