【区块链安全 | 第十六篇】类型之值类型(三)

在这里插入图片描述

值类型

函数类型

函数类型用于表示函数本身作为一种类型。可以通过函数为函数类型的变量赋值,也可以将函数作为参数进行传递或作为返回值返回。

函数类型分为两类:内部函数(internal function) 和 外部函数(external function)。

  • 内部函数类型 仅可在当前合约中调用(更准确地说,是在当前代码单元中调用,包括内部的库函数或继承的函数)。这类函数调用不会触发实际的消息调用,而是通过跳转(jump)到函数入口实现,就像调用普通内部函数一样。

  • 外部函数类型 则包括一个合约地址和函数签名,可以用于合约之间的外部调用,也可以作为函数参数进行传递或返回。

需要注意的是,当前合约中的 public 函数既可以作为内部函数使用(通过函数名直接调用),也可以作为外部函数使用(通过 this.f 的方式调用)。使用 this.f 会发起一笔真正的消息调用(external call)。

如果函数类型的变量未被初始化,调用它会导致 Panic 错误。在对函数类型变量使用 delete 后再调用同样也会引发此错误。

声明语法

函数类型的声明语法如下:

function (<参数类型>) {internal|external} [pure|view|payable] [returns (<返回类型>)]

其中:

  • internal 或 external 表示函数的可见性;

  • pure / view / payable 表示状态可变性;

  • returns (<返回类型>) 指定返回值类型。

返回值部分必须完整地写明(或完全省略)。如果函数没有返回值,则应省略 returns (…) 这一段。

默认情况下,函数类型是 internal,因此可以省略 internal 关键字(仅适用于类型定义;实际函数定义时必须显式声明可见性)。

函数间的转换

函数类型 A 可以隐式转换为函数类型 B,仅当以下条件同时满足:

  • 两者的参数类型、返回类型、internal/external 属性完全相同;

  • A 的状态可变性比 B 更严格(即副作用更少)。

具体规则如下:

  • pure 函数可转换为 view 和非 payable 函数;

  • view 函数可转换为非 payable 函数;

  • payable 函数可转换为非 payable 函数;

  • 其他类型间不支持任何隐式或显式转换。

如果在 Solidity 的上下文之外使用外部函数类型,它们会被视为一种特殊的函数类型。这种类型将合约地址和函数标识符一起编码为一个 bytes24 类型。

内部函数类型可以被分配给一个内部函数类型的变量,而不受其定义位置的限制。无论该函数是合约中的 private、internal、public 函数,还是库中的函数,甚至是自由函数,均可被作为内部函数类型的变量进行赋值。相比之下,外部函数类型只能与合约中的 public 或 external 函数兼容。

注意:
带有 calldata 参数的外部函数类型与带有相同类型 calldata 参数的外部函数类型之间不兼容。然而,它们与相应的内存(memory)参数类型是兼容的。

举例来说,没有一个函数可以被类型为 function(string calldata) external 的值所指向,而 function(string memory) external 可以指向如下两种函数:

function f(string memory) external {}
function g(string calldata) external {}

这是因为,无论是 calldata 还是 memory,参数都以相同的方式传递给函数:调用方始终会将参数编码到内存中,不能直接将 calldata 传递给外部函数。

因此,虽然在外部函数的实现中,标记为 calldata 参数会影响其实现细节,但在调用时,对于函数指针来说并没有实际意义。

成员

外部(或公共)函数具有以下成员:

  • .address:返回函数所在合约的地址。

  • .selector:返回函数的 ABI 函数选择器(即该函数签名的前 4 个字节,用于识别函数)。

外部(或公共)函数曾经有两个额外的成员:.gas(uint) 和 .value(uint),这两个成员在 Solidity 0.6.2 中已被弃用,并在 Solidity 0.7.0 中被完全移除。现在,应使用 {gas: …} 和 {value: …} 来指定传递给函数的 gas 数量或 wei 数量。

