文章目录
前言
用solidity批量生成一些内容,方便在测试中使用大量的地址和值
一、Solidity批量生成地址
1 . 代码如下
在Solidity中,你可以使用makeAddr
函数来生成测试地址。这个函数通常用于开发和测试环境,以便于生成预定义的地址。下面是一个批量生成10个地址的示例代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract AddressGenerator {
// 用于生成预定义地址的函数
function makeAddr(string memory name) internal pure returns (address) {
bytes memory b = bytes(name);
bytes32 hash = keccak256(abi.encodePacked(b));
return address(uint160(uint256(hash)));
}
// 生成10个地址并存储在合约状态变量中
address[] public generatedAddresses;
constructor() {
for (uint256 i = 0; i < 10; i++) {
string memory playerName = string(abi.encodePacked("player", toString(i)));
address playerAddr = makeAddr(playerName);
generatedAddresses.push(playerAddr);
}
}
// 辅助函数,将数字转换为字符串
function toString(uint256 value) internal pure returns (string memory) {
// 处理0的情况
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);
}
// 用于获取生成的地址列表
function getGeneratedAddresses() external view returns (address[] memory) {
return generatedAddresses;
}
}
2.makeAddr 代码解释
makeAddr
函数:- 这个内部函数接受一个字符串参数,并使用Keccak-256哈希函数生成一个地址。这个地址是通过对哈希值进行类型转换得到的。在foundry的forge-std/src/StdCheats.sol,中有类似的源代码:
function makeAddrAndKey(string memory name) internal virtual returns (address addr, uint256 privateKey) {
privateKey = uint256(keccak256(abi.encodePacked(name)));
addr = vm.addr(privateKey);
vm.label(addr, name);
}
// creates a labeled address
function makeAddr(string memory name) internal virtual returns (address addr) {
(addr,) = makeAddrAndKey(name);
}
这段代码展示了两个Solidity函数,它们用于在智能合约测试环境中生成地址和关联的私钥。这些函数通常用于创建测试账户,并且它们依赖于用于智能合约测试的虚拟机(如Foundry的Forge测试框架)提供的特定功能。
函数解释
-
makeAddrAndKey
函数:function makeAddrAndKey(string memory name) internal virtual returns (address addr, uint256 privateKey)
- 这是一个内部虚拟函数,它接受一个字符串参数
name
,用于生成一个地址和对应的私钥。 privateKey = uint256(keccak256(abi.encodePacked(name)));
- 使用Keccak-256哈希函数和
abi.encodePacked
对字符串name
进行编码和哈希,然后将哈希值转换为uint256
类型,作为私钥。
- 使用Keccak-256哈希函数和
addr = vm.addr(privateKey);
- 使用虚拟机的
addr
函数,根据私钥生成对应的地址。这在实际的以太坊环境中不是可能的,因为地址是通过公钥和ECDSA算法计算得出的,而不是直接从私钥生成。但在测试环境中,这种方法可以用来模拟地址生成。
- 使用虚拟机的
vm.label(addr, name);
- 使用虚拟机的
label
函数给生成的地址设置一个标签,方便在测试日志中识别。
- 使用虚拟机的
-
makeAddr
函数:function makeAddr(string memory name) internal virtual returns (address addr)
- 这是一个内部虚拟函数,它调用
makeAddrAndKey
函数,但只返回地址部分。 (addr,) = makeAddrAndKey(name);
- 这行代码通过解构赋值,忽略
makeAddrAndKey
返回的私钥,只获取地址。
示例
假设你正在编写一个测试脚本,需要生成几个测试账户:
// 在测试脚本中
function testMyContract() public {
address player1 = makeAddr("player1");
address player2 = makeAddr("player2");
// 进行一些测试操作...
}
在这个示例中,makeAddr
函数被用来生成两个有标签的测试账户地址,这些地址可以在测试中用于与智能合约交互。
3.toString( )函数解释:
这段代码展示了如何在Solidity智能合约中实现两个函数:一个用于将uint256
类型的数字转换为字符串,另一个用于获取一个存储在合约中的地址列表。以下是对这两个函数的详细解释:
这个函数用于将uint256
类型的数字转换为字符串。这对于在智能合约中需要将数字以文本形式展示或记录时非常有用。
function toString(uint256 value) internal pure returns (string memory) {
// 处理0的情况
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);
}
逻辑解释
- 首先,函数检查如果输入值为0,直接返回字符串"0"。
- 然后,计算数字的位数(digits),通过不断除以10直到数字变为0。
- 根据位数,分配一个
bytes
数组作为缓冲区。 - 再次遍历数字,每次取余数(个位数),将其转换为字符并存储到缓冲区中,然后数字除以10。
- 最后,将
bytes
数组转换为字符串并返回。
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)))的作用
这行代码是Solidity中将uint256
类型的数字转换为字符串的一部分。它的作用是将数字的最低位(个位)转换为对应的ASCII字符,并存储到一个bytes
数组中。这是在实现数字到字符串转换的过程中非常关键的一步。下面是对这行代码的详细解释:
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)))
-
buffer[digits]
:- 这是
bytes
数组的一个索引位置,用于存储转换后的字符。digits
变量表示当前应该填充的数组位置。
- 这是
-
uint256(value % 10)
:- 这是一个取模运算,用于获取
value
的最低位(个位)。例如,如果value
是123,那么value % 10
的结果是3。
- 这是一个取模运算,用于获取
-
48 + uint256(value % 10)
:- 这个表达式将个位数字转换为对应的ASCII码。在ASCII表中,数字字符’0’到’9’的编码是48到57。因此,通过将个位数字加48,可以得到相应的ASCII码。例如,3加上48得到的是51,即字符’3’的ASCII码。
-
uint8(...)
:- 将计算结果转换为
uint8
类型,这是因为bytes1
只能存储一个字节的数据,而uint8
正好是一个字节的大小。
- 将计算结果转换为
-
bytes1(...)
:- 将
uint8
类型的值转换为bytes1
类型,这样就可以将字符存储到bytes
数组中。
这行代码的作用是将数字的最低位转换为字符,并存储到bytes
数组的适当位置。这个过程在一个循环中重复进行,每次循环都会将value
除以10(去掉最低位),直到value
变为0。这样,就可以从数字的最低位到最高位依次转换并存储,最终得到一个表示数字的字符串。
假设value
是123,digits
是3(因为123是三位数),那么这个过程如下:
- 将
-
第一次循环:
value % 10
是3,所以计算48 + 3
得到51,即字符’3’的ASCII码。将这个值存储到buffer[3]
。 -
value
更新为12,digits
减1。 -
第二次循环:
value % 10
是2,所以计算48 + 2
得到50,即字符’2’的ASCII码。将这个值存储到buffer[2]
。 -
value
更新为1,digits
减1。 -
第三次循环:
value % 10
是1,所以计算48 + 1
得到49,即字符’1’的ASCII码。将这个值存储到buffer[1]
。 -
value
变为0,循环结束。
最终,buffer
中存储的内容是字符串"123"的ASCII表示。
优化与进一步提升
在 openzeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/utils/Strings.sol#L24-L25
库中,有自己定义的toString
,
这段代码是Solidity智能合约中将uint256
类型的数字转换为字符串的另一种实现方式。它使用了Solidity的内置函数和一些低级汇编来高效地进行转换。以下是对这段代码的详细解释:
function toString(uint256 value) internal pure returns (string memory) {
unchecked {
uint256 length = Math.log10(value) + 1;
string memory buffer = new string(length);
uint256 ptr;
/// @solidity memory-safe-assembly
assembly {
ptr := add(buffer, add(32, length))
}
while (true) {
ptr--;
/// @solidity memory-safe-assembly
assembly {
mstore8(ptr, byte(mod(value, 10), HEX_DIGITS))
}
value /= 10;
if (value == 0) break;
}
return buffer;
}
}
-
unchecked
块:- 用于禁用算术溢出和下溢的检查,这在确定不会发生溢出的情况下可以节省gas。
-
计算字符串长度:
uint256 length = Math.log10(value) + 1;
- 使用
Math.log10
函数计算数字的位数,并加1以确保有足够的空间存储字符串的null终止符。
-
创建字符串缓冲区:
string memory buffer = new string(length);
- 分配一个字符串,长度为计算出的位数。
-
设置指针:
uint256 ptr;
- 定义一个指针变量,用于在字符串缓冲区中存储字符。
- 使用汇编代码设置指针到字符串的末尾,这是为了从字符串的末尾开始填充字符,以便于从高位到低位构建数字字符串。
-
转换循环:
while (true) { ... }
- 一个无限循环,用于将数字的每一位转换为字符并存储到缓冲区中。
- 在循环中,使用
ptr--
将指针向前移动,以便在正确的位置存储下一个字符。
-
汇编代码:
assembly { mstore8(ptr, byte(mod(value, 10), HEX_DIGITS)) }
- 使用低级汇编指令
mstore8
在内存中存储一个字节。这里它存储的是数字value
除以10的余数(即当前最低位的数字),并将其转换为对应的ASCII字符。
-
更新数字:
value /= 10;
- 将数字除以10,准备处理下一位。
-
循环退出条件:
if (value == 0) break;
- 如果数字减少到0,表示所有位数都已处理完毕,退出循环。
Math.log10(value)算法
这段代码定义了一个名为 log10
的 Solidity 函数,用于计算一个 uint256
类型数值的以 10 为底的对数。这个函数通过连续除以 10 的幂来计算对数,直到数值小于 10。这种方法避免了使用浮点运算,因为 Solidity 只支持整数运算。以下是对这段代码的详细解释:
function log10(uint256 value) internal pure returns (uint256) {
uint256 result = 0;
unchecked {
if (value >= 10 ** 64) {
value /= 10 ** 64;
result += 64;
}
if (value >= 10 ** 32) {
value /= 10 ** 32;
result += 32;
}
if (value >= 10 ** 16) {
value /= 10 ** 16;
result += 16;
}
if (value >= 10 ** 8) {
value /= 10 ** 8;
result += 8;
}
if (value >= 10 ** 4) {
value /= 10 ** 4;
result += 4;
}
if (value >= 10 ** 2) {
value /= 10 ** 2;
result += 2;
}
if (value >= 10 ** 1) {
result += 1;
}
}
return result;
}
-
初始化结果:
uint256 result = 0;
初始化一个变量result
来存储对数值。
-
unchecked
块:unchecked
关键字用于禁用溢出检查,这在处理大数值时可以节省 gas。
-
连续除法和累加:
- 函数通过一系列
if
语句检查value
是否大于或等于 10 的不同幂次方。 - 如果是,将
value
除以相应的 10 的幂次方,并将结果加到result
上。例如,如果value >= 10 ** 64
,则除以10 ** 64
并将result
增加 64。
- 函数通过一系列
-
返回结果:
- 最后,函数返回计算出的对数值。
这段代码的巧妙之处在于它避免了使用循环来计算数字的位数,而是通过一系列条件判断和整数除法来实现。这种方法利用了对数的数学性质,即一个数可以表示为10的幂次方乘以一个小于该基数的数(例如,( N = 10^m \times n ),其中 ( n < 10 ))。通过逐步确定 ( m ) 的值,我们可以确定数字 ( N ) 的位数。
-
效率:在 Solidity 中,循环(特别是涉及状态变化的循环)可能会非常昂贵,因为每个循环迭代都可能产生额外的 gas 成本。通过使用条件判断代替循环,这段代码减少了执行步骤,从而可能降低了 gas 消耗。
-
可读性:虽然这种方法在一开始可能看起来不那么直观,但它将问题分解成了更小、更易于理解的部分。每一段代码都清晰地表示了数字的一个特定范围,这有助于开发者快速把握代码的逻辑。
-
确定性:循环的迭代次数取决于输入值,这可能导致难以预测的执行时间和 gas 成本。通过使用条件判断,这段代码提供了一种更可预测的执行路径。
- 代码首先检查数字是否大于或等于 ( 10^{64} ),如果是,则除以 ( 10^{64} ) 并将结果加到总数上。
- 接着,它继续检查更小的幂次,如 ( 10^{32} )、( 10^{16} ) 等,直到 ( 10^{1} )。
- 每次检查都缩小了数字的范围,并逐步构建起对数值。
假设我们要计算 ( N = 12345678901234567890 ) 的位数:
- ( N ) 大于 ( 10^{64} ),所以除以 ( 10^{64} ),结果为 ( 1.23456789 ),并将结果加 64。
- ( 1.23456789 ) 大于 ( 10^{32} ),所以除以 ( 10^{32} ),结果为 ( 0.012345678 ),并将结果加 32。
- 继续这个过程,直到 ( 10^{1} )。
最终,我们得到的结果将是 ( 19 ),这正是 ( N ) 的位数。这种方法展示了如何在 Solidity 中高效且巧妙地处理数值问题,通过避免循环并利用条件判断来实现。这种方法不仅提高了代码的执行效率,还增强了代码的可读性和可预测性。
mstore8的妙用
这段代码是一个在Solidity智能合约中用于将 uint256
类型的数字转换为字符串的循环部分。它使用了Solidity的内联汇编来直接与EVM(Ethereum虚拟机)的内存进行交互。以下是对这段代码的详细解释:
while (true) {
ptr--;
/// @solidity memory-safe-assembly
assembly {
mstore8(ptr, byte(mod(value, 10), HEX_DIGITS))
}
value /= 10;
if (value == 0) break;
}
-
无限循环:
while (true)
创建了一个无限循环,这意味着循环会一直执行,直到遇到break
语句。
-
指针递减:
ptr--
将指针向前移动一个字节,以便在内存中从字符串的末尾开始填充字符。
-
内联汇编:
assembly { mstore8(ptr, byte(mod(value, 10), HEX_DIGITS)) }
mstore8
是一个汇编指令,用于在给定的内存地址(ptr
)存储一个字节(8位)的数据。byte(mod(value, 10), HEX_DIGITS)
计算value
除以 10 的余数,并将其转换为对应的ASCII字符。这里假设HEX_DIGITS
是一个定义了数字到字符映射的数组或常量。
-
更新值:
value /= 10
将value
除以 10,以移除最低位的数字,为处理下一位数字做准备。
-
退出条件:
if (value == 0) break
检查value
是否为 0,如果是,则退出循环。
这里的细节是8字节的十进制,在计算机科学中,当我们谈论“8字节十进制对应的uint256”时,我们通常是在讨论一个无符号整数(uint256
)类型可以存储的最大十进制数值。在Solidity中,uint256
是一个无符号的 256 位整数类型,用于存储非常大的数值。 8字节十进制数值范围
一个“字节”通常指的是 8 位(bit),而在讨论数字的大小时,我们通常使用字节(byte)作为单位,1 个字节等于 8 位。对于 uint256
来说:
- 1 字节(8 位)可以表示的最大无符号整数是 (2^8 - 1 = 255)(十进制)。
- 同理,8 字节(64 位)可以表示的最大无符号整数是 (2^{64} - 1)。
因此,8 字节(64 位)可以表示的十进制数值范围是从 (0) 到 (18,446,744,073,709,551,615)。
二,solidity许多值值的生成
在Solidity中,你不能直接在合约状态变量声明时初始化动态数组的大小,因为Solidity是一种静态类型语言,它需要在编译时知道存储需求。但是,你可以在合约的构造函数或任何函数中后续初始化和填充数组。以下是如何在合约中声明一个 uint256[]
类型的 rewards
数组,并在合约的构造函数中批量生成10个元素的示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract RewardsManager {
uint256[] public rewards;
constructor() {
// 批量生成10个奖励并添加到数组中
for (uint256 i = 0; i < 10; i++) {
rewards.push(uint256(i + 1) * 100); // 例如,生成1到10的100倍的奖励
}
}
}
在这个示例中,我们使用了一个循环来填充 rewards
数组。在每次循环迭代中,我们调用 push
方法将一个新的 uint256
值添加到数组的末尾。这里,我们简单地生成了从100到1000(1100到10100)的奖励值。
其他生成数组的方法
如果你事先知道要生成的元素的值,你也可以使用Solidity的构造函数直接初始化数组:
pragma solidity ^0.8.0;
contract RewardsManager {
uint256[] public rewards;
constructor() {
rewards = new uint256[](10);
for (uint256 i = 0; i < 10; i++) {
rewards[i] = uint256(i + 1) * 100;
}
}
}
这种方法首先使用 new uint256[](10)
创建一个具有10个元素的数组,然后使用循环填充每个元素的值。
注意事项
- 在Solidity中,数组的
push
方法会将元素添加到数组的末尾,并自动调整数组的大小。这意味着你不需要预先知道数组的大小,但这也意味着使用push
方法可能会稍微增加gas成本,因为它需要动态调整数组大小。 - 如果你使用数组的直接初始化方法(如第二个示例),则在构造函数中直接设置数组的大小和值。这种方法在某些情况下可能更高效,因为它避免了动态调整数组大小的需要。
选择哪种方法取决于你的具体需求和偏好。如果你需要动态地添加元素,使用 push
方法可能更合适。如果你在初始化时就知道所有元素的值,直接初始化数组可能更有效。