Solidity(Day2)(继承-目标合约)

继承

继承是面向对象编程很重要的组成部分,可以显著减少重复代码。如果把合约看作是对象的话,solidity也是面向对象的编程,也支持继承。

规则

  • virtual: 父合约中的函数,如果希望子合约重写,需要加上virtual关键字。

  • override:子合约重写了父合约中的函数,需要加上override关键字。

注意:用override修饰public变量,会重写与变量同名的getter函数

注意:重写方法后,父函数就不再执行。如果想要执行父方法,可以使用 super。

简单继承

我们先写一个简单的爷爷合约Yeye,里面包含1个Log事件和3个function: hip(), pop(), yeye(),输出都是”Yeye”。

contract Yeye {

   event Log(string msg);

   // 定义3个function: hip(), pop(), man(),Log值为Yeye。

   function hip() public virtual{

       emit Log("Yeye");

   }

   function pop() public virtual{

       emit Log("Yeye");

   }

   function yeye() public virtual {

       emit Log("Yeye");

   }

}

多重继承

solidity的合约可以继承多个合约。规则:

  1. 继承时要按辈分最高到最低的顺序排。比如我们写一个Erzi合约,继承Yeye合约和Baba合约,那么就要写成contract Erzi is Yeye, Baba,而不能写成contract Erzi is Baba, Yeye,不然就会报错。

  2. 如果某一个函数在多个继承的合约里都存在,比如例子中的hip()和pop(),在子合约里必须重写,不然会报错。

  3. 重写在多个父合约中都重名的函数时,override关键字后面要加上所有父合约名字,例如override(Yeye, Baba)。

contract Erzi is Yeye, Baba{

   // 继承两个function: hip()和pop(),输出值为Erzi。

   function hip() public virtual override(Yeye, Baba){

       emit Log("Erzi");

   }

   function pop() public virtual override(Yeye, Baba) {

       emit Log("Erzi");

   }

修饰器的继承

Solidity中的修饰器(Modifier)同样可以继承,用法与函数继承类似,在相应的地方加virtual和override关键字即可。

contract Base1 {

   modifier exactDividedBy2And3(uint _a) virtual {

       require(_a % 2 == 0 && _a % 3 == 0);

       _;

   }

}

contract Identifier is Base1 {

   //计算一个数分别被2除和被3除的值,但是传入的参数必须是2和3的倍数

   function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) {

       return getExactDividedBy2And3WithoutModifier(_dividend);

   }

   //计算一个数分别被2除和被3除的值

   function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){

       uint div2 = _dividend / 2;

       uint div3 = _dividend / 3;

       return (div2, div3);

   }

}

构造函数的继承

abstract contract A {

   uint public a;

   constructor(uint _a) {

       a = _a;

   }

}

contract C is A {

   constructor(uint _c) A(_c * _c) {}

}

调用父合约的函数

  1. 直接调用:子合约可以直接用父合约名.函数名()的方式来调用父合约函数,例如Yeye.pop()。

function callParent() public{

       Yeye.pop();

   }

2,`super`关键字:子合约可以利用`super.函数名()`来调用最近的父合约函数。`solidity`继承关系按声明时从右到左的顺序是:`contract Erzi is Yeye, Baba`,那么`Baba`是最近的父合约,`super.pop()`将调用`Baba.pop()`而不是`Yeye.pop()`:

function callParentSuper() public{

       // 将调用最近的父合约函数,Baba.pop()

       super.pop();

   }

钻石继承

在面向对象编程中,钻石继承(菱形继承)指一个派生类同时有两个或两个以上的基类。

在多重+菱形继承链条上使用super关键字时,需要注意的是使用super会调用继承链条上的每一个合约的相关函数,而不是只调用最近的父合约。

我们先写一个合约God,再写Adam和Eve两个合约继承God合约,最后让创建合约people继承自Adam和Eve,每个合约都有foo和bar两个函数。

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

/* 继承树:

 God

/  \

Adam Eve

\  /

people

*/

contract God {

   event Log(string message);

   function foo() public virtual {

       emit Log("God.foo called");

   }

   function bar() public virtual {

       emit Log("God.bar called");

   }

}