合约更新时的值稳定性

在使用函数类型的值时,需要考虑一个重要的方面,即如果底层代码发生变化,值是否仍然有效。

区块链的状态并非完全不可变,并且有多种方法可以在同一地址下放置不同的代码:

  • 通过加盐合约创建直接部署不同的代码。

  • 通过 DELEGATECALL 委托到不同的合约(代理合约背后的可升级代码是一个常见的例子)。

  • EIP-7702 定义的账户抽象。

外部函数类型可以被视为与合约的 ABI 一样稳定,这使得它们非常可移植。它们的 ABI 表示始终由合约地址和函数选择器组成,长期存储或在合约之间传递是完全安全的。虽然被引用的函数可能会发生变化或消失,但直接的外部调用将受到相同的影响,因此在这种使用方式下没有额外的风险。

然而,对于内部函数,值是一个与合约字节码紧密相关的标识符。标识符的实际表示是实现细节,并且可能在不同的编译器版本之间,甚至在不同的后端之间发生变化。以给定表示分配的值是确定性的(即,只要源代码相同,值就会保持不变),但容易受到变化的影响,例如添加、删除或重新排序函数。编译器还可以自由地删除从未使用的内部函数,这可能会影响其他标识符。一些表示方式,例如将标识符仅仅作为跳转目标的方式,可能会受到几乎任何变化的影响,即使这些变化与内部函数完全无关。

为了应对这种情况,Solidity 限制了在有效上下文之外使用内部函数类型。这就是为什么内部函数类型不能作为外部函数的参数(或以其他方式在合约 ABI 中暴露)使用的原因。然而,仍然有一些情况下,用户需要自行判断是否安全使用这些值。例如,虽然不推荐将这些值长期存储在状态变量中,但如果合约代码不会更新,那么这种做法可能是安全的。此外,使用内联汇编可以绕过这些安全限制,但这种做法需要格外小心。

示例

1、使用成员的代码示例:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.4 <0.9.0; // 设置 Solidity 编译器版本范围

contract Example {
    // 定义一个公共的、可支付的函数 f
    function f() public payable returns (bytes4) {
        // 使用 assert 检查函数 f 的地址是否与当前合约的地址相同
        assert(this.f.address == address(this));  // 检查 f 函数的地址是否等于当前合约地址
        // 返回 f 函数的 ABI 选择器(前 4 字节的哈希值)
        return this.f.selector;
    }

    // 定义一个公共函数 g,用来调用函数 f
    function g() public {
        // 使用 .f{gas: 10, value: 800}() 调用 f 函数,指定发送 800 wei 并且限制 gas 为 10
        this.f{gas: 10, value: 800}();
    }
}

2、使用内部函数类型的代码示例:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0; // 设置 Solidity 编译器版本范围

// 定义一个库 ArrayUtils,提供对数组进行操作的函数
library ArrayUtils {

    // 内部函数 map:接受一个 uint 数组和一个函数 f,将 f 应用于数组中的每个元素,并返回新的数组
    function map(uint[] memory self, function (uint) pure returns (uint) f)
        internal
        pure
        returns (uint[] memory r)
    {
        r = new uint[](self.length); // 创建一个与输入数组大小相同的新数组
        for (uint i = 0; i < self.length; i++) {
            r[i] = f(self[i]); // 对每个元素应用函数 f
        }
    }

    // 内部函数 reduce:接受一个 uint 数组和一个二元函数 f,对数组进行归约操作(折叠)
    function reduce(
        uint[] memory self,
        function (uint, uint) pure returns (uint) f
    )
        internal
        pure
        returns (uint r)
    {
        r = self[0]; // 初始化结果为数组的第一个元素
        for (uint i = 1; i < self.length; i++) {
            r = f(r, self[i]); // 对数组的每个元素应用函数 f
        }
    }

    // 内部函数 range:生成一个从 0 到 length-1 的 uint 数组
    function range(uint length) internal pure returns (uint[] memory r) {
        r = new uint[](length); // 创建一个指定长度的数组
        for (uint i = 0; i < r.length; i++) {
            r[i] = i; // 填充数组,元素为 0 到 length-1
        }
    }
}

