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
- 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). - 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.
- 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:
Name | Type |
---|---|
txn_hash | bytes32 |
block_number | uint64 |
initiator | address |
uniswapV2Callee | address |
token0 | address |
token1 | address |
pair | address |
is_official_v2_pair | bool |
pair_factory | address |
- Add a Persistence component and connect it to the Schema component. Save the schema and set the persistence.
- 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
- 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! - Making calls to arbitrary contracts from within sim callbacks is easy and allows us to compute anything we need to persist.
Updated about 1 month ago