Skip to content

Latest commit

 

History

History
665 lines (582 loc) · 26.2 KB

File metadata and controls

665 lines (582 loc) · 26.2 KB
timezone
Asia/Shanghai

Kurt

  1. 自我介绍 java程序员转web3,啥都想学
  2. 你认为你会完成本次残酷学习吗? 会

Notes

2024.09.23

今天复习solidity-101,重温一下solidity基础部分,把solidity-101测试题全部做到100%。记录一下 image

2024.09.24

  1. 函数重载

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

在调用重载函数时,会把输入的实际参数和函数参数的变量类型做匹配。 如果出现多个匹配的重载函数,则会报错。下面这个例子有两个叫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;
}

我们调用f(50),因为50既可以被转换为uint8,也可以被转换为uint256,因此会报错。

  1. 库合约

库合约是一种特殊的合约,为了提升Solidity代码的复用性和减少gas而存在。库合约是一系列的函数合集,由大神或者项目方创作,咱们站在巨人的肩膀上,会用就行了。

他和普通合约主要有以下几点不同:

不能存在状态变量 不能够继承或被继承 不能接收以太币 不可以被销毁 需要注意的是,库合约重的函数可见性如果被设置为public或者external,则在调用函数时会触发一次delegatecall。而如果被设置为internal,则不会引起。对于设置为private可见性的函数来说,其仅能在库合约中可见,在其他合约中不可用。

如何使用库合约 我们用Strings库合约的toHexString()来演示两种使用库合约中函数的办法。

利用using for指令

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

// 利用using for指令
using Strings for uint256;
function getString1(uint256 _number) public pure returns(string memory){
    // 库合约中的函数会自动添加为uint256型变量的成员
    return _number.toHexString();
}

通过库合约名称调用函数

// 直接通过库合约名调用
function getString2(uint256 _number) public pure returns(string memory){
    return Strings.toHexString(_number);
}
  1. Import

在Solidity中,import语句可以帮助我们在一个文件中引用另一个文件的内容,提高代码的可重用性和组织性。本教程将向你介绍如何在Solidity中使用import语句。

import用法 1.通过源文件相对位置导入,例子:

// 通过文件相对位置import
import './Yeye.sol';

2.通过源文件网址导入网上的合约的全局符号,例子:

// 通过网址引用
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';

3.通过npm的目录导入,例子:

import '@openzeppelin/contracts/access/Ownable.sol';

4.通过指定全局符号导入合约特定的全局符号,例子:

import {Yeye} from './Yeye.sol';

5.引用(import)在代码中的位置为:在声明版本号之后,在其余代码之前。

2024.09.25

  1. 接收ETH receive和fallback

Solidity支持两种特殊的回调函数,receive()和fallback(),他们主要在两种情况下被使用:

接收ETH 处理合约中不存在的函数调用(代理合约proxy contract)

1.接收ETH函数 receive

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

2.回退函数 fallback

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

3.receive和fallback的区别 receive和fallback都能够用于接收ETH,他们触发的规则如下:

触发fallback() 还是 receive()?
           接收ETH
              |
         msg.data是空?
            /  \
          是    否
          /      \
receive()存在?   fallback()
        / \
       是  否
      /     \
receive()   fallback()
  1. 发送ETH

Solidity有三种方法向其他合约发送ETH,他们是:transfer(),send()和call(),其中call()是被鼓励的用法。

1.transfer 用法是接收方地址.transfer(发送ETH数额)。 transfer()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。 transfer()如果转账失败,会自动revert(回滚交易)。

// 用transfer()发送ETH
function transferETH(address payable _to, uint256 amount) external payable{
    _to.transfer(amount);
}

2.send 用法是接收方地址.send(发送ETH数额)。 send()的gas限制是2300,足够用于转账,但对方合约的fallback()或receive()函数不能实现太复杂的逻辑。 send()如果转账失败,不会revert。 send()的返回值是bool,代表着转账成功或失败,需要额外代码处理一下。

error SendFailed(); // 用send发送ETH失败error

