🎓 A1Learner
🔍
0 of 48 lessons completed

Solidity Liquidity Pools

A liquidity pool is a contract that holds two token balances and lets people trade between them. Depositors add both tokens and receive pool shares that stand for their part of the pool.

A deposit mints shares and fills both reserves

The first deposit adds 100 of token A and 200 of token B. The depositor receives 100 shares.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ToyToken {
    string public name;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);
    constructor(string memory tokenName, address holder, uint256 supply) {
        name = tokenName;
        totalSupply = supply;
        balanceOf[holder] = supply;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}


contract Pool {
    ToyToken public tokenA;
    ToyToken public tokenB;
    uint256 public reserveA;
    uint256 public reserveB;
    uint256 public totalShares;
    mapping(address => uint256) public shares;
    constructor(address a, address b) {
        tokenA = ToyToken(a);
        tokenB = ToyToken(b);
    }
    function deposit(uint256 a, uint256 b) public returns (uint256 minted) {
        require(a > 0 && b > 0, "empty deposit");
        if (totalShares == 0) {
            minted = a;
        } else {
            minted = a * totalShares / reserveA;
        }
        tokenA.transferFrom(msg.sender, address(this), a);
        tokenB.transferFrom(msg.sender, address(this), b);
        reserveA += a;
        reserveB += b;
        totalShares += minted;
        shares[msg.sender] += minted;
    }
    function swapAForB(uint256 amountIn) public returns (uint256 amountOut) {
        require(amountIn > 0, "empty swap");
        amountOut = reserveB * amountIn / (reserveA + amountIn);
        require(amountOut > 0, "no output");
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenB.transfer(msg.sender, amountOut);
        reserveA += amountIn;
        reserveB -= amountOut;
    }
}

A second deposit at the same ratio mints matching shares

A second deposit adds half the first amounts. It mints half the shares, so the pool ratio stays steady.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ToyToken {
    string public name;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);
    constructor(string memory tokenName, address holder, uint256 supply) {
        name = tokenName;
        totalSupply = supply;
        balanceOf[holder] = supply;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}


contract Pool {
    ToyToken public tokenA;
    ToyToken public tokenB;
    uint256 public reserveA;
    uint256 public reserveB;
    uint256 public totalShares;
    mapping(address => uint256) public shares;
    constructor(address a, address b) {
        tokenA = ToyToken(a);
        tokenB = ToyToken(b);
    }
    function deposit(uint256 a, uint256 b) public returns (uint256 minted) {
        require(a > 0 && b > 0, "empty deposit");
        if (totalShares == 0) {
            minted = a;
        } else {
            minted = a * totalShares / reserveA;
        }
        tokenA.transferFrom(msg.sender, address(this), a);
        tokenB.transferFrom(msg.sender, address(this), b);
        reserveA += a;
        reserveB += b;
        totalShares += minted;
        shares[msg.sender] += minted;
    }
    function swapAForB(uint256 amountIn) public returns (uint256 amountOut) {
        require(amountIn > 0, "empty swap");
        amountOut = reserveB * amountIn / (reserveA + amountIn);
        require(amountOut > 0, "no output");
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenB.transfer(msg.sender, amountOut);
        reserveA += amountIn;
        reserveB -= amountOut;
    }
}

A swap moves the ratio

Swapping 100 of token A returns 100 of token B. The reserves move to 200 and 100, so the ratio has changed.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ToyToken {
    string public name;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);
    constructor(string memory tokenName, address holder, uint256 supply) {
        name = tokenName;
        totalSupply = supply;
        balanceOf[holder] = supply;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}


contract Pool {
    ToyToken public tokenA;
    ToyToken public tokenB;
    uint256 public reserveA;
    uint256 public reserveB;
    uint256 public totalShares;
    mapping(address => uint256) public shares;
    constructor(address a, address b) {
        tokenA = ToyToken(a);
        tokenB = ToyToken(b);
    }
    function deposit(uint256 a, uint256 b) public returns (uint256 minted) {
        require(a > 0 && b > 0, "empty deposit");
        if (totalShares == 0) {
            minted = a;
        } else {
            minted = a * totalShares / reserveA;
        }
        tokenA.transferFrom(msg.sender, address(this), a);
        tokenB.transferFrom(msg.sender, address(this), b);
        reserveA += a;
        reserveB += b;
        totalShares += minted;
        shares[msg.sender] += minted;
    }
    function swapAForB(uint256 amountIn) public returns (uint256 amountOut) {
        require(amountIn > 0, "empty swap");
        amountOut = reserveB * amountIn / (reserveA + amountIn);
        require(amountOut > 0, "no output");
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenB.transfer(msg.sender, amountOut);
        reserveA += amountIn;
        reserveB -= amountOut;
    }
}

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ToyToken {
    string public name;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);
    constructor(string memory tokenName, address holder, uint256 supply) {
        name = tokenName;
        totalSupply = supply;
        balanceOf[holder] = supply;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}


contract Pool {
    ToyToken public tokenA;
    ToyToken public tokenB;
    uint256 public reserveA;
    uint256 public reserveB;
    uint256 public totalShares;
    mapping(address => uint256) public shares;
    constructor(address a, address b) {
        tokenA = ToyToken(a);
        tokenB = ToyToken(b);
    }
    function deposit(uint256 a, uint256 b) public returns (uint256 minted) {
        require(a > 0 && b > 0, "empty deposit");
        if (totalShares == 0) {
            minted = a;
        } else {
            minted = a * totalShares / reserveA;
        }
        tokenA.transferFrom(msg.sender, address(this), a);
        tokenB.transferFrom(msg.sender, address(this), b);
        reserveA += a;
        reserveB += b;
        totalShares += minted;
        shares[msg.sender] += minted;
    }
    function swapAForB(uint256 amountIn) public returns (uint256 amountOut) {
        require(amountIn > 0, "empty swap");
        amountOut = reserveB * amountIn / (reserveA + amountIn);
        require(amountOut > 0, "no output");
        tokenA.transferFrom(msg.sender, address(this), amountIn);
        tokenB.transfer(msg.sender, amountOut);
        reserveA += amountIn;
        reserveB -= amountOut;
    }
}

Key points

  • Liquidity pool: a contract holding two token balances for trading.
  • Pool share: a record of one depositor part of the pool.
  • Constant product: the rule that the two reserves multiplied together stay fixed during a swap.
  • Price move: buying one token shrinks its reserve, so the next unit costs more.
  • Deposit ratio: deposits add both tokens so the pool ratio stays steady.

In this example, a pool starts with 200 of token A and 400 of token B, then a swap of 50 token A returns 80 token B. The reserves end at 250 and 320, and the pool token balances match those reserves.

Note

this pool uses toy tokens on a local chain only, with no real funds.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

After a deposit of 100 token A and 200 token B, what does the pool hold?