LapTrinhBlockchain

Chia sẻ kiến thức về Lập Trình Blockchain

An toàn Bảo mật, Lập trình Blockchain, Lập trình Smart Contract

Hướng dẫn triển khai Flashloan / Flashswap sử dụng MCL Flashloan, CREAM Finance Flashloan và PancakeSwap trên Binance Smart Chain BSC Testnet

Hướng dẫn triển khai Flashloan trên Binance Smart Chain

Hướng dẫn triển khai Flashloan trên Binance Smart Chain

Chia sẻ bài viết
5
(88)

Về Flashloan thì chắc các bạn không xa lạ gì nữa. Nó là một khoản vay nhanh không cần thế chấp nhưng phải trả lại ở cuối xử lý trong cùng Block. Bài viết này tập trung vào hướng dẫn triển khai sử dụng Flashloan trên mạng lưới Binance Smart Chain BSC Testnet. Và sau đó chúng ta mở rộng tìm hiểu về Flashswap sử dụng PancakeSwap.

Có thể bạn quan tâm: Hướng dẫn triển khai Flashloan / Flashswap sử dụng Uniswap V3 trên Goerli Testnet

Hướng dẫn triển khai Flashloan trên Binance Smart Chain
Hướng dẫn triển khai Flashloan trên Binance Smart Chain BSC

Triển khai Flashloan sử dụng MCL Flashloan

Gần đây Multiplier Finance đã cho ra mắt hệ thống Lending/Borrowing trên BSC có hỗ trợ Flashloan. Do đó chúng ta có thể sử dụng hệ thống này cho việc triển khai thử nghiệm Flashloan, đặc biệt MCL có hỗ trợ Testnet nên chúng ta triển khai trên BSC Testnet cho tiết kiệm. Một số tài nguyên bạn có thể tham khảo tại:

MCL đã cung cấp sẵn source code demo cho phần Flashloan, bạn xem tại địa chỉ: MCL-FlashloanDemo. Trong bài viết này tôi không tập trung vào việc hướng dẫn bạn build smart contract thế nào, mà yêu cầu bạn phải có kiến thức này rùi.

Ở đây chúng ta quan tâm tới tệp demo.sol, là contract demo sử dụng flashloan. Chúng ta có 1 số chú ý:

  • Hàm khởi tạo chúng ta cần truyền 2 địa chỉ:
    • Tham số addressesProvider: Địa chỉ LendingPool để vay flashloan
    • Tham số defi: Là địa chỉ của ứng dụng chúng ta sử dụng để làm các nghiệp vụ sau khi vay. Ở đây có contract defi.sol là contract đơn giản để demo với nghiệp vụ nạp và rút tiền. Thực tế để kiếm được tiền, nghiệp vụ này phức tạp hơn nhiều.
  • Hàm flashloanBnb() là hàm người dùng gọi để bắt đầu thực hiện nghiệp vụ Flashloan => Sau khi thực hiện vay, hàm executeOperation sẽ được gọi để thực hiện nghiệp vụ.

Bạn có thể sử dụng Truffle hoặc Remix IDE để triển khai. Nói chung sử dụng Remix IDE là đơn giản và tiện nhất. Các bước thực hiện như sau:

Các bạn có thể không phải triển khai lại mà test trực tiếp trên contract Demo: 0xc0dfdce817c6d4919f4f9a198d312a14221733ee => Nhớ đẩy 1 ít BNB làm phí trả cho pool nếu contract không có BNB nhé.

Triển khai Flashloand sử dụng CREAM Finance Flashloan

CREAM Finance là nền tảng DEFI hỗ trợ trên nhiều network khác nhau trong đó có Binance Smart Chain (BSC). Cream Finance có hỗ trợ FlashLoan trên BSC, chi tiết xem tại: https://docs.cream.finance/v/binance-smart-chain/flash-loanshttps://docs.cream.finance/flash-loans

CREAM Finance cũng có ví dụ về sử dụng Flashloan, chi tiết xem tại địa chỉ: https://github.com/CreamFi/flashloan-playground

Danh sách pool hỗ trợ Flashloan bạn xem tại: https://docs.cream.finance/v/binance-smart-chain/lending/lending-contract-address

Do không hỗ trợ testnet nên khá khó để thử nghiệm Flashloan trên Testnet BSC.

Triển khai Flashswap sử dụng Pancake

Sử dụng Flashswap trên Pancake theo cách thông thường

PancakeSwap không hỗ tường minh Flashloan, nhưng trong code của PancakePair.sol của PancakeSwap lại tiềm ẩn khả năng giúp user có thể vay nhanh được. Khi vay nhanh từ các nền tảng AMM như PancakeSwap này người ta gọi là Flashswap:

Đoạn code giúp khai thác Flashloan trên Pancake
Đoạn code giúp khai thác Flashloan trên Pancake

Bạn để ý đoạn code trên sẽ thấy:

  • (1) PancakePair chuyển token0 token1 sang địa chỉ to.
  • (2) PancakePair gọi hàm pancakeCall(…) của địa chỉ to nếu điều kiện data.length>0 được thỏa mãn.

Như vậy nếu to là một contract có cài đặt hàm pancakeCall(…) thì trong hàm này bạn có thể thực hiện các nghiệp vụ gì đó trước khi trả lại cho token0 token1 cho PancakePair kèm theo phí 0.3% (Tùy phiên bản) để đảm bảo phần check phía sau không bị lỗi => Đây chính là kẽ hở giúp bạn thực hiện Flashloan (Chính xác hơn gọi là Flashswap). Đặc biệt bạn có thể vay nhanh đồng thời token0, token1 hoặc chỉ vay nhanh 1 trong 2 token.

Bây giờ ta cài đặt đoạn mã nhỏ thực hiện vay nhanh WBNB trên môi trường testnet. Một số tham số thiết lập cố định trong Smart Contract để cho code đơn giản.

Đầu tiên ta tạo tệp Defi.sol với nghiệp vụ đơn giản:

pragma solidity ^0.5.0;

interface IERC20 {
     function totalSupply() external view returns (uint256);
     function balanceOf(address account) external view returns (uint256);
     function transfer(address recipient, uint256 amount) external returns (bool);
     function allowance(address owner, address spender) external view returns (uint256);
     function approve(address spender, uint256 amount) external returns (bool);
     function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

     event Transfer(address indexed from, address indexed to, uint256 value);
     event Approval(address indexed owner, address indexed spender, uint256 value);
}

interface IDefi {
    function withdraw(uint256 _amount) external ;
}

contract Defi is IDefi {
     IERC20 public wbnb = IERC20(0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd);

     event WithdrawBnbEvent(address indexed _user, uint256 _amount);

     function withdraw(uint256 _amount) external {
          wbnb.transfer(msg.sender, _amount);
          emit WithdrawBnbEvent(msg.sender, _amount);
     }
}

Sau triển khai ta được địa chỉ của Defi: 0xa16cBa9874Eb8DA206CB588638da8A580e019754

Giờ tạo tệp Flashswap.sol được thực hiện vay và gọi tới contract Defi để thực hiện nghiệp vụ:

// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.6.12;

interface Ipancake {
   function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}

interface IBEP20 {
    function mint(address account, uint256 amount) external;
    function burn(address account, uint256 amount) external;
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );
}

interface IDefi {
    function withdraw(uint256 _amount) external ;
}

contract FlashSwap {

    address public owner;
    Ipancake public ipancake = Ipancake(0xF855E52ecc8b3b795Ac289f85F6Fd7A99883492b);
    IBEP20 public wbnb = IBEP20(0xae13d989daC2f0dEbFf460aC112a837C89BAa7cd);
    uint256 public fee = 3;
    IDefi public app = IDefi(0xa16cBa9874Eb8DA206CB588638da8A580e019754);
    
    modifier onlyOwner() {
        require(msg.sender == owner, '1');
        _;
    }
    
    constructor() public {
        owner = msg.sender;
    }
    
    function runApprove() public onlyOwner {
        wbnb.approve(address(ipancake), uint256(-1));
    }
    
    function adminOut() public onlyOwner {
        wbnb.transfer(owner, wbnb.balanceOf(address(this)));
    }
    
    function flashloan(uint amount) public onlyOwner {
       ipancake.swap(0, amount, address(this), abi.encodePacked(uint256(-1)));
    }
    
    function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) public {
        // Do business actions here to use WBNB
        wbnb.transfer(address(app), amount1);
        app.withdraw(amount1);

        // Send back WBNB to PancakePair
        uint256 amount = amount1 + amount1*fee/1000;
        wbnb.transfer(address(ipancake), amount);
    }
}

Sau khi triển khai ta được địa chỉ của Flashswap là: 0x973c56E134631fe6eE04b5722AfA84E43a1C3D34

