Solidity高级编程——深入学习ABI

  • 链创通
  • 更新于 2024-07-17 22:32
  • 阅读 5712

ABI(ApplicationBinaryInterface)是智能合约与外部世界(包括其他智能合约和用户)之间的接口。它定义了合约的函数和事件,使得不同语言编写的代码可以相互通信。

第一部分:ABI 概述

1. ABI 定义与重要性

  • 什么是 ABI?

    • ABI (Application Binary Interface) 是智能合约与外部世界(包括其他智能合约和用户)之间的接口。它定义了合约的函数和事件,使得不同语言编写的代码可以相互通信。
  • 特点

    • 标准化接口: ABI 提供了一种标准化的方式来描述智能合约的接口,包括函数和事件的规范,使得不同的工具和库能够一致地与智能合约进行交互。
    • 描述性: ABI 包含了合约中每个函数的名称、输入参数、输出参数及其类型,以及事件的名称和参数类型。通过这些描述,可以清晰地知道如何调用合约中的函数和如何解析事件。
    • 静态和动态类型支持: ABI 支持以太坊的静态类型和动态类型,包括 uint256addressstringbytes等。ABI 使得这些类型的编码和解码变得标准化和自动化。
    • 事件日志解析: ABI 定义了事件的格式,使得开发者可以轻松解析区块链上的事件日志。事件日志是以太坊中合约与外界通讯的一种重要机制,通过 ABI 可以准确解析这些日志。
    • 自动生成: 当编译 Solidity 合约时,会自动生成对应的 ABI 文件。开发者无需手动编写 ABI,这减少了出错的可能性并提高了开发效率。
    • 工具支持广泛: 很多以太坊开发工具(如 Remix、Truffle、Hardhat 等)和库(如 Web3.js、Ethers.js 、viem.sh等)都对 ABI 提供了良好的支持,使得与智能合约的交互更加便捷。
    • 增强安全性: ABI 通过明确函数和参数的类型,减少了由于类型错误而导致的安全问题。开发者可以更明确地知道每个函数需要的输入和输出是什么。
  • Web3 ABI与 Web2 API的对比

    特点 Web2 API Web3 ABI
    用途 服务器与客户端通信 DApp 与智能合约通信
    定义内容 端点、HTTP 方法、请求/响应格式 函数名、参数类型、事件定义
    通信协议 HTTP/HTTPS 以太坊 JSON-RPC
    抽象层 服务器功能 智能合约功能
    依赖 RESTful、GraphQL 等规范 以太坊智能合约规范
    工具支持 Postman、Swagger 等 Web3.js、Ethers.js、viem.sh 等

    Web2 API 示例

    一个简单的 RESTful API 端点可能如下所示:

    • 端点/api/users

    • 方法GET

    • 描述:获取用户列表

    • 响应:

    [
      {
        "id": 1,
        "name": "Alice"
      },
      {
        "id": 2,
        "name": "Bob"
      }
    ]

    一个简单的智能合约 ABI 定义如下:

    [
    {
      "constant": true,
      "inputs": [],
      "name": "getUsers",
      "outputs": [
        {
          "name": "",
          "type": "address[]"
        }
      ],
      "payable": false,
      "stateMutability": "view",
      "type": "function"
    },
    {
      "constant": false,
      "inputs": [
        {
          "name": "name",
          "type": "string"
        }
      ],
      "name": "addUser",
      "outputs": [],
      "payable": false,
      "stateMutability": "nonpayable",
      "type": "function"
    }
    ]
  • 为什么 ABI 在智能合约开发中至关重要?

    • ABI 是调用智能合约函数和监听事件的必要条件。
    • ABI 提供了合约方法和事件的精确定义,确保数据的正确编码和解码。

2. ABI 的构成元素

  • 数据类型(基本类型和复杂类型)
    • 基本类型:uint、int、bool、address、bytes、string 等。
    • 复杂类型:数组、结构体(struct)。
  • 函数定义
    • 函数签名:包括函数名称和参数类型。
    • 返回值类型:函数返回的数据类型。
  • 事件定义
    • 事件签名:包括事件名称和参数类型。
    • 事件索引:事件参数的索引,用于过滤事件日志。
  • 构造函数和析构函数
    • 构造函数:初始化合约的特殊函数。
    • 析构函数:目前 Solidity 不支持析构函数。

