一文学会 如何在以太坊上使用SQL查询交易数据

原文地址:https://ath.mirror.xyz/mbR1n_CvflL1KIKCTG42bnM4HpfGBqDPNndH8mu2eJw

原文作者:Andrew Hong

你的钱包里有什么?

如果你曾经在以太坊(或任何启用智能合约的区块链)上进行过交易,那么你可能已经在区块浏览器上查找过,并看到了这堆信息。

学习阅读交易的细节将是所有以太坊数据分析和知识的基础,所以让我们来介绍所有的部分以及如何在SQL中使用它们。我将使用Dune Analytics来运行我的查询,但还有许多其他工具,你可以用来查询链,如Big Query和Flipside Crypto。

如果你对SQL和以太坊完全陌生,我建议先阅读:初学者概述

指南概述

我们将分四个层次介绍交易:

  1. 事务的基础知识
  2. 函数调用和状态
  3. 内部交易 (Traces)
  4. 日志 (Events Emitted)

作为我们交易实例的基础,我们将使用Mirror Crowdfund合约。简单地说,这是一个智能合约,允许你向该合约捐赠ETH,以获得ERC20(可替换)或ERC721(NFTs)代币。合约的创建者可以通过关闭众筹基金来提取这些资金。这绝不是一个简单的合约,但我想在这里说的是,你不需要了解所有的solidity代码来开始分析--你只需要知道如何浏览上面的四个层次。

我们要研究的三个交易是:

  1. 创建/部署众筹合约
  2. 向合约缴纳的ETH
  3. 关闭和提取合约中的资金

题外话,我们也刚刚开放了众筹给任何人使用,所以如果你好奇或想创建一个众筹,请到mirror.xyz/dashboard开始。

创建/部署众筹合约

第一个交易:
0x5e5ef5dd9d147028f9bc21127e3de774a80c56a2e510d95f41984e6b7af1b8db

让我们从交易的基本情况开始。

  1. 每笔交易都有一个独特的keccak256交易哈希值(transaction hash),包括几个不同的变量
  2. 有一个基于交易开采时间的相关区块号码(blocknumber),通常每15秒就有一个新的区块被创建。
  3. From是签署交易的人,To是被交互的合约地址
  4. Value是指从签名者的钱包中转移的ETH价值。即使该值为0,也不意味着交易中没有转移ETH。
  5. Gas有点复杂(尤其是EIP-1559),但只要记住这个公式:Gas Price * Gas Used by Transaction = Transaction Fee

这是函数调用和传入的参数的字节码。前8个字符(4个字节)是函数签名0x849a3aa3,是函数名称和参数类型的哈希值。然而,这个哈希并不一定是唯一的,这有可能导致被黑等安全问题。在这个案例中,这个函数调用合约功能来创建众筹合约(它是一个代理,但我们不会去讨论这个问题)。

Function:createCrowdfund((uint256,uint256,bytes32)[], (address,uint256), string, string, address, address, uint256, uint256, uint256)

如果你点击 "decode input data"(解码输入数据),这就会显示出来,你也可以看到各种变量值的设置。随后的每64个字符(32字节)都是不同的输入变量。众筹有三个层次的版本。在BLVKHVND的这次众筹中,他们使用的数量为1000、250和50,价格分别为0.1、0.3和1ETH。

注意,价格实际上显示为100000000000000000 ,这是因为前18个零代表小数。我们必须在数据中通过除以10^18来进行转换。

这是一个很大的问题,让我们开始查询吧。Dune有一个名为ethereum.transactions的表,其中有我们上面谈到的自第一个区块以来每笔交易的所有变量。我们可以查询这个表,看看在过去几个月里0x849a3aa3的出现。

SELECT * FROM ethereum.transactions 
WHERE "block_time" > now() - interval '3 months'
AND "data" is not null
AND SUBSTRING ( encode("data", 'hex'), 1, 8 ) = '849a3aa3'

