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

在这里插入图片描述

Solidity 是一种静态类型语言,这意味着每个变量(无论是状态变量还是局部变量)都需要在声明时指定类型。Solidity 提供了多种基本类型,这些类型可以组合成复杂的数据类型。

此外,不同类型可以在包含运算符的表达式中相互作用,并且运算符具有优先级区分。

Solidity 没有“未定义”或“空”值的概念,但新声明的变量总会有默认值,且默认值取决于其类型。如果遇到意外的值,应使用 revert 函数回滚交易,或者返回一个布尔值的元组,其中第二个 bool 值表示操作是否成功。

值类型

值类型的变量总是按值传递,即在作为函数参数或赋值时会被复制。

与引用类型不同,值类型的声明不指定数据存储位置,因为它们足够小,可以存储在栈中。唯一的例外是状态变量,状态变量默认存储在存储区,但也可以标记为 transient、constant 或 immutable。

布尔值

bool:值只能是 true 或 false。

整数

int / uint:有符号和无符号整数,支持不同大小。

关键字 uint8 到 uint256(以 8 为步长,表示 8 位到 256 位的无符号整数),以及 int8 到 int256。

uint 和 int 分别是 uint256 和 int256 的别名。

运算符

比较运算符:<=,<,==,!=,>=,>(结果为布尔值)

位运算符:&,|,^(按位异或),~(按位取反)

移位运算符:<<(左移),>>(右移)

算术运算符:+,-,一元 -(仅适用于有符号整数),*,/,%(取模),**(指数运算)

对于整数类型 X,可以使用 type(X).min 和 type(X).max 获取该类型的最小值和最大值。

运算符 || 和 && 遵循短路规则。例如,在 f(x) || g(y) 表达式中,如果 f(x) 结果为 true,则 g(y) 不会被计算。

注意
Solidity 中的整数有大小限制。例如,对于 uint32,其范围是 0 到 2**32 - 1。

Solidity 中的整数运算有两种模式:“溢出”模式(wrapping/unchecked mode)和 “检查”模式(checked mode)。

  • 默认情况下,运算在检查模式下进行。如果运算结果超出了类型的范围,则会因失败的断言而回滚。

  • 可以使用 unchecked { … } 切换到溢出模式,但应谨慎使用。

取模运算

取模运算符 % 返回操作数 a 除以 n 后的余数 r,即 r = a - (n * q),其中 q = int(a / n)。取模运算的结果与左操作数的符号一致,并且对于负数 a,a % n == -(-a % n) 恒成立。

例如:

int256(5) % int256(2) == int256(1)

int256(5) % int256(-2) == int256(1)

int256(-5) % int256(2) == int256(-1)

int256(-5) % int256(-2) == int256(-1)

注意: 使用 0 作为除数会导致 Panic 错误,此检查无法通过 unchecked { … } 关闭。

指数运算

** 运算符仅适用于无符号整数作为指数。指数运算的结果类型与底数相同。在检查模式下,较小的底数使用较廉价的 EXP 操作码,因此推荐进行 Gas 成本测试 并使用优化器。EVM 规定 0**0 的结果为 1。

定点数

定点数在 Solidity 中尚不完全支持,尽管可以声明,但不能进行赋值。

  • fixed / ufixed:带符号和无符号定点数,具有不同的大小。
  • ufixedMxN 和 fixedMxN,其中 M 是类型所占的位数,N 是小数位数。M 必须是 8 的倍数,范围从 8 到 256 位,N 必须在 0 到 80 之间。

操作符

  • 比较运算符:<=,<,==,!=,>=,>(结果为布尔值)

  • 算术运算符:+,-,一元 -(仅对带符号数),*,/,%(取模)

浮动点数和定点数的主要区别在于,浮动点数的整数和小数部分的位数是灵活的,而定点数严格定义了每部分所占的位数。

地址(Address)

地址类型有两种主要的变体:

  • address:表示 20 字节的值(以太坊地址大小)。

  • address payable:与 address 相同,但具有额外的 transfer 和 send 成员。

这种区分是为了使 address payable 成为一个可以接收以太币的地址,而普通的 address 不能接收以太币,可能是一个不支持接收以太币的智能合约。

类型转换

1.允许从 address payable 到 address 的隐式转换,但从 address 到 address payable 需要显式转换(例如 payable(

))。

2.允许显式转换为 address 类型,并返回 uint160、整数、bytes20 和合约类型。

3.只有 address 和合约类型的表达式可以通过显式转换 payable(…) 转换为 address payable,且合约必须能够接收以太币(有 receive 或 payable 函数)。

如果需要向一个地址发送以太币,最好将其声明为 address payable,以便明确这一需求。

地址成员

balance 和 transfer

使用 balance 查询地址余额,并使用 transfer 函数将以太币发送到可支付的地址:

address payable x = payable(0x123);
address myAddress = address(this);
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);

如果当前合约余额不足或接收方拒绝接收以太币,transfer 函数会失败并回滚交易。

注意:
如果 x 是合约地址,它的代码会在调用 transfer 时执行。如果执行失败,转账会回滚并抛出异常。