3. 生成 ABI 的方式

  • 由 Solidity 编译器生成

    https://docs.soliditylang.org/zh/latest/installing-solidity.html

    Mac 下安装编译器,使用命令 solc --abi Contract.sol 生成 ABI 文件。

    brew tap ethereum/ethereum
    brew install solidity
    solc --abi --pretty-json Counter.sol
  • 用 Foundry 调用 solc 编译

    使用 Foundry 工具,通过命令 forge build 自动生成 ABI 文件

    forge build Counter.sol

    此时,在 project/out/Counter.sol/Counter.json 中包含ABI

  • 用 Hardhat 调用 solc 编译

    使用 Hardhat 工具,通过命令 npx hardhat compile 生成 ABI 文件。

    npm install --save-dev hardhat
    npx hardhat
    npx hardhat compile

    编译后的合约和 ABI 会存储在 artifacts/contracts 目录下,每个合约对应一个 JSON 文件,里面包含 ABI。

  • Remix编译

    基于浏览器的 Solidity 开发环境,可以在线编写、编译和部署合约。在 Remix 编辑器中编写你的合约,例如 MyContract.sol,点击 Remix IDE 中的编译按钮,编译合约,在 Remix 中编译合约后,点击 “Details” 按钮,展开后会看到 ABI,点击复制即可。

第二部分:ABI 编码与解码

1. ABI 编码规则

  • 固定大小的数据类型编码
    • 例如:uint256, address, bool 等。
    • 编码方式:每个数据类型按照固定的字节数进行编码,例如 uint256 占用 32 字节。
  • 动态大小的数据类型编码
    • 例如:string, bytes, 数组等。
    • 编码方式:数据的实际内容和长度信息分开存储,长度信息占用 32 字节,实际内容紧随其后。
  • 函数选择器编码
    • 函数选择器是函数签名的前 4 个字节的哈希值,用于识别函数调用。

2. ABI 解码规则

  • 固定大小的数据类型解码
    • 解析固定大小的数据类型时,直接从固定位置读取数据。
  • 动态大小的数据类型解码
    • 解析动态大小的数据类型时,先读取长度信息,再读取实际内容。

3. ABI 语义

  • function

    函数描述是一个带有字段的JSON对象:

    • type: function, constructor, receive 或者 fallback ;

    • name: 函数名称;

    • inputs: 函数入参,是一个数组对象,每个数组对象会包含:

    ​ ◦ name: 参数名称;

    ​ ◦ type: 参数类型

    ​ ◦ components: 供元组(tuple) 类型使用;

    • outputs:函数返回值,是一个类似于 inputs 的数组对象。

    • stateMutability: 为下列值之一: pure , view , nonpayable 和 payable 。

    {
      "type": "function"
      "name": "setValue",
      "inputs": [
          {
              "internalType": "uint256",
              "name": "_value",
              "type": "uint256"
          }
      ],
      "outputs": [],
      "stateMutability": "nonpayable",
    }
  • event

    • 事件的名称和参数类型定义了事件的接口,用于日志记录,描述也是一个带有字段的 JSON对象:

    • type:总是 event;

    • name: 事件名称;

    • inputs: 事件输出的参数信息,是一个数组对象,每个数组对象会包含:

    ​ ◦ name: 参数名称。

    ​ ◦ type: 参数类型。

    ​ ◦ components: 供元组(tuple) 类型使用;

    ​ ◦ indexed: 如果字段是日志主题(event topic)的一部分,则为 true;如果它是日志数据段的一部分,则为 false。

    • anonymous: 如果事件被声明为 anonymous,则为 true。如果事件被声明为 anonymous,那么 topics[0] 不会被生成。

    {
      "type": "event",
      "name": "Transfer",
      "inputs": [
        {
          "name": "from",
          "type": "address",
          "indexed": true
        },
        {
          "name": "to",
          "type": "address",
          "indexed": true
        },
        {
          "name": "value",
          "type": "uint256",
          "indexed": false
        }
      ],
      "anonymous": false
    }
    
  • error

    • 错误的名称和参数类型定义了错误的接口,用于异常处理。Error 对象的描述:

    • type:总是 error;

    • name: Error名称;

    • inputs: Error 参数信息,是一个数组对象,每个数组对象会包含:

    ​ ◦ name: 参数名称。

    ​ ◦ type: 参数类型。

    ​ ◦ components: 供元组(tuple) 类型使用;

    {
      "type": "error",
      "name": "CustomError",
      "inputs": [
        {
          "name": "errorCode",
          "type": "uint256"
        },
        {
          "name": "errorMessage",
          "type": "string"
        }
      ]
    }
    