ethereum.transactions是一个非常大的表,所以如果你在没有过滤器的情况下查询,查询会超时(需要30分钟以上)。按区块时间过滤通常是最有用的,在这种情况下,我们要取3个月内发生的所有行。另外,许多交易只是ETH转账,没有附加任何数据,所以我们将通过只保留数据不为空来过滤掉。现在为了检查函数签名,我们需要将数据从十六进制编码成一个字符串,然后使用SUBSTRING只取位置1到位置8的字符。

现在是复杂的部分,内部事务和事件的排列。对于这一点,看一下代码会更容易。如果你进入etherscan的合约标签,在文件1 of 10上做ctrl+f,你会发现以下代码(我编辑了一些片段,使其更易读)。

function createCrowdfund(
        ...variables...
    ) external returns (address crowdfundProxy) {
        ...some variable prep code...

        crowdfundProxy = address(
            new CrowdfundWithPodiumEditionsProxy{
                salt: keccak256(abi.encode(symbol_, operator_))
            }(treasuryConfig, operator_)
        );

        emit CrowdfundDeployed(crowdfundProxy, name_, symbol_, operator_);

        ...register to treasury code...
    }

这里的第一个关键行是crowdfundProxy = address(contract_to_be_created),这就是部署新的合约并创建一个CREATE 0类型的内部交易。

转移ETH也会创建一个CALL类型的内部交易,我们将在下一个交易中看到它。

我们可以通过如下sql查询所有被创建的众筹合约:

SELECT tx."block_time", tx."from", tr."type", tr."code"
FROM ethereum.transactions tx 
LEFT JOIN ethereum.traces tr ON tx."hash"=tr."tx_hash" --tracks internal transactions
WHERE tx."to" = '\x15312b97389a1dc3bcaba7ae58ebbd552e606ed2' -- crowdfund podiums edition
AND tr."type" = 'create' 

我们需要ethereum.transactions,因为我们想过滤只与工厂合约相关联的交易内部交易)。这样做的主要原因是内部交易并不总是与整体交易有相同的目的合约地址(To)。我们可以在事务哈希上连接这些表,然后只过滤create类型的内部事务。

这里的第二个关键行是emit CrowdfundDeployed,它创建了一个存储在节点中的日志,但不在块中。如果你看下日志,会发现EditionCreated事件也被排了出来,但这是来自另一个实际创建ERC721代币的合约(因此是不同的合约地址)。

与函数签名类似,事件也有一个唯一的哈希值,位于Topic 0中。对应上面的事件:0x5133bb164b64ffa4461bc0c782a5c0e71cdc9d6c6ef5aa9af84f7fd2cd966d8e是CrowdfundDeployed的哈希值,0xbaf1f6ab5aa5406df2735e70c52585e630f9744f4ecdedd8b619e983e927f0b6是EditionCreated的哈希值。

我们可以通过查询dune中的ethereum.logs表,可以看到所有被创建的众筹:

SELECT * FROM ethereum.logs
WHERE "topic1"='\x5133bb164b64ffa4461bc0c782a5c0e71cdc9d6c6ef5aa9af84f7fd2cd966d8e'::bytea

topic2和topic3通常保存ETH转账的数据,不然事件数据将显示在数据栏中。我们将在后面详细介绍如何处理这个问题。

日志是非常有用的,因为它们可以用来排列状态变量,而不仅仅是函数调用值(TheGraph使用日志来为GraphQL查询建立子图模型)。接下来,我们将动用我们所涉及的一切来研究ETH对我们新创建的众筹合约的贡献(地址:0x320d83769eb64096ea74b686eb586e197997f930 )。

如果你能走到这一步,那么你已经通过了所有艰难的概念。给自己拍拍胸脯吧! 在接下来的两节中,我们将真正进入细节,所以如果你需要的话,可以休息一下。

向合约缴纳的ETH

第二个交易:
0xd4ce80a5ee62190c5f5d5a5a7e95ba7751c8f3ef63ea0e4b65a1abfdbbb9d1ef

这个交易比较容易理解。Jesse支付了1个ETH,从BLVKHVND众筹中铸造了一个tokenId 167的NFT。他还得到了1000个HVND,这是众筹基金根据捐款规模发放的ERC20代币。

