闪电贷学习和编写

1.什么是闪电贷?

关于闪电贷的概念,网络上有很多。一句话总结就是一个区块内借款和还款同时完成。

听起来第一个想法就是:有个毛用?

对,普通情况下就是没卵用。但是没用也要去学习他。为了学习它,总得找个理由让自己打个鸡血。

emmm,想到了!

那就是发起一笔闪电贷,拿来买大房子。这样子我就可以拥有它1-12秒钟!

2.和谁贷款?

提供闪电贷的很多。有aave,uni,keeperdao等等。。

其他的相对复杂,就先拿keeperDao来举例吧。因为它最简单,也是yueying大佬最先开始写的。

它的合约地址是

3.怎么借?

知道合约地址了可以查看代码,还有去官网看说明。

最主要的函数

/// @notice borrow assets from this LP, and return them within the       same transaction.
    ///
    /// @param _token The address of the token contract.
    /// @param _amount The amont of token.
    /// @param _data The implementation specific data for the Borrower.
    function borrow(address _token, uint256 _amount, bytes calldata _data) external nonReentrant whenNotPaused {
        require(address(kTokens[_token]) != address(0x0), "Token is not registered");
        uint256 initialBalance = borrowableBalance(_token);
        _transferOut(_msgSender(), _token, _amount);
        borrower.lend(_msgSender(), _data);
        uint256 finalBalance = borrowableBalance(_token);
        require(finalBalance >= initialBalance, "Borrower failed to return the borrowed funds");

        uint256 fee = finalBalance - initialBalance;
        uint256 poolFee = calculateFee(poolFeeInBips, fee);
        emit Borrowed(_msgSender(), _token, _amount, fee);
        _transferOut(feePool, _token, poolFee);
    }

好了。知道了主要的函数之后,就可以想一想应该怎么做了。

4.开始写发起闪电贷的代码

PS:再写之前,先去上个洗手间,洗洗手,泡杯咖啡。思路清晰之后再去做。写这篇文章的时候,我是在公司写的,有效利用摸鱼时间。毕竟我是十年java程序员,工作效率极高之下,时间多一点是很正常的。

先创建ExInterface.sol文件,里面主要编写引用的外部合约。文件内容如下:

pragma solidity ^0.8.17;

//ERC20标准token的接口
interface IERC20 {
    //总供应量(totalSupply)
    function totalSupply() external view returns (uint256);
    //余额(balanceOf)
    function balanceOf(address account) external view returns (uint256);
    //转账(transfer)
    function transfer(address recipient, uint256 amount) external returns (bool);
    //授权(approve)
    function approve(address spender, uint256 amount) external returns (bool);
    //请求转账(transferFrom)
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    function decimals() external view returns (uint8);

    function allowance(address owner, address spender) external view returns (uint256);
}
//将以太坊原生代币ETH与ERC20token互相转换的合约
//合约地址 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
interface IWETH {
    //将ETH转换为WETH
    function deposit() external payable;
    //将ETH转换为WETH
    function withdraw(uint wad) external;
}

//KeeperDao的LiquidityPool合约
//合约地址0x4F868C1aa37fCf307ab38D215382e88FCA6275E2
interface ILiquidity {
    //_token 所借的代币名称;_amount 借多少金额;calldata 回调函数
    function borrow(address _token, uint256 _amount, bytes calldata _data) external;
}

相信代码解释已经足够清晰。这里就不再详细解释。最主要的是ILiquidity 接口的borrow()一眼就能看懂了吧。

至于IERC20 和IWETH 为什么要这么写,也暂时不用去管它。

然后,创建我们的主要合约文件FlashLoan.sol

PS:前辈们说的,只要你写合约,第一件事就是先写上代币转出的功能。

所以,求生欲望极强的我,马上写上(复制)以下几个函数

 //把合约里面的ETH转到钱包
    function turnOutETH(uint256 amount) public onlyOwner {
        payable(owner).transfer(amount);
    }
    //把合约里面的钱转到钱包
    function turnOutToken(address token, uint256 amount) public onlyOwner {
        IERC20(token).transfer(owner, amount);
    }
    // WETH转换为ETH
    function WETHToETH(uint256 amount) public onlyOwner {
        IWETH(WETH).withdraw(amount);
    }
    // ETH转换为WETH
    function ETHtoWETH(uint256 amount) public onlyOwner {
        IWETH(WETH).deposit{value : amount}();
    }