4. ABI 编码

  • method selector

    • 函数签名的前 4 个字节哈希值,用于识别函数调用。

    • 在以太坊智能合约中,函数调用通过前四个字节来指定具体的函数。这四个字节是函数签名的 Keccak-256 哈希值的前四个字节。函数签名由函数名和括号中的参数类型列表组成,参数类型列表之间用逗号分隔,不包含参数名称、空格,返回值类型和修饰

    符。函数ID = hash(函数签名值) 的前4 字节

    cast keccak 'setNumber(uint256)'
    //0x3fb5c1cb9d57cc981b075ac270f9215e697bc33dacd5ce87319656ebf8fc7b92
    cast sig "setNumber(uint256)"
    // 0x3fb5c1cb
    //Input Data = MethodID + abi.encode(args…)
    //transferFrom(address,address,uint256) 
    //复杂结构体参数
    hashStruct(((string,address),(string,address),string))
  • abi.encode

    • 编码函数参数或事件参数为 ABI 格式。

image-20240717213341718.png

image-20240717213404467.png

    cast abi-encode "bar(uint256 a,uint8 b,bool c,address d,int256 e)" 9 8 true 0x605E0971f416301CF81Cf83C580123DCB6A8277E -2
    参数编码如下:
    0x000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000
    001000000000000000000000000605e0971f416301cf81cf83c580123dcb6a8277efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe

    cast abi-encode "bar(string)" "hi"
    参数编码如下:
    0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000026869000000000000000000000000000000000000000000000000000000000
    000

    cast abi-encode "bar(bytes)" 
    参数编码如下:
    0x605e0971f416301cf81cf83c580123dcb6a8277efffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe
    0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000034605e0971f416301cf81cf83c580123dcb6a8277effffffffffffffffffffffffffffffff
    fffffffffffffffffffffffffffffffe000000000000000000000000

    cast abi-encode "bar((address,uint256),bool)" "(0x605E0971f416301CF81Cf83C580123DCB6A8277E,8)" true
    参数编码如下:
    0x000000000000000000000000605e0971f416301cf81cf83c580123dcb6a8277e000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000
    01

    cast abi-encode "bar((address,uint32)[])" "[(0x605E0971f416301CF81Cf83C580123DCB6A8277E,1),(0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326,2)]"
    参数编码如下:
    0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000002000000000000000000000000605e0971f416301cf81cf83c580123dcb6a827
    7e00000000000000000000000000000000000000000000000000000000000000010000000000000000000000001f9090aae28b8a3dceadf281b0f12828e676c32600000000000000000000000000000000000000000000000000000000000000
    02
  • Event 编码

    • 编码事件日志的参数,用于日志记录和过滤。

image-20240717213758492.png

5.demo

image-20240717214304966.png

image-20240717214332558.png

image-20240717214403365.png

ABI数据库 https://openchain.xyz/,可以查询比对

