Post

W2-1 Solidity基本类型

W2-1 Solidity基本类型

Solidity 概览图

Solidity-Cheat-Sheet

基本类型、数组、结构体、映射

合约的组成

pragma solidity ^0.8.0; // 1.编译器版本声明

contract Counter {// 2.定义合约 
    uint public counter;// 3.状态变量
    constructor() {
        counter = 0;}
    function count() public {// 4.合约函数
        counter = counter + 1;
    }
}

Solidity语言

  • 静态类型、编译型、高级语言
  • 针对EVM专门设计
  • 受C++、JavaScript等语言影响
    • 如:变量声明、for循环、重载函数等概念来自于C++
    • 函数关键字、导入语法来自于JavaScript
  • ⽂档
    • 中⽂:https://learnblockchain.cn/docs/solidity/
    • 英⽂:https://docs.soliditylang.org/

数据类型

值类型

  • 布尔
  • 整型
    • int/uint,uint8 … unit256
    • 在使⽤整型时,要特别注意整型的⼤⼩及所能容纳的最⼤值和最⼩值, 如uint8的最⼤值为0xf(255),最⼩值是0 从 solidity 0.6.0 版本开始可以通过 Type(T).min 和 Type(T).max 获得整型的最⼩值与最⼤值。
  • 定长浮点型
  • 定长字节数组
  • 枚举
  • 函数类型
  • 地址类型
    • Solidity使用地址类型来表示一个账号,地址类型有两种形式
      • address: 一个20字节的值
      • address payable: 表示可支付地址,与address相同也是20字节,不过它有成员函数transfersend
    • 成员函数
      • <address>.balance(uint256): 返回地址的余额
      • <address payable>.transfer(uint256 amount): 向地址发送以太币,失败时抛出异常 (gas:2300)
      • <address payable>.send(uint256 amount) returns (bool): 向地址发送以太币,失败时返回false
    • 3个底层成员函数,通常用于合约交互,直接控制编码的方式调用合约函数
      • call
        • 切换上下⽂,附加gas {gas: } , 附加value {value: }
        • addr.call{value: 1ether}("") 功能等价 transfer(1 ether)但是没有 gas 限制
        • 失败不是发⽣异常,⼀定要检查返回值
      • delegatecall
        • 保持上下⽂
        • 不⽀持附加value {value: }
      • staticcall
  • 十六进制常量
  • 有理数和整型常量
  • 字符串常量
  • 地址常量

引用类型

值类型赋值时总是完整拷⻉。⽽复杂类型占⽤的空间较⼤(>32个字节),拷⻉开销很⼤,这时就可以使⽤引⽤的⽅式,即通过多个不同名称的变量指向⼀个值。

引⽤类型都有⼀个额外属性来标识数据的存储位置:

  • memory(内存): ⽣命周期只存在于函数调⽤期间
  • storage(存储): 状态变量保存的位置,gas开销最⼤
  • calldata(调⽤数据): ⽤于函数参数不可变存储区域
结构体

使⽤Struct 声明⼀个结构体,定义⼀个新类型。

contract testStruct {
    struct Funder {
        address addr;
        uint amount;}

    mapping(uint => Funder) funders;

    function contribute(uint id) public payable {
        funders[id] = Funder({addr : msg.sender, amount : msg.value});
        funders[id] = Funder(msg.sender, msg.value);
    }

    function getFund(uint id) public view returns (address, uint) {
        return (funders[id].addr, funders[id].amount);
    }
}
数组
  • T[k] : 元素类型为T,固定⻓度为k的数组
  • T[] : 元素类型为T,⻓度动态调整
  • 数组通过下标进⾏访问,序号是从0开始
  • bytes、string 是⼀种特殊的数组
    • bytes是动态分配⼤⼩字节的数组,类似于byte[],但是bytes的gas费⽤更低,bytes和string 都可以⽤来表达字符串,对任意⻓度的原始字节数据使⽤bytes,对任意⻓度字符串 (UTF-8)数据使⽤string。
  • 成员
    • Lengh属性:表示当前数组的⻓度
    • push():添加新的零初始化元素到数组末尾,返回引⽤
    • push(x): 数组末尾添加⼀个给定的元素
    • pop(): 从数组末尾删除元素

注意数组不能过大,导致整个区块的gas都够执行成功

映射类型

  • 声明形式:mapping(KeyType => ValueType)
    • 例如:mapping(address => uint) public balances;
  • 使⽤⽅式类似数组,通过 key 访问,例如: balances[userAddr];
  • 映射没有⻓度、没有 key 的集合或 value 的集合的概念
  • 只能作为状态变量
  • 如果访问⼀个不存在的键,返回的是默认值。
pragma solidity >=0.8.0;

contract MappingExample {
    mapping(address => uint) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;
    }
}

合约类型

  • 每个合约都是⼀个类型,可声明⼀个合约类型。
    • 如:Hello c; 则可以使⽤c.sayHi() 调⽤函数
  • 合约可以显式转换为address类型,从⽽可以使⽤地址类型的成员函数

使用工厂模式进行部署可以大大节省Gas费用

call跟delegatecall的区别

  • image1
  • image1

全局变量及函数

  • block.number ( uint ): 当前区块号
  • block.timestamp ( uint): ⾃ unix epoch 起始当前区块以秒计的时间戳
  • msg.sender ( address ): 消息发送者(当前调⽤)
  • msg.value ( uint ): 随消息发送的 wei 的数量
  • tx.origin (address payable): 交易发起者(完全的调⽤链)

API介绍