Bây giờ ta chạy thử, ta thực hiện các bước sau:

  • B1: Chạy hàm runApprove() lần đầu tiên. Chỉ cần gọi 1 lần duy nhất.
  • B2: Send ít nhất 0.03 WBNB vào contract để bù phần phí.
  • B3: Ta thực hiện vay 10 WBNB bằng cách gọi hàm flashloan(10000000000000000000)
  • B4: Kiểm tra giao dịch. Đây là giao dịch mà tôi thử: 0x35fdcf367e93077d8c8b42c2e47625fc5d291eae85a58e8526e7de2005d37c99

Sử dụng Flashswap trên Pancake theo cách đặc biệt

Theo như ví dụ trên chúng ta đang sử dụng theo cách vay token nào thì trả lại token đó, vay cả hai token thì trả lại cả hai token. Nhưng với Flashswap thì bạn có thể vay 1 token và trả lại bằng token còn lại. Đây là tính năng đặc biệt được nhiều bot Arbitrage tận dụng để giảm vốn sử dụng.

Ví dụ bạn tìm được một cơ hội Arbitrage trên PancakeSwap với 10 WBNB và bạn thư được 10.5 WBNB theo path như sau: WBNB-ETH-BUSD-USDT-WBNB. Như vậy để thực hiện được swap này và kiếm được lợi nhuận 0.5 BNB bạn phải bỏ vốn 10 WBNB. Trường hợp nếu không bỏ vốn thì ta phải vay WBNB từ nơi khác và phải mất thêm phí vay, đồng nghĩa với việc lợi nhuận giảm. Đây chính là trường hợp mà Flashswap tỏ ra lợi thế. Chúng ta tận dụng Flashswap trong trường hợp này như sau:

  • B1: Thực hiện vay ETH ở pair đầu tiên WBNB-ETH
    Giả sử theo tính toán của bạn thì pair WBNB-ETH thì 10 WBNB khi swap bạn nhận được 1.42 ETH
    => Ở pair đầu tiên này ta sẽ không thực hiện swap theo cách thông thường mà ta sẽ thực hiện vay 1.42 ETH ở pair WBNB-ETH
  • B2: Thực hiện swap bình thường ở các cặp tiếp theo
    Sau khi có 1.42 ETH ta thực hiện swap bình thường theo path ETH-BUSD-USDT-WBNB và ta sẽ nhận được 10.5 WBNB.
  • B3: Trả lại WBNB cho pair đầu tiên
    Bây giờ ta trả lại 10 WBNB cho pair đầu tiên WBNB-ETH
    => Đến đây chúng ta đã hoàn thành lệnh mà không cần phải có 10 WBNB vốn và chúng ta cũng không mất thêm phí gì cả.
  • B4: Lợi nhuận còn lại ta có thể để trực tiếp trên contract hoặc rút sang ví khác cho an toàn.

Bạn có thể mở rộng ra ứng dụng cho các trường hợp khác.

Bây giờ tôi sẽ thử chạy một ví dụ trên BSC Testnet để thực hiện mô phỏng quá trình vay 1 token và trả lại bằng 1 token khác.

Đầu tiên ta viết FlashSwap02.sol với nội dung như sau:

// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.6.12;

interface IBEP20 {
    function mint(address account, uint256 amount) external;
    function burn(address account, uint256 amount) external;
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );
}

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 IPancakeFactory {
    function getPair(address tokenA, address tokenB) external view returns (address pair);
}

interface IPancakeRouter {
    function factory() external pure returns (address);
    function swapExactTokensForTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external returns (uint[] memory amounts);

    function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts);
}