实际操作

  • 使用 ethers.js 进行 ABI 编码与解码

    const { ethers } = require('ethers');
    
    // 编码函数调用数据
    const abi = ["function transfer(address to, uint amount)"];
    const iface = new ethers.utils.Interface(abi);
    const data = iface.encodeFunctionData("transfer", ["0xaddress", 1000]);
    
    // 解码函数返回数据
    const decoded = iface.decodeFunctionResult("transfer", data);
    console.log(decoded);
  • 使用 web3.js 进行 ABI 编码与解码

    const Web3 = require('web3');
    const web3 = new Web3();
    
    // 编码函数调用数据
    const data = web3.eth.abi.encodeFunctionCall({
      name: 'transfer',
      type: 'function',
      inputs: [{
          type: 'address',
          name: 'to'
      },{
          type: 'uint256',
          name: 'value'
      }]
    }, ['0xaddress', '1000']);
    
    // 解码函数返回数据
    const decoded = web3.eth.abi.decodeParameters(['bool'], '0xdata');
    console.log(decoded);
  • 使用 viem.sh 进行 ABI 编码与解码

    import { encodeFunctionData, decodeFunctionResult } from 'viem';
    
    // 编码函数调用数据
    const data = encodeFunctionData({
    name: 'transfer',
    type: 'function',
    inputs: [{ type: 'address', name: 'to' }, { type: 'uint256', name: 'value' }]
    }, ['0xaddress', '1000']);
    
    // 解码函数返回数据
    const decoded = decodeFunctionResult({
    name: 'transfer',
    type: 'function',
    outputs: [{ type: 'bool' }]
    }, '0xdata');
    console.log(decoded);

第三部分:函数调用与事件监听

1. 函数调用

合约方法的 ABI 格式 合约方法的 ABI 格式是一个 JSON 对象,描述了函数的名称、参数类型和返回值类型。

  • 函数名称、参数类型、返回值类型的定义

    function transfer(address to, uint256 value) public returns (bool)

    生成合约方法的调用数据 使用编码规则生成调用数据。可以使用 ethers.js 库来实现。

    const { ethers } = require("ethers");
    
    // 函数 ABI
    const abi = [
    "function transfer(address to, uint256 value) public returns (bool)"
    ];
    
    // 创建接口
    const iface = new ethers.utils.Interface(abi);
    
    // 生成调用数据
    const data = iface.encodeFunctionData("transfer", ["0xRecipientAddress", ethers.utils.parseUnits("1.0", 18)]);
    console.log(data);

    解析合约方法的调用结果 使用解码规则解析调用结果。可以使用 ethers.js 库来实现。

    // 假设 `result` 是从区块链获得的调用结果
    const result = "0x"; // 示例数据
    
    // 解码调用结果
    const decodedResult = iface.decodeFunctionResult("transfer", result);
    console.log(decodedResult);

    2. 事件监听

    事件的 ABI 格式 事件的 ABI 格式是一个 JSON 对象,包含事件名称和参数类型。

    {
    "type": "event",
    "name": "Transfer",
    "inputs": [
      {
        "name": "from",
        "type": "address",
        "indexed": true
      },
      {
        "name": "to",
        "type": "address",
        "indexed": true
      },
      {
        "name": "value",
        "type": "uint256",
        "indexed": false
      }
    ],
    "anonymous": false
    }
  • 事件名称、参数类型、索引参数的定义

    event Transfer(address indexed from, address indexed to, uint256 value);

    创建事件过滤器 根据事件的 ABI 和过滤条件创建过滤器。可以使用 ethers.js 库来实现。

    const { ethers } = require("ethers");
    
    // 连接到以太坊节点
    const provider = new ethers.providers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID");
    
    // 创建合约实例
    const contractAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // USDC 合约地址
    const abi = [
    "event Transfer(address indexed from, address indexed to, uint256 value)"
    ];
    const contract = new ethers.Contract(contractAddress, abi, provider);
    
    // 创建过滤器
    const filter = contract.filters.Transfer();
    
    // 监听事件
    provider.on(filter, (log) => {
    const parsedLog = contract.interface.parseLog(log);
    console.log(`从 ${parsedLog.args.from} 转账给 ${parsedLog.args.to} ${ethers.utils.formatUnits(parsedLog.args.value, 6)} USDC`);
    });

    解析事件日志 使用解码规则解析事件日志。可以使用 ethers.js 库来实现。

    // 假设 `log` 是从区块链获得的事件日志
    const log = {
    // 示例日志数据
    data: "0x",
    topics: [
      // 示例主题数据
    ]
    };
    
    // 解析事件日志
    const parsedLog = contract.interface.parseLog(log);
    console.log(parsedLog.args);

    这个示例代码展示了如何使用事件 ABI 格式创建事件过滤器,并监听和解析 USDC 转账事件。通过这种方式,你可以实时捕获和处理区块链上的特定事件。