合约函数、函数修改器、函数修饰符,及各类特殊函数

合约

使⽤contract关键字来声明⼀个合约,⼀个合约通常由状态变量、函数、函数修改器以及事件组成。

pragma solidity >=0.8.0;

contract C {
    function f(uint a) private pure returns (uint b) {return a + 1;}

    function setData(uint a) internal {data = a;}

    uint public data;
}

创建合约的⼏个⽅法

  • 外部部署(Remix/Hardhat/Truffle)Web.js
  • 合约使⽤New
  • 最⼩代理合约(克隆):
    • https://eips.ethereum.org/EIPS/eip-1167
    • https://github.com/optionality/clone-factory
  • Create2
    • C c = new C{salt: _salt}();

合约地址的确认

  • 根据创建者(sender)的地址以及创建者发送过的交易数量(nonce)来计算确定
    • keccak256(rlp.encode([normalize_address(sender), nonce]))[12:]
  • Create2
    • keccak256(0xff ++ senderAddress ++ salt ++ keccak256(init_code))[12:]

可见性

使⽤public 、private、external、internal可⻅性关键字来控制变量和函数是否可以被外部使⽤

  • public 内/外
  • private 内部
  • external 外部访问
  • internal 内部及继承

合约函数

  • 构造函数(constructor): 初始化逻辑
  • 视图函数(view)、纯函数:(pure) 不修改状态,不⽀付⼿续费
    • 但同时也要满足区块gas的限制
  • getter 函数: 所有 public 状态变量创建 getter 函数
  • receive 函数: 接收以太币时回调。
  • fallback 函数: 没有匹配函数标识符时, fallback 会被调⽤。
  • 函数修改器(modifier): 可⽤来改变⼀个函数的⾏为,如检查输⼊条件、控制访问、重⼊控制
    pragma solidity ^0.8.0;

contract testFunc {
    address public owner;
    uint private deposited;
    constructor() {
        owner = msg.sender;
    }
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
        // 函数修改器修饰函数时,函数体被插⼊到 “_;”
    }
    fallback() external payable {
        deposited += msg.value;}

    function getDeposited() public view returns (uint) {
        return deposited;}

    function withdraw() public onlyOwner {
        payable(owner).transfer(deposited);
    }
}

重入问题

调用外部函数时,要时刻注意重入问题

sequenceDiagram
User ->>+ A: 1.Func1
A ->>+ B: 2.Func2
B ->>- A: 3.Func1

# ( A ->> A: Re-Entrance)

Note over A,B: Re-Entrance
A ->>- User: 

错误处理、合约继承、接口、库及OpenZeppelin合约库

错误处理

  • 在程序发⽣错误时的处理⽅式:EVM通过回退状态来处理错误的,以便保证状态修改的事务性
  • assert()和require()⽤来进⾏条件检查,并在条件不满⾜时抛出异常
  • revert():终⽌运⾏并撤销状态更改
  • try/catch: 捕获合约中外部调⽤的异常

继承

  • 和⼤多数⾼级语⾔⼀样,Solidity 也⽀持继承
  • 使⽤关键字 is
  • 继承时,链上实际只有⼀个合约被创建,基类合约的代码会被编译进派⽣合约。
  • 派⽣合约可以访问基类合约内的所有⾮私有(private)成员,因此内部(internal)函数和状态变量在派⽣合约⾥是可以直接使⽤的

接口

  • 函数的抽象,作为⼀个类型声明,⼴泛⽤于合约之间的调⽤
  • 不可以有实现的函数
  • 不能继承⾃其他接⼝
  • 没有构造⽅法
  • 没有状态变量
pragma solidity ^0.8.0;

contract Counter {
    uint public count;

    function increment() external {
        count += 1;
    }
}

interface ICounter {
    function count() external view returns (uint);

    function increment() external;}

contract MyContract {
    function incrementCounter(address _counter) external {
        ICounter(_counter).increment();
    }

    function getCount(address _counter) external view returns (uint) {
        return ICounter(_counter).count();
    }
}

  • 与合约类似(⼀个特殊合约),是函数的封装,⽤于代码复⽤。
  • 如果库函数都是 internal 的,库代码会嵌⼊到合约。
  • 如果库函数有external或 public ,库需要单独部署,并在部署合约时进⾏链接,使⽤委托调⽤
  • 没有状态变量
  • 不能给库发送 Ether
  • 给类型扩展功能:Using lib for type; 如: using SafeMath for uint;
pragma solidity ^0.8.0;

library SafeMath {
    function add(uint x, uint y) internal pure returns (uint) {
        uint z = x + y;
        require(z >= x, "uint overflow");
        return z;
    }
}

contract TestLib {
    using SafeMath for uint;
    function testAdd(uint x, uint y) public pure returns (uint) {
        return x.add(y);
    }
}

OpenZeppelin

  • 善于复⽤库,不仅提⾼效率,还可以提⾼安全性
  • OpenZeppelin 功能丰富、经过反复验证的库函数集合

理解ABI

ABI:Application Binary Interface 应⽤程序⼆进制接⼝

  • ABI 接⼝描述: 定义如何与合约交互
  • ABI 编码
    • 函数选择器:对函数签名计算Keccak-256哈希,取前 4 个字节
      • bytes4(keccak256("count()")) = 0x06661abd
    • 参数编码
  • image1

工具

推荐学习资料

  • https://ethernaut.openzeppelin.com/
  • https://cryptozombies.io/en/course/
  • https://solidity-by-example.org/
  • https://learnblockchain.cn/column/1
This post is licensed under CC BY 4.0 by the author.