但是,如果我们想看看在一段时间内筹集了多少ETH,或者卖出了多少个NFT呢?有时合约会在etherscan上的Read Contract里有一个查看功能,你可以看到总余额。但目前这个案例中,合约并没有这个功能。

记住,函数调用会改变状态数据,我们需要通过对交易历史的汇总来拼凑整体状态数据。有时,合约的整体状态可以在事件中发出,比如Compound V2的AccrueInterest事件。

在我们的例子中,需要在一个查询中做两件事来获得贡献的ETH总量:

  1. 获取调用了"contribute"方法的交易
  2. 通过过滤具有CALL类型的内部交易,求出转移的ETH总量

记住,可以通过对etherscan上的输入数据(input data)进行解码来获得方法的函数签名(function signature)。

SELECT SUM(tr."value"/1e18) as contribution FROM ethereum.transactions tx 
LEFT JOIN ethereum.traces tr ON tx."hash" = tr."tx_hash"
--transactions filtering 
WHERE tx."to" = '\x320d83769eb64096ea74b686eb586e197997f930'::bytea
AND tx."data" is not null
AND SUBSTRING ( encode(tx."data", 'hex'), 1, 8 ) IN ('a08f793c', 'ce4661bb')
--traces filtering 
AND tr."success"
AND tr."value" > 0
AND tr."call_type" = 'call'

严格来说,还有一个叫做contributionForPodium的方法,这就是我们在上面检查两个函数签名的原因。CALL类型实际上在操作码层面也有子类型,所以我们需要指定具体的基础调用类型的调用(如果你熟悉delegatecall,那么你会知道这将给我们一个双数)。我们加上交易哈希,然后除以10^18,得到正确的ETH的值。

让我们来看看最后一笔交易,在这里,数据开始变得非常棘手。

关闭和提取合约中的资金

第三个交易:
0xe9d5fefde77d4086d0f64dd1403f9b6e8e12aac74db238ebf11252740c3f65a8

在这个交易中,我们可以看到转移了337个ETH和1,012,965个HVND代币(后者在第一次交易中由operatorPercent方法处理)。在这个函数被调用后,合约会正常的ERC20的方式执行。

在一个众筹已经结束的情况下,我们可以从这个交易的数据中得到筹集的总额--比如在CALL类型的内部交易中转移的价值。不过,最好是把它和一个事件联系起来,以防有一些我们不知道的转账行为。但是,等等,为什么日志是不可读的?

好吧,这就是我们开始进入一些相当混乱的模式的地方了。早些时候我提到,这个众筹基金是作为代理部署的--这意味着它就像一个空的USB,插在实际持有逻辑规则的电脑上。创建USB要比电脑便宜得多--这种逻辑规则对链上也适用(除了gas花费的成本)。如果你想更详细的了解代理模式,可以看看OpenZeppelin团队的这篇好文章

在这种情况下,计算机被称为逻辑规则,只部署一次。代理被多次部署,它没有合约代码中的逻辑功能或事件。因此,etherscan并不具备在日志中显示解码数据的能力。那么,我们如何把这些拼凑起来呢?我们可以采取事件的keccak256哈希值,就像我们对函数签名所做的那样。但在这里,阅读代码将帮助你节省一些时间。如果你去查看工厂合约上的Read Contract,你会看到逻辑合约的地址。

在那里,我们可以找到代码中的closeFunding()函数:

function closeFunding() external onlyOperator nonReentrant {
        ...code...

        _mint(operator, operatorTokens);
        // Announce that funding has been closed.
        emit FundingClosed(address(this).balance, operatorTokens);

        ...ETH value transfers...
    }

ETH价值转移不会发出事件,因为它们只是内部交易。而如果你熟悉ERC20标准的运作方式,你就会知道mint实际上创建了一个转移事件(意味着已经涵盖了我们的第一个事件)。这意味着FundingClosed必须是第二个日志,主题为0x352ce94da8e3109dc06c05ed84e8a0aaf9ce2c4329dfd10ad1190cf620048972。你能想出为什么不可能是第三条日志吗(提示:前两条日志和第三条日志之间有什么关键区别)?