contract FlashSwap02 {
    address public owner;
    IPancakeRouter public router = IPancakeRouter(0x9Ac64Cc6e4415144C455BD8E4837Fea55603e5c3);

    modifier onlyOwner() {
        require(msg.sender == owner, 'Not only owner!');
        _;
    }
    
    constructor() public {
        owner = msg.sender;
    }
    
    function approve(address token) public onlyOwner {
        IBEP20(token).approve(address(router), uint256(-1));
    }
    
    function withdraw(address token) public onlyOwner {
        IBEP20(token).transfer(owner, IBEP20(token).balanceOf(address(this)));
    }
    
    function flashSwap(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) public onlyOwner {
        require(path.length>2, "The path length must be greater than 2!");
        require(path[0]==path[path.length-1], "Input and output tokens are same!");

        IPancakePair pair = IPancakePair(IPancakeFactory(router.factory()).getPair(path[0], path[1]));
        address[] memory newPath = getNewPath(path, 0, 1);
        uint amountOut = router.getAmountsOut(amountIn, newPath)[1];
        if (amountOut>0) amountOut = amountOut - 1;
        bytes memory data = abi.encode(amountIn, amountOutMin, deadline, pair, to, path);
        if (pair.token0()==path[0]) {
            pair.swap(0, amountOut, address(this), data);
        } else {
            pair.swap(amountOut, 0, address(this), data);
        }
    }
    
    function pancakeCall(address sender, uint amount0, uint amount1, bytes calldata data) public {
        // Decode data
        (uint amountIn, uint amountOutMin, uint deadline, address pair, address to, address[] memory path) = abi.decode(data, (uint, uint, uint, address, address, address[]));

        // Swap
        address[] memory newPath = getNewPath(path, 1, path.length-1);
        if (IPancakePair(pair).token0()==path[0]) {
            router.swapExactTokensForTokens(amount1, amountOutMin, newPath, address(this), deadline);
        } else {
            router.swapExactTokensForTokens(amount0, amountOutMin, newPath, address(this), deadline);
        }
        
        // Send back tokenIn to pair
        IBEP20(path[0]).transfer(pair, amountIn);

        // Send remaining asset
        IBEP20 tokenOut = IBEP20(path[path.length - 1]);
        tokenOut.transfer(to, tokenOut.balanceOf(address(this)));
    }

    function getNewPath(address[] memory path, uint fromIndex, uint toIndex) internal pure returns (address[] memory newPath) {
        newPath = new address[](toIndex - fromIndex + 1);
        for (uint idx=fromIndex; idx<=toIndex; idx++) {
            newPath[idx-fromIndex] = path[idx];
        }
    }
}

Vì thực tế rất ít khi có cơ hội Arbitrage thực sự nên amountOut thường sẽ nhỏ hơn amountIn, do đó tôi sửa code một chút giúp bù lại phần tài sản thiếu hụt để đảm bảo giao dịch thành công. Tôi đã triển khai contract này trên Remix ta được contract có địa chỉ: 0xd1B0632a7F2190b68Ff49b0668a70D1D44c3F816

Bây giờ ta thử thực hiện swap với path là WBNB-ETH-BUSD-USDT-WBNB, theo địa chỉ sẽ là:
[“0xae13d989dac2f0debff460ac112a837c89baa7cd”, “0x8babbb98678facc7342735486c851abd7a0d17ca”, “0x78867BbEeF44f2326bF8DDd1941a4439382EF2A7”, “0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684”, “0xae13d989dac2f0debff460ac112a837c89baa7cd”]

Theo path này với 0.1 WBNB (amountIn) ta sẽ thu được 0.0971 WBNB (amountOut), để thực hiện swap này đúng ra ta phải có 0.1 WBNB có sẵn trong contract, nhưng theo cách này ta chỉ cần có 0.003 WBNB là đủ, đây là lượng để bù lại do amountOut nhỏ hơn amountIn. Bây giờ ta thử thực hiện flashSwap() theo các bước sau:

  • B1: Gọi hàm approve(0x8babbb98678facc7342735486c851abd7a0d17ca) để cho phép PancakeRouter sử dụng ETH trong contract.
  • B2: Gửi 0.003 WBNB tới địa chỉ contract 0xd1B0632a7F2190b68Ff49b0668a70D1D44c3F816 để bù phần chênh lệch khi swap.
  • B3: Gọi hàm flashSwap() để thực hiện lệnh swap thông qua cơ chế vay:
    flashSwap(100000000000000000, 1, [“0xae13d989dac2f0debff460ac112a837c89baa7cd”, “0x8babbb98678facc7342735486c851abd7a0d17ca”, “0x78867BbEeF44f2326bF8DDd1941a4439382EF2A7”, “0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684”, “0xae13d989dac2f0debff460ac112a837c89baa7cd”], 0xE42C439D836708f43F77a65C198A2d7f55b3f3f4, 1687115633)
    Ta nhận được giao dịch thành công: 0x75fa89f1e9ba24fbd51111997d34eed0e7bb3bf6c670d86f8fd90ac28e747f6f

Phân tích giao dịch flashSwap trên BscScan Testnet, mục Tokens Transferred bạn sẽ thấy:

  • B1: Contract vay 0.024885936984742241 ETH từ pair WBNB-ETH
  • B2: Contract đổi từ ETH được 39.107066320319795487 BUSD trên pair ETH-BUSD
  • B3: Tiếp tục đổi BUSD được 39.875176744520586697 USDT
  • B4: Tiếp tục đổi USDT được 0.09710666639028922 WBNB chuyển về contract. Cộng thêm 0.003 WBNB ta chuyển trước vào contract trước đó thì lúc này contract có trên 0.1 WBNB.
  • B5: Gửi trả lại 0.1 WBNB tới pair WBNB-ETH đã vay trước đó. Đúng như ta mong muốn, ban đầu ta vay ETH, bây giờ ta trả lại bằng WBNB. Thực chất của quá trình này là ta swap 0.1 WBNB thành 0.024885936984742241 ETH, nhưng với cách làm này thì ta không cần phải có 0.1 WBNB => Đúng kiểu tay không bắt giặc.
  • B6: Cuối cùng để an toàn cho tài sản thì lợi nhuận ta chuyển tới 1 ví khác.
