Solidity基础与入门案例

Solidity基础

合约结构

Solidity 合约通常包含以下几个主要部分:

  1. SPDX 许可标识:指定代码的开源许可。
  2. pragma 指令:声明 Solidity 版本。
  3. 导入语句:引入其他合约或库。
  4. 合约声明:使用 contract 关键字。
  5. 状态变量:存储在区块链上的持久数据。
  6. 事件:用于记录重要操作,可被外部监听。
  7. 修饰符:用于修改函数行为的可重用代码。
  8. 函数:合约的可执行代码单元。

以下是一个简单的合约结构示例:

// 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 支持多种数据类型,包括基础类型(如 uintintbool)、复杂类型(如 structenum、数组、映射)以及地址类型 address。了解这些数据类型的特性对于编写高效和安全的合约至关重要。

值类型

  • uint: 无符号整数,uint256是默认类型,表示0到2^256-1的整数。可以使用不同的位宽,如uint8uint16等。
  • int: 有符号整数,范围为-2^(n-1)到2^(n-1)-1。
  • bool: 布尔类型,只有truefalse两个值。
  • address: 20字节的以太坊地址类型,分为addressaddress 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/

函数修饰符与类型

函数修饰符决定了函数的可见性和行为:

  1. 可见性修饰符
    • public:内部和外部都可调用
    • private:只能在定义的合约内部调用(虽然区块链上的数据是公开的,但限制了其他合约的直接访问)
    • internal:只能在内部和派生合约中调用
    • external:只能从外部调用
  2. 状态修饰符
    • view:不修改状态(但可以读取)
    • pure:不读取也不修改状态
  3. 支付相关
    • payable:允许函数接收以太币

注意事项

  • 使用private并不意味着数据绝对安全,仍需注意数据泄露的可能性。
  • external函数比public函数消耗更少Gas,适用于只需外部访问的函数。
  • viewpure 声明的函数直接执行不会消耗 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倍)

image

参考资料:https://www.evm.codes/#54?fork=shanghai

高级特性与优化

常量与不可变变量

使用 constantimmutable 可以优化 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
}

Recieve and Fallback

修饰器(Modifier)

修饰器用于在函数执行前后添加检查或修改行为:

modifier 修饰符名称(参数) {
    // 前置条件检查
    require(条件, "错误消息");
    _; // 表示被修饰函数的代码
    // 后置操作(如果有)
}

使用示例

modifier onlyOwner() {
    require(msg.sender == owner, "只有合约拥有者才能调用此函数");
    _;
}

function withdrawFunds() public onlyOwner {
    // 提款逻辑
}

注意事项

  1. 可以组合多个modifier
  2. 执行顺序:从左到右依次执行modifier
  3. 可以在modifier中使用参数
  4. _;表示被修饰函数的代码插入点

错误处理与安全性

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;
    }
}

作者:加密鲸拓

版权:此文章版权归 加密鲸拓 所有,如有转载,请注明出处!