contract Adam is God {

   function foo() public virtual override {

       emit Log("Adam.foo called");

   }

   function bar() public virtual override {

       emit Log("Adam.bar called");

       super.bar();

   }

}

contract Eve is God {

   function foo() public virtual override {

       emit Log("Eve.foo called");

       Eve.foo();

   }

   function bar() public virtual override {

       emit Log("Eve.bar called");

       super.bar();

   }

}

contract people is Adam, Eve {

   function foo() public override(Adam, Eve) {

       super.foo();

   }

   function bar() public override(Adam, Eve) {

       super.bar();

   }

这种继承属于菱形继承问题,solidity需要保证继承不会重复,god如果再显示继承,就会继承多次god

重复继承是不符合程序设计的原则。

抽象合约

如果一个智能合约里至少有一个未实现的函数,即某个函数缺少主体{}中的内容,则必须将该合约标为abstract,不然编译会报错;另外,未实现的函数需要加virtual,以便子合约重写。拿我们之前的插入排序合约为例,如果我们还没想好具体怎么实现插入排序函数,那么可以把合约标为abstract,之后让别人补写上。

abstract contract InsertionSort{

   function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);

}

接口

接口类似于抽象合约,但它不实现任何功能。接口的规则:

  1. 不能包含状态变量

  2. 不能包含构造函数

  3. 不能继承除接口外的其他合约

  4. 所有函数都必须是external且不能有函数体

  5. 继承接口的非抽象合约必须实现接口定义的所有功能

