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?