0

Description

I'm in the process of digging into the functions for liquidity management in more detail, as well as their corresponding events that are emitted. Let's take the Uniswap V2 Router (github link) as an example, here we have the functions addLiquidity(ETH) and removeLiquidity(ETH).

addLiquidity functions

    // **** ADD LIQUIDITY ****
    function _addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin
    ) internal virtual returns (uint amountA, uint amountB) {
        // create the pair if it doesn't exist yet
        if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
            IUniswapV2Factory(factory).createPair(tokenA, tokenB);
        }
        (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
        if (reserveA == 0 && reserveB == 0) {
            (amountA, amountB) = (amountADesired, amountBDesired);
        } else {
            uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
            if (amountBOptimal <= amountBDesired) {
                require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
                (amountA, amountB) = (amountADesired, amountBOptimal);
            } else {
                uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
                assert(amountAOptimal <= amountADesired);
                require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
                (amountA, amountB) = (amountAOptimal, amountBDesired);
            }
        }
    }
    function addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
        (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
        TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
        liquidity = IUniswapV2Pair(pair).mint(to);
    }
    function addLiquidityETH(
        address token,
        uint amountTokenDesired,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
        (amountToken, amountETH) = _addLiquidity(
            token,
            WETH,
            amountTokenDesired,
            msg.value,
            amountTokenMin,
            amountETHMin
        );
        address pair = UniswapV2Library.pairFor(factory, token, WETH);
        TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
        IWETH(WETH).deposit{value: amountETH}();
        assert(IWETH(WETH).transfer(pair, amountETH));
        liquidity = IUniswapV2Pair(pair).mint(to);
        // refund dust eth, if any
        if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
    }

removeLiquidity functions

    // **** REMOVE LIQUIDITY ****
    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
        (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
        (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
        (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
        require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
        require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
    }
    function removeLiquidityETH(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
        (amountToken, amountETH) = removeLiquidity(
            token,
            WETH,
            liquidity,
            amountTokenMin,
            amountETHMin,
            address(this),
            deadline
        );
        TransferHelper.safeTransfer(token, to, amountToken);
        IWETH(WETH).withdraw(amountETH);
        TransferHelper.safeTransferETH(to, amountETH);
    }

Contract events

There are no events emitted here. Lets look at the Uniswap V2 FactoryContract (github link) as well as the V2 PairContract (github link).

We know we have the PairCreated on the factory, but that is only emitted when a new pair is created. I know we can use the pair address to check for liquidity in ethers.

event PairCreated(address indexed token0, address indexed token1, address pair, uint);

On the PairContract, we have Mint, Burn, Swap, and Sync. We could listen to the Sync event to monitor when reserves are updated.

    event Mint(address indexed sender, uint amount0, uint amount1);
    event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
    event Swap(
        address indexed sender,
        uint amount0In,
        uint amount1In,
        uint amount0Out,
        uint amount1Out,
        address indexed to
    );
    event Sync(uint112 reserve0, uint112 reserve1);

Using ethers.js

Let's try to use ethers.js and listen to AddLiquidity - where does this event come from?

const ethers = require('ethers');
const provider = new ethers.providers.JsonRpcProvider(PROVIDER_URL);

const UNISWAP_V2_ROUTER_ADDRESS = '0x..';
const TOKEN_ADDRESS = '0x..';

const filter = {
  address: UNISWAP_V2_ROUTER_ADDRESS,
  topics: [
    ethers.utils.id('AddLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256)'),
    null,
    TOKEN_ADDRESS
  ]
};

provider.on(filter, async (log) => {
  const pairAddress = ethers.utils.getAddress(log.topics[2].slice(26));
  console.log(`Liquidity was added to ${pairAddress}.`);
});

Wrapping up

  • How do these events play together?
  • Where is the AddLiquidity event actually executed, and does this always happen on the Router contract?
  • Am I missing something important, how the AddLiquidity and RemoveLiquidity are handled by the Router, Factory, and the Pair?

I totally get the fundamental concept of liquidity management, this is more kind of a deep dive how Solidity actually executes this logic. If anyone can explain the dependencies and how liquidity management and its events are handled (and can be monitored in ethers.js f.e.) in Solidity by the various contracts, I'll appreciate that.

2

1 Answer 1

1

Events are emitted from the contracts in which they are written (unless there's a proxy, but this is not the case).

So you have to monitor the pair contract and not the router to listen to mints, burns and swaps.

const filter = {
  address: pairAddress,
  topics: [pairInterface.getEventTopic("Mint")],
};

provider.on(filter, async (log) => {
  const parsedLog = pairInterface.parseLog(log);
  const sender = parsedLog.args.sender;
  const amount0 = parsedLog.args.amount0;
  const amount1 = parsedLog.args.amount1;
  console.log(`Liquidity was added by address ${sender}.`);
});

If you aren't interested in a single pair but all of the them, you can just leave empty the address key on the filter:

const filter = {
  topics: [pairInterface.getEventTopic("Mint")],
};

and use log.address to get the pair from which the event came from.

One other mistake is using the name of the called function (AddLiquidity, etc..) as topic to listen to. This doesn't make sense, we can only listen using the name of the event. You can try listening to those by actually listening to all transactions in a block and filtering based on tx.to and tx.data, but it's more complicated and you will miss users that don't use the official uniswap router (think for example at aggregators like 1inch).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.