

在区块链世界,闪电贷是一项伟大的金融创新。闪电贷一词常与攻击一同被提及。但究其本质,问题的根源却在于协议本身。 闪电贷只是让人拥有了对于大量资金的控制能力。
假如给你一个亿贷款,但是3秒钟你就必须还回去,你会拿他来做什么?
闪电贷是一种无抵押借贷工具,由于区块链交易的原子性和回滚特性得以诞生。用户需要在一个原子交易内完成借款和还款并且不需要任何抵押品。让用户拥有在接近0成本情况下使用巨量资金的能力。
在大众的认知下,大量的闪电贷被用于攻击各种项目,并被冠以闪电贷攻击的标签。事实上,闪电贷技术并不是用来进行项目攻击。恰恰相反,它是区块链世界一项伟大的金融创新。它让人们拥有了对于大量资金的控制能力,而真正有问题的是它本身各种协议自身的实现缺陷,被不法分子加以利用。
在实际的用途中,闪电贷还有着在借贷市场自我清算、在更换抵押物、债务迁移等用例。协议只关心你是否在你的transaction的最后是否如约归还了你的贷款,而不关心这笔贷款在你的transaction的用途是什么。因此,闪电贷只是一种能力。
DODO是一个由主动做市商(PMM)算法驱动的去中心化交易平台。在用户将流动性聚合到平台时,其提供了去中心化的闪电贷服务。具体而言,我们可以使用闪电贷调用DODO的资金,在使用后注意在操作的最后归还即可,否则这笔原子交易将会判定为失败,之前的所有操作被判定无效,不发生实际价值
如何在DODO借100个BNB并归还,示例代码如下
在remix编译如下代码:
pragma solidity 0.8.0;
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
}
interface IDODO {
function flashLoan(
uint256 baseAmount,
uint256 quoteAmount,
address assetTo,
bytes calldata data
) external;
function _BASE_TOKEN_() external view returns (address);
}
contract DODOFlashloan {
function uint2str(uint _i) internal pure returns (string memory _uintAsString) {
if (_i == 0) {
return "0";
}
uint j = _i;
uint len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len;
while (_i != 0) {
k = k-1;
uint8 temp = (48 + uint8(_i - _i / 10 * 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}
constructor(){
// 不可在构造函数调用
// dodoFlashLoan(100 * 10**18, 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);
}
address flashLoanPool = 0x0fe261aeE0d1C4DFdDee4102E82Dd425999065F4; // DODOV2池地址
function dodoFlashLoan(
uint256 loanAmount,
address loanToken
) public {
bytes memory data = abi.encode(flashLoanPool, loanToken, loanAmount);// 构造abi配置
address flashLoanBase = IDODO(flashLoanPool)._BASE_TOKEN_();
if(flashLoanBase == loanToken) {
IDODO(flashLoanPool).flashLoan(loanAmount, 0, address(this), data);//贷取资金,执行环境切换到flashloan方法内
} else {
IDODO(flashLoanPool).flashLoan(0, loanAmount, address(this), data);//贷取资金,执行环境切换到flashloan方法内
}
}
// Note: CallBack function executed by DODOV2(DVM) flashLoan pool--DVM池回调
function DVMFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount,bytes calldata data) external {
_flashLoanCallBack(sender,baseAmount,quoteAmount,data);
}
// Note: CallBack function executed by DODOV2(DPP) flashLoan pool--DPP池回调
function DPPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external {
_flashLoanCallBack(sender,baseAmount,quoteAmount,data);
}
// Note: CallBack function executed by DODOV2(DSP) flashLoan pool--DSP池回调
function DSPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external {
_flashLoanCallBack(sender,baseAmount,quoteAmount,data);
}
function _flashLoanCallBack(address sender, uint256, uint256, bytes calldata data) internal {// flashLoan方法最终回调
(address flashLoanPool, address loanToken, uint256 loanAmount) = abi.decode(data, (address, address, uint256));
require(sender == address(this) && msg.sender == flashLoanPool, "HANDLE_FLASH_NENIED");
//Note: 借来后的逻辑实现
revert(uint2str(IERC20(loanToken).balanceOf(address(this))));
//资金归还
IERC20(loanToken).transfer(flashLoanPool, loanAmount);
}
}
2.链接ganache-cli

3.在fork链上部署合约代码并调用dodoFlashLoan方法

4.通过报错报出本地址的WBNB的amount,证明回调了DVMFlashLoanCall/DPPFlashLoanCall/DSPFlashLoanCall方法且代币已转发到了本地址。

如何在pancake-dex借100个BNB并归还,示例代码如下
在remix编译如下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external;
function approve(address spender, uint256 amount) external returns (bool);
function deposit() external payable;
}
interface IpancakeCallee{
function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external;
}
interface IpancakeFactory{
function getPair(address tokenA, address tokenB) external view returns (address pair);
}
interface IpancakePair{
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
function token0() external view returns (address);
function token1() external view returns (address);
}
interface IpancakeRouter{
function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
external
payable
returns (uint[] memory amounts);
function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
external
returns (uint[] memory amounts);
}
contract pancakeFlashloan is IpancakeCallee {
function uint2str(uint _i) internal pure returns (string memory _uintAsString) {
if (_i == 0) {
return "0";
}
uint j = _i;
uint len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len;
while (_i != 0) {
k = k-1;
uint8 temp = (48 + uint8(_i - _i / 10 * 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}
IERC20 WBNB = IERC20(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);//wbnb
address borrowPair = 0x0eD7e52944161450477ee417DE9Cd3a859b14fD0;
constructor(){
}
uint amount0Out = 0;
uint amount1Out = 1 gwei;
function startAttack() public{
bytes memory data = abi.encode(address(WBNB), amount1Out);// 构data 备pancakeCall使用
IpancakePair(borrowPair).swap(amount0Out, amount1Out, address(this), bytes("1"));//贷取资金,执行环境切换到swap方法内
}
function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) external override {// swap方法回调
// logic here
revert(uint2str(IERC20(WBNB).balanceOf(address(this))));
uint fee = ((amount1Out * 3) / 997);
uint amoutnToRepay = amount1Out + fee + 1;
IERC20(WBNB).transfer(borrowPair, amoutnToRepay);
}
}
2.链接ganache-cli

3.在fork的私链上部署闪电贷合约,并调用startSwap方法执行闪电贷

4.通过报错报出本地址的WBNB的amount,证明回调了pancakeCall方法且代币已转发到了本地址。

在区块链世界中,一个调用的顺序执行会成为一笔交易,这样的一笔交易具有原子性。最通俗而言若发生异常(EVM操作码REVERT)那么这笔调用会执行失败,前面所有的操作均处于未执行的状态;

我们将其在DODO中观察,其在回调DVMFlashLoanCall/DPPFlashLoanCall/DSPFlashLoanCall完成后,返回到上一级函数flashloan并通过require检查是否自己的余额没有减少,否则主动抛出REVERT:

同样的,对pancake而言,其swap回调pancakeCall后也检测了余额和储备的关系,同时swap也是交换的核心函数,这也是笔者认为该协议设计的神来之笔(设计取自Uniswap)

本文中我们讲述了闪电贷的价值和具体实例,结合函数报错我们将借来的WBNB进行了显示,证明了WBNB已在该步骤发送到了执行合约。在借来贷款的后续步骤用户可具体利用该WBNB用于相应的套利/自我清算/更换抵押物/债务迁移并在最后进行归还。否则在回调完成回到上级函数时在检查闪贷池子余额时将会主动回滚,由于函数调用的原子性,若回滚后这些所有借取操作将不复存在。