contract Pyramid {
    using ArrayUtils for *; // 使用 ArrayUtils 库中的函数

    // 函数 pyramid:创建一个范围为 l 的数组,应用 square 函数进行变换,再使用 reduce 函数对数组进行求和
    function pyramid(uint l) public pure returns (uint) {
        return ArrayUtils.range(l).map(square).reduce(sum); // 生成数组,映射每个元素为平方值,然后进行求和
    }

    // 内部函数 square:返回输入值的平方
    function square(uint x) internal pure returns (uint) {
        return x * x;
    }

    // 内部函数 sum:返回两个输入值的和
    function sum(uint x, uint y) internal pure returns (uint) {
        return x + y;
    }
}

}

contract Pyramid {
    using ArrayUtils for *;

    function pyramid(uint l) public pure returns (uint) {
        return ArrayUtils.range(l).map(square).reduce(sum);
    }

    function square(uint x) internal pure returns (uint) {
        return x * x;
    }

    function sum(uint x, uint y) internal pure returns (uint) {
        return x + y;
    }
}

map 和 reduce 函数的定义使用了内部函数类型作为参数。

在 Pyramid 合约中,传递的 square 和 sum 函数正是符合这些内部函数类型的内部函数。

3、使用外部函数类型的代码示例:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0; // 设置 Solidity 编译器版本范围

// 定义一个 Oracle 合约,用于请求数据并通过回调返回结果
contract Oracle {
    // 定义一个 Request 结构体,包含请求的数据和回调函数
    struct Request {
        bytes data; // 请求的数据,存储为字节数组
        function(uint) external callback; // 回调函数,接收一个 uint 参数并是外部可见的
    }

    Request[] private requests; // 存储所有请求的数组
    event NewRequest(uint); // 事件,用于记录新请求的 ID

    // query 函数:接受请求数据和一个外部回调函数
    function query(bytes memory data, function(uint) external callback) public {
        // 将请求添加到 requests 数组中
        requests.push(Request(data, callback));
        // 触发 NewRequest 事件,通知有新请求
        emit NewRequest(requests.length - 1);
    }

    // reply 函数:用于处理来自外部的响应,并触发回调
    function reply(uint requestID, uint response) public {
        // 这里进行检查,确保回复来自可信源
        requests[requestID].callback(response); // 调用请求中的回调函数,将响应传递给它
    }
}

// 定义一个 OracleUser 合约,模拟从 Oracle 获取汇率并处理响应
contract OracleUser {
    // 定义一个常量,Oracle 合约的地址,指向已知的 Oracle 合约
    Oracle constant private ORACLE_CONST = Oracle(address(0x00000000219ab540356cBB839Cbe05303d7705Fa)); // 已知合约
    uint private exchangeRate; // 存储汇率的状态变量

    // buySomething 函数:向 Oracle 请求数据(例如汇率)
    function buySomething() public {
        // 调用 Oracle 的 query 函数,传递数据和回调函数
        ORACLE_CONST.query("USD", this.oracleResponse);
    }

    // oracleResponse 函数:接收 Oracle 返回的响应,并更新汇率
    function oracleResponse(uint response) public {
        // 确保只有 Oracle 合约可以调用该函数
        require(
            msg.sender == address(ORACLE_CONST),
            "Only oracle can call this."
        );
        exchangeRate = response; // 更新汇率
    }
}

callback 作为外部函数类型,可以允许外部合约通过 Oracle 合约进行回调。

通过传递外部函数类型,Oracle 合约能够调用 OracleUser 合约中的 oracleResponse 函数,这样响应就可以被处理并且进行合约间交互。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋说

感谢打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值