Sử dụng Flashswap trên Binance Smart Chain BSC Testnet
Sử dụng Flashswap trên Binance Smart Chain BSC Testnet

Các ứng dụng của Flashloan

Như vậy việc sử dụng Flashloan không còn là vấn đề nữa. Vấn đề bây giờ là làm sao sử dụng vốn từ Flashloan để kiếm lợi nhuận cho bạn.

Bạn có thể sử dụng Flashloan để:

  • Arbitrage (Kinh doanh chênh lệch giá): Đây chính là phần các bạn nên đầu tư trí tuệ và thời gian để kiếm được lợi nhuận.
    • Locational Arbitrage: Mua token từ 1 sàn DEX và bán nó ở một sàn khác để ăn chênh lệch giá.
    • Triangular Arbitrage: Giao dịch liên quan đến ba token để ăn chênh lệch giá.
  • Hoán đổi tài sản thế chấp: Tức là bạn đã thế chấp đồng A để vay đồng B rùi, và đồng B bạn đã sử dụng vào việc nào đó rồi. Nhưng giờ bạn muốn đổi đồng A rủi ro giá cao sang đồng tiền C khác an toàn hơn, trong khi bạn không còn đồng B => Flashloan sẽ rất hữu ích.
  • Thanh lý tài sản: Bạn đã thế chấp đồng A để vay đồng B, và bạn mang đồng B đi đầu tư và đã mất hết. Bạn muốn rút phần tiền còn lại ra khỏi hệ thống, nhưng bạn ko có tiền để mua đồng B nữa => Flashloan sẽ hữu ích trong trường hợp này.
  • Khai thác lỗ hổng nghiệp vụ: Từ khi ra đời Flashloan, các hacker đã khai thác tối đa để kiếm tiền bất hợp pháp từ các lỗ hổng nghiệp vụ hay chủ động thao túng tỉ giá. Vì thế có rất nhiều người trì trích về Flashloan và là nguyên nhân gây sụp đổ hệ thống DeFi. Mình đứng trên góc nhìn khác thì mình nhờ Flashloan mà lỗ hổng nghiệp vụ được phát hiện ra sớm, và các ứng dụng DeFi sẽ sớm sửa lỗi và hoàn thiện hơn. Bình thường có thể phải mất 10 năm để các ứng dụng DeFi đi vào hoạt động ổn đình, thì nhờ có Flashloan có thể chỉ cần 2-3 năm mà thôi.

Có thể bạn quan tâm: Hướng dẫn hack hợp đồng thông minh Smart Contract với Ethernaut CTF – Cách học bảo mật Smart Contract hiệu quả nhất

Nguồn: Tổng hợp

Bài viết này có hữu ích với bạn?

Kích vào một biểu tượng ngôi sao để đánh giá bài viết!

Xếp hạng trung bình 5 / 5. Số phiếu: 88

Bài viết chưa có đánh giá! Hãy là người đầu tiên đánh giá bài viết này.

4 Bình luận

  1. Luckyyyy

    defi: Là địa chỉ của ứng dụng chúng ta sử dụng để làm các nghiệp vụ sau khi vay. Ở đây có contract defi.sol là contract đơn giản để demo với nghiệp vụ nạp và rút tiền. Thực tế để kiếm được tiền, nghiệp vụ này phức tạp hơn nhiều. Cho mình hỏi rõ là địa chỉ này lúc mình tạo metamask hay là hợp đồng thông minh vây bạn ?
    Hợp đồng thông minh tạo token bình thường có đc không ? Mình không hiểu lắm

    • – B1. Đầu tiên bạn phải triển khai smart contract “defi.sol” trước nhé => Bạn sẽ được 1 địa chỉ contract.
      – B2. Sau đó triển khai “demo.sol” => Tham số _defi chính là địa chỉ trong B1

  2. Luckyyyy

    Ad ơi vậy là phải tìm cod smartcontract bsc ? Ad có thể chia sẽ link cho mình với ạ ? Mình tìm thì chỉ cod để tạo token ?

Trả lời

Giao diện bởi Anders Norén