前几天我分享了web3学习笔记的Day 1和Day2,原本以为没有人感兴趣,我就暂停了相关内容的分享。结果今天看到有朋友在学习笔记下留言了,看来还是有感兴趣的伙伴,所以决定继续分享学习笔记。由于Day 3的内容比较多,我会分2次分享,今天主要分享一下Day 3的理论部分。
📋 学习目标
📚 理论部分 (35分钟)
3.1 合约进阶概念
继承 (Inheritance)
什么是继承?
继承允许一个合约从另一个合约继承属性和方法,类似于面向对象编程。
继承的优点:
基本继承示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// 父合约
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0), "Invalid address");
owner = newOwner;
}
}
// 子合约继承父合约
contract MyContract is Ownable {
uint256 private value;
function setValue(uint256 _value) public onlyOwner {
value = _value;
}
function getValue() public view returns (uint256) {
return value;
}
}
多重继承:
contract A {
function funcA() public pure returns (string memory) {
return "A";
}
}
contract B {
function funcB() public pure returns (string memory) {
return "B";
}
}
contract C is A, B {
function getAll() public pure returns (string memory, string memory) {
return (funcA(), funcB());
}
}
//思考:如果这A和B都有一个同名函数:funcX(),如何在C里面调用?
继承顺序与 C3 线性化:
Solidity 使用 C3 线性化算法确定继承顺序。基本原则是:"更派生"的合约应该放在前面。
contract A { function a() public {} }
contract B is A { function b() public {} }
contract C is A { function c() public {} }
// 菱形继承:两种写法都合法
contract D is B, C {} // 线性化: D → B → C → A
contract E is C, B {} // 线性化: E → C → B → A
// 真正的错误示例:基类不能出现在派生类之后
// contract F is A, B {} // ❌ 错误:B 继承自 A,B 应该在 A 前面
使用 super 时的调用顺序:
contract D is B, C {
function callSuper() public {
super.a(); // 按照 C3 线性化顺序调用:先 C.a(),再 B.a(),最后 A.a()
}
}
多重继承同名函数的处理:
当多个父合约有同名函数时,必须在子合约中重写并显式指定调用哪个版本:
contract A {
function funcX() public pure virtual returns (string memory) {
return "A";
}
}
contract B {
function funcX() public pure virtual returns (string memory) {
return "B";
}
}
contract C is A, B {
// 必须重写,指定使用哪个父合约的实现
function funcX() public pure override(A, B) returns (string memory) {
return A.funcX(); // 选择 A 的实现
// 或 return B.funcX(); // 选择 B 的实现
// 或 return string.concat(A.funcX(), B.funcX()); // 组合两者
}
}
错误处理
Solidity 的三种错误处理方式:
1. require - 验证条件
function withdraw(uint256 amount) public {
require(amount > 0, "Amount must be greater than 0");
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
2. revert - 主动回滚
function withdraw(uint256 amount) public {
if (amount > balances[msg.sender]) {
revert("Insufficient balance");
}
// 或者使用自定义错误
revert InsufficientBalance(balances[msg.sender], amount);
}
// 自定义错误(Solidity 0.8.4+)
error InsufficientBalance(uint256 available, uint256 required);
3. assert - 验证内部不变量
function transfer(address to, uint256 amount) public {
uint256 oldBalance = balances[msg.sender];
balances[msg.sender] -= amount;
balances[to] += amount;
// 断言总余额不变(不应该失败)
assert(balances[msg.sender] + balances[to] == oldBalance + balances[to] - amount);
}
自定义错误的优势:
// ❌ 使用字符串
require(balances[msg.sender] >= amount, "Insufficient balance");
// Gas 成本: ~243 gas
// ✅ 使用自定义错误
error InsufficientBalance(uint256 available, uint256 required);
if (balances[msg.sender] < amount) {
revert InsufficientBalance(balances[msg.sender], amount);
}
// Gas 成本: ~91 gas(节省 ~60%)
修饰符 (Modifiers)
什么是修饰符?
修饰符用于修改函数的行为,通常用于访问控制和前置条件检查。
修饰符的工作原理:
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_; // 函数体插入的位置
}
function sensitiveFunction() public onlyOwner {
// 函数代码
}
// 编译后等价于:
function sensitiveFunction() public {
require(msg.sender == owner, "Not owner");
// 函数代码
}
带参数的修饰符:
modifier validAddress(address _addr) {
require(_addr != address(0), "Invalid address");
_;
}
function transfer(address _to, uint256 _amount) public validAddress(_to) {
// 函数代码
}
多个修饰符:
function importantFunction() public onlyOwner validAddress(msg.sender) {
// 先执行 onlyOwner,再执行 validAddress
}
3.2 ERC-20 代币标准
什么是 ERC-20?
ERC-20 是以太坊上同质化代币的标准,定义了代币必须实现的接口。
ERC-20 的特点:
- • 标准化:所有 ERC-20 代币遵循相同的接口
- • 兼容性:所有 DeFi 协议都支持 ERC-20
ERC-20 vs 其他代币标准:
ERC-20 标准接口
interface IERC20 {
// ====== 基本信息函数 ======
/**
* @dev 返回代币名称
*/
function name() external view returns (string memory);
/**
* @dev 返回代币符号
*/
function symbol() external view returns (string memory);
/**
* @dev 返回代币精度(通常为 18)
*/
function decimals() external view returns (uint8);
/**
* @dev 返回总供应量
*/
function totalSupply() external view returns (uint256);
// ====== 余额查询 ======
/**
* @dev 查询账户余额
* @param account 账户地址
*/
function balanceOf(address account) external view returns (uint256);
// ====== 转账功能 ======
/**
* @dev 转账代币
* @param to 接收地址
* @param amount 转账数量
* @return bool 是否成功
*/
function transfer(address to, uint256 amount) external returns (bool);
// ====== 授权机制 ======
/**
* @dev 授权spender使用代币
* @param spender 被授权地址
* @param amount 授权数量
* @return bool 是否成功
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev 查询授权额度
* @param owner 所有者地址
* @param spender 被授权地址
* @return uint256 授权数量
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev 使用授权额度转账
* @param from 发送地址
* @param to 接收地址
* @param amount 转账数量
* @return bool 是否成功
*/
function transferFrom(address from, address to, uint256 amount) external returns (bool);
// ====== 事件 ======
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
授权机制详解
为什么需要授权?
授权机制允许第三方(如 DEX、借贷协议)使用你的代币,而无需你手动转账。
授权流程:
1. 用户授权给 Uniswap
approve(uniswapAddress, 100)
状态:
- 用户余额: 1000
- 授权额度: 用户 → Uniswap: 100
2. Uniswap 使用授权额度
transferFrom(user, pool, 50)
状态:
- 用户余额: 950
- 授权额度: 用户 → Uniswap: 50
- Pool 余额: 50
授权机制图解:
┌──────────┐ approve(100) ┌──────────┐
│ Owner │ ──────────────────>│ Spender │
│ │ │ │
│ balance: │ allowance: │ │
│ 1000 │ Owner→Spender:100│ │
└──────────┘ └──────────┘
│ │
│ transferFrom(Owner, Recv, 50) │
│<───────────────────────────────│
▼ ▼
┌──────────┐ ┌──────────┐
│ Owner │ │ Receiver │
│ balance: │ │ balance: │
│ 950 │ │ 50 │
└──────────┘ └──────────┘
无限授权的风险:
// ❌ 危险:无限授权
approve(uniswapAddress, 2^256 - 1);
// ✅ 安全:仅授权需要的数量
approve(uniswapAddress, 100);
代币精度 (Decimals)
什么是精度?
精度决定了代币可以分割的最小单位。
// 示例:精度为 18
1 Token = 1 * 10^18 = 1000000000000000000 wei
0.5 Token = 0.5 * 10^18 = 500000000000000000 wei
常见精度:
计算实际数量:
function mint(uint256 amount) public {
// amount 已经考虑了精度
_mint(msg.sender, amount * 10 ** decimals());
}
function getBalanceInToken(address account) public view returns (uint256) {
// 转换为 Token 单位
return balanceOf(account) / 10 ** decimals();
}
3.3 安全最佳实践
常见安全漏洞
1. 重入攻击 (Reentrancy Attack)
什么是重入攻击?
攻击者在合约完成状态更新前,通过回调函数再次调用合约,导致状态被多次修改。
重入攻击示例:
// ❌ 容易遭受重入攻击
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount);
(bool success, ) = msg.sender.call{value: amount}(""); // 先调用外部
require(success);
balances[msg.sender] -= amount; // 后修改状态 - 危险!
}
// 攻击流程:
// 1. 攻击者调用 withdraw(100)
// 2. 合约发送 100 ETH 到攻击者
// 3. 攻击者的 receive 函数再次调用 withdraw(100)
// 4. 合约再次发送 100 ETH(余额还未更新)
// 5. 重复直到合约耗尽
防护方法 - Checks-Effects-Interactions 模式:
// ✅ 正确示范
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient"); // 1. Checks
balances[msg.sender] -= amount; // 2. Effects(先更新状态)
(bool success, ) = msg.sender.call{value: amount}(""); // 3. Interactions
require(success, "Transfer failed");
}
// 或使用 ReentrancyGuard 修饰符
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
function withdraw(uint256 amount) public nonReentrant {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
2. 整数溢出 (Integer Overflow)
什么是整数溢出?
当数值超过类型的最大值时,会回绕到最小值。
// Solidity 0.8.0 之前
uint8 a = 255;
a + 1; // 结果为 0(溢出)
uint8 b = 0;
b - 1; // 结果为 255(下溢)
防护方法:
// Solidity 0.8.0+ 自动检查溢出
uint8 a = 255;
a + 1; // 自动回滚交易
// 使用 SafeMath(旧版本)
import "@openzeppelin/contracts/math/SafeMath.sol";
using SafeMath for uint256;
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a.add(b); // 自动检查溢出
}
3. 访问控制缺失
问题:
// ❌ 危险:任何人都可以调用
function mint(uint256 amount) public {
_mint(msg.sender, amount);
}
解决方法:
// ✅ 使用修饰符限制访问
function mint(uint256 amount) public onlyOwner {
_mint(msg.sender, amount);
}
4. 未检查的返回值
问题:
// ❌ 危险:不检查转账是否成功
function withdraw() public {
payable(msg.sender).transfer(address(this).balance);
}
解决方法:
// ✅ 检查返回值
function withdraw() public {
(bool success, ) = payable(msg.sender).call{value: address(this).balance}("");
require(success, "Transfer failed");
}
安全检查清单
- • 遵循 Checks-Effects-Interactions 模式
- • 使用
nonReentrant 修饰符防止重入 - • 使用
msg.sender 而不是 tx.origin 进行身份验证 - 今天就分享这么多,接下来是实操部分,内容比较多,我会下次分享。
📌 关于“令狐冲AI”
聚焦AI与SaaS出海,分享AI时代如何打造超级个体,探索更聪明的工作与生活方式。
喜欢这篇文章? 请点赞👍、在看👀、转发📤给更多朋友!