send

send 是 transfer 的低级对等函数。如果执行失败,当前合约不会停止并抛出异常,而是返回 false。

使用 send 时存在一定的风险:如果调用堆栈深度达到 1024(调用者可以强制此情况),或者接收方耗尽了 Gas,则转账将失败。因此,为了确保安全地转账以太币,始终检查 send 的返回值,或者使用 transfer。更好的方式是采用接收方自行提取以太币的模式。

call,delegatecall 和 staticcall

为了与不符合 ABI 标准的合约进行交互,或者为了更精确地控制编码,可以使用 call、delegatecall 和 staticcall 函数。这些函数都接受字节类型的内存参数,并返回执行结果(布尔值)和返回的数据(字节内存)。可以使用 abi.encode、abi.encodePacked、abi.encodeWithSelector 和 abi.encodeWithSignature 等函数来编码结构化数据。

示例:

bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);

需要注意的是,这些函数是低级函数,使用时要小心。特别是,任何未知的合约可能是恶意的,调用它将使你把控制权交给该合约,而该合约可能会再次调用你的合约。因此,调用返回时,需要做好处理可能改变状态变量的准备。与其他合约交互的常规方式是直接调用合约对象的函数(例如 x.f())。

我们还可以通过 gas 修饰符来控制提供的 Gas 数量:

address(nameReg).call{gas: 1000000}(abi.encodeWithSignature("register(string)", "MyName"));

类似地,还可以控制传递的 Ether 数量:

address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));

这些修饰符可以同时使用,顺序无关:

address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));

同样地,delegatecall 函数的使用方法与 call 相似,唯一的区别是,它只会执行目标地址的代码,所有其他相关信息(如存储、余额等)来自当前合约。delegatecall 主要用于调用存储在其他合约中的库代码。使用 delegatecall 时,用户必须确保两个合约的存储布局是兼容的。

code 和 codehash

我们可以查询任何智能合约的已部署代码,可使用 .code 获取 EVM 字节码(以字节内存形式表示,可能为空)、使用 .codehash 获取该字节码的 Keccak-256 哈希(类型为 bytes32)。需要注意,addr.codehash 比使用 keccak256(addr.code) 更加高效。

如果与 addr 关联的帐户为空或不存在(即它没有代码、零余额和零 nonce,如 EIP-161 所定义),则 addr.codehash 的输出将为 0。如果该帐户没有代码,但有非零余额或 nonce,则 addr.codehash 将返回空数据的 Keccak-256 哈希(即 keccak256(“”),其结果为 c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470)。

需要注意的是,所有合约都可以转换为 address 类型,因此可以使用 address(this).balance 来查询当前合约的余额。

合约类型(Contract Types)

每个合约都定义了自己的类型。你可以将合约隐式转换为其继承的合约类型。合约类型也可以显式地转换为 address 类型,反之亦然。

显式转换到 address payable 类型仅在合约具有 receive 或 payable 回退函数时才可行。转换仍然可以通过 address(x) 完成。如果合约没有 receive 或 payable 回退函数,可以通过 payable(address(x)) 来进行转换。

注意:
1.如果你声明了一个合约类型的局部变量(例如 MyContract c),你可以在该合约上调用函数。需要注意的是,必须从与之相同类型的合约赋值给该变量。
2.你也可以实例化合约(意味着它们是新创建的)。有关更多细节,请参考“通过 new 创建合约”部分。
3.合约的显示方式与 address 类型相同,并且该类型也用于 ABI 中。
4.合约类型不支持任何操作符。
5.合约类型的成员包括该合约的外部函数以及任何标记为 public 的状态变量。
6.对于合约 C,你可以使用 type© 来获取有关该合约的类型信息。

固定大小字节数组(Fixed-size byte arrays)

值类型 bytes1, bytes2, bytes3, …, bytes32 用于存储从 1 到 32 字节的字节序列。

操作符:

  • 比较操作符:<=, <, ==, !=, >=, >(返回布尔值)

  • 位操作符:&, |, ^(按位异或),~(按位取反)

  • 移位操作符:<<(左移),>>(右移)

  • 索引访问:如果 x 是类型 bytesI,则 x[k](0 <= k < I)返回第 k 个字节(只读)。

移位操作符与无符号整数类型作为右操作数一起工作(但返回左操作数的类型),使用有符号类型进行移位会导致编译错误。

成员 .length 可以返回字节数组的固定长度(只读)。

注意:
类型 bytes1[] 是字节的数组,但由于填充规则,每个元素会浪费 31 字节的空间(存储中除外)。因此,最好使用 bytes 类型。

地址字面量(Address Literals)

地址字面量是通过地址校验和测试的十六进制字面量。例如,0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF 是一个有效的 address 类型。长度在 39 到 41 位之间且没有通过校验和测试的十六进制字面量会导致错误。

我们可以在前面(对于整数类型)或后面(对于 bytesNN 类型)加零,来去除错误。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

秋说

感谢打赏

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

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

打赏作者

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

抵扣说明:

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

余额充值