Post

Simple FlashLoan, Based on Aave V3

개요

플래시론은 DeFi에서의 무담보 대출이다. 사용자는 플래시론을 통해서 원하는 ERC 20 토큰을 대출받아 사용할 수 있으며 이렇게 대출받은 대금은 한 트랜잭션 내에서 상환까지 모두 이루어져야 한다. 만약 대출금 상환이 제대로 이루어지지 않는다면 플래시론을 시작으로 생성된 트랜잭션은 없던 것으로 간주되며 Pool로 돈이 상환되게 된다.

사용자가 유동성 풀에 원하는 토큰에 대해서 대출을 요청하면 풀에서 대출이 실행되며, 대출받은 토큰을 이용해서 우리가 원하는 작업을 수행할 수 있다. 원하는 작업을 수행한 이후에는 풀로 대출금과 프리미엄을 상환하도록 승인하고, 대출금 + 프리미엄을 풀로 다시 상환하며 하나의 트랜잭션이 마무리된다.

Flashloan.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.10;

import {FlashLoanSimpleReceiverBase} from "@aave/core-v3/contracts/flashloan/base/FlashLoanSimpleReceiverBase.sol";
import {IPoolAddressesProvider} from "@aave/core-v3/contracts/interfaces/IPoolAddressesProvider.sol";
import {IERC20} from "@aave/core-v3/contracts/dependencies/openzeppelin/contracts/IERC20.sol";
import "hardhat/console.sol";

contract FlashLoan is FlashLoanSimpleReceiverBase {
    address payable public owner;
    
    constructor(address _addressProvider)
        FlashLoanSimpleReceiverBase(IPoolAddressesProvider(_addressProvider))
    {
        owner = payable(msg.sender);
    }

    function executeOperation(
        address asset,
        uint256 amount,
        uint256 premium,
        address initiator,
        bytes calldata params
    ) external override returns (bool) {
        uint256 amountOwed = amount + premium;
        IERC20(asset).approve(address(POOL), amountOwed);

        return true;
    }

    function requestFlashLoan(address _token, uint256 _amount) public onlyOwner {
        address receiverAddress = address(this); 
        address asset = _token; 
        uint256 amount = _amount; 
        bytes memory params = "";
        uint16 referralCode = 0; 

        POOL.flashLoanSimple(
            receiverAddress,
            asset,
            amount,
            params,
            referralCode
        );
    }

    function getBalance(address _tokenAddress) external view returns (uint256) {
        return IERC20(_tokenAddress).balanceOf(address(this));
    }

    function withdraw(address _tokenAddress) external onlyOwner {
        IERC20 token = IERC20(_tokenAddress);                           // Create an instance of the token contract.
        token.transfer(msg.sender, token.balanceOf(address(this)));     // Transfer the token balance to the contract owner.
    }

    modifier onlyOwner() {
        require(
            msg.sender == owner,
            "You are not the owner!"
        );
        _;
    }

    receive() external payable {}
}

위 컨트랙트는 Aave 프로토콜의 simple 함수를 활용하여 플래시론을 실행할 수 있는 컨트랙트이다. 대출자는 requestFlashLoan() 함수를 호출해서 대출을 요청하면 POOL.flashLoanSimple() 함수를 통해서 풀에 대출을 요청한다.

lib/aave-v3-core/contracts/protocol/pool/Pool.sol:flashLoanSimple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  function flashLoanSimple(
    address receiverAddress,
    address asset,
    uint256 amount,
    bytes calldata params,
    uint16 referralCode
  ) public virtual override {
    DataTypes.FlashloanSimpleParams memory flashParams = DataTypes.FlashloanSimpleParams({
      receiverAddress: receiverAddress,
      asset: asset,
      amount: amount,
      params: params,
      referralCode: referralCode,
      flashLoanPremiumToProtocol: _flashLoanPremiumToProtocol,
      flashLoanPremiumTotal: _flashLoanPremiumTotal
    });
    FlashLoanLogic.executeFlashLoanSimple(_reserves[asset], flashParams);
  }

flashLoanSimple() 함수를 보면 FlashloanSimpleParams 구조체로 flashParams 변수를 메모리로 저장하고, _reserves 매핑에 저장된 데이터와 flashParams 값을 executeFlashLoanSimple() 함수르 전달한다.

