Solidity基础
合约结构
Solidity 合约通常包含以下几个主要部分:
- SPDX 许可标识:指定代码的开源许可。
- pragma 指令:声明 Solidity 版本。
- 导入语句:引入其他合约或库。
- 合约声明:使用
contract
关键字。 - 状态变量:存储在区块链上的持久数据。
- 事件:用于记录重要操作,可被外部监听。
- 修饰符:用于修改函数行为的可重用代码。
- 函数:合约的可执行代码单元。
以下是一个简单的合约结构示例:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract SimpleStorage { uint256 public storedData; constructor(uint256 initialValue) { storedData = initialValue; } function set(uint256 x) public { storedData = x; } function get() public view returns (uint256) { return storedData; } }
数据类型与数据结构
Solidity 支持多种数据类型,包括基础类型(如 uint
、int
、bool
)、复杂类型(如 struct
、enum
、数组、映射)以及地址类型 address
。了解这些数据类型的特性对于编写高效和安全的合约至关重要。
值类型
- uint: 无符号整数,
uint256
是默认类型,表示0到2^256-1的整数。可以使用不同的位宽,如uint8
、uint16
等。 - int: 有符号整数,范围为-2^(n-1)到2^(n-1)-1。
- bool: 布尔类型,只有
true
和false
两个值。 - address: 20字节的以太坊地址类型,分为
address
和address payable
(后者可用于接收以太币)。 - bytes1 ~ bytes32:固定大小字节数组
引用类型
- string:动态大小的 UTF-8 编码字符串
- bytes:动态大小的字节数组
- 数组:如
uint[]
(动态大小)或uint[5]
(固定大小) - 结构体 (Struct):自定义的复杂数据类型,例:
struct Person { string name; uint age; }
- 映射 (Mapping):键值对存储,如
mapping(address => uint)
注意事项
- Mapping不支持直接遍历,需结合其他结构记录键值。
- 动态数组操作(如
push
)会增加Gas,尽量减少不必要的操作。
参考资料
https://solidity-by-example.org/array/
https://solidity-by-example.org/mapping/
https://solidity-by-example.org/structs/
函数修饰符与类型
函数修饰符决定了函数的可见性和行为:
- 可见性修饰符:
public
:内部和外部都可调用private
:只能在定义的合约内部调用(虽然区块链上的数据是公开的,但限制了其他合约的直接访问)internal
:只能在内部和派生合约中调用external
:只能从外部调用
- 状态修饰符:
view
:不修改状态(但可以读取)pure
:不读取也不修改状态
- 支付相关:
payable
:允许函数接收以太币
注意事项
- 使用
private
并不意味着数据绝对安全,仍需注意数据泄露的可能性。 external
函数比public
函数消耗更少Gas,适用于只需外部访问的函数。view
和pure
声明的函数直接执行不会消耗 Gas,只是做了个调用,没有发送交易,但如果是别的需要消耗 Gas 的函数调用了view
或者pure
的函数,还是会消耗对应的 Gas 的。
参考资料
https://docs.soliditylang.org/en/v0.8.23/cheatsheet.html#function-visibility-specifiers
https://solidity-by-example.org/view-and-pure-functions/
内存管理与数据位置
Solidity中的数据存储位置决定了数据的生命周期和Gas消耗:
- Storage: 永久存储,数据保存在区块链上。默认的状态变量存储位置,Gas成本高。
- Memory: 临时数据位置,函数调用结束即释放。适合在函数内处理临时数据。
- Calldata: 只读数据位置,通常用于外部函数调用的参数。不可修改,效率高。
注意事项
- 尽量减少Storage的读写次数以节省Gas。
- 在复杂数据操作中,优先考虑Memory。
- 静态数据类型如固定大小的数组或基本类型不需要指定数据位置。
参考资料:https://docs.alchemy.com/docs/when-to-use-storage-vs-memory-vs-calldata-in-solidity
从 storage 中存取数据的 gas 开销要远大于直接从 memory 中存取(相差33倍)
参考资料:https://www.evm.codes/#54?fork=shanghai
高级特性与优化
常量与不可变变量
使用 constant
和 immutable
可以优化 gas 使用:
constant
不允许赋值(除初始化以外),在编译时确定的常量,不占用存储空间。immutable
可在合约构造时赋值,之后不可更改,存储在代码中。
参考资料:https://docs.soliditylang.org/zh/v0.8.16/cheatsheet.html#index-3
特殊函数: Receive和Fallback
receive
的功能是当合约收到纯以太币(无数据)时,就会触发此函数。该函数还必须标记为“payable”。
receive() external payable { // This function is executed when a contract receives plain Ether (without data) }
fallback
函数是一个特殊函数,当合约收到 Ether 并调用合约中不存在的函数时,或者交易中没有提供任何数据时,就会执行该函数。如果希望合约能够以这种方式接收以太币,则必须将此函数标记为payable
。
fallback() external payable { // This function is executed on a call to the contract if none of the other // functions match the given function signature, or if no data is supplied at all }
修饰器(Modifier)
修饰器用于在函数执行前后添加检查或修改行为:
modifier 修饰符名称(参数) { // 前置条件检查 require(条件, "错误消息"); _; // 表示被修饰函数的代码 // 后置操作(如果有) }
使用示例
modifier onlyOwner() { require(msg.sender == owner, "只有合约拥有者才能调用此函数"); _; } function withdrawFunds() public onlyOwner { // 提款逻辑 }
注意事项
- 可以组合多个modifier
- 执行顺序:从左到右依次执行modifier
- 可以在modifier中使用参数
_;
表示被修饰函数的代码插入点
错误处理与安全性
Solidity提供了多种错误处理机制:
- require: 用于输入验证和外部调用的错误检测。
- assert: 用于内部一致性检查。
- revert: 提供自定义错误消息,回滚状态。
使用示例
contract ErrorHandlingExample { function requireExample(uint x) public pure { require(x > 10, "x must be greater than 10"); } function assertExample(uint x) public pure { assert(x != 0); // 用于内部错误检查 } function revertExample(uint x) public pure { if (x <= 10) { revert("x must be greater than 10"); } } // 自定义错误 error InsufficientBalance(uint requested, uint available); function withdraw(uint amount) public { uint balance = address(this).balance; if (amount > balance) { revert InsufficientBalance({ requested: amount, available: balance }); } // 处理提款 } }
安全性注意事项
- 避免重入攻击:使用“检查-效果-交互”模式。
- 防止整数溢出:使用Solidity 0.8+的内置检查或
SafeMath
库。
常用全局变量
msg
对象
msg.sender
: 当前调用者的地址,常用于权限验证。msg.value
: 当前交易发送的以太币数量,常用于支付逻辑。msg.data
: 调用数据的完整字节数组,适用于低级调用。msg.sig
: 调用数据的前4字节函数选择器。
block
对象
block.timestamp
: 当前区块的时间戳(Unix时间),常用于时间限制。block.number
: 当前区块的编号,可以用于获取链上数据的时间顺序。block.difficulty
: 当前区块的难度。
tx
对象
tx.origin
: 交易发起者的原始地址,通常不建议用于权限验证,因为可能导致安全问题。
其他
gasleft()
: 剩余的Gas量,用于监控Gas消耗。
合约间交互与继承
合约导入
使用 import
语句导入其他合约或库:
// File: ImportExample.sol import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "./MyOtherContract.sol"; contract ImportExample is ERC20, MyOtherContract { constructor() ERC20("MyToken", "MTK") { // 构造函数逻辑 } }
合约继承
如果要继承某个 contract 的话,使用 is 关键词
contract AddFiveStorage is SimpleStorage
Solidity 支持多重继承:
contract Ownable { address public owner; constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "Not the owner"); _; } } contract Pausable { bool public paused; modifier whenNotPaused() { require(!paused, "Contract is paused"); _; } } contract MyContract is Ownable, Pausable { function doSomething() public onlyOwner whenNotPaused { // 函数逻辑 } }
如果想修改继承过来的合约里的函数,则需要用 override 关键词,并且父级合约中的函数需要带上 virtual 关键词,没有 virtual 的函数都无法被重写
// 函数中有 virtual 修饰符才能被继承改写 function store(uint256 _favoriteNumber) public virtual { myFavoriteNumber = _favoriteNumber; } // 要改写继承过来的函数,需要带上 override 关键词 function store(uint256 _newNumber) public override { myFavoriteNumber = _newNumber + 5; }
接口与抽象合约
接口和抽象合约用于定义合约的标准结构:
interface IERC20 { function totalSupply() external view returns (uint256); function balanceOf(address account) external view returns (uint256); function transfer(address recipient, uint256 amount) external returns (bool); // 其他 ERC20 函数... } abstract contract ERC20Base is IERC20 { mapping(address => uint256) private _balances; function balanceOf(address account) public view virtual override returns (uint256) { return _balances[account]; } // 其他实现... } contract MyToken is ERC20Base { // 实现剩余的抽象函数 }
入门案例
FavoriteNumber
本案例实现了用户存储和检索与名字关联的喜好数字:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract FavoriteNumber { mapping(string => uint256) private nameToFavoriteNumber; function createOrUpdateFavoriteNumber(string memory name, uint256 number) public { nameToFavoriteNumber[name] = number; } function getNumber(string memory name) public view returns(uint256) { return nameToFavoriteNumber[name]; } }
ProfileStatus
本案例实现了用户设置和检索个人信息
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract ProfileStatus { struct Status{ string name; string message; } mapping (address => Status) private userStatus; function createOrUpdateStatus (string memory _name, string memory _message) public { userStatus[msg.sender].name = _name; userStatus[msg.sender].message = _message; } function getStatus() public view returns (string memory, string memory) { return (userStatus[msg.sender].name, userStatus[msg.sender].message); } }
TipJar
本案例实现了一个简单的小费罐功能:可以支付小费,提取余额
// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract TipJar { address public owner; constructor() { owner = msg.sender; } modifier onlyOwner() { require(msg.sender == owner, "You are not owner!"); _; } function tip() public payable { require(msg.value > 0, "You should send a tip to use this function"); } function withdraw() public onlyOwner { uint256 contractBalance = address(this).balance; require(contractBalance > 0, "There are no tips to withdraw"); payable(owner).transfer(contractBalance); } function getBalance() public onlyOwner view returns (uint256) { return address(this).balance; } }
作者:加密鲸拓
版权:此文章版权归 加密鲸拓 所有,如有转载,请注明出处!