Skip to content

How to implement LP-Tokens in Solidity

Table of contents

Open Table of contents

Introduction

In most DeFi Protocols users trade against a pool of liquidity implemented via a Smart Contract. This Peer-to-Contract model allows for asynchronicity and therefore removes the Coincidence of wants problem present in traditional Peer-to-Peer solutions where users interact with one another directly.

The liquidity Protocol users can trade against originates from “Liquidity Providers” (LPs) that decide to lock-up their funds in a Smart Contract. The incentive to deposit assets into a Smart Contract and provide liquidity is the generation of yield. Upon withdrawal users get their principal back plus all the fees that accumulated during the time their assets were used to facilitate trades.

Quick Recap on LP-Tokens

The Concept of “Liquidity Provider Tokens” (LP-Tokens) allows for tracking of ownership of deployed assets and accrued fees. Upon deposit of assets, LP-Tokens are minted and transferred to the depositor. Later on, the user can burn these LP-Tokens to access the deposited funds plus the yield that was generated while the assets were used by the Protocol.

We won’t go into the details of LP-Tokens here as we already did a deep dive into the mechanics in a dedicated Blog Post which I encourage you to read if you’re new to the concept.

Depositing Funds and minting LP-Tokens

As mentioned above, LP-Tokens are minted when funds are deposited into the Smart Contract. The following formula calculates how many LP-Tokens should be minted upon deposit:

AmountL=AmountT×SupplyLBalanceTAmount_L = \frac{Amount_T \times Supply_L}{Balance_T}

Where AmountLAmount_L is the amount of new LP-Tokens to mint, AmountTAmount_T is the amount of Tokens deposited, SupplyLSupply_L is the total circulating supply of LP-Tokens and BalanceTBalance_T is the current balance of Tokens in the Smart Contract.

A special case we need to account for is the situation in which no Tokens have been deposited into the Smart Contract yet (BalanceT=0Balance_T = 0). In this scenario we’ll mint an amount of LP-Tokens equal to the Tokens deposited:

AmountL=AmountTAmount_L = Amount_T

Burning LP-Tokens to unlock Funds

Upon Withdrawal users need to burn their LP-Tokens to get access to their funds plus the yield that was generated. The formula to calculate the Token amount the users is entitled to looks like this:

AmountT=AmountL×BalanceTSupplyLAmount_T = \frac{Amount_L \times Balance_T}{Supply_L}

Where AmountTAmount_T is the amount of Tokens to return, AmountLAmount_L is the amount of LP-Tokens to burn, BalanceTBalance_T is the current balance of Tokens in the Smart Contract and SupplyLSupply_L is the total circulating supply of LP-Tokens.

Implementation in Solidity

With the Math out of the way we can now jump straight into our Solidity implementation. Again, if you want to learn more about the underlying mechanics of LP-Tokens you might want to read the introductory Blog Post before continuing.

Contract Scaffold

To start tings off we’ll write the SPDX-License-Identifier followed by the pragma declaration that outlines the Solidity Compiler version used to compile the contract.

In addition to that we’ll import the ERC-20 implementation by the OpenZeppelin Contracts library which offers a common set of audited building blocks to author Smart Contracts.

Our contract will be called Vault given that users entrust it with their funds to provide liquidity.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8;

import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

contract Vault {}

Add Token Support

Next up we need to add a functionality which allows the Smart Contract to take custody of user funds.

To do this we’ll set the token supported by our Vault in our contract’s constructor. Note that the value is immutable, which means that once its set during deployment it can’t be changed anymore.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8;

import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

contract Vault {
    IERC20 public immutable token;

    constructor(IERC20 token_) {
        token = token_;
    }
}

Add LP-Token Support

Our Smart Contract is now capable to manage user funds. The next thing we need to add is support to mint and burn LP-Tokens upon deposit / withdrawal.

There are a few different ways to implement this. We’ll follow a pattern that emerged in DeFi dApps in which the Smart Contract is turned into an ERC-20 itself.

Don’t get too confused by the concept as it’s actually quite simple to understand once you see the code. Our Vault contract itself represents LP-Tokens and therefore will be able to manage the supply via mint- and burn operations.