// send()发送ETH
function sendETH(address payable _to, uint256 amount) external payable{
    // 处理下send的返回值,如果失败,revert交易并发送error
    bool success = _to.send(amount);
    if(!success){
        revert SendFailed();
    }
}

3.call 用法是接收方地址.call{value: 发送ETH数额}("")。 call()没有gas限制,可以支持对方合约fallback()或receive()函数实现复杂逻辑。 call()如果转账失败,不会revert。 call()的返回值是(bool, bytes),其中bool代表着转账成功或失败,需要额外代码处理一下。

error CallFailed(); // 用call发送ETH失败error

// call()发送ETH
function callETH(address payable _to, uint256 amount) external payable{
    // 处理下call的返回值,如果失败,revert交易并发送error
    (bool success,) = _to.call{value: amount}("");
    if(!success){
        revert CallFailed();
    }
}

2024.09.26

  1. 调用其他合约

  2. 传入合约地址

我们可以在函数里传入目标合约地址,生成目标合约的引用,然后调用目标函数。以调用OtherContract合约的setX函数为例,我们在新合约中写一个callSetX函数,传入已部署好的OtherContract合约地址_Address和setX的参数x:

function callSetX(address _Address, uint256 x) external{
    OtherContract(_Address).setX(x);
}
复制OtherContract合约的地址,填入callSetX函数的参数中,成功调用后,调用OtherContract合约中的getX验证x变为123
  1. 传入合约变量
我们可以直接在函数里传入合约的引用,只需要把上面参数的address类型改为目标合约名,比如OtherContract。下面例子实现了调用目标合约的getX()函数。

注意:该函数参数OtherContract _Address底层类型仍然是address,生成的ABI中、调用callGetX时传入的参数都是address类型

function callGetX(OtherContract _Address) external view returns(uint x){
    x = _Address.getX();
}
复制OtherContract合约的地址,填入callGetX函数的参数中,调用后成功获取x的值
  1. 创建合约变量
我们可以创建合约变量,然后通过它来调用目标函数。下面例子,我们给变量oc存储了OtherContract合约的引用:

function callGetX2(address _Address) external view returns(uint x){
    OtherContract oc = OtherContract(_Address);
    x = oc.getX();
}
复制OtherContract合约的地址,填入callGetX2函数的参数中,调用后成功获取x的值
  1. 调用合约并发送ETH 如果目标合约的函数是payable的,那么我们可以通过调用它来给合约转账:_Name(_Address).f{value: _Value}(),其中_Name是合约名,_Address是合约地址,f是目标函数名,_Value是要转的ETH数额(以wei为单位)。
OtherContract合约的setX函数是payable的,在下面这个例子中我们通过调用setX来往目标合约转账。

function setXTransferETH(address otherContract, uint256 x) payable external{
    OtherContract(otherContract).setX{value: msg.value}(x);
}
复制OtherContract合约的地址,填入setXTransferETH函数的参数中,并转入10ETH
  1. Call

call 是address类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, bytes memory),分别对应call是否成功以及目标函数的返回值。

call是Solidity官方推荐的通过触发fallback或receive函数发送ETH的方法。 不推荐用call来调用另一个合约,因为当你调用不安全合约的函数时,你就把主动权交给了它。推荐的方法仍是声明合约变量后调用函数,见第21讲:调用其他合约 当我们不知道对方合约的源代码或ABI,就没法生成合约变量;这时,我们仍可以通过call调用对方合约的函数。 call的使用规则 call的使用规则如下:

目标合约地址.call(字节码);
其中字节码利用结构化编码函数abi.encodeWithSignature获得:
abi.encodeWithSignature("函数签名", 逗号分隔的具体参数)
函数签名为"函数名(逗号分隔的参数类型)"。例如abi.encodeWithSignature("f(uint256,address)", _x, _addr)。

另外call在调用合约时可以指定交易发送的ETH数额和gas数额:

目标合约地址.call{value:发送数额, gas:gas数额}(字节码);
  1. Delegatecall