第四部分:实战应用(后续补充)

1. ABI 反编译

  • 使用公共工具逆向解析 ABI
    • 使用 etherscan 等工具查看合约 ABI。
  • 手动解析交易数据的意图
    • 根据已知的 ABI 格式手动解析交易数据。

2. 高级应用

  • 动态生成和管理 ABI
    • 根据需要动态生成 ABI,并进行管理。
  • 多合约交互中的 ABI 使用技巧
    • 在多合约交互中有效使用 ABI 进行调用和监听。

3. 实际案例

  • 利用Cast工具逆向解码交易数据

题目:当合约部署者没有上传合约源代码时,我们是否能逆向分析合约的方法信息呢?通过学习ABI相关知识,你可以结合公共数据来尝试逆向解析出一笔交易的执行意图!请解析这笔交易数据所表达的意图0xa9059cbb0000000000000000000000005494befe3ce72a2ca0001fe0ed0c55b42f8c358f000000000000000000000000000000000000000000000000000000000836d54c

1、这笔交易数据对应的合约方法是什么?

前 4 个字节是函数选择器。函数选择器是函数签名的 Keccak-256 哈希的前 4 个字节。我们先来获取前 4 个字节:0xa9059cbb,经过查询https://www.4byte.directory/ 这段编码是一个标准的 ERC-20 transfer 函数的编码调用。ERC-20 transfer 函数的签名为 :transfer(address,uint256)

2、这笔交易对应的方法调用的第一个参数值是多少?

第一个参数是(地址,32字节)address:0000000000000000000000005494befe3ce72a2ca0001fe0ed0c55b42f8c358f所以地址是0x5494befe3ce72a2ca0001fe0ed0c55b42f8c358f,经过https://web3-tools.netlify.app/的checkAddressChecksum ,地址大小写转换为:0x5494befe3CE72A2CA0001fE0Ed0C55B42F8c358f

3、这笔交易对应的方法调用的第二个参数值是多少?

