通过闪电贷 FlashLoan 了解以太坊

闪电贷 FlashLoan

FlashLoan 是一种无抵押借贷。任何人都可以随时从资金池中借出大量资产,用它在区块链上做任何交易,最后把连本带利还给资金池。整个过程需要在一个交易里完成。

闪电贷攻击

  1. 2020年10月26日总锁仓量超过10亿美元的 DeFi 项目 Harvest Finance 曝出遭到黑客攻击,已造成大约2400万美元的损失。
  2. 2020年11月17日,起源协议 Origin Protocol 稳定币OUSD被爆出遭到闪电贷攻击,Origin Protocol共损失225万美元的 DAI 和100万美元的 ETH。
  3. 2021年5月12日,DeFi 质押和流动性策略平台 xToken 遭到攻击,xBNTa Bancor 池以及 xSNXa Balancer 池被耗尽,xToken 损失约2500万美元。
  4. 2021年 5 月 20 日,BSC 链上的 DeFi 收益聚合器 PancakeBunny 遭到闪电贷攻击。攻击者利用 PancakeSwap 操纵 LP Token(BNB-BUSDT/BNB-BUNNY)的价格,造成损失 4,500 多万美元。
  5. 2021 年 Cream Finance 多次遭受闪电贷攻击,2 月份损失 3750 万美元,8 月份损失 1900 万美元,10月份又损失约 1.3 亿美元。

2022 还没结束,闪电贷攻击还在继续 ...

闪电贷可以用来做什么

市场永远存在大量的套利空间和漏洞,通过一次性借出大量的金额,可以

  1. 找到套利空间,进行大规模的低买高卖,获得合理收益;比如资产在交易所 A 的价格低于交易所 B 的价格 0.1%,大规模资金很容易套利。
  2. 将大量资金注入市场,操纵物价/币价,扰乱市场价格(预言机),利用极端情况下某些协议出现的漏洞,进行攻击。

如何确保还钱?

这个在以太坊上不是一个问题,因为以太坊是一个单实例的计算机,也是一个单实例的数据库,可以把任何操作放在同一个区块里进行原子操作。执行闪电贷的前提就是所有的交易需要在同一个区块里完成,这个由闪电贷提供者通过程序实现。

直觉

区块链给人最直观的感觉是去中心化的网络,特点是运行节点分散,并且数据永久。但是理解闪电贷的过程让人意识到,在 trustless 的场景下很多事情的做法不同。由于 trust 不需要任何成本,trust 本身是没有价值的,那么传统世界里本身基于 trust 发生的事情,在以太坊上面就会不一样。

在以太坊上,默认信任第三方的假设是不存在的,并且可以随时回滚整个交易,所以默认不信任没关系,但自己要管理好自己的状态。在互联网上,如果没有默认信任第三方,就无法完成任何交互。

以太坊运行

合约执行的特点

  1. 状态更新需要合约执行,合约执行需要发起交易,矿工进行分布式记账,有人为之买单
  2. 交易是一系列操作的集合,交易成功以后,状态才会改变,交易失败不会改变任何状态
  3. 合约不会并行执行,交易串行执行,合约执行也是串行的,所有状态变更都有明确的先后顺序
  4. 合约无法自动执行,没有 0 成本的定时任务
  5. 合约执行成本高,全量更新等洗数据操作要通过优化过的数据结构来替代

以太坊的状态,包括余额,都是通过记账的方式进行维护,一次记账就是一次状态变更记录,如果记账项目被删除,状态的变更也就会回退。举个例子:

初始状态 a = 0, b = 10
交易 1: a = a + 1, b = b - 1 执行成功 ✅
交易 2: a = a + 1, b = b - 1 执行失败 ❌
交易 3: a = a + 1, b = b - 1 执行成功 ✅
三次交易以后, 状态 a 等于 2,状态 b 等于 8

交易 Transaction

交易 = 执行代码并改变状态,交易的形式确保执行权限和执行费用,一个交易包括:

 from: 交易发起人
   to: 交易对端
value: from 发送给 to 的 ETH 数量
 data: 额外信息
  gas: from 支付给矿工的燃气费

一个转账交易的例子

X 给 Y 转 100 ETH,上面 a 和 b 的例子就是一个转账交易的状态变更过程:from 告诉矿工,把自己的余额减少 100 ETH,在把 to 的余额增加 100 ETH。

 from: X
   to: Y
value: 100ETH
 data: 无
  gas: 0.01ETH

执行的规程如下:

Transaction 1
  |- X 的余额 -100ETH
  |- Y 的余额 +100ETH

如果交易 Transaction 1 执行失败,交易内部产生的所有状态变更都不会被记录,所以不会出现 X 的余额减少但是 Y 的余额增加的情况。除非是这么操作:

Transaction 1
  |- X 的余额 -100ETH
Transaction 2
  |- Y 的余额 +100ETH

交易 Transaction 2 会在 Transaction 1 执行以后再执行,不管 Transaction 1 执行的结果是什么,Transaction 2 都会修改 Y 的余额。转账操作一定要把转账双方余额变更的操作放在一个交易里执行。

一个合约执行的交易的例子

X 执行合约 C,在合约里把自己的名字改成 zhangsan

 from: X
   to: C
value: 0
 data: rename("zhangsan")
  gas: 0.03ETH

地址 C 上面部署着一个合约,合约里实现了 rename 方法,接收一个字符串作为参数,交易执行过程:

Transaction 1
  |- name = "zhangsan"

一个小问题,质押收益如何分配

问题描述

  • 有一个资金池,会持续产生收入
  • 每产生一笔收入,就会按照当前池子里所有人的份额直接分配收益,人越多,每个人分配到的就越少
  • 所有人(Stakeholder)可以在池子里质押(Stake)某个特定代币证明自己的份额
  • 可以随时追加或者减少自己的份额,但是收益分配在收入产生的那个瞬间直接发生