虽然接口不实现任何功能,但它非常重要。接口是智能合约的骨架,定义了合约的功能以及如何触发它们:如果智能合约实现了某种接口(比如ERC20或ERC721),其他Dapps和智能合约就知道如何与它交互。因为接口提供了两个重要的信息:

  1. 合约里每个函数的bytes4选择器,以及函数签名函数名(每个参数类型)。

  2. 接口id(更多信息见EIP165

另外,接口与合约ABI(Application Binary Interface)等价,可以相互转换:编译接口可以得到合约的ABI,利用abi-to-sol工具也可以将ABI json文件转换为接口sol文件。

我们以ERC721接口合约IERC721为例,它定义了3个event和9个function,所有ERC721标准的NFT都实现了这些函数。我们可以看到,接口和常规合约的区别在于每个函数都以;代替函数体{ }结尾。

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。

什么时候使用接口?

如果我们知道一个合约实现了IERC721接口,我们不需要知道它具体代码实现,就可以与它交互。

无聊猿BAYC属于ERC721代币,实现了IERC721接口的功能。我们不需要知道它的源代码,只需知道它的合约地址,用IERC721接口就可以与它交互,比如用balanceOf()来查询某个地址的BAYC余额,用safeTransferFrom()来转账BAYC。

异常

写智能合约经常会出bug,solidity中的异常命令帮助我们debug。

Error

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

error TransferNotOwner(); // 自定义error

我们也可以定义一个携带参数的异常,来提示尝试转账的账户地址

error TransferNotOwner(address sender); // 自定义的带参数的error

在执行当中,error必须搭配revert(回退)命令使用。

function transferOwner1(uint256 tokenId, address newOwner) public {

        if(_owners[tokenId] != msg.sender){

            revert TransferNotOwner();

            // revert TransferNotOwner(msg.sender);

        }

        _owners[tokenId] = newOwner;

    }

Require

require命令是solidity 0.8版本之前抛出异常的常用方法,目前很多主流合约仍然还在使用它。它很好用,唯一的缺点就是gas随着描述异常的字符串长度增加,比error命令要高。使用方法:require(检查条件,"异常的描述"),当检查条件不成立的时候,就会抛出异常。

我们用require命令重写一下上面的transferOwner函数:

    function transferOwner2(uint256 tokenId, address newOwner) public {

        require(_owners[tokenId] == msg.sender, "Transfer Not Owner");

        _owners[tokenId] = newOwner;

    }

Assert

assert命令一般用于程序员写程序debug,因为它不能解释抛出异常的原因(比require少个字符串)。它的用法很简单,assert(检查条件),当检查条件不成立的时候,就会抛出异常。

我们用assert命令重写一下上面的transferOwner函数:

function transferOwner3(uint256 tokenId, address newOwner) public {

        assert(_owners[tokenId] == msg.sender);

        _owners[tokenId] = newOwner;

    }

重载

solidity中允许函数进行重载(overloading),即名字相同但输入参数类型不同的函数可以同时存在,他们被视为不同的函数。注意,solidity不允许修饰器(modifier)重载。

函数重载

举个例子,我们可以定义两个都叫saySomething()的函数,一个没有任何参数,输出"Nothing";另一个接收一个string参数,输出这个string。

实参匹配(Argument Matching)

在调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。 如果出现多个匹配的重载函数,则会报错。下面这个例子有两个叫f()的函数,一个参数为uint8,另一个为uint256:

    function f(uint8 _in) public pure returns(uint8 out){

out=_in

}

    function f(uint256 _in)public pure returns(uint256 out){

out=_in;

}

String库合约

String库合约是将uint256类型转换为相应的string类型的代码库,样例代码如下:

library Strings {

    bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";

    /**

     * @dev Converts a `uint256` to its ASCII `string` decimal representation.

     */

    function toString(uint256 value) public pure returns (string memory) {

        // Inspired by OraclizeAPI's implementation - MIT licence

        // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol

        if (value == 0) {

            return "0";

        }

        uint256 temp = value;

        uint256 digits;

        while (temp != 0) {

            digits++;

            temp /= 10;

        }

        bytes memory buffer = new bytes(digits);

        while (value != 0) {

            digits -= 1;

            buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));

            value /= 10;

        }

        return string(buffer);

    }

    /**

     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.

     */

    function toHexString(uint256 value) public pure returns (string memory) {

        if (value == 0) {

            return "0x00";

        }

        uint256 temp = value;

        uint256 length = 0;

        while (temp != 0) {

            length++;

            temp >>= 8;

        }

        return toHexString(value, length);

    }

    /**

     * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.

     */

    function toHexString(uint256 value, uint256 length) public pure returns (string memory) {

        bytes memory buffer = new bytes(2 * length + 2);

        buffer[0] = "0";

        buffer[1] = "x";

        for (uint256 i = 2 * length + 1; i > 1; --i) {

            buffer[i] = _HEX_SYMBOLS[value & 0xf];

            value >>= 4;

        }

        require(value == 0, "Strings: hex length insufficient");

        return string(buffer);

    }

}

1. 利用using for指令

指令using A for B;可用于附加库合约(从库 A)到任何类型(B)。添加完指令后,库A中的函数会自动添加为B类型变量的成员,可以直接调用。注意:在调用的时候,这个变量会被当作第一个参数传递给函数

2. 通过库合约名称调用函数

// 直接通过库合约名调用

    function getString2(uint256 _number) public pure returns(string memory){

        return Strings.toHexString(_number);

    }

接收ETH函数 receive

receive()函数是在合约收到ETH转账时被调用的函数。一个合约最多有一个receive()函数,声明方式与一般函数不一样,不需要function关键字:receive() external payable { ... }。receive()函数不能有任何的参数,不能返回任何值,必须包含external和payable。

当合约接收ETH的时候,receive()会被触发。receive()最好不要执行太多的逻辑因为如果别人用send和transfer方法发送ETH的话,gas会限制在2300,receive()太复杂可能会触发Out of Gas报错;如果用call就可以自定义gas执行更复杂的逻辑(这三种发送ETH的方法我们之后会讲到)。

我们可以在receive()里发送一个event,例如:

    // 定义事件

    event Received(address Sender, uint Value);

    // 接收ETH时释放Received事件

    receive() external payable {

        emit Received(msg.sender, msg.value);

    }

回退函数 fallback

fallback()函数会在调用合约不存在的函数时被触发。可用于接收ETH,也可以用于代理合约proxy contract。fallback()声明时不需要function关键字,必须由external修饰,一般也会用payable修饰,用于接收ETH:fallback() external payable { ... }。