lib/aave-v3-core/contracts/protocol/libraries/logic/FlashLoanLogic.sol:executeFlashLoanSimple()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  function executeFlashLoanSimple(
    DataTypes.ReserveData storage reserve,
    DataTypes.FlashloanSimpleParams memory params
  ) external {
    // The usual action flow (cache -> updateState -> validation -> changeState -> updateRates)
    // is altered to (validation -> user payload -> cache -> updateState -> changeState -> updateRates) for flashloans.
    // This is done to protect against reentrance and rate manipulation within the user specified payload.

    ValidationLogic.validateFlashloanSimple(reserve);

    IFlashLoanSimpleReceiver receiver = IFlashLoanSimpleReceiver(params.receiverAddress);
    uint256 totalPremium = params.amount.percentMul(params.flashLoanPremiumTotal);
    IAToken(reserve.aTokenAddress).transferUnderlyingTo(params.receiverAddress, params.amount);

    require(
      receiver.executeOperation(
        params.asset,
        params.amount,
        totalPremium,
        msg.sender,
        params.params
      ),
      Errors.INVALID_FLASHLOAN_EXECUTOR_RETURN
    );

    _handleFlashLoanRepayment(
      reserve,
      DataTypes.FlashLoanRepaymentParams({
        asset: params.asset,
        receiverAddress: params.receiverAddress,
        amount: params.amount,
        totalPremium: totalPremium,
        flashLoanPremiumToProtocol: params.flashLoanPremiumToProtocol,
        referralCode: params.referralCode
      })
    );
  }

executeFlashLoanSimple() 함수를 보면 제일 먼저 validateFlashloanSimple() 함수를 이용하여 대출이 가능한 상태인지 확인한다. 이후 대출자의 IFlashLoanSimpleReceiver 인터페이스를 호출하는 것을 볼 수 있다. 대출을 하는 사용자는 컨트랙트 내에 꼭 IFlashLoanSimpleReceiver 인터페이스를 포함해야 한다. 이후 flashLoanPremiumTotal를 기준으로 프리미엄을 계산하고, 대출자에게 대금을 전송해 준다.

이제 대출자 입장에서는 대출금을 받은 상태이며, 대출자의 executeOperation() 함수가 호출된다. 이 함수 내에 대출금을 이용해 원하는 작업의 코드를 실행시킬 수 있다.

1
2
        uint256 amountOwed = amount + premium;
        IERC20(asset).approve(address(POOL), amountOwed);

여기서 이 함수를 호출할 때 미리 계산된 프리미엄 값도 함께 넘겨주고 있는데, executeOperation() 함수 내에서는 대출금 + 프리미엄 값을 위와 같이 승인해 주어야 한다. 이렇게 상환할 대출금까지 승인이 되었다면 _handleFlashLoanRepayment() 함수를 호출해서 상환을 시작한다.

lib/aave-v3-core/contracts/protocol/libraries/logic/FlashLoanLogic.sol:_handleFlashLoanRepayment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
  function _handleFlashLoanRepayment(
    DataTypes.ReserveData storage reserve,
    DataTypes.FlashLoanRepaymentParams memory params
  ) internal {
    uint256 premiumToProtocol = params.totalPremium.percentMul(params.flashLoanPremiumToProtocol);
    uint256 premiumToLP = params.totalPremium - premiumToProtocol;
    uint256 amountPlusPremium = params.amount + params.totalPremium;

    DataTypes.ReserveCache memory reserveCache = reserve.cache();
    reserve.updateState(reserveCache);
    reserveCache.nextLiquidityIndex = reserve.cumulateToLiquidityIndex(
      IERC20(reserveCache.aTokenAddress).totalSupply() +
        uint256(reserve.accruedToTreasury).rayMul(reserveCache.nextLiquidityIndex),
      premiumToLP
    );

    reserve.accruedToTreasury += premiumToProtocol
      .rayDiv(reserveCache.nextLiquidityIndex)
      .toUint128();

    reserve.updateInterestRates(reserveCache, params.asset, amountPlusPremium, 0);

    IERC20(params.asset).safeTransferFrom(
      params.receiverAddress,
      reserveCache.aTokenAddress,
      amountPlusPremium
    );

    IAToken(reserveCache.aTokenAddress).handleRepayment(
      params.receiverAddress,
      params.receiverAddress,
      amountPlusPremium
    );

    emit FlashLoan(
      params.receiverAddress,
      msg.sender,
      params.asset,
      params.amount,
      DataTypes.InterestRateMode(0),
      params.totalPremium,
      params.referralCode
    );
  }

_handleFlashLoanRepayment() 함수를 보면 제일 먼저 totalPremium 값을 프로토콜에 줄 수수료와 LP에게 줄 수수료를 나누어서 저장을 하고, 유동성 풀 상태, 수수료, 이자율을 업데이트하고, 플래시론을 받은 컨트랙트로부터 상환할 금액을 유동성 풀로 전송을 하고 있다. 이후 handleRepayment() 함수를 통해서 대출을 모두 상환했다는 내용에 대한 기록을 하며 마무리를 짓는다.

플래시론 트랜잭션을 보면 위와 같이 하나의 트랜잭션 내에서 1,000 USDC 대출 이후에 바로 상환까지 이루어지는 것을 확인할 수 있다. 상환된 잔액을 보면 .5 USDC가 더 추가되었는데 이는 프리미엄 값이 포함되었기 때문이다.

This post is licensed under CC BY 4.0 by the author.

© Some rights reserved.

Using the Chirpy theme for Jekyll.