# 闪电贷学习和编写

By [想住大房子的java程序员](https://paragraph.com/@java-3) · 2022-11-01

---

### 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个核桃。

![](https://storage.googleapis.com/papyrus_images/b13312e416d1c391c0749cfd9f239fe5696c6fb418e3937e69138e1bfb849dbc.png)

---

*Originally published on [想住大房子的java程序员](https://paragraph.com/@java-3/in5NdNN5IICqTfyKm8xB)*