2024.09.27

  1. 在合约中创建新合约 在以太坊链上,用户(外部账户,EOA)可以创建智能合约,智能合约同样也可以创建新的智能合约。去中心化交易所uniswap就是利用工厂合约(PairFactory)创建了无数个币对合约(Pair)。 Uniswap V2核心合约中包含两个合约:
UniswapV2Pair: 币对合约,用于管理币对地址、流动性、买卖。
UniswapV2Factory: 工厂合约,用于创建新的币对,并管理币对地址。

下面我们用create方法实现一个极简版的Uniswap:Pair币对合约负责管理币对地址,PairFactory工厂合约用于创建新的币对,并管理币对地址。 Pair合约

contract Pair{
    address public factory; // 工厂合约地址
    address public token0; // 代币1
    address public token1; // 代币2

    constructor() payable {
        factory = msg.sender;
    }

    // called once by the factory at time of deployment
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
        token0 = _token0;
        token1 = _token1;
    }
}

PairFactory

contract PairFactory{
    mapping(address => mapping(address => address)) public getPair; // 通过两个代币地址查Pair地址
    address[] public allPairs; // 保存所有Pair地址

    function createPair(address tokenA, address tokenB) external returns (address pairAddr) {
        // 创建新合约
        Pair pair = new Pair(); 
        // 调用新合约的initialize方法
        pair.initialize(tokenA, tokenB);
        // 更新地址map
        pairAddr = address(pair);
        allPairs.push(pairAddr);
        getPair[tokenA][tokenB] = pairAddr;
        getPair[tokenB][tokenA] = pairAddr;
    }
}
  1. CREATE2 CREATE2 操作码使我们在智能合约部署在以太坊网络之前就能预测合约的地址。Uniswap创建Pair合约用的就是CREATE2而不是CREATE。这一讲,我将介绍CREATE2的用法

CREATE2如何计算地址 CREATE2的目的是为了让合约地址独立于未来的事件。不管未来区块链上发生了什么,你都可以把合约部署在事先计算好的地址上。用CREATE2创建的合约地址由4个部分决定:

0xFF:一个常数,避免和CREATE冲突 CreatorAddress: 调用 CREATE2 的当前合约(创建合约)地址。 salt(盐):一个创建者指定的bytes32类型的值,它的主要目的是用来影响新创建的合约的地址。 initcode: 新合约的初始字节码(合约的Creation Code和构造函数的参数)。 新地址 = hash("0xFF",创建者地址, salt, initcode) Copy CREATE2 确保,如果创建者使用 CREATE2 和提供的 salt 部署给定的合约initcode,它将存储在 新地址 中。

如何使用CREATE2

2024.09.28

  1. 删除合约

    selfdestruct命令可以用来删除智能合约,并将该合约剩余ETH转到指定地址。selfdestruct是为了应对合约出错的极端情况而设计的。它最早被命名为suicide(自杀),但是这个词太敏感。为了保护抑郁的程序员,改名为selfdestruct;在 v0.8.18 版本中,selfdestruct 关键字被标记为「不再建议使用」,在一些情况下它会导致预期之外的合约语义,但由于目前还没有代替方案,目前只是对开发者做了编译阶段的警告,相关内容可以查看 EIP-6049。

然而,在以太坊坎昆(Cancun)升级中,EIP-6780被纳入升级以实现对Verkle Tree更好的支持。EIP-6780减少了SELFDESTRUCT操作码的功能。根据提案描述,当前SELFDESTRUCT仅会被用来将合约中的ETH转移到指定地址,而原先的删除功能只有在合约创建-自毁这两个操作处在同一笔交易时才能生效。所以目前来说:

已经部署的合约无法被SELFDESTRUCT了。 如果要使用原先的SELFDESTRUCT功能,必须在同一笔交易中创建并SELFDESTRUCT。

  1. ABI编码解码

    ABI (Application Binary Interface,应用二进制接口)是与以太坊智能合约交互的标准。数据基于他们的类型编码;并且由于编码后不包含类型信息,解码时需要注明它们的类型。 ABI编码有4个函数:abi.encode, abi.encodePacked, abi.encodeWithSignature, abi.encodeWithSelector。而ABI解码有1个函数:abi.decode