我们定义一个fallback()函数,被触发时候会释放fallbackCalled事件,并输出msg.sender,msg.value和msg.data:

    event fallbackCalled(address Sender, uint Value, bytes Data);

    // fallback

    fallback() external payable{

        emit fallbackCalled(msg.sender, msg.value, msg.data);

    }

触发fallback() 还是 receive()?

           接收ETH

              |

         msg.data是空?

            /  \

          是    否

          /      \

receive()存在?   fallback()

        / \

       是  否

      /     \

receive()   fallback()

简单来说,合约接收ETH时,msg.data为空且存在receive()时,会触发receive();msg.data不为空或不存在receive()时,会触发fallback(),此时fallback()必须为payable。

receive()和payable fallback()均不存在的时候,向合约直接发送ETH将会报错(你仍可以通过带有payable的函数向合约发送ETH)。

接收ETH合约

我们先部署一个接收ETH合约ReceiveETH。ReceiveETH合约里有一个事件Log,记录收到的ETH数量和gas剩余。还有两个函数,一个是receive()函数,收到ETH被触发,并发送Log事件;另一个是查询合约ETH余额的getBalance()函数。

contract ReceiveETH {

    // 收到eth事件,记录amount和gas

    event Log(uint amount, uint gas);

    

    // receive方法,接收eth时被触发

    receive() external payable{

        emit Log(msg.value, gasleft());

    }

    

    // 返回合约ETH余额

    function getBalance() view public returns(uint) {

        return address(this).balance;

    }

}

发送ETH合约

我们将实现三种方法向ReceiveETH合约发送ETH。首先,先在发送ETH合约SendETH中实现payable的构造函数和receive(),让我们能够在部署时和部署后向合约转账。

contract SendETH {

    // 构造函数,payable使得部署的时候可以转eth进去

    constructor() payable{}

    // receive方法,接收eth时被触发

    receive() external payable{}

}

transfer

  • 用法是接收方地址.transfer(发送ETH数额)。

  • transfer()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。

  • transfer()如果转账失败,会自动revert(回滚交易)。

代码样例,注意里面的_to填ReceiveETH合约的地址,amount是ETH转账金额:

// 用transfer()发送ETH

function transferETH(address payable _to, uint256 amount) external payable{

    _to.transfer(amount);

}

send

  • 用法是接收方地址.send(发送ETH数额)。

  • send()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。

  • send()如果转账失败,不会revert。

  • send()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下。

代码样例:

// send()发送ETH

function sendETH(address payable _to, uint256 amount) external payable{

    // 处理下send的返回值,如果失败,revert交易并发送error

    bool success = _to.send(amount);

    if(!success){

        revert SendFailed();

    }

}

call

  • 用法是接收方地址.call{value: 发送ETH数额}("")。

  • call()没有gas限制,可以支持对方合约fallback()或receive()函数实现复杂逻辑。

  • call()如果转账失败,不会revert。

  • call()的返回值是(bool, data),其中bool代表着转账成功或失败,需要额外代码处理一下。

代码样例:

// call()发送ETH

function callETH(address payable _to, uint256 amount) external payable{

    // 处理下call的返回值,如果失败,revert交易并发送error

    (bool success,) = _to.call{value: amount}("");

    if(!success){

        revert CallFailed();

    }

}

目标合约

我们先写一个简单的合约OtherContract,用于被其他合约调用。

contract OtherContract {

    uint256 private _x = 0; // 状态变量_x

    // 收到eth的事件,记录amount和gas

    event Log(uint amount, uint gas);

    

    // 返回合约ETH余额

    function getBalance() view public returns(uint) {

        return address(this).balance;

    }

    // 可以调整状态变量_x的函数,并且可以往合约转ETH (payable)

    function setX(uint256 x) external payable{

        _x = x;

        // 如果转入ETH,则释放Log事件

        if(msg.value > 0){

            emit Log(msg.value, gasleft());

        }

    }

    // 读取_x

    function getX() external view returns(uint x){

        x = _x;

    }

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值