跳到主要内容

学习资料

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 种)

变量存的是数据本身,修改副本不会影响原数据

  1. bool(布尔):true / false
  2. uint(无符号整数):正整数(你最熟的 uint256 属于它)
  3. int(有符号整数):正负整数
  4. address(普通地址):钱包 / 合约地址
  5. address payable(可支付地址):能收 ETH 的地址
  6. bytes1~bytes32(定长字节):固定长度的二进制数据
  7. enum(枚举):自定义常量列表
  8. ufixed(无符号小数):极冷门
  9. fixed(有符号小数):极冷门

2. 引用类型(赋值传地址 | 共 3 种)

变量存的是数据的位置,修改会直接改原数据

  1. bytes(动态字节):不固定长度的二进制
  2. string(字符串):存文字、中文
  3. array(数组):存一组数据([1,2,3]

3. 映射类型(键值对 | 仅 1 种)

区块链最核心存储结构,类似字典

  1. mappingmapping(address => uint256) 查余额必备

4. 函数类型(仅 1 种)

  1. function:定义合约的方法

5. 自定义 / 特殊类型(共 4 种)

  1. struct(结构体):自定义复合数据(打包多个变量)
  2. contract(合约类型):合约本身
  3. interface(接口):合约交互标准
  4. library(库):工具函数集合

8种高频

  • bool
  • uint
  • address
  • address payable
  • string
  • array
  • mapping
  • struct

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 种,全部需要指定存储位置

  1. 动态数组uint[] / address[]
  2. 固定长度数组uint[10]
  3. 字符串string(本质是特殊的字节数组)
  4. 动态字节数组bytes
  5. 结构体struct
  6. 特殊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 的状态

合约的存储布局