很多金融工具都需要这样的运行机制,比如比如活期存款利息,比如公司股权。

举个例子

以区块作为时间单位

  1. 区块1,A 和 B 分别质押 300 股和 100 股,A 占 75%,B 占 25%
  2. 区块2,进来一笔收益 $100,A 和 B 分别得到 $75 和 $25
  3. 区块3,B 追加 200 股,最终 A 和 B 各占 50%
  4. 区块4,进来一笔收益 $200,A 和 B 分别得到 $100 和 $100

最终,4个区块过后,A 和 B 的收益分别为 $175 和 $125

要注意的是

收益没法是积累到一定程度然后按照那时候的份额再分配,因为过程中所有人的份额都可以随时调整,合理的情况是质押时间越久,分享的收益就越多。比如上面的情况里,A 和 B 在 4 个区块后,最终的份额都是 50%,如果收益积累了 4 个区块再分配,那 A 和 B 就会各得 $150,显然 A 投资时间长,应该得到更多。

简单的解决方案

每次收益进来以后,根据比例,直接计算每个人的所得,然后直接转账分配收益。

这个过程一般通过异步任务完成。但是以太坊上,写入数据(改变状态)的成本很高,也就是计算复杂度十分敏感,上面的做法,每一次分配的复杂度是 O(N),其中 N 是质押者的数量,因为每次收益都要更新所有人的账本。随着质押者越来越多,交易成本也越来越高,这个在以太坊上难以接受。

一种 O(1) 复杂度的更新算法

大概的想法是这样,首先需要改变一下体验:

  • 可以在每次收益进来的时候更改某些变量就可以随时计算出质押者的总收益 O(1)
  • 质押者需要通过一个提取操作来最终获得收益,合约在每次提取的时候记录下 O(1)

算法如下

T 当前的总股数
V 当前每一股可以获得的收益
stake[X] 质押者 X 质押的数量
snapshot[X] 质押者 X 上一次操作时候的 V 值

步骤

1. 区块1,A 和 B 分别质押 300 股和 100 股

T = 400, V = $0
stake[A] = 300, stake[B] = 100
snapshot[A] = snapshot[B] = 0

2. 区块2,进来一笔收益 $100

T = 400,V = $100 / 400 = $0.25
stake[A] = 300, stake[B] = 100
snapshot[A] = snapshot[B] = 0

3. 区块3,B 追加 200 股

B 自动提取收益 stake[B] * V = 100 * 0.25 =$25T = 600, V = $0.25
stake[A] = 300,stake[B] = 300
snapshot[A] = 0,snapshot[B] = $0.25
// 只需更新 B 自己的状态, V 不变, 因为 B 已经提取了截止当前的收益

4. 区块4,进来一笔收益 $200

T = 600,V = $0.25 + $200 / 600 = $0.5833         V = V' + revenue / T
stake[A] = 300, stake[B] = 300
snapshot[A] = 0, snapshot[B] = 0.25

5. 来看看双方剩余待提取的收益

A: stake[A] * V = 300 * $0.5833 =$175
B: stake[B] *(V - snapshot[B]) = 300 * ($0.5833 - $0.25) = $100
// B 已提取 $25, 还可以提取 $100, 应得总收益是 $125

其他类似的问题

固定速度收益(挖矿)

如果收益不是资金进来的时候产生,而是以一个固定的速度持续产生,比如每个区块产生 $1 收益。可以用前面类似的算法来实现,但是 V 是一个随着时间增长的变量。

存款利息(复利)

如果质押的不是“股”而是 $,也就是质押的资产和收益的资产是一样的,在固定速度收益的场景下,持续产生收益的过程中,质押的数量也会越来越大。用前面类似的算法可以算出复利所得。

复利是天然简单的计息方式,但不直观。而单利是在特定时间窗口里折算出来的利息,更直观。

如果资金池通过单利来控制收益,资金池就会有资金结余,作为储备金,通过单利调节需求。

闪电贷实现

合约代码

function flashLoan(
    address receiverAddress,    // 接收资产的地址
    address asset,              // 资产名称
    uint256 amount,             // 数量
    bytes calldata params       // 自定义执行参数
) {

    // 1. 记录余额, 把指定数量的资产转给 receiverAddress
    uint256 balance = IERC20(asset).balanceOf(this);
    IERC20(asset).transferTo(receiverAddress, amount);

    // 2. 执行自定义方法
    IFlashLoanReceiver receiver = IFlashLoanReceiver(receiverAddress);
    // receiver.executeOperation 里可以做任何事情, 但最后需要把 asset 转回来
receiver.executeOperation(params);

    // 3. 检查 receiverAddress 是否在步骤 2 执行完以后归还资产并支付手续费 0.3%
    require(IERC20(asset).balanceOf(this) >= balance * 1003 / 1000);

    // 4. 如果上面的检查失败, 则 flashLoan 执行失败, 交易回滚(状态变更不会被记录)

}

提供闪电贷的资金哪里来

来自于 DeFi 项目

  1. Decentralized Exchanges 去中心化交易所: Uniswap, Sushiswap, etc.
  2. Lending Protocols 借贷协议: Aave, Compound, etc.
  3. Stablecoins 稳定币协议: MakerDAO

不是所有 DeFi 项目都支持闪电贷,最早支持的项目是 Aave,后来还有其他链的 DeFi 项目比如 BSC 的 DODO 也添加了这个功能,比较早期或者规模较小的链上存在大量的套利空间,需求更多,同时闪电贷可以给 DeFi 项目带来手续费收益。

去中心化交易所 DEX

借贷协议

稳定币

未完待续

Subscribe to web3nomad.eth
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.