有了这些知识,我们就可以像其他事件一样,通过一些花哨的数据解码来查询这个事件(记住参数是每64个字符(32字节)。我们必须把它变成一个字符串来切分,然后我们把它改成一个数字,再除以10^18来去掉小数。

SELECT "contract_address", 
        bytea2numeric( decode ( SUBSTRING ( encode("data", 'hex') , 1, 64 ), 'hex'))/1e18 as eth_raised, 
        bytea2numeric ( decode ( SUBSTRING ( encode("data", 'hex') , 65 , 64 ), 'hex'))/1e18 as tokens_allocated_owned
FROM ethereum.logs
WHERE "topic1"='\x352ce94da8e3109dc06c05ed84e8a0aaf9ce2c4329dfd10ad1190cf620048972'::bytea
AND "contract_address"='\x320d83769eb64096ea74b686eb586e197997f930'::bytea

恭喜你,你现在知道如何处理ethereum.transactions、ethereum.traces和ethereum.logs。它们总是可以通过交易哈希来连接,然后剩下的就是知道如何用encode/decode、substring和一些bytea操作符来处理数据。Woohoo!

我们也可以对上一个事务中的contribute方法做这个练习。因为这一切都发生在代理合同上。

把它们放在一起

现在,如果我们不得不去跟踪函数签名和事件主题--以及对每个查询中的所有变量进行解码--我想我们现在都会放弃数据分析。幸运的是,大多数数据服务都有一些合约解码的变化,这意味着我可以给一个合约地址,ABI和Dune将为我解决解码的问题。这样一来,事件/功能就变成了它们自己的表,我就可以很容易地用这个来做之前的"total contributions"查询。

WITH 
    union_sum as (
        SELECT SUM("amount")/1e18 as raised FROM mirror."CrowdfundWithPodiumEditionsLogic_evt_Contribution"
        WHERE "contract_address"='\x320d83769eb64096ea74b686eb586e197997f930'
        
        UNION ALL 
        
        SELECT SUM("amount")/1e18 as raised FROM mirror."CrowdfundWithPodiumEditionsLogic_evt_ContributionForEdition"
        WHERE "contract_address"='\x320d83769eb64096ea74b686eb586e197997f930'
    )
    
SELECT SUM("raised") FROM union_sum

值得庆幸的是,这种查询的可读性更强,更容易编写。他们甚至还照顾到了代理/工厂的逻辑模式--感谢团队! 如果没有这个抽象,我保证数据分析的编写会混乱十倍,调试会糟糕一百倍。Dune还有很多其他有用的表格,比如Prices.usd用于每日代币价格,dex.trades用于所有主要交易所的所有代币交易(以及事件nft.trades用于OpenSea NFT交易)。

虽然大部分时间你都在玩解码数据,但了解这些数据下面的真正内容将帮助你在Web3中更快地提高水平!此外,你现在是以太坊的使用者。另外,你现在可以流利地使用etherscan了--我保证这将是未来每个密码工作中重要的一部分。我希望这对你有帮助,如果你需要一些帮助,请随时联系我。

写在最后

数据里藏着黄金屋,学会分析链上数据是一把掘金铲,虽然有些难,但还是应该花时间去做。

我试着用上面学到的内容,对我发起的一个众筹做了个简单的数据分析:

select count(tx.*) as Backers ,  SUM(tx."value"/1e18) as Raised, sum((tx.gas_price/1e18)*tx.gas_used) as GasFee from ethereum.transactions tx
where tx."to" = '\x2B3b54932db9a8edd31DCd03aC34B45E98f3FC34'::bytea

《DeFi入门手册》 ,目前支持次数205,筹集到7.6ETH,总花费交易手续费1.21ETH,可以看出在主网做资金筹集成本还是有些高,大概占到项目资金的15%。


近期文章:
Mirror.xyz 通向web3.0的一道窄门
使用Mirror发起一个知识付费项目,做一次WEB3.0的探索

Subscribe to 不确定思维
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.