Flash Loan on Uniswap v3

Aaron Li
Cryptocurrency Scripts
7 min readApr 26, 2024

--

Most powerful funding source in DeFi ecosystem

What is Flash Loan?

A flash loan is a type of uncollateralized loan in the DeFi ecosystem that allows users borrow assets with no upfront collateral, as long as the borrowed assets are paid back within the same blockchain transaction.

Image generated by AI

With large sums of tokens, what would one do? Here are some examples:

  • Arbitrage: The borrower purchases crypto assets at a lower price and sells them at a higher price. The transcations can happen in the same exchange or in different exchanges.
  • Crypto price attack: Hackers manipulate the price of a cryptocurrency asset on one exchange before quickly selling it on another.
  • Collateral swaps: The borrower uses the loan to close an existing loan before immediately taking out a new loan with better terms.

DeFi platforms that offer Flash Loans

Many DeFi protocols offer flash loans:

  • Aave: Aave was one of the first DeFi protocols to offer flash loans. It allows users to borrow up to 25% of the liquidity pool without any collateral.
  • dYdX: dYdX is another DeFi protocol that offers flash loans. It allows users to borrow up to 50% of their account balance without any collateral.
  • Uniswap: Uniswap is another popular DeFi protocol that offers flash loans. It allows users to borrow up to 0.05% of the total liquidity pool without any collateral.
  • PancakeSwap: PancakeSwap is a fork of Uniswap, which uses the Binance Smart Chain (BSC) to run its network as opposed to Ethereum original blaockchain.

This article will focus on how to implment flash loans on Uniswap v3 using Solidity in Hardhat. In fact, due to the kinship between Uniswap and PancakeSwap, with no much changes, one can easily get the code working on PancakeSwap as well.

What happens hehind the scene?

Simplified sequence diagram of Flash Loan on Uniswap v3 (Factory and borrowed token contracts are removed from this diagram for simplicity)

Getting the pool key (getPoolKey)

Think of pool as a smart contract that provides liquidity for a key pair. In an example of USDC_WETH on Ethereum Mainnet (see link), combination of token0, token1 and fee will be used as the key to look up the pool (using the factory) for flash loan application.

Under [Read Contract] tab, value of fee, token0, token1 can be found
    function getPoolKey(
address tokenA,
address tokenB,
uint24 fee
) internal pure returns (PoolKey memory) {
if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA);
return PoolKey({token0: tokenA, token1: tokenB, fee: fee});
}

Uniswap v3 introduces multiple pools for each token pair, each with a different swapping fee. Liquidity providers initially created pools at three fee levels: 0.05%, 0.30%, and 1%. Another fee level 0.01% later added in November 2021 to provide liquidity for stablecoin pairs. Above example USDC_WETH can be looked up by using key (USDC, WETH, 3000).

Computing the pool address (computeAddress)

One may get pool adress by executing getPool function on Uniswap v3 factory contract by passing in the pool key. In our implementation, the pool address compuation is done by a library which provides better transparency for learning purpose:

bytes32 internal constant POOL_INIT_CODE_HASH 
= 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54;

function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) {
require(key.token0 < key.token1);
pool = address(
uint160(
uint256(
keccak256(
abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encode(key.token0, key.token1, key.fee)),
POOL_INIT_CODE_HASH
)
)
)
)
)
}

The pool key is hashed with keccak256, encoded with factory address and initial code hash and got hashed again before getting the pool address. keccak256 as a hashing algorithm that belongs to SHA-3 family is stronger than SHA-256.

Calling flash function on pool contract

Flash callback data that consists of amount0, amount1 and caller address is then encoded and pass in when calling the flash function on pool contract:

function flash(uint256 amount0, uint256 amount1) private {
bytes memory data = abi.encode(
FlashCallbackData({
amount0: amount0,
amount1: amount1,
caller: address(this)
})
);
pool.flash(address(this), amount0, amount1, data);
}

In our example, we would like to take a flash loan of 1,000 USDC. Therefore, amount0 shall be 1,000,000,000 (decimals = 6) whereas amount1 shall be 0.

The callback (uniswapV3FlashCallback)

As you expect, the pool contract will invoke the callback function defined on Flashloan contract. Please note the pool contract knows where to call back is because we passed in the address of Flashloan contract when calling flash function, not because the address encoded in the FlashCallbackData. Try to change the latter to something else and you will find out.

function uniswapV3FlashCallback(
uint256 fee0,
uint256 fee1,
bytes calldata data
) external {
FlashCallbackData memory decoded = abi.decode(
data,
(FlashCallbackData)
);
// DO YOUR BUSINESS HERE

// Repay borrow
if (fee0 > 0) {
console.log("tokenBorrowed0OnThisContract=",token0.balanceOf(address(this)));
console.log("returning=", decoded.amount0 + fee0);
token0.transfer(address(pool), decoded.amount0 + fee0);
}
if (fee1 > 0) {
console.log("tokenBorrowed1OnThisContract=", IERC20(USDC).balanceOf(address(this)));
console.log("returning=", decoded.amount1 + fee1);
IERC20(USDC).transfer(address(pool), decoded.amount1 + fee1);
}
}