2024.09.29

未更新

2024.09.30

  1. Hash

    Hash的性质 一个好的哈希函数应该具有以下几个特性:

单向性:从输入的消息到它的哈希的正向运算简单且唯一确定,而反过来非常难,只能靠暴力枚举。 灵敏性:输入的消息改变一点对它的哈希改变很大。 高效性:从输入的消息到哈希的运算高效。 均一性:每个哈希值被取到的概率应该基本相等。 抗碰撞性: 弱抗碰撞性:给定一个消息x,找到另一个消息x',使得hash(x) = hash(x')是困难的。 强抗碰撞性:找到任意x和x',使得hash(x) = hash(x')是困难的。 Hash的应用 生成数据唯一标识 加密签名 安全加密 Keccak256 Keccak256函数是Solidity中最常用的哈希函数,用法非常简单:

哈希 = keccak256(数据);

  1. 函数选择器Selector

2024.10.02

  1. Try Catch

在Solidity中,try-catch只能被用于external函数或创建合约时constructor(被视为external函数)的调用。基本语法如下:

try externalContract.f() {
    // call成功的情况下 运行一些代码
} catch {
    // call失败的情况下 运行一些代码
}

其中externalContract.f()是某个外部合约的函数调用,try模块在调用成功的情况下运行,而catch模块则在调用失败时运行。

同样可以使用this.f()来替代externalContract.f(),this.f()也被视作为外部调用,但不可在构造函数中使用,因为此时合约还未创建。

如果调用的函数有返回值,那么必须在try之后声明returns(returnType val),并且在try模块中可以使用返回的变量;如果是创建合约,那么返回值是新创建的合约变量。

try externalContract.f() returns(returnType val){
    // call成功的情况下 运行一些代码
} catch {
    // call失败的情况下 运行一些代码
}

另外,catch模块支持捕获特殊的异常原因:

try externalContract.f() returns(returnType){
    // call成功的情况下 运行一些代码
} catch Error(string memory /*reason*/) {
    // 捕获revert("reasonString") 和 require(false, "reasonString")
} catch Panic(uint /*errorCode*/) {
    // 捕获Panic导致的错误 例如assert失败 溢出 除零 数组访问越界
} catch (bytes memory /*lowLevelData*/) {
    // 如果发生了revert且上面2个异常类型匹配都失败了 会进入该分支
    // 例如revert() require(false) revert自定义类型的error
}

2024.10.03

刷题image

2024.10.04

ERC20 ERC20是以太坊上的代币标准,来自2015年11月V神参与的EIP20。它实现了代币转账的基本逻辑:

账户余额(balanceOf()) 转账(transfer()) 授权转账(transferFrom()) 授权(approve()) 代币总供给(totalSupply()) 授权转账额度(allowance()) 代币信息(可选):名称(name()),代号(symbol()),小数位数(decimals()) 事件 IERC20定义了2个事件:Transfer事件和Approval事件,分别在转账和授权时被释放

/**
 * @dev 释放条件:当 `value` 单位的货币从账户 (`from`) 转账到另一账户 (`to`)时.
 */
event Transfer(address indexed from, address indexed to, uint256 value);
/**
 * @dev 释放条件:当 `value` 单位的货币从账户 (`owner`) 授权给另一账户 (`spender`)时.
 */
event Approval(address indexed owner, address indexed spender, uint256 value);

函数 IERC20定义了6个函数,提供了转移代币的基本功能,并允许代币获得批准,以便其他链上第三方使用。

totalSupply()返回代币总供给

/**
 * @dev 返回代币总供给.
 */
function totalSupply() external view returns (uint256);

balanceOf()返回账户余额

/**
 * @dev 返回账户`account`所持有的代币数.
 */
function balanceOf(address account) external view returns (uint256);

transfer()转账