然后创建两个函数,一个是发起闪电贷的主函数,一个是接收闪电贷的回调函数。

    //发起一个eth的闪电贷。这里先不自定义传入token类别了
    function doFlashEth(uint256 amount){
address liquidityPool = 0x4F868C1aa37fCf307ab38D215382e88FCA6275E2;
        address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
        ILiquidity(liquidityPool).borrow(weth, amount,
            abi.encodeWithSelector(this.receiveBuyBigHouse.selector, abi.encode(amount)));
    }

    // callback ,收到钱后可以拿来买一个大房子。
    function receiveBuyBigHouse(bytes memory data) public {
        require(msg.sender == borrowerProxy, "bu shi BorrorwerProxy,paochu yichang.");
//buy big house--->.
address liquidityPool = 0x4F868C1aa37fCf307ab38D215382e88FCA6275E2;
        address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
uint256 amount=abi.decode(data, (uint256));
IERC20(weth).transfer(liquidityPool, amount);
    }

在发起闪电贷的函数中doFlashEth,由于我比较懒,所以直接把地址都写在函数里面了。后面我们再想办法进行扩展。嗯,那句话怎么说的?先实现,右面再优化~(我都辞职了,你还叫我优化?)

abi.encodeWithSelector 这个后面再具体说明,这里先这么写。就是告诉它,携带参数回调我们的购买大房子函数。

然后在receiveBuyBigHouse购买大房子里面,我们会看到屎山已经形成,不得不去稍微优化一下,否则代码又臭又长。

当然,不要忘记把购买大房子的代码删了。

最终,我们的代码如下

address liquidityPool = 0x4F868C1aa37fCf307ab38D215382e88FCA6275E2;
    address weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;

    //发起一个eth的闪电贷
    function doFlashEth(uint256 amount){
        ILiquidity(liquidityPool).borrow(weth, amount,
            abi.encodeWithSelector(this.receiveBuyBigHouse.selector, abi.encode(amount)));
    }

    // callback ,收到钱后可以拿来买一个大房子。
    function receiveBuyBigHouse(bytes memory data) public {
        require(msg.sender == borrowerProxy, "bu shi BorrorwerProxy,paochu yichang.");
        //购买大房子
        uint256 amount = abi.decode(data, (uint256));
        IERC20(weth).transfer(liquidityPool, amount);
    }

千万记得要先购买房子,再还钱,否则你白忙一场。也记得要还钱。否则整个闪电贷就不会成功。也就是这一句代码

require(finalBalance >= initialBalance, "Borrower failed to return the borrowed funds");

大家看到我对大房子的执念得有多深。。。

到这里,接下来便可以进行合约的部署和测试。

我把自己写的代码注释放上。有助于理解。

/// @notice borrow assets from this LP, and return them within the same transaction.
    ///
    /// @param _token The address of the token contract.
    /// @param _amount The amont of token.
    /// @param _data The implementation specific data for the Borrower.
    function borrow(address _token, uint256 _amount, bytes calldata _data) external nonReentrant whenNotPaused {
        //检测是否支持该代币
        require(address(kTokens[_token]) != address(0x0), "Token is not registered");
        //获得代币里面的余额
        uint256 initialBalance = borrowableBalance(_token);
        //执行转账。如果不成功,会出现异常并回退操作。
        _transferOut(_msgSender(), _token, _amount);
        //执行借款检查和回调。
        borrower.lend(_msgSender(), _data);
        //再次获得代币里面的余额
        uint256 finalBalance = borrowableBalance(_token);
        //如果最终余额大于等于初始余额,证明已经归还借款。否则抛出异常。
        require(finalBalance >= initialBalance, "Borrower failed to return the borrowed funds");
        //比较两个数之间的差值--费用
        uint256 fee = finalBalance - initialBalance;
        //计算手续费
        uint256 poolFee = calculateFee(poolFeeInBips, fee);
        //通知事件
        emit Borrowed(_msgSender(), _token, _amount, fee);
        //执行转账 给费用池
        _transferOut(feePool, _token, poolFee);
    }
contract BorrowerProxy {
    address liquidityPool;

    constructor() {
        liquidityPool = msg.sender;
    }

    function lend(address _caller, bytes calldata _data) external payable {
        //检测是谁调用了合约。如果不是borrowerProxy,则抛出异常。
        require(msg.sender == borrowerProxy, "BorrowerProxy: Caller is not the borrowerProxy pool");
        //调用我们的的回调函数
        (bool success,) = _caller.call{value : msg.value}(_data);
        require(success, "BorrowerProxy: Borrower contract reverted during execution");
    }

}

好了。下一节,我们将使用truffle进行合约测试。

PS:最近脑袋有点晕,如果文章帮助到你,可以打赏一点给我买8个核桃。

post image