笔者把自己为数不多的 ETH 兑换为 stETH 后,发现 stETH 每天都在自然增长,不断获取收益。但是却没有看到账户有交易产生,这是为什么呢?本文带大家一起来看看背后的巧妙设计,揭开收益发放的秘密。
1 个 stETH 过去几天后已经有了一些收益
在这之前先介绍一下 stETH 赚取收益背后的逻辑,也就是以太坊的质押(Staking),已经了解这部分概念的读者可以直接跳到后面。
最初的以太坊和比特币一样是通过工作量证明(Proof of Work,PoW)来作为它的共识机制,但是 PoW 因为耗电以及其它安全性和性能上发展的考虑,以太坊从 2022 年 9 月开始升级为权益证明(Proof of Stake,PoS)。
原本依靠算力挖矿来吸引矿工实现共识的以太坊,摇身一变,变成了依靠大家通过质押 ETH 获取投票权,通过投票来获取收益,从而激励大家通过 PoS 的方式来实现共识。
通过质押 32 个 ETH 可以加入以太坊网络,可以成为验证者,负责存储数据、处理交易以及向区块链添加新区块。只要运行将交易正确打包为新区块并检查其他验证者工作,就会获得 ETH 奖励,这样就相当于你可以通过质押的方式让 ETH 可以拥有相对稳定的收益。
但是这样的质押对于普通用户来说还是太麻烦,毕竟 32 个以太坊和一台要能够全年无休接入以太坊网络的专用计算机还是有一定的门槛的。而且质押 ETH 会使得丧失了这部分 ETH 的流动性。于是就有了流动性质押衍生品(Liquid Staking Derivatives,LSD),它旨在解决传统质押中的门槛和流动性问题,允许用户质押 32 个以下的 ETH,以及不需要自己拥有节点,而是把 ETH 委托给第三方质押,并获取相应的质押代币(如 Lido 的 stETH 或 Rocket Pool 的 rETH),这些流动性代币可以在其他平台上交易、借贷或用于其他金融活动,这样,用户既能更便捷参与到质押中获得奖励,又能保持资金的灵活性。
所以 stETH 本质的逻辑就是把 ETH 给到 Lido,Lido 会用这些 ETH 去参与以太坊的 PoS 以获取收益,用户会得到对应的 stETH 作为凭证。接下来就是 Lido 要把收益发放给这些拥有 stETH 的地址。
我们可以看到 stETH 的收益每天都会自动更新,下图是我们测试的收益情况,对应每天都可以检查加密钱包验证相关内容。
但是到这里我想熟悉智能合约开发的同学就会疑惑了:每天发放这么少的收益,可能收益都不够付 GAS 的。
确实,如果 Lido 按照最简单的做法来发放收益的话,那确实难以覆盖 GAS 的成本。从我们的直觉来看,要往如此众多的地址发送代币,GAS 是难以想象的。
但是确实 Lido 就实现了钱包中的 stETH 收益自动增长,而且我们并没有发现该地址有任何交易,这是怎么实现的呢?
我们找到了 Lido 的合约https://etherscan.io/token/0xae7ab96520de3a18e5e111b5eaab095312d7fe84追溯到合约的 balanceOf 方法:
balanceOf 是符合 ERC20 规范的方法,钱包就是通过该方法获取用户有多少 token 的。
我们可以看到 stETH 的合约中这里调用了 getPooledEthByShares方法。该方法入参是 mapping (address => uint256) private shares;。这代表用户有多少 stETH?显然不是,不然每天都需要更新每个地址的数据,虽然这样也可以做到只要调用合约中的方法来更新 shares来实现一次交易就能更新所有地址的 token,但是显然这样做 GAS 的消耗同样也是巨大的。
想必到这里大家已经要猜到合约是怎么实现的了,我们继续来看 getPooledEthByShares方法。
可以看到最终返回的结果是用地址中 sharesAmount 乘以 _getTotalPooledEther()再除 _getTotalShares。
_getTotalPooledEther代表总共有多少 stETH(按照 stETH 兑 ETH 为 1:1 的话也代表有多少 ETH),_getTotalShares代表有多少份额。这样一算每个地址有多少 stETH 就是动态计算出来的了。
举例说如果现在一共有 1000 个份额(Shares,也就是 _getTotalShares方法返回的数量),其中 A 地址有 100 份(对应上面的 sharesAmount)。这 1000 个份额对应 1000 个 stETH(也就是 _getTotalPooledEther)返回的数量。那么按照这个计算,A 地址就对应有 100 个 stETH。那 Lido 拿这总得 1000 个 ETH 去质押获取到 1 个 ETH 的收益后对应更新 _getTotalPooledEther 为 1001,也就是最初总共 1000 个的 stETH 变多变成 1001 个了,那么新的计算出来 A 地址就拥有了 100 * 1001 / 1000 = 100.1 个 stETH。
简单点说就是每个地址拥有的股份不变,股份对应的 stETH 变多了,那么一计算自然 stETH 就变多了。
我们继续看代码, _getTotalPooledEther 中的逻辑是会受到 handleOracleReport方法影响,而这个方法则会更新合约中的相关数据。具体的调用是会通过 https://etherscan.io/address/0x852deD011285fe67063a08005c71a85690503Cee合约定期调用 submitReportData 更新数据(submitReportData 中会调用 Lido 合约的 handleOracleReport):
我们可以看到每天都会有调用更新相关内容,这就是为什么虽然我们无法看到我们的地址中有发放收益的交易,但是金额依然每天在变化的原因。
这背后其实体现了以太坊 ERC20 智能合约的一个特点,就是这些 ERC20 的合约拥有多少代币并不是写死在地址上的,而是合约方法返回的,所以可能会出现账号虽然没有任何交易,但是代币的数量也可能发生变化。这一方面让 ERC20 合约更灵活,但是另外一方面也给很多对合约不熟悉的朋友带来了很多困惑,希望本文可以帮助大家更多的理解智能合约,更安全的和智能合约交互。
另外,虽然通过把 ETH 质押为 stETH 能够获得看似稳定的质押收益,但是依然有可能的风险存在,本文只作为对质押合约的技术研究参考,不构成任何投资建议。
另外之前我们发布了一篇关于『再质押』的文章 https://www.panewslab.com/zh/articledetails/789w051y.html,感兴趣的读者可以继续阅读,深入了解更多内容。