Solidity 全局变量
什么是全局变量?
Solidity 提供了一些内置的 全局变量(Global Variables),用于访问区块链状态、交易信息等数据。这些变量不需要额外声明,可以在智能合约中直接调用。
主要全局变量分类
Solidity 全局变量主要分为以下几类:
区块信息(Block Information)
示例:获取当前区块号和时间戳
contract BlockInfo { function getBlockDetails() public view returns (uint, uint) { return (block.number, block.timestamp); } }
2.2 交易信息(Transaction Information)
⚠️ 注意:tx.origin 可能导致安全漏洞,避免用于身份验证! 示例:检查交易发起者 contract TxInfo { function getTxOrigin() public view returns (address) { return tx.origin; } }
消息信息(Message Information)
示例:获取调用者地址和发送的 ETH 数量
contract MessageInfo { function getMessageDetails() public payable returns (address, uint) { return (msg.sender, msg.value); } }
以太坊环境信息(Ethereum Environment Information)
示例:获取当前合约的地址和余额
contract ContractInfo { function getContractDetails() public view returns (address, uint) { return (address(this), address(this).balance); } }
this 关键字
this 关键字用于引用当前合约的实例。它可以用于:
- 获取当前合约的地址。
- 在合约内部调用自身的函数(但会创建新的交易)。
- 访问当前合约的 balance。 示例:使用 this 关键字
contract ThisExample { function getContractAddress() public view returns (address) { return address(this); } function getContractBalance() public view returns (uint) { return address(this).balance; } }
注意事项:
- this 关键字调用合约自身的函数时,会触发新的交易,因此会消耗额外的 gas。
- this.balance 获取的是合约的 ETH 余额,而 msg.sender.balance 可以用于获取调用者的余额。
其他特殊全局变量
示例:使用 keccak256 计算哈希值
contract HashExample { function getHash(string memory input) public pure returns (bytes32) { return keccak256(abi.encodePacked(input)); } }
- 应用场景 ✅ 随机数生成(⚠️ 仅限低安全性用途)
function getRandomNumber() public view returns (uint) { return uint(keccak256(abi.encodePacked(block.timestamp, msg.sender))) % 100; }
✅ 支付合约
contract Payment { function pay() public payable {} function getBalance() public view returns (uint) { return address(this).balance; } }
✅ 安全身份验证(避免 tx.origin)
contract Auth { address public owner; constructor() { owner = msg.sender; } function isOwner() public view returns (bool) { return msg.sender == owner; } }
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract ABI{
function encodeData(string memory text,uint256 number) public pure returns (bytes memory,bytes memory){
//编码
return (
//0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000045a6f6f6400000000000000000000000000000000000000000000000000000000
abi.encode(text,number),//返回会带0
//0x5a6f6f640000000000000000000000000000000000000000000000000000000000000012
abi.encodePacked(text,number//返回不是带0,压缩了,这个没办法解码,需要补齐才可以
));
}
//解码
function decodeData(bytes memory encodedData) public pure returns (string memory text, uint256 number) {
return abi.decode(encodedData, (string, uint256));
}
//当前函数签名
function getSelector () public pure returns (bytes4){
return msg.sig;
}
function computeSelector(string memory fun) public pure returns (bytes4){
return bytes4(keccak256(bytes(fun)));
}
//0xa9059cbb0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000000000000000000000064
function transfer (address addr, uint256 amount) public pure returns (bytes memory){
return msg.data;
}
//调研函数生成 msg.data
//0xa9059cbb0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000000000000000000000064
function encodeFunctionCall () public pure returns (bytes memory){
return abi.encodeWithSignature("transfer(address,uint256)",0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 100);//encodeWithSignature时,注意去掉空格和变量,否则,transfer和此方法的返回不一致
}
//哈希运算
function hashFunctions(string memory input) public pure returns (bytes32,bytes32,bytes32){
bytes memory data = abi.encodePacked(input);
return (
keccak256(data),
sha256(data),
ripemd160(data)
);
}
//数学运算
function modulaMath(uint256 x,uint256 y,uint256 z) public pure returns (uint256 ,uint256){
return (
addmod(x, y, z),
mulmod(x, y, z)
);
}
//椭圆曲线恢复公钥(ecrecover)
function recoverAddress(bytes32 hash,uint8 v,bytes32 r,bytes32 s)public pure returns (address){
return ecrecover(hash, v, r, s);
}
}
错误处理
1. require
语句
require
用于在执行函数之前检查条件是否满足。如果条件不满足,require
会抛出异常并回滚所有状态更改。它通常用于验证用户输入或外部调用的合法性。
语法
require(条件, "Error code"); 示例
function transfer(address to, uint256 amount) public { require(to != address(0), "无效的接收地址"); require(amount > 0, "转账金额必须大于0"); // ... 转账逻辑 ... }
特点
- 用于输入验证
- 抛出异常时会回滚状态
- 可以附带错误信息
2. assert
语句
assert
用于检查内部逻辑的正确性,通常用于验证合约内部状态是否一致。如果条件不满足,assert
会抛出异常并回滚所有状态更改。它通常用于调试和确保合约逻辑的正确性。
语法
assert(条件); 示例 function divide(uint256 a, uint256 b) public pure returns (uint256) { uint256 result = a / b; assert(b != 0); // 确保除数不为0 return result; } 特点
- 用于内部逻辑验证
- 抛出异常时会回滚状态
- 通常不附带错误信息
revert 关键字
revert
是 Solidity 中用于 回退交易(transaction)并恢复状态 的一个关键字。它经常用于错误处理,当某些条件不满足时立即终止执行并返还剩余的 gas。
基本语法
revert(); revert("错误信息");
工作机制
当调用 revert()
时:
当前函数执行被中止;
所有状态更改(包括调用之前的更改)都会被回滚;
未使用的 gas 会被退还给调用者;
如果有错误信息,会被返回给调用者(前端或调用合约可以读取这个信息)。
使用场景
- 条件检查失败:
if (msg.value < price) { revert("Insufficient funds"); }
- 配合自定义错误使用(gas 更省):
error NotOwner(); function onlyOwner() public view { if (msg.sender != owner) { revert NotOwner(); } }
自定义错误
// 简单错误(不带参数) error Unauthorized(); // 带参数的错误 error InsufficientBalance(uint256 available, uint256 required); // 复杂参数的错误 error TransferFailed(address from, address to, uint256 amount, string reason);
触发自定义错误
function withdraw(uint256 amount) public { if (amount > balances[msg.sender]) { revert InsufficientBalance(balances[msg.sender], amount); } // 其他逻辑... }
优势
Gas 效率:比
require
/revert
节省约 50-70% Gas普通 require: ~45 Gas
自定义错误: ~20 Gas
参数化错误信息:可以携带任意数量和类型的参数
类型安全:编译器会检查错误类型和参数匹配
ABI 编码:自动生成错误签名,方便客户端解析
💡 例子
pragma solidity ^0.8.0; contract Shop { address public owner; uint256 public price = 1 ether; constructor() { owner = msg.sender; } function buy() public payable { if (msg.value < price) { revert("Not enough ETH sent"); } // 购买逻辑 } }
三者对比
特性 | require |
revert |
assert |
---|---|---|---|
✅ 功能 | 条件检查,不满足则终止执行 | 手动触发终止执行,带/不带错误信息 | 检查程序内部逻辑是否永远成立 |
🎯 用途 | 验证输入、权限、调用状态 | 更灵活地中止执行 | 用来发现bug或不变量失效 |
🔄 状态回滚 | ✅ 会回滚 | ✅ 会回滚 | ✅ 会回滚 |
💸 Gas 返还 | ✅ 未消耗的 gas 会返还 | ✅ 会返还 | ❌ 不返还(panic,重大错误) |
📢 可带信息 | ✅ "错误信息" |
✅ "错误信息" 或 error Type() |
❌ 无信息(只有 Panic code) |
🧱 推荐使用 | 外部输入条件、require权限检查等 | 业务逻辑失败、自定义 error 抛错 | 测试 / 内部断言,永远不该失败 |
try-catch 语句
Solidity 从 0.6.0 版本开始引入了 try-catch
语句,用于处理外部函数调用和合约创建中的错误。这是 Solidity 中错误处理的重要机制之一。
适用场景
try-catch
只能用于以下两种情况:
外部函数调用(使用
address
调用或合约实例调用)合约创建(使用
new
关键字)
完整的 try-catch 结构
try externalContract.someFunction(arg1, arg2) returns (uint256 result) { // 调用成功时执行的代码 // 可以使用返回值 result } catch Error(string memory reason) { // 当 revert(reasonString) 或 require(false, reasonString) 被调用时 // 可以访问 reason } catch Panic(uint errorCode) { // 当发生 panic 错误时(如除以零、数组越界等) // errorCode 表示错误类型 } catch (bytes memory lowLevelData) { // 当错误不符合上述任何类型时 // 包含低级错误数据 }
错误类型详解
Error(string memory reason)
对应
revert("description")
或require(false, "description")
可以获取错误描述字符串
2. Panic(uint errorCode)
对应 Solidity 的 "panic" 错误(类似断言失败)
常见错误码:
0x01: 断言失败
0x11: 算术运算溢出
0x12: 除以零
0x21: 无效的数组索引
0x31: 分配过多内存
0x32: 调用未初始化的内部函数类型变量
- 默认的
catch
块
捕获所有其他类型的错误
包含原始错误数据(bytes 类型)
自定义修饰符
自定义修饰符(Modifier)用于在函数执行前或执行后添加额外的检查或逻辑。它可以复用代码,并且常用于权限控制或状态检查。
语法
modifier 修饰符名 { // 前置逻辑 _; // 执行函数体 // 后置逻辑 }
示例
contract Owner { address public owner; constructor() { owner = msg.sender; } // 自定义修饰符,仅允许合约所有者调用 modifier onlyOwner { require(msg.sender == owner, "仅合约所有者可调用"); _; } function changeOwner(address newOwner) public onlyOwner { owner = newOwner; } }
特点
- 可以复用代码
- 常用于权限控制
- 可以在函数执行前后添加逻辑
综合示例
contract Bank { mapping(address => uint256) public balances; // 自定义修饰符,检查余额是否足够 modifier hasSufficientBalance(uint256 amount) { require(balances[msg.sender] >= amount, "余额不足"); _; } // 存款函数 function deposit() public payable { require(msg.value > 0, "存款金额必须大于0"); balances[msg.sender] += msg.value; } // 取款函数 function withdraw(uint256 amount) public hasSufficientBalance(amount) { balances[msg.sender] -= amount; payable(msg.sender).transfer(amount); assert(balances[msg.sender] >= 0); // 确保余额不为负 } }
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
contract code {
function requireError(bool condition) public pure returns (uint256){
require(condition,"input is not true");
return 1;
}
function assertError (bool condition) public pure returns (uint256){
assert(condition);
return 2;
}
function revertError(bool condition) public pure returns (uint256){
if(!condition){
revert("123");
}
return 3;
}
}
地址类型
在 Solidity 中,address
类型用于存储以太坊地址。地址可以是外部账户(EOA)或合约账户。地址类型是 Solidity 中最基本的数据类型之一,常用于处理以太坊账户之间的交互。
地址类型的基本操作
// 声明一个地址变量 address public myAddress; // 获取当前调用者的地址 address public caller = msg.sender; // 地址类型之间的比较 function compareAddress(address addr1, address addr2) public pure returns (bool) { return addr1 == addr2; } // 地址类型的转换 function toBytes(address addr) public pure returns (bytes memory) { return abi.encodePacked(addr); }
地址类型的成员变量和方法
// 获取地址的余额 function getBalance(address addr) public view returns (uint256) { return addr.balance; } // 向地址转账 function sendEther(address payable recipient) public payable { recipient.transfer(msg.value); } // 调用地址的代码(低级别调用) function callContract(address addr, bytes memory data) public returns (bool, bytes memory) { (bool success, bytes memory result) = addr.call(data); return (success, result); }
接口
接口(Interface)是 Solidity 中用于定义合约之间交互的一种方式。通过接口,一个合约可以调用另一个合约的函数,而无需知道其具体实现。
定义接口
// 定义一个简单的接口 interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); function balanceOf(address account) external view returns (uint256); }
使用接口
contract MyContract { // 使用接口与另一个合约交互 function transferToken(address tokenAddress, address to, uint256 amount) public returns (bool) { IERC20 token = IERC20(tokenAddress); return token.transfer(to, amount); } // 获取代币余额 function getTokenBalance(address tokenAddress, address account) public view returns (uint256) { IERC20 token = IERC20(tokenAddress); return token.balanceOf(account); } }
接口的特点
- 接口只能声明函数,不能实现函数。
- 接口中的函数必须标记为
external
。 - 接口不能定义状态变量或构造函数。
- 接口可以继承其他接口。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// 定义一个简单的银行接口
interface IBank {
function deposit() external payable;
function withdraw(uint256 amount) external;
function getBalance() external view returns (uint256);
}
// 实现银行接口的合约
contract Bank is IBank {
mapping(address => uint256) public balances;
function deposit() external payable override {
require(msg.value > 0, "save amount must than o");
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) external override {
require(balances[msg.sender] >= amount, "amout not enough");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
function getBalance() external view override returns (uint256) {
return balances[msg.sender];
}
}
// 使用银行接口的合约
contract BankUser {
function depositToBank(address bankAddress) external payable {
IBank bank = IBank(bankAddress);
bank.deposit{value: msg.value}();
}
function withdrawFromBank(address bankAddress, uint256 amount) external {
IBank bank = IBank(bankAddress);
bank.withdraw(amount);
}
receive() external payable { }
fallback() external payable { }
function getBankBalance(address bankAddress) external view returns (uint256) {
IBank bank = IBank(bankAddress);
return bank.getBalance();
}
}
import
导包方式
在 Solidity 中,import
用于引入其他合约或库文件,以便在当前合约中使用它们的功能。import
语句可以用于引入本地文件或远程文件。
语法
// 引入本地文件 import "./MyContract.sol"; // 引入远程文件 import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol"; // 引入整个目录 import "./utils/*"; // 引入并重命名 import { MyContract as MC } from "./MyContract.sol";
示
// 引入 OpenZeppelin 的 ERC20 合约 import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MyToken is ERC20 { constructor() ERC20("MyToken", "MTK") {} }
继承
Solidity 支持合约之间的继承,允许一个合约继承另一个合约的功能。继承可以复用代码,并且可以通过 override
关键字来重写父合约的函数。
语
contract Parent { function foo() public virtual returns (string memory) { return "Parent"; } } contract Child is Parent { function foo() public virtual override returns (string memory) { return "Child"; } }
示例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
// 父合约
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this function");
_;
}
function test () public pure virtual returns (uint256){
return 2;
}
}
// 子合约
contract MyContract is Ownable {
function changeOwner(address newOwner) public onlyOwner {
owner = newOwner;
}
function test () public pure override returns (uint256){
return 4;
}
}
OpenZeppelin
OpenZeppelin 是一个广泛使用的 Solidity 库,提供了许多经过审计的智能合约模板,如 ERC20、ERC721、Ownable 等。使用 OpenZeppelin 可以大大减少开发时间并提高合约的安全性。
安装 OpenZeppelin
hardhat 会用到
npm install @openzeppelin/contracts
使用 OpenZeppelin 的 ERC20 合约
// 引入 OpenZeppelin 的 ERC20 合约 import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract MyToken is ERC20 { constructor() ERC20("MyToken", "MTK") { _mint(msg.sender, 1000000 * 10 ** decimals()); } }
使用 OpenZeppelin 的 Ownable 合约
// 引入 OpenZeppelin 的 Ownable 合约 import "@openzeppelin/contracts/access/Ownable.sol"; contract MyContract is Ownable { function specialFunction() public onlyOwner { // 只有合约所有者可以调用此函数 } }
综合示例
// 引入 OpenZeppelin 的 ERC20 和 Ownable 合约 import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract MyToken is ERC20, Ownable { constructor() ERC20("MyToken", "MTK") { _mint(msg.sender, 1000000 * 10 ** decimals()); } function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } }