solidity学习(一)

12/19/2024 2977 阅读需要15分钟
0
0
AI总结
Solidity是一种面向合约的高级编程语言,专为以太坊智能合约设计。它支持多种数据类型,包括布尔值、整数、地址、字符串、数组、结构体、枚举和映射。Solidity中的函数具有不同的可见性(public、private、external、internal)和权限(pure、view、payable),并且可以返回多个值。事件用于记录合约中的重要信息,修饰器可以在函数执行前后执行操作。Solidity支持合约继承,允许子合约重写父合约的方法。错误处理通过assert、require和revert实现,分别用于检查内部错误、输入参数和合约状态。抽象合约和接口用于定义未实现的函数或标准API,接口不能包含代码实现。

Solidity学习(一)

1. 什么是Solidity

Solidity是一种面向合约的、为实现智能合约而创建的高级编程语言。它是专门为以太坊设计的,以太坊是一个开源的区块链平台,可以用它来创建智能合约。

2. Solidity语法

2.1 数据类型

Solidity支持多种数据类型,包括布尔值、整数、地址、字符串、数组、结构体、枚举等。

2.1.1 布尔值

布尔值有两个取值:truefalse

和其它语言一样,solidity支持逻辑运算符&&||!

bool public _bool1 = !_bool; // 取非
bool public _bool2 = _bool && _bool1; // 与
bool public _bool3 = _bool || _bool1; // 或
bool public _bool4 = _bool == _bool1; // 相等
bool public _bool5 = _bool != _bool1; // 不相等
bool flag = true;

2.1.2 整数

Solidity支持有符号整数和无符号整数,有符号整数有intint8int256,无符号整数有uintuint8uint256

int a = 10;
uint b = 20;

2.1.3 地址

地址类型是一个20字节的值,它存储了一个以太坊地址。

地址类型有2中,一种是普通地址,一种是payable address,这种地址可以用来接收资产,比普通地址多了 transfer 和 send 两个成员方法,用于接收转账。定义的时候用修饰符payable

address addr = 0x1234567890123456789012345678901234567890;

address payable public _address1 = payable(_address); // payable address,可以转账、查余额

2.1.4 字符串

字符串类型是一个动态的字节数组,它存储了一个UTF-8编码的字符串。

string str = "Hello, World!"

2.1.5 数组

数组是一种存储相同类型数据的有序集合,它可以是固定长度的数组,也可以是动态长度的数组。

uint[] arr1 = [1, 2, 3, 4, 5];
uint[5] arr2 = [1, 2, 3, 4, 5];

2.1.6 结构体

结构体是一种用户自定义的数据类型,它可以包含多个不同类型的数据。

struct Person {
    string name;
    uint age;
}

Person person = Person("Alice", 20);

2.1.7 枚举

枚举是一种用户自定义的数据类型,它可以包含多个枚举值。

enum Color { Red, Green, Blue }

Color color = Color.Red;

2.1.8 Mapping

mapping是一种键值对的数据结构,它可以存储多个键值对。mapping的键可以是任意类型,值可以是任意类型,但键的类型和值的类型必须是一致的。且mapping的键是不可迭代的。存储在storage中。


mapping(address => uint) public balances;

// set
balances[0x1234567890123456789012345678901234567890] = 100;

// get
unit256 count = balances[0x1234567890123456789012345678901234567890];

// delete
delete balances[0x1234567890123456789012345678901234567890];

2.2 函数

Solidity中的函数是合约中的一种特殊类型,它可以被外部调用,也可以被其他函数调用。

function <function name>(<parameter types>) {internal|external|public|private} [pure|view|payable] [returns (<return types>)]

看着有一些复杂,让我们从前往后逐个解释(方括号中的是可写可不 写的关键字):

function:声明函数时的固定用法。要编写函数,就需要以 function 关键字开头。

function name:函数名。

<parameter types>:圆括号内写入函数的参数,即输入到函数的变量类型和名称。

internal|external|public|private:函数可见性说明符,共有4种。

  • public:内部和外部均可见。
  • private:只能从本合约内部访问,继承的合约也不能使用。
  • external:只能从合约外部访问(但内部可以通过 this.f() 来调用,f是函数名)。
  • internal: 只能从合约内部访问,继承的合约可以用。 注意 1:合约中定义的函数需要明确指定可见性,它们没有默认值。

注意 2:public|private|internal 也可用于修饰状态变量。public变量会自动生成同名的getter函数,用于查询数值。未标明可见性类型的状态变量,默认为internal。

[pure|view|payable]:决定函数权限/功能的关键字。payable(可支付的)很好理解,带着它的函数,运行的时候可以给合约转入 ETH,pure函数不会读取或写入区块链状态,view函数不会写入区块链状态。

[returns ()]:函数返回的变量类型和名称。


contract MyContract {
    function add(uint a, uint b) public pure returns (uint) {
        return a + b;
    }
}

// 合约A
contract MyToken {
    uint256 public num = 100;

    // 内部函数
    function min() internal  {
        num -= 1;
    }
    // 可被外部调用
    function minCell()  external  {
        // 合约内部调用直接通过f()调用
        min();
    }

}

// 合约B
contract B is MyToken {
    // 继承MyToken合约
    constructor() MyToken() {

    }
    // 可被外部调用
    function Bmin() external {
        this.minCell();
    }
}

internal & external

通过remix编译部署后,我们可看到,只有external的函数才可以看到。

函数的返回值可以使用多个返回值,也可以使用命名返回值。


contract MyContract {
    // return 返回
    function add(uint a, uint b) public pure returns (uint) {
        return a + b;
    }

    function sub(uint a, uint b) public pure returns (uint) {
        return a - b;
    }
    // 命名返回,在函数声明时,可以指定返回值的名称,其中定义的变量名可以在函数体中直接使用会直接返回无需return
    function addSub(uint a, uint b) public pure returns (uint sum, uint diff) {
        sum = a + b;
        diff = a - b;
    }

    // 支持解构返回
    function addSub2(uint a, uint b) public pure returns (uint sum, uint diff) {
        (_, diff) = addSub(a, b);
        // 此处的diff是addSub的返回值
    }
}

2.3 事件

事件是合约中的一种特殊类型,它用来记录合约中的重要信息。


contract MyContract {
    event Log(string message);

    function doSomething() public {
        emit Log("Something happened!");
    }
}

2.4 修饰器

修饰器是一种特殊的函数,它可以在函数执行前或执行后执行一些操作。

modifier

modifier是一种特殊的函数,它可以在函数执行前或执行后执行一些操作。


contract MyContract {
    // 定义一个onlyOwner修饰器函数
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function");
        _;
    }
    // 当执行doSomething函数时,会先执行onlyOwner修饰器函数
    function doSomething() public onlyOwner {
        // Only owner can call this function
    }
}

2.5 事件

事件是合约中的一种特殊类型,它用来记录合约中的重要信息。

应用程序(ethers.js)可以通过RPC接口订阅和监听这些事件,并在前端做响应。


contract MyContract {
    // 定义一个Log事件
    event Log(string message);

    // 当执行doSomething函数时,会触发Log事件
    function doSomething() public {
        emit Log("Something happened!");
    }
}

2.6 继承

Solidity支持合约之间的继承,一个合约可以继承另一个合约的所有属性和方法。

例如:

contract A {
    uint public num = 100;
}

contract B is A {
    function getNum() public view returns (uint) {
        return num;
    }
}

也支持多个合约继承,多重继承,例如C继承B和A。


contract A {
    uint public num = 100;
}

contract B {
    uint public num = 200;
}

contract C is A, B {
    function getNum() public view returns (uint, uint) {
        return (num, num);
    }
}

如果需要对父合约中的方法重写,可以使用override关键字,能被重用的合约方法需要使用virtual关键字。


调用父合约的方法,有2中方式,一种是通过父合约的合约名调用,另一种是通过super关键字调用。

 - 通过父合约的合约名调用
    parentContract.functionName();
 - 通过super关键字调用,会调用最近的父类的functionName方法
    super.functionName();

```solidity

contract A {
    function doSomething() public virtual {
        // A
    }
}

contract B is A {
    function doSomething() public override {
        // B
        // B中的doSomething方法会覆盖A中的doSomething方法

        // B调用A中的doSomething方法
        super.doSomething();
        A.doSomething();
    }
}

2.7 错误处理

错误处理有三种方式,requirerevertassert

2.7.1 assert

断言是一种用来检查合约状态的机制,如果断言失败,合约会立即停止执行。


contract MyContract {
    function doSomething() public {
        assert(1 == 2);
    }
}

2.7.2 require


contract MyContract {
    function doSomething() public {
        require(1 == 2, "1 is not equal to 2");
    }
}

2.7.3 revert

revert是一种用来回滚交易的机制,如果revert被调用,交易会被回滚。


contract MyContract {
    function doSomething() public {
        if (1 != 2) {
            revert("Something went wrong");
        }
    }
}

2.7.4 Error

error是solidity 0.8.4版本新加的内容,方便且高效(省gas)地向用户解释操作失败的原因,同时还可以在抛出异常的同时携带参数,帮助开发者更好地调试。人们可以在contract之外定义异常。下面,我们定义一个TransferNotOwner异常,当用户不是代币owner的时候尝试转账,会抛出错误:

error TransferNotOwner(address sender)

// error的使用必须搭配revert
function transferOwner1(uint256 tokenId, address newOwner) public {
    if(_owners[tokenId] != msg.sender){
        // revert TransferNotOwner();
        revert TransferNotOwner(msg.sender);
    }
    _owners[tokenId] = newOwner;
}

2.7.4 错误处理的区别

  • assert:用于检查合约的内部错误,如果断言失败,合约会立即停止执行。
  • require:用于检查合约的输入参数,如果条件不满足,合约会回滚。
  • revert:用于检查合约的状态,如果条件不满足,合约会回滚。

2.8 抽象合约和接口

如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体中的内容,则必须将该合约标为abstract,不然编译会报错;另外,未实现的函数需要加virtual,以便子合约重写。

拿我们之前的插入排序合约为例,如果我们还没想好具体怎么实现插入排序函数,那么可以把合约标为abstract,之后让别人补写上。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/interfaces/IERC165.sol";

abstract contract InsertionSort{
    function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);
}

接口是一种不能包含任何代码的合约,它只能包含函数声明,不能包含函数实现。例如:


interface IERC721 is IERC165 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
    
    function balanceOf(address owner) external view returns (uint256 balance);

    function ownerOf(uint256 tokenId) external view returns (address owner);

    function safeTransferFrom(address from, address to, uint256 tokenId) external;

    function transferFrom(address from, address to, uint256 tokenId) external;

    function approve(address to, uint256 tokenId) external;

    function getApproved(uint256 tokenId) external view returns (address operator);

    function setApprovalForAll(address operator, bool _approved) external;

    function isApprovedForAll(address owner, address operator) external view returns (bool);

    function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external;
}

IERC721事件

IERC721包含3个事件,其中Transfer和Approval事件在ERC20中也有。

  • Transfer事件:在转账时被释放,记录代币的发出地址from,接收地址to和tokenId。
  • Approval事件:在授权时被释放,记录授权地址owner,被授权地址approved和tokenId。
  • ApprovalForAll事件:在批量授权时被释放,记录批量授权的发出地址owner,被授权地址operator和授权与否的approved。

IERC721函数

  • balanceOf:返回某地址的NFT持有量balance。
  • ownerOf:返回某tokenId的主人owner。
  • transferFrom:普通转账,参数为转出地址from,接收地址to和tokenId。
  • safeTransferFrom:安全转账(如果接收方是合约地址,会要求实现ERC721Receiver接口)。参数为转出地址from,接收地址to和tokenId。
  • approve:授权另一个地址使用你的NFT。参数为被授权地址approve和tokenId。
  • getApproved:查询tokenId被批准给了哪个地址。
  • setApprovalForAll:将自己持有的该系列NFT批量授权给某个地址operator。
  • isApprovedForAll:查询某地址的NFT是否批量授权给了另一个operator地址。
  • safeTransferFrom:安全转账的重载函数,参数里面包含了data。

什么时候使用接口呢?

  • 当你想要定义一个标准的API,让其他合约实现这个API时,可以使用接口。
contract interactBAYC {
    // 利用BAYC地址创建接口合约变量(ETH主网)
    IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D);

    // 通过接口调用BAYC的balanceOf()查询持仓量
    function balanceOfBAYC(address owner) external view returns (uint256 balance){
        return BAYC.balanceOf(owner);
    }

    // 通过接口调用BAYC的safeTransferFrom()安全转账
    function safeTransferFromBAYC(address from, address to, uint256 tokenId) external{
        BAYC.safeTransferFrom(from, to, tokenId);
    }
}