When the callback function is executed, consider you’ve got your loan Approval in Principle (AIP)! Isn’t it faster than any unsecure loan you can get from the market? But don’t celebrate too early — Make sure you repay the loan with the fee. Not until you pay off the loan with the fee, the transcation yet to be successful. And for any reason that you can’t make a successful transaction, any profit or loss made in between will not be recognized. Before repaying the loan, do your businesss and make your profit: be it arbitrage, swaps or anything. Pay off the loan with the fee and bag your profit home!

Please take note that for simplility purposes, the sequence diagram is drawn is the way as if USDC is directly transferred from Flashloan contract back to the pool contract. ERC20 tokens can not be passed around in this way — behind the scene, what happens is that USDC token contract will be updated so that Flashloan contract will be holding less as much as the pool contract will be holding more.

Testing

We’ve discuss about what could be a pratical strategy when testing DeFi apps using data from Mainnet network — In another writeup of mine, I explored on how to fork mainnet and inpersonate whale account. Let’s play the stategy here by impersonating an USDC whale and top up the Flashloan contract. I would like to add 2 more test cases though: 1. borrow 1,000 USDC using the flash loan and 2. calculate the gas fees for this flash loan transcation.

Borrow1,000 USDC using flash loan

In my test cases, I would have transferred 10 USDC to the Flashloan contract alreay in hoping this is good enough to cover the fee. As we are working with 0.3% pool, the fee to repay along with the principal will be 1000 * 0.3% = 3 USDC.

const WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const POOL_FEE = 3000; // 0.30% tier
const DECIMALS = 6;
const BORROW_AMOUNT = ethers.parseUnits(1000, DECIMALS);

it("borrow USDC flash loan", async () => {
const tx = await flashloan.initArbPool(USDC, WETH, POOL_FEE,BORROW_AMOUNT, 0);
receipt = await tx.wait();
console.log(`Borrowing ${BORROW_AMOUNT} USDC`);
balance = await flashloan.tokenBalance(USDC);
console.log(`Current balance of USDC = ${balance}`);
expect(balance).equal(7000000);
});

Flash loan test case passed and here is what we got from the result:

USDC balance of whale:  170961308210457n
Impersonation Started.
Impersonation completed.
tokenBorrowed0OnThisContract= 1010000000
returning= 1003000000
Borrowing 1000000000 USDC
Current balance of USDC = 7000000
✔ borrow USDC flash loan

When the loan is approved, balance on the flash loan contract becomes 1,010,000,000, out of which 1,000,000,000 is the loan principal amount and 10,000,000 is credited from impersonation. We will need to repay 1,000,000,000 * (1+0.3%) = 1,003,000,000 back to the pool for the transcation to be successful and when repayment is done, we expect 7,000,000 left on the contract as the remaining balance.

Get a sense of gas fees for flash loan transcations

Besides the flash loan fee, gas fees as cost of the transcation is not something you can ignore also when getting your flash loan executed. As the time when ETH price is high, the transcations can be very costly. To make sure your flash loan funded trading strategy is profitable, you may want to leverage mainnet forking technique to get a sense on how much the transcation would cost in mainnet before deploying it.

  it("Get Gas in USD", async () => {
const gasPrice = receipt.gasPrice;
const gasUsed = receipt.gasUsed;
const gasUsedETH = gasPrice * gasUsed;
//exchange rate on 20 April 2024 BTC Halving
console.log(
"Total Gas USD: " +
ethers.formatEther(gasUsedETH.toString()) * 3157.01
);
expect(gasUsedETH).not.equal(0);
});

Receipt of the transcantion was returned from the flash loan test case executed previously and here is the calculation of the gas used in ETH for the transcation and also in USD at the rate on the memorial day of BTC halving.

USDC balance of whale:  170961298210457n
Impersonation Started.
Impersonation completed.
Total Gas USD: 22.244343565147762
✔ Get Gas in USD

In my example, I borrowed in USD and repaid in USD. Gas fees in USD would be a very good factor for me to have an idea if my flash loan funded trading strategy is profitable or not.

Conclusion

Flash loan is a very powerful source of fund in DeFi ecosystem. With no collateral needed, a huge sums of fund can be raised in a snap of a finger and be put into the trading strategy for speculation, as long as the principal and the fee can be paid off at the end of the transcation. Flash loan is implemented using Uniswap v3 contracts in this article. With no pain, it can be ported to PancakeSwap v3, which is forked off from Uniswap. It also won’t be too difficult to implement Flash loan in other protocols using the same concept by referring to the specifications.

In my next writeup, we are going to explore multi-hop swap on Uniswap v3 and when we put what we have disucssed so far, a flash loan funded triangular arbitrage trading strategy in DeFi can be constructed.

Source code

Check out the souce code for Flash loan implemented on Uniswap v3 and the test cases -https://github.com/aaronliruns/uniswapv3-flashloan-arb

--

--