FlashLoan 是一种无抵押借贷。任何人都可以随时从资金池中借出大量资产,用它在区块链上做任何交易,最后把连本带利还给资金池。整个过程需要在一个交易里完成。
2022 还没结束,闪电贷攻击还在继续 ...
市场永远存在大量的套利空间和漏洞,通过一次性借出大量的金额,可以
这个在以太坊上不是一个问题,因为以太坊是一个单实例的计算机,也是一个单实例的数据库,可以把任何操作放在同一个区块里进行原子操作。执行闪电贷的前提就是所有的交易需要在同一个区块里完成,这个由闪电贷提供者通过程序实现。
区块链给人最直观的感觉是去中心化的网络,特点是运行节点分散,并且数据永久。但是理解闪电贷的过程让人意识到,在 trustless 的场景下很多事情的做法不同。由于 trust 不需要任何成本,trust 本身是没有价值的,那么传统世界里本身基于 trust 发生的事情,在以太坊上面就会不一样。
在以太坊上,默认信任第三方的假设是不存在的,并且可以随时回滚整个交易,所以默认不信任没关系,但自己要管理好自己的状态。在互联网上,如果没有默认信任第三方,就无法完成任何交互。
以太坊的状态,包括余额,都是通过记账的方式进行维护,一次记账就是一次状态变更记录,如果记账项目被删除,状态的变更也就会回退。举个例子:
初始状态 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"
很多金融工具都需要这样的运行机制,比如比如活期存款利息,比如公司股权。
以区块作为时间单位
最终,4个区块过后,A 和 B 的收益分别为 $175 和 $125
收益没法是积累到一定程度然后按照那时候的份额再分配,因为过程中所有人的份额都可以随时调整,合理的情况是质押时间越久,分享的收益就越多。比如上面的情况里,A 和 B 在 4 个区块后,最终的份额都是 50%,如果收益积累了 4 个区块再分配,那 A 和 B 就会各得 $150,显然 A 投资时间长,应该得到更多。
每次收益进来以后,根据比例,直接计算每个人的所得,然后直接转账分配收益。
这个过程一般通过异步任务完成。但是以太坊上,写入数据(改变状态)的成本很高,也就是计算复杂度十分敏感,上面的做法,每一次分配的复杂度是 O(N),其中 N 是质押者的数量,因为每次收益都要更新所有人的账本。随着质押者越来越多,交易成本也越来越高,这个在以太坊上难以接受。
大概的想法是这样,首先需要改变一下体验:
算法如下
设
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 项目
不是所有 DeFi 项目都支持闪电贷,最早支持的项目是 Aave,后来还有其他链的 DeFi 项目比如 BSC 的 DODO 也添加了这个功能,比较早期或者规模较小的链上存在大量的套利空间,需求更多,同时闪电贷可以给 DeFi 项目带来手续费收益。
未完待续