Solidity Token Swaps
A token swap is a trade that takes one token and returns another token at the pool ratio. A swapper contract carries out the trade through a pool on the trader behalf.
A swap through the swapper moves both balances
A trader swaps 100 token A through the swapper. The trader gains 100 token B.
// 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;
}
}
contract Swapper {
ToyToken public tokenA;
ToyToken public tokenB;
Pool public pool;
constructor(address a, address b, address poolAddr) {
tokenA = ToyToken(a);
tokenB = ToyToken(b);
pool = Pool(poolAddr);
}
function quoteAForB(uint256 amountIn) public view returns (uint256) {
return pool.reserveB() * amountIn / (pool.reserveA() + amountIn);
}
function swapAForB(uint256 amountIn, uint256 minOut) public returns (uint256 amountOut) {
tokenA.transferFrom(msg.sender, address(this), amountIn);
tokenA.approve(address(pool), amountIn);
amountOut = pool.swapAForB(amountIn);
require(amountOut >= minOut, "output too low");
tokenB.transfer(msg.sender, amountOut);
}
}A swap below the minimum output is refused
A swap that would return less than the minimum is refused. The pool reserves stay exactly as they were.
// 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;
}
}
contract Swapper {
ToyToken public tokenA;
ToyToken public tokenB;
Pool public pool;
constructor(address a, address b, address poolAddr) {
tokenA = ToyToken(a);
tokenB = ToyToken(b);
pool = Pool(poolAddr);
}
function quoteAForB(uint256 amountIn) public view returns (uint256) {
return pool.reserveB() * amountIn / (pool.reserveA() + amountIn);
}
function swapAForB(uint256 amountIn, uint256 minOut) public returns (uint256 amountOut) {
tokenA.transferFrom(msg.sender, address(this), amountIn);
tokenA.approve(address(pool), amountIn);
amountOut = pool.swapAForB(amountIn);
require(amountOut >= minOut, "output too low");
tokenB.transfer(msg.sender, amountOut);
}
}A quote changes after a swap runs
A quote predicts the output before any trade. After a swap, the same quote predicts a smaller output.
// 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;
}
}
contract Swapper {
ToyToken public tokenA;
ToyToken public tokenB;
Pool public pool;
constructor(address a, address b, address poolAddr) {
tokenA = ToyToken(a);
tokenB = ToyToken(b);
pool = Pool(poolAddr);
}
function quoteAForB(uint256 amountIn) public view returns (uint256) {
return pool.reserveB() * amountIn / (pool.reserveA() + amountIn);
}
function swapAForB(uint256 amountIn, uint256 minOut) public returns (uint256 amountOut) {
tokenA.transferFrom(msg.sender, address(this), amountIn);
tokenA.approve(address(pool), amountIn);
amountOut = pool.swapAForB(amountIn);
require(amountOut >= minOut, "output too low");
tokenB.transfer(msg.sender, 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;
}
}
contract Swapper {
ToyToken public tokenA;
ToyToken public tokenB;
Pool public pool;
constructor(address a, address b, address poolAddr) {
tokenA = ToyToken(a);
tokenB = ToyToken(b);
pool = Pool(poolAddr);
}
function quoteAForB(uint256 amountIn) public view returns (uint256) {
return pool.reserveB() * amountIn / (pool.reserveA() + amountIn);
}
function swapAForB(uint256 amountIn, uint256 minOut) public returns (uint256 amountOut) {
tokenA.transferFrom(msg.sender, address(this), amountIn);
tokenA.approve(address(pool), amountIn);
amountOut = pool.swapAForB(amountIn);
require(amountOut >= minOut, "output too low");
tokenB.transfer(msg.sender, amountOut);
}
}Key points
- Swapper: a contract that trades through a pool for a user.
- Quote: the output a pool ratio predicts before a trade runs.
- Minimum out: the smallest output a trader accepts for a swap.
- Slippage: the price moving between a quote and execution.
- Failed swap: a swap below the minimum output is refused and changes nothing.
In this example, a swapper quotes 80 token B for 50 token A, then runs the swap with a minimum of 70. The trader ends with 750 token A and 680 token B.
Note
this swapper trades 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
What does a minimum-out check protect against?