最后 32 字节是无符号整数参数,经过https://tool.oschina.net/hexconvert ,在线转换为十进制 image-20240717220302508.png

  • 使用 Viem 查询 USDC 最近100个区块内的转账记录

    详细内容可以查看:https://learnblockchain.cn/article/8758

    第五部分:常见问题与解决方案

    1. 常见错误及其排查

    • 常见编码和解码错误

    • 数据类型不匹配导致的编码/解码错误

      当函数调用或事件监听时,数据类型的不匹配可能会导致编码或解码错误。例如,将 uint256 类型的数据编码为 uint8 会导致错误。应确保输入数据类型与 ABI 定义中的类型一致。

      const { ethers } = require("ethers");
      
      // 示例:调用 transfer 函数,传入的参数类型不匹配
      const abi = [
      "function transfer(address to, uint256 value) public returns (bool)"
      ];
      
      const iface = new ethers.utils.Interface(abi);
      
      try {
      // 错误:value 应该是 uint256 类型,而不是 uint8
      const data = iface.encodeFunctionData("transfer", ["0xRecipientAddress", 255]); 
      console.log(data);
      } catch (error) {
      console.error("编码错误:", error.message);
      }
    • 函数选择器错误导致的调用失败

      函数选择器错误会导致调用失败。函数选择器是由函数签名生成的 4 字节哈希值。确保函数签名正确。

      const { ethers } = require("ethers");
      
      const abi = [
      "function transfer(address to, uint256 value) public returns (bool)"
      ];
      
      const iface = new ethers.utils.Interface(abi);
      
      try {
      const data = iface.encodeFunctionData("transferr", ["0xRecipientAddress", ethers.utils.parseUnits("1.0", 18)]);
      console.log(data);
      } catch (error) {
      console.error("函数选择器错误:", error.message);
      }
    • 事件监听中的常见问题

    • 事件过滤器配置错误

      事件过滤器配置错误会导致无法正确监听事件。例如,使用错误的地址或主题配置。

      const { ethers } = require("ethers");
      
      const provider = new ethers.providers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_INFURA_PROJECT_ID");
      
      const abi = [
      "event Transfer(address indexed from, address indexed to, uint256 value)"
      ];
      
      const contract = new ethers.Contract("0xIncorrectAddress", abi, provider); // 错误的地址
      
      const filter = contract.filters.Transfer();
      
      provider.on(filter, (log) => {
      console.log("监听到事件:", log);
      });
    • 事件日志解析错误

      事件日志解析错误可能由于 ABI 定义不正确或日志格式不匹配导致。

      const { ethers } = require("ethers");
      
      const abi = [
      "event Transfer(address indexed from, address indexed to, uint256 value)"
      ];
      
      const iface = new ethers.utils.Interface(abi);
      
      const log = {
      data: "0xIncorrectData", // 错误的日志数据
      topics: [
        "0xIncorrectTopic"
      ]
      };
      
      try {
      const parsedLog = iface.parseLog(log);
      console.log(parsedLog.args);
      } catch (error) {
      console.error("事件日志解析错误:", error.message);
      }

    2. 最佳实践

    • 编写和管理 ABI 的最佳实践

    • 规范化 ABI 定义

      编写清晰、规范的 ABI 定义,包含函数、事件和错误的详细描述。

      [
      {
        "type": "function",
        "name": "transfer",
        "inputs": [
          {
            "name": "to",
            "type": "address"
          },
          {
            "name": "value",
            "type": "uint256"
          }
        ],
        "outputs": [
          {
            "name": "",
            "type": "bool"
          }
        ],
        "stateMutability": "nonpayable"
      },
      {
        "type": "event",
        "name": "Transfer",
        "inputs": [
          {
            "name": "from",
            "type": "address",
            "indexed": true
          },
          {
            "name": "to",
            "type": "address",
            "indexed": true
          },
          {
            "name": "value",
            "type": "uint256",
            "indexed": false
          }
        ],
        "anonymous": false
      }
      ]
    • 版本控制和管理 ABI 文件

      使用版本控制工具(如 Git)管理 ABI 文件,确保不同版本的 ABI 文件能够被追踪和管理。

      # 将 ABI 文件提交到 Git 仓库
      git add path/to/abi.json
      git commit -m "添加合约 ABI 文件"
      git push origin main
    • 安全考虑

    • 确保 ABI 定义的函数和事件符合安全规范

      在编写 ABI 定义时,确保函数和事件的定义符合安全规范,避免潜在的安全漏洞。

      pragma solidity ^0.8.0;
      
      contract SafeContract {
      event Transfer(address indexed from, address indexed to, uint256 value);
      
      function transfer(address to, uint256 value) public returns (bool) {
        require(to != address(0), "无效的接收地址");
        // 其他安全检查
        return true;
      }
      }
    • 避免未授权的合约调用和事件监听

      确保合约的函数和事件只能被授权的地址调用和监听,避免恶意行为。

      pragma solidity ^0.8.0;
      
      contract AuthorizedContract {
      address public owner;
      
      modifier onlyOwner() {
        require(msg.sender == owner, "未授权的调用");
        _;
      }
      
      event Transfer(address indexed from, address indexed to, uint256 value);
      
      constructor() {
        owner = msg.sender;
      }
      
      function transfer(address to, uint256 value) public onlyOwner returns (bool) {
        require(to != address(0), "无效的接收地址");
        // 其他安全检查
        return true;
      }
      }

    通过遵循这些最佳实践,可以有效地编写和管理 ABI,并确保合约调用和事件监听的安全性。

附录

1. 参考资料

  • 官方文档和标准
    • Ethereum 官方文档
    • Solidity 官方文档
  • 开源库和工具
  • 在线学习资源和社区

2. 实用工具

通过这个详细的大纲和内容补充,学习者可以逐步掌握 ABI 的理论知识和实际应用技巧,为智能合约开发和逆向解析提供坚实的基础。

点赞 5
收藏 4
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
链创通
链创通
0x5312...1e69
歪脖山徒步虾