Flash loan gas cost comparison

1 Overview

Flash loans can be used for various business operations like
  • arbitrage trades
  • self-hedging
  • self-liquidation
  • collateral swaps
  • debt refinancing (interest rate swap, currency swap)
  • other
All operations have an operational cost (transaction gas) that is made of two parts
  • borrowing and returning a flash loan
  • performing business operations
Business operations cost is dictated by the operational cost of actions on different decentralized exchanges or DeFi platforms. The operational cost of borrowing and returning a flash loan depends on the flash loan provider. In this document, we compare the operational costs of borrowing and returning flash loans via different platforms.

2 Operational cost of borrowing and returning a flash loan

We present the results of an objective comparison of flash loan costs in terms of gas. To date, we compared the following platforms
Platform
Transaction cost [Gas]
Gas cost compared to Equalizer
Gas profile
Transaction
Code
Equalizer
111752
100%
Tenderly
Tx
See Etherscan or Appendix A.1
AAVE
204493
183%
Tenderly
Tx
See Etherscan or Appendix A.2
dXdY
225223
200%
Tenderly
Tx
See Etherscan or Appendix A.3
All tests are performed on Ethereum testnets.

Appendix

A.1 Equalizer flash loan receiver code

1
// Equalizer
2
/**
3
Flash borrower example code
4
*/
5
contract FlashBorrowerExample is IERC3156FlashBorrower {
6
uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
7
8
// @dev ERC-3156 Flash loan callback
9
function onFlashLoan(
10
address initiator,
11
address token,
12
uint256 amount,
13
uint256 fee,
14
bytes calldata data
15
) external override returns (bytes32) {
16
// Set the allowance to payback the flash loan
17
IERC20(token).approve(msg.sender, MAX_INT);
18
19
// This contract now has the funds requested.
20
// Your logic goes here.
21
22
// At the end of your logic above, this contract owes
23
// the flashloaned amounts + premiums/fee.
24
// Therefore ensure your contract has enough to repay
25
// these amounts.
26
27
// Return success to the lender, he will transfer get the funds back if allowance is set accordingly
28
return keccak256('ERC3156FlashBorrower.onFlashLoan');
29
}
30
}
31
32
Copied!

A.2 AAVE flash loan receiver code

1
// AAVE
2
/**
3
Flash borrower example code
4
*/
5
contract Flashloan is FlashLoanReceiverBase {
6
7
constructor(address _addressProvider) FlashLoanReceiverBase(_addressProvider) public {}
8
9
/**
10
This function is called after your contract has received the flash loaned amount
11
*/
12
function executeOperation(
13
address _reserve,
14
uint256 _amount,
15
uint256 _fee,
16
bytes calldata _params
17
)
18
external
19
override
20
{
21
require(_amount <= getBalanceInternal(address(this), _reserve), "Invalid balance, was the flashLoan successful?");
22
23
// This contract now has the funds requested.
24
// Your logic goes here.
25
26
// At the end of your logic above, this contract owes
27
// the flashloaned amounts + premiums/fee.
28
// Therefore ensure your contract has enough to repay
29
// these amounts.
30
31
uint totalDebt = _amount.add(_fee);
32
transferFundsBackToPoolInternal(_reserve, totalDebt);
33
}
34
35
/**
36
Flash loan 1000000000000000000 wei (1 ether) worth of _asset
37
*/
38
function flashloan(address _asset) public onlyOwner {
39
bytes memory data = "";
40
uint amount = 1 ether;
41
42
ILendingPool lendingPool = ILendingPool(addressesProvider.getLendingPool());
43
lendingPool.flashLoan(address(this), _asset, amount, data);
44
}
45
}
Copied!

A.3 dXdY flash loan receiver