/**
 * @dev 转账 `amount` 单位代币,从调用者账户到另一账户 `to`.
 *
 * 如果成功,返回 `true`.
 *
 * 释放 {Transfer} 事件.
 */
function transfer(address to, uint256 amount) external returns (bool);

allowance()返回授权额度

/**
 * @dev 返回`owner`账户授权给`spender`账户的额度,默认为0。
 *
 * 当{approve} 或 {transferFrom} 被调用时,`allowance`会改变.
 */
function allowance(address owner, address spender) external view returns (uint256);

approve()授权

/**
 * @dev 调用者账户给`spender`账户授权 `amount`数量代币。
 *
 * 如果成功,返回 `true`.
 *
 * 释放 {Approval} 事件.
 */
function approve(address spender, uint256 amount) external returns (bool);

transferFrom()授权转账

/**
 * @dev 通过授权机制,从`from`账户向`to`账户转账`amount`数量代币。转账的部分会从调用者的`allowance`中扣除。
 *
 * 如果成功,返回 `true`.
 *
 * 释放 {Transfer} 事件.
 */
function transferFrom(
    address from,
    address to,
    uint256 amount
) external returns (bool);

2024.10.05

实现ERC20

mapping(address => uint256) public override balanceOf;

mapping(address => mapping(address => uint256)) public override allowance;

uint256 public override totalSupply;   // 代币总供给

string public name;   // 名称
string public symbol;  // 代号

uint8 public decimals = 18; // 小数位数

函数 构造函数:初始化代币名称、代号。

constructor(string memory name_, string memory symbol_){
    name = name_;
    symbol = symbol_;
}

transfer()函数:实现IERC20中的transfer函数,代币转账逻辑。调用方扣除amount数量代币,接收方增加相应代币。土狗币会魔改这个函数,加入税收、分红、抽奖等逻辑。

function transfer(address recipient, uint amount) public override returns (bool) {
    balanceOf[msg.sender] -= amount;
    balanceOf[recipient] += amount;
    emit Transfer(msg.sender, recipient, amount);
    return true;
}

approve()函数:实现IERC20中的approve函数,代币授权逻辑。被授权方spender可以支配授权方的amount数量的代币。spender可以是EOA账户,也可以是合约账户:当你用uniswap交易代币时,你需要将代币授权给uniswap合约。

function approve(address spender, uint amount) public override returns (bool) {
    allowance[msg.sender][spender] = amount;
    emit Approval(msg.sender, spender, amount);
    return true;
}

transferFrom()函数:实现IERC20中的transferFrom函数,授权转账逻辑。被授权方将授权方sender的amount数量的代币转账给接收方recipient。

function transferFrom(
    address sender,
    address recipient,
    uint amount
) public override returns (bool) {
    allowance[sender][msg.sender] -= amount;
    balanceOf[sender] -= amount;
    balanceOf[recipient] += amount;
    emit Transfer(sender, recipient, amount);
    return true;
}

mint()函数:铸造代币函数,不在IERC20标准中。这里为了教程方便,任何人可以铸造任意数量的代币,实际应用中会加权限管理,只有owner可以铸造代币:

function mint(uint amount) external {
    balanceOf[msg.sender] += amount;
    totalSupply += amount;
    emit Transfer(address(0), msg.sender, amount);
}

burn()函数:销毁代币函数,不在IERC20标准中。

function burn(uint amount) external {
    balanceOf[msg.sender] -= amount;
    totalSupply -= amount;
    emit Transfer(msg.sender, address(0), amount);
}

2024.10.06

  1. 代币水龙头 我们在水龙头合约中定义3个状态变量
amountAllowed设定每次能领取代币数量(默认为100,不是一百枚,因为代币有小数位数)。
tokenContract记录发放的ERC20代币合约地址。
requestedAddress记录领取过代币的地址。
uint256 public amountAllowed = 100; // 每次领 100 单位代币
address public tokenContract;   // token合约地址
mapping(address => bool) public requestedAddress;  

事件 水龙头合约中定义了1个SendToken事件,记录了每次领取代币的地址和数量,在requestTokens()函数被调用时释放。

