前言:

通过这段时间的做题,发现在 CTF 中,solidity 经常把类型转换放在 modifier 中,所以就有了现在这篇文章也算是自己整理一下。

基本类型之间的转换

隐式转换

在某些情况下,在赋值过程中,在向函数传递参数和应用运算符时,编译器会自动应用隐式类型转换。一般来说,如果在语义上有意义, 并且不会丢失信息,那么值-类型之间的隐式转换是可能的。(来自 solidity 官方文档)

代码举例:

uint8 y;
uint16 z;
uint32 x = y + z;

上述式子便存在隐式转换。因为不同的值类型之间,不能进行运算,上述代码能够成功运行的原因就是因为发生了隐式转换,y 和 x 两个参数都被隐式转换为了 uint32 的形式。

solidity 官方文档中的描述是这样的:

在上面的例子中,yz,即加法的操作数,没有相同的类型, 但是 uint8 可以隐式转换为 uint16,反之则不行。正因为如此, y 被转换为 z 的类型,然后在 uint16 类型中进行加法。 结果表达式 y + z 的类型是 uint16。 因为它被分配到一个 uint32 类型的变量中,所以在加法后又进行了一次隐式转换。

显式转换

如果编译器不允许隐式转换(确保转换会成功),有时可以进行显示类型转换。这可能会导致意想不到的行为。

在 CTF 比赛中,大多是显示转换

1)将一个负的int转换为uint

int y = -3;
uint x = uint(y);

在这个代码片段的最后,x变成0xffff...fd的值(64个十六进制字符),这在 256 位的二进制补码中表示是 -3。

2)将一个整数被明确的转换为一个较小的类型

如果一个整数被明确地转换为一个较小的类型,高阶位就会被切断:

uint32 a = 0x12345678;
uint16 b = uint16(a); // b 现在是 0x5678

3)将一个整数明确的转换为一个较大的类型

如果一个整数被明确地转换为一个较大的类型,他将在 左边 被填充(即在高阶的一端)。转换的结构将与原整数比较相等:

uint16 a = 0x1234;
uint32 b = uint32(a); // b 现在会是 0x00001234
assert(a == b);

4)固定大小的字节类型转换

固定大小的字节类型在转换过程中的行为是不同的。它们可以被认为是单个字节的序列

转换到一个较小的类型:

如果一个固定大小的字节类型被明确地转换到一个较小的类型将切断序列

bytes2 a = 0x1234;
bytes1 b = bytes1(a); // 现在是 0x12

转换到一个较大的类型

如果一个固定大小的字节类型被明确地转换为一个更大的类型,它将在 右边 被填充。 访问固定索引的字节将导致转换前后的数值相同(如果索引仍在范围内):

bytes2 a = 0x1234;
uint32 b = uint16(a); // b 将变为 0x12340000
assert(a[0] == b[0]);
assert(a[1] == b[1]);

5)整数和字节数组转换

只有在整数和固定大小的字节数组具有具有相同大小的情况下,才允许在两者之间进行显式转换。如果想在不同大小的整数和固定大小的字节数组之间进行转换,必须使用中间转换,使所需的截断和填充规则明确:

bytes2 a = 0x1234;
uint32 b = uint16(a) // b 将会是 0x00001234
uint32 c = uint32(bytes4(a)); // c 将会是 0x12340000
uint8 d = uint8(uint16(a)); // 将会是 0x34
uint8 e = uint8(bytes1(a)); // 将会是 0x12

bytes数组和bytes calldata 切片可以明确转换为固定字节类型(bytes1/ … /bytes32)。如果数组比目标的固定字节类型长,在末端会发生截断的情况。如果数组比目标的固定字节类型长,在末端会发生截断的情况。如果数组比目标类型短,他将在末端被填充零。

// SPDX-License-Identiifier: GPL-3.0
pragma solidity ^0.8.5;

contract C {
	bytes s = "abcdefgh";
	function f(bytes calldata c, bytes memory m) public view returns (bytes16, bytes3) {
		require(c.length == 16,'');
		bytes16 b = bytes16(m); // 如果 m 的长度大于16,将发生截断。
		b = bytes16(s); // 右边进行填充,所以结果是 "abcdefgh\0\0\0\0\0\0\0\0"
		bytes3 b1 = bytes3(s); // 发生截断,b1 相当于 "abc"
		b = bytes16(c[:8]); // 同样用0进行填充
		return (b, b1);
	}
}

字面常数和基本类型之间的转换

整数类型

十进制和十六进制的数字字面常数可以隐含地转换为任何足够大的整数类型去表示它而不被截断:

uint8 = 12; // 可行
uint32 b = 1234; // 可行
uint16 c = 0x123456; // 报错,因为这将会被截断成 0x3456

在0.8.0版本之前,任何十进制或十六进制的数字字面常数都可以显式转换为整数类型。 从0.8.0开始,这种显式转换和隐式转换一样严格,也就是说,只有当字面意义符合所产生的范围时,才允许转换。

固定大小的字节数组

十进制数字字面常量不能被隐含地址转换为固定大小的字节数组。十六进制数字字面常数是可以的,但只有当十六进制数字的数量正好符合字节类型的大小时才可以。但有一个例外,数值为 0 的十进制和十六进制数字字面常量都可以被转换为固定大小的字节类型:

bytes2 a = 54321; // no
bytes2 b = 0x12; // no
bytes2 c = 0x123; // no
bytes2 d = 0x1234; // 可
bytes2 e = 0x0012; // 可
bytes4 f = 0; // 可
bytes4 g = 0x0; // 可

字符串和十六进制字符串字面常数可以被隐含地转换为固定大小的字节数组, 如果它们的字符数与字节类型的大小相匹配:

bytes2 a = hex"1234"; // 可
bytes2 b = "xy"; // 可
bytes2 c = hex"12"; // no
bytes2 d = hex"123"; // no
bytes2 e = "x"; // 不允许
bytes2 f = "xyz"; // 不允许

地址类型

只允许从 bytes20uint160 显式转换到 address

address a 可以通过 payable(a) 显式转换为 address payable

备注
在 0.8.0 版本之前,可以显式地从任何整数类型(任何大小,有符号或无符号)转换为 addressaddress payable 类型。 从 0.8.0 开始,只允许从 uint160 转换。