1
// dXdY
2
/**
3
Flash borrower example code
4
*/
5
function flashLoan(uint loanAmount) external {
6
/*
7
The flash loan functionality in dydx is predicated by their "operate" function,
8
which takes a list of operations to execute, and defers validating the state of
9
things until it's done executing them.
10
11
We thus create three operations, a Withdraw (which loans us the funds), a Call
12
(which invokes the callFunction method on this contract), and a Deposit (which
13
repays the loan, plus the 2 wei fee), and pass them all to "operate".
14
15
Note that the Deposit operation will invoke the transferFrom to pay the loan
16
(or whatever amount it was initialised with) back to itself, there is no need
17
to pay it back explicitly.
18
19
The loan must be given as an ERC-20 token, so WETH is used instead of ETH. Other
20
currencies (DAI, USDC) are also available, their index can be looked up by
21
calling getMarketTokenAddress on the solo margin contract, and set as the
22
primaryMarketId in the Withdraw and Deposit definitions.
23
*/
24
25
Actions.ActionArgs[] memory operations = new Actions.ActionArgs[](3);
26
27
operations[0] = Actions.ActionArgs({
28
actionType: Actions.ActionType.Withdraw,
29
accountId: 0,
30
amount: Types.AssetAmount({
31
sign: false,
32
denomination: Types.AssetDenomination.Wei,
33
ref: Types.AssetReference.Delta,
34
value: loanAmount // Amount to borrow
35
}),
36
primaryMarketId: 0, // WETH
37
secondaryMarketId: 0,
38
otherAddress: address(this),
39
otherAccountId: 0,
40
data: ""
41
});
42
43
operations[1] = Actions.ActionArgs({
44
actionType: Actions.ActionType.Call,
45
accountId: 0,
46
amount: Types.AssetAmount({
47
sign: false,
48
denomination: Types.AssetDenomination.Wei,
49
ref: Types.AssetReference.Delta,
50
value: 0
51
}),
52
primaryMarketId: 0,
53
secondaryMarketId: 0,
54
otherAddress: address(this),
55
otherAccountId: 0,
56
data: abi.encode(
57
// Replace or add any additional variables that you want
58
// to be available to the receiver function
59
msg.sender,
60
loanAmount
61
)
62
});
63
64
operations[2] = Actions.ActionArgs({
65
actionType: Actions.ActionType.Deposit,
66
accountId: 0,
67
amount: Types.AssetAmount({
68
sign: true,
69
denomination: Types.AssetDenomination.Wei,
70
ref: Types.AssetReference.Delta,
71
value: loanAmount + 2 // Repayment amount with 2 wei fee
72
}),
73
primaryMarketId: 0, // WETH
74
secondaryMarketId: 0,
75
otherAddress: address(this),
76
otherAccountId: 0,
77
data: ""
78
});
79
80
Account.Info[] memory accountInfos = new Account.Info[](1);
81
accountInfos[0] = Account.Info({owner: address(this), number: 1});
82
83
soloMargin.operate(accountInfos, operations);
84
}
85
86
// This is the function called by dydx after giving us the loan
87
function callFunction(address sender, Account.Info memory accountInfo, bytes memory data) external override {
88
// Decode the passed variables from the data object
89
(
90
// This must match the variables defined in the Call object above
91
address payable actualSender,
92
uint loanAmount
93
) = abi.decode(data, (
94
address, uint
95
));
96
97
// We now have a WETH balance of loanAmount. The logic for what we
98
// want to do with it goes here. The code below is just there in case
99
// it's useful.
100
101
// It can be useful for debugging to have a verbose error message when
102
// the loan can't be paid, since dydx doesn't provide one
103
require(WETH.balanceOf(address(this)) > loanAmount + 2, "CANNOT REPAY LOAN");
104
// Leave just enough WETH to pay back the loan, and convert the rest to ETH
105
// WETH.withdraw(WETH.balanceOf(address(this)) - loanAmount - 2);
106
// Send any profit in ETH to the account that invoked this transaction
107
// actualSender.transfer(address(this).balance);
108
}
109
Copied!