// SendToken事件    
event SendToken(address indexed Receiver, uint256 indexed Amount);

函数 合约中只有两个函数:

构造函数:初始化tokenContract状态变量,确定发放的ERC20代币地址。

// 部署时设定ERC20代币合约
constructor(address _tokenContract) {
  tokenContract = _tokenContract; // set token contract
}

requestTokens()函数,用户调用它可以领取ERC20代币。

// 用户领取代币函数
function requestTokens() external {
    require(!requestedAddress[msg.sender], "Can't Request Multiple Times!"); // 每个地址只能领一次
    IERC20 token = IERC20(tokenContract); // 创建IERC20合约对象
    require(token.balanceOf(address(this)) >= amountAllowed, "Faucet Empty!"); // 水龙头空了

    token.transfer(msg.sender, amountAllowed); // 发送token
    requestedAddress[msg.sender] = true; // 记录领取地址 
    
    emit SendToken(msg.sender, amountAllowed); // 释放SendToken事件
}

2024.10.07

  1. 空投合约

投代币合约 Airdrop空投合约逻辑非常简单:利用循环,一笔交易将ERC20代币发送给多个地址。合约中包含两个函数

getSum()函数:返回uint数组的和。

// 数组求和函数
function getSum(uint256[] calldata _arr) public pure returns(uint sum){
    for(uint i = 0; i < _arr.length; i++)
        sum = sum + _arr[i];
}

multiTransferToken()函数:发送ERC20代币空投,包含3个参数:

_token:代币合约地址(address类型) _addresses:接收空投的用户地址数组(address[]类型) _amounts:空投数量数组,对应_addresses里每个地址的数量(uint[]类型) 该函数有两个检查:第一个require检查了_addresses和_amounts两个数组长度是否相等;第二个require检查了空投合约的授权额度大于要空投的代币数量总和。

/// @notice 向多个地址转账ERC20代币,使用前需要先授权
///
/// @param _token 转账的ERC20代币地址
/// @param _addresses 空投地址数组
/// @param _amounts 代币数量数组(每个地址的空投数量)
function multiTransferToken(
    address _token,
    address[] calldata _addresses,
    uint256[] calldata _amounts
    ) external {
    // 检查:_addresses和_amounts数组的长度相等
    require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL");
    IERC20 token = IERC20(_token); // 声明IERC合约变量
    uint _amountSum = getSum(_amounts); // 计算空投代币总量
    // 检查:授权代币数量 >= 空投代币总量
    require(token.allowance(msg.sender, address(this)) >= _amountSum, "Need Approve ERC20 token");

    // for循环,利用transferFrom函数发送空投
    for (uint8 i; i < _addresses.length; i++) {
        token.transferFrom(msg.sender, _addresses[i], _amounts[i]);
    }
}

multiTransferETH()函数:发送ETH空投,包含2个参数:

_addresses:接收空投的用户地址数组(address[]类型) _amounts:空投数量数组,对应_addresses里每个地址的数量(uint[]类型)

/// 向多个地址转账ETH
function multiTransferETH(
    address payable[] calldata _addresses,
    uint256[] calldata _amounts
) public payable {
    // 检查:_addresses和_amounts数组的长度相等
    require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL");
    uint _amountSum = getSum(_amounts); // 计算空投ETH总量
    // 检查转入ETH等于空投总量
    require(msg.value == _amountSum, "Transfer amount error");
    // for循环,利用transfer函数发送ETH
    for (uint256 i = 0; i < _addresses.length; i++) {
        // 注释代码有Dos攻击风险, 并且transfer 也是不推荐写法
        // Dos攻击 具体参考 https://github.com/AmazingAng/WTF-Solidity/blob/main/S09_DoS/readme.md
        // _addresses[i].transfer(_amounts[i]);
        (bool success, ) = _addresses[i].call{value: _amounts[i]}("");
        if (!success) {
            failTransferList[_addresses[i]] = _amounts[i];
        }
    }
}

2024.10.08

2024.10.09

2024.10.10