4. Flash swaps on Uniswap V2

Let's track flash swaps on Uniswap V2

Goal

A regular Uniswap V2 swap involves an exchange of one token for another within a liquidity pool (LP). Uniswap V2 pioneered flash swaps, in which a user can withdraw as much as they want of any token in a liquidity pool instantly with zero upfront cost. By transaction end, the user must: 1) Return the withdrawn tokens themselves OR 2) Return an equivalent amount in other tokens. In either case, there's also an LP fee. If the conditions above are not met, the transaction fails, and the entire swap is reverted.

Flash swaps allow users of the protocol to create callbacks that will act upon initiating a swap via some UniswapV2Pair pool. These callbacks are predefined in contracts that implement the IUniswapV2Callee interface. In this example, we will create a canvas that tracks and provides data on flash swaps.

If you want to skip to the end, here's the canvas that already has everything built.


Steps

Setting up the pipeline

Adding components

  1. Start by adding a Data source component. We want our pipeline to run at the tip, so we leave To blank and set From to the UniswapV2Factory's deployment block (10000835).
  2. Add an EVM Lambda component and connect it to the right handle of the data source. For now, leave it as is. We'll come back to it in a bit.
  3. Add a Schema and name it uniswap_v2_flash_swaps. Draw a connection from the lambda to the schema component and populate the schema with some important data we want to persist about each flash swap, as follows:
NameType
txn_hashbytes32
block_numberuint64
initiatoraddress
uniswapV2Calleeaddress
token0address
token1address
pairaddress
is_official_v2_pairbool
pair_factoryaddress
  1. Add a Persistence component and connect it to the Schema component. Save the schema and set the persistence.
  2. Finally, add an API component that we will use to query the data in our table. Leave the query empty for now--we'll come back to it later.

You're all set! 🎉

Writing the code

We want to hook on all calls to the uniswapV2Call(address,uint256,uint256,bytes) function of all contracts. That means we need to target a specific ABI. For that we can use the Input ABI Manually option in our EVM Lambda component. This will hook on all contracts whose ABI include the inputted ABI. Let's input the JSON formatted ABI for our IUniswapV2Callee interface:

[
    {
        "inputs":
        [
            {
                "internalType": "address",
                "name": "sender",
                "type": "address"
            },
            {
                "internalType": "uint256",
                "name": "amount0",
                "type": "uint256"
            },
            {
                "internalType": "uint256",
                "name": "amount1",
                "type": "uint256"
            },
            {
                "internalType": "bytes",
                "name": "data",
                "type": "bytes"
            }
        ],
        "name": "uniswapV2Call",
        "outputs":
        [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
]

Now we can add a post hook on the uniswapV2Call method and add a callback:

function handleFlashSwap() public {
    handleFlashSwapContext storage ctx = getHandleFlashSwapContext();
    address token0 = IUniswapV2Pair(ctx.txn.from()).token0(); // fetch the address of token0
    address token1 = IUniswapV2Pair(ctx.txn.from()).token1(); // fetch the address of token1
    address factory = IUniswapV2Pair(ctx.txn.from()).factory(); // fetch the factory of the pair
    simEmitToSchema_uniswap_v2_flash_swaps(
        SchemaUniswap_v2_flash_swapsColumns({
            txn_hash: ctx.txn.hash(),
            block_number: uint64(block.number),
            initiator: ctx.inputs.sender,
            uniswapV2Callee: ctx.txn.to(),
            token0: token0,
            token1: token1,
            pair: ctx.txn.from(),
            is_official_v2_pair: ctx.txn.from() == IUniswapV2Factory(FACTORY_V2).getPair(token0, token1),
            pair_factory: factory
        })
    );
}

Note that we want to verify if the call made was initiated by an official UniswapV2Pair contract. For that, we call the UniswapV2Factory and verify that the contract calling the callee is indeed the pair for token0 and token1, as registered in the factory.

Execution

You can test the execution within the code editor using the range 19000000 to 19000010. You should see some results. Then we hit the play button on the execution edge to launch the full tip and backfill jobs.

Setting up the API

If we want to do some analysis, we could open the query editor and write many SQL queries against our created data. But let's just go ahead and build an API that will serve the data for the N most recent flash swaps for a given LP. Go to the API component and input the following query:

select * from @org.uniswap_v2_flash_swaps
where pair = $pool
order by block_number desc
limit $limit

The pool parameter should be set to address and the limit should be set to uint32. If you want, you could specify default values of 0xf848e97469538830b0b147152524184a255b9106 and 1 respectively. Then test and set the API, and it's ready to be used.

Key takeaways

  1. Targeting custom ABIs is useful when we want to target a specific family of contracts that only have a limited set of functions/logs in common (such as IUniswapV2Callee contracts). sim makes this easy!
  2. Making calls to arbitrary contracts from within sim callbacks is easy and allows us to compute anything we need to persist.