To turn our Vault into an ERC-20 we extend ERC20 and call the ERC20 constructor during the contract initialization.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8;

import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

contract Vault is ERC20 {
    IERC20 public immutable token;

    constructor(IERC20 token_) ERC20("Liquidity Provider Token", "LP-TKN") {
        token = token_;
    }
}

Depositing Tokens

Let’s now focus on the deposit function. As a quick reminder, here are the two formulas we need to implement:

AmountL=AmountT×SupplyLBalanceTAmount_L = \frac{Amount_T \times Supply_L}{Balance_T}

And if the Vault has no Token balance yet:

AmountL=AmountTAmount_L = Amount_T

Translating the math into Solidity code results in the following:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8;

import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

contract Vault is ERC20 {
    IERC20 public immutable token;

    constructor(IERC20 token_) ERC20("Liquidity Provider Token", "LP-TKN") {
        token = token_;
    }

    function deposit(uint256 amount) external {
        require(amount > 0);

        uint256 amountToken = amount;
        uint256 supplyLPToken = this.totalSupply();
        uint256 balanceToken = token.balanceOf(address(this));

        uint256 amountLPToken;
        if (supplyLPToken == 0) {
            amountLPToken = amountToken;
        } else {
            amountLPToken = (amountToken * supplyLPToken) / balanceToken;
        }

        _mint(msg.sender, amountLPToken);
        token.transferFrom(msg.sender, address(this), amountToken);
    }
}

As you can see we first check if the amount that’s to be deposited is greater than zero. If so, we run the calculation to figure out how many LP-Tokens should be minted. We then mint the LP-Tokens to the user’s address and transfer the Tokens from the user to the Vault contract.

Note: It’s important to call out that the transfer of user Tokens to the Vault via token.transferFrom needs to happen at the end of the function call. The reason being that the LP-Token calculation should only incorporate token balances before the transfer happened.

The same is true for the _mint function. The newly minted tokens shouldn’t be part of the LP-Token calculation.

Withdrawing Tokens

The last missing piece is the withdraw function. Here’s the formula we need to implement:

AmountT=AmountL×BalanceTSupplyLAmount_T = \frac{Amount_L \times Balance_T}{Supply_L}

The code is pretty straightforward to write:

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8;

import {ERC20} from "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

contract Vault is ERC20 {
    IERC20 public immutable token;

    constructor(IERC20 token_) ERC20("Liquidity Provider Token", "LP-TKN") {
        token = token_;
    }

    function deposit(uint256 amount) external {
        require(amount > 0);

        uint256 amountToken = amount;
        uint256 supplyLPToken = this.totalSupply();
        uint256 balanceToken = token.balanceOf(address(this));

        uint256 amountLPToken;
        if (supplyLPToken == 0) {
            amountLPToken = amountToken;
        } else {
            amountLPToken = (amountToken * supplyLPToken) / balanceToken;
        }

        _mint(msg.sender, amountLPToken);
        token.transferFrom(msg.sender, address(this), amountToken);
    }

    function withdraw(uint256 amount) external {
        require(amount > 0);

        uint256 amountLPToken = amount;
        uint256 supplyLPToken = this.totalSupply();
        uint256 balanceToken = token.balanceOf(address(this));

        uint256 amountToken = (amountLPToken * balanceToken) / supplyLPToken;

        _burn(msg.sender, amountLPToken);
        token.transfer(msg.sender, amountToken);
    }
}

We once again check if the amount of LP-Tokens to be burnt is greater than zero. If that’s the case we run our calculation to figure out how many Tokens our contract needs to return to the user. We then burn the LP-Tokens and transfer the Tokens to the user’s address.

Note: Keep in mind that the amount we pass into the function isn’t the Token amount but the LP-Token amount we’re about to burn.

Conclusion

And there you have it. A full LP-Token implementation written in Solidity.

Every time a user deposits funds into the Vault she gets LP-Tokens minted and transferred proportional to the liquidity contributed.

Once the user decides to unwind the position she can burn her LP-Tokens and get access to the assets initially deployed plus the yield those assets generated proportional to her share of assets within the Vault.

References

The following resources have been invaluable for me to learn the concepts discussed in this article.

You should definitely give them a read if you